4/24/2015

Runtime Setting of SSL Params when invoking web services(apache cxf client)

By default, when we config the SSL Key for WS call, we use below system parameters:

System.setProperty("javax.net.ssl.keyStoreType", "pkcs12");
System.setProperty("javax.net.ssl.keyStore", "keystore.p12");
System.setProperty("javax.net.ssl.keyStorePassword", "1234");
System.setProperty("javax.net.ssl.trustStore", "cacerts");

But it is not flexible enough if the application has gateway structure and needs different SSL configuration for each different channels. So we need a workaround.

Below example is based on the client stub that generated with Apache CXF.(Recommanded)
Personally I prefer to use Apache CXF to generate client stub instead of Apache Axis. It is more flexible and more user friendly.

Maven Config to generate client stub:

<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>3.0.4</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <sourceRoot>${project.basedir}/src/main/java</sourceRoot>
                <wsdlOptions>
                    <wsdlOption>
                        <wsdl>${project.basedir}/src/main/resources/schemas/sample-ws.wsdl</wsdl>
                        <bindingFiles>
                            <bindingFile>${project.basedir}/src/main/resources/schemas/bindings/binding.xjb</bindingFile>
                        </bindingFiles>
                        <extraargs>
                            <extraarg>-impl</extraarg>
                            <extraarg>-client</extraarg>
                            <extraarg>-verbose</extraarg>
                            <extraarg>-p</extraarg>
                            <extraarg>com.sample.ws</extraarg>
                            <extraarg>-xjc-Xvalue-constructor</extraarg>
                        </extraargs>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.jvnet.jaxb2_commons</groupId>
            <artifactId>jaxb2-value-constructor</artifactId>
            <version>3.0</version>
        </dependency>
        <dependency>
            <groupId>org.jvnet.jaxb2_commons</groupId>
            <artifactId>jaxb2-basics</artifactId>
            <version>0.6.4</version>
        </dependency>
    </dependencies>
</plugin>

After the client stub is generated, if we want to make web service call, the client code is like below:
public SampleResponse authenticate(SampleRequest request) {
    
    SampleServiceInterface port = this.getPort();
    return port.authenticate(request);
}

public SampleServiceInterface getPort() {

    SampleServiceInterfaceService ss = new SampleServiceInterfaceService();
    SampleServiceInterface port = ss.getSampleServiceAPI();
    
    return port;
}

To customize the SSL configuration, we need to write the customized SSLSocketFactory. To implement this, we can create one SSLSocketFactoryGenerator, like below:

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.apache.commons.lang.StringUtils;


public class SSLSocketFactoryGenerator {

    
    private byte[] keyStore = null;
    private byte[] trustStore = null;
    
    private String alias = null;
    private String keyStoreType = null;
    private String keyStorePassword = null;
    
    private String trustStoreType = null;
    private String trustStorePassword = null;

    public SSLSocketFactoryGenerator(ClientConfig config) {
        
        this.alias = config.getKeyAlias();
        this.keyStore = config.getKeyStoreBytes();
        this.trustStore = config.getTrustStoreBytes();
        this.keyStoreType = config.getKeyStoreType();
        this.keyStorePassword = config.getKeystorePassword();
        this.trustStoreType = config.getTrustStoreType();
        this.trustStorePassword = config.getTruststorePassword();
    }

    public SSLSocketFactory getSSLSocketFactory() throws IOException,
            GeneralSecurityException {

        KeyManager[] keyManagers = getKeyManagers();
        TrustManager[] trustManagers = getTrustManagers();

        SSLContext context = SSLContext.getInstance("SSL");
        context.init(keyManagers, trustManagers, null);

        SSLSocketFactory ssf = context.getSocketFactory();
        return ssf;
    }

    public String getAlias() {
        return alias;
    }

    public void setAlias(String alias) {
        this.alias = alias;
    }

    public byte[] getKeyStore() {
        return keyStore;
    }

    public void setKeyStore(byte[] keyStore) {
        this.keyStore = keyStore;
    }

    public byte[] getTrustStore() {
        return trustStore;
    }

    public void setTrustStore(byte[] trustStore) {
        this.trustStore = trustStore;
    }

    public String getKeyStoreType() {
        return keyStoreType;
    }

    public void setKeyStoreType(String keyStoreType) {
        this.keyStoreType = keyStoreType;
    }

    public String getKeyStorePassword() {
        return keyStorePassword;
    }

    public void setKeyStorePassword(String keyStorePassword) {
        this.keyStorePassword = keyStorePassword;
    }

    public String getTrustStoreType() {
        return trustStoreType;
    }

    public void setTrustStoreType(String trustStoreType) {
        this.trustStoreType = trustStoreType;
    }

    public String getTrustStorePassword() {
        return trustStorePassword;
    }

    public void setTrustStorePassword(String trustStorePassword) {
        this.trustStorePassword = trustStorePassword;
    }

    private KeyManager[] getKeyManagers() throws IOException,
            GeneralSecurityException {

        String alg = KeyManagerFactory.getDefaultAlgorithm();
        KeyManagerFactory kmFact = KeyManagerFactory.getInstance(alg);

        InputStream fis = new ByteArrayInputStream(getKeyStore());
        
        KeyStore ks = KeyStore.getInstance(getKeyStoreType());
        
        ks.load(fis, getKeyStorePassword().toCharArray());
        fis.close();

        kmFact.init(ks, StringUtils.isEmpty(getKeyStorePassword())?null:getKeyStorePassword().toCharArray());

        KeyManager[] kms = kmFact.getKeyManagers();
        return kms;
    }

    protected TrustManager[] getTrustManagers() throws IOException,
            GeneralSecurityException {

        String alg = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmFact = TrustManagerFactory.getInstance(alg);

        InputStream fis = new ByteArrayInputStream(getTrustStore());
        KeyStore ks = KeyStore.getInstance(getTrustStoreType());
        ks.load(fis, StringUtils.isEmpty(getTrustStorePassword())?null:getTrustStorePassword().toCharArray());
        fis.close();

        tmFact.init(ks);

        TrustManager[] tms = tmFact.getTrustManagers();
        return tms;
    }
}

This is a quite basic sample, for different scenarios, it should be quite easy to make complementary or revision. In above sample, we can create customized SSLSocketFactory by passing in different configuration data(ClientConfig.java).

In this case, we also  need to revise the previous getPort function to use this customized SSLSocketFactory.
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Properties;

import javax.annotation.PostConstruct;
import javax.naming.ConfigurationException;
import javax.xml.ws.BindingProvider;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

public class SampleServiceImpl implements SampleService {

    private static final Logger logger = Logger.getLogger(SampleServiceImpl.class);

    private static final String URI = "/auth";

    protected ClientConfig clientConfig;

    public SampleServiceImpl() throws ConfigurationException, IOException {
        super();
    }

    void loadDefaultIfEmptyClientConfig() throws IOException, ConfigurationException {
        if (this.getClientConfig() != null)
            return;

        Properties properties = new Properties();
        properties.load(this.getClass().getClassLoader().getResourceAsStream("config/sample.properties"));
        setClientConfig(new ClientConfig(properties));
    }

    public SampleServiceImpl(Properties properties) throws ConfigurationException, IOException {
        super();
        this.setClientConfig(new ClientConfig(properties));
    }
    
    public SampleServiceImpl(ClientConfig clientConfig) throws ConfigurationException {
        super();
        this.setClientConfig(clientConfig);
    }

    public SampleServiceInterface getPort(String relUrl) {

        SampleServiceInterfaceService ss = new SampleServiceInterfaceService();
        SampleServiceInterface port = ss.getSampleServiceAPI();
        
        BindingProvider bindingProvider = (BindingProvider) port; 
        try {
            bindingProvider.getRequestContext().put("com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory", 
                    new SSLSocketFactoryGenerator(clientConfig).getSSLSocketFactory());
            
            if(StringUtils.isNotEmpty(relUrl)){
                bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, String.format("%s%s", this.clientConfig.getServiceUrl(), relUrl));
            }
        } catch (IOException e) {
            logger.error("Error happened when initializing SSLFactory.", e);
            throw new IllegalArgumentException(e);
        } catch (GeneralSecurityException e) {
            logger.error("Error happened when initializing SSLFactory.", e);
            throw new IllegalArgumentException(e);
        } 
        return port;
    }
    
    public ClientConfig getClientConfig() {
        return clientConfig;
    }

    public void setClientConfig(ClientConfig clientConfig) throws ConfigurationException {

        this.clientConfig = clientConfig;
    }
    
    @Override
    public SampleResponse authenticate(SampleRequest request) {
    
        SampleServiceInterface port = this.getPort();
        return port.authenticate(request);
    }

}
In this case, for each different channels that need different SSL configuration. We can create different service.




9/26/2014

Spring RestTemplate does not capture response body when error code 40x is returned

When consuming REST web services using SpringTemplate default configuration, when the response returns error HTTP status code, SpringTemplate will throw HttpClientErrorException . But you can still get the response body by calling:

clientEx.getResponseBodyAsByteArray();

OR

clientEx.getResponseBodyAsString();

But there is one special case, when error code is 40x, it fails to capture the response body.

Reason is described in ticket SPR-9999

While solution is quite simple as some one mentioned in the ticket. Use the "org.springframework.http.client.HttpComponentsClientHttpRequestFactory" instead of the default one.

So the configuration should be:

<bean id="clientHttpRequestFactory" class="org.springframework.http.client.HttpComponentsClientHttpRequestFactory"/>
    
<bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
     <property name="requestFactory" ref="clientHttpRequestFactory"/>
</bean>




9/10/2014

Spring Test: Inject Mock Object into proxy-based Spring bean.(Like bean with @Transactional or @Aync ...)

Spring Test, which I have to say, is the perfect lib for testing projects build on Spring.

There is one very useful class called "ReflectionTestUtils". With that, we dont need to put any setter or getter  for autowired fields inside our services and we can partially replace one autowired field/service to our defined mock object. Very convenient and flexible.

Example:

FooDao mockFooDao = Mockito.mock(FooDao.class);
Mockito.when(mockFooDao.foo()).thenReturn("Var");
ReflectionTestUtils.setField(fooService, "fooDao", mockFooDao);

There is one thing we need to pay attention, is, component bean may be wrapped by proxy class when we use annotations from spring like @Transactional or @Aync. In this case, we need to unwrap the bean before we inject the mock object, otherwise, the "ReflectionTestUtils" cannot inject the mock object, because the "fooService" is proxy object, does not have the "fooDao" field inside it!

So we need to unwrap the bean like this:

protected Object unwrapService(Object service) throws Exception {
   if(AopUtils.isAopProxy(service) && service instanceof Advised) {
 Object target = ((Advised) service).getTargetSource().getTarget();
 return target;
   }
   return null;
}
And:
ReflectionTestUtils.setField(unwrapService(fooService), "fooDao", mockFooDao);

In this way, it works perfect.

Enjoy.



8/15/2014

MongoDB escape special characters in a collection

Regarding how to escape dots in map key, please refer to below page:

MongoDB-Escape dots '.' in map key


If we have a collection named "test-core". then we do the operation using script directly, like:
db.test-core.ensureIndex( { userid: 1 } )
The server will throw exception: ReferenceError: core is not defined

This is because the character '-' inside the collection name is not correctly read.

The way to resolve is : 
db["test-core"].ensureIndex( { userid: 1 } )

6/10/2014

Unit Test for Web Services Client who use Spring Template

Recently was doing one client project which will call the web services provided by our core platform. That project is using SpringTemplate to call the Web services. How to write Junit tests for the client code brought up my interest.

After doing some research, the most commonly used method is to use Mock, of course. Some uses Mockito or EasyMock, which are all fine to me. But what I thought was we are using Spring framework anyway, there should be some Mock classes specially for SpringTemplate, and it should already be there. Come on, it's SPRING! In this case, we do not need to write Mock classed for SpringTemplate ourselves. There it is, as I thought, there are plenty of Mock classes sitting there for easy usage.

I made several test cases, although setup the dependencies costs me some time, but that is because of our project's limitation, which is not...you know. All the test cases are easy to implement, and covers well. I'm pretty satisfied. So just gives a record here, because I think it's interesting and the problems that I have encounted may have been faced by other devs too.

Dependencies:

The lib that I use is

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test-mvc</artifactId>
    <version>1.0.0.M2</version>
    <scope>test</scope>
</dependency>

Actually this library has already been integrated into spring-test in Spring version 3.2.X, but our projects are all using Spring 3.1.3.RELEASE. So I have to add it saparately. And be cautious on the dependency conflict(This will really cause trouble...when there are multiple versions of lib exist or same name of class exist but with different content and if the wrong lib/class is loaded, the class not found exception/method not found exception will be thrown, you dont want to see this...).

There are several ways to check the dependencies.

1. Use mvn dependency:tree/mvn dependency:analyze. It will print the structure of the dependencies.
2. You can check on the website: http://mvnrepository.com/artifact/junit/junit/4.10. It will normally tells you which dependencies the jar depends on.
3. You could do Ctrl+shift+T in Eclipse to check how many classes with same name exist in your work space.

The best practice is to make sure there is no dependency conflict. You will never know when it will gives you that exception.

Junit 4.10 and Mockito-all:1.9.5 CANNOT be used, because they all include hamcrest-core:1.1 inside its package which will not work with spring-test-mvc:1.0.0.M2 and there is no way to exclude them. So use Junit 4.11 and Mockito-core:1.9.5 instead.

But, Junit 4.11 and Mockito-core:1.9.5 both have dependency org.hamcrest:hamcrest-core, but they require different version of them. So they are not compatible, and we need to exclude hamcrest-core in both of them and use org.hamcrest:hamcrest-all:1.3

So the dependencies that I use is like:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11/version>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.9.5</version>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test-mvc</artifactId>
    <version>1.0.0.M2</version>
    <scope>test</scope>
</dependency>

So if you are using different versions of the dependencies, check before you put it!

Ok, we can go to the main part.

Code to be tested: 

The code that I was testing is:

    @SuppressWarnings("rawtypes")
    @Override
    public long count(UserRef ref) throws IntegrationException {
        try {
            UserMessageCountCriteria criteria = new UserMessageCountCriteria();
            criteria.setIncludeAgentMessage(true);
            criteria.setOrgName(ref.getLoginOrganizationName());
            criteria.setReadStatus(MessageReadStatusType.unRead);
            criteria.setLabels(new ArrayList<String>());
            criteria.getLabels().add(serviceInfo.getMessageCountLabel());
            criteria.setUserName(ref.getUser().getUserName());
            
            ResponseEntity<Map> responseEntity = null;
            HttpEntity<Map<String, Object>> entity = new HttpEntity<Map<String, Object>>(this.getUserAuthRequestMap(criteria, ref), this.getHttpHeaders());
            responseEntity = restTemplate.exchange(serviceInfo.getServiceMessageCountUrl(), HttpMethod.POST, entity, Map.class);
            
            return (Long) getResponseBody(responseEntity, Long.class);
        } catch (Exception ex) {
            ServiceExceptionHandler.handle(ex, this.getClass().getSimpleName());
        }
        
        return 0L;
    }

protected Object getResponseBody(ResponseEntity<Map> responseEntity, Class clazz) throws IntegrationException {

        if (LOGGER.isDebugEnabled()) {
            try {
                LOGGER.debug(JsonCodec.marshal(responseEntity));
            } catch (Exception ex) {
                throw new IllegalArgumentException(ex);
            }
        }

        if (responseEntity == null) {
            throw new RuntimeException("Response is empty");
        }

        HttpHeaders headers = responseEntity.getHeaders();
        if (headers == null) {
            throw new RuntimeException("Response Header is empty");
        }

        Map<String, Object> responseBody = responseEntity.getBody();
        Map<String, String> status = (Map<String, String>) responseBody.get("status");
        String statusCode = status.get("code");
        String statusMsg = status.get("message");
        if (!StringUtils.equals(statusCode, serviceInfo.getAPISuccessCode())) {
            throw new IntegrationException(statusCode, statusMsg);
        }

        Object object = responseBody.get("content");
        if (clazz == null) {
            return object;
        }
        
        if(object == null)
            return null;

        try {
            String json = JsonCodec.marshal(object);
            if (StringUtils.isEmpty(json)) {
                return null;
            }
            return JsonCodec.unmarshal(clazz, json);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex);
        }
    }

It's pretty basic.

Test Case:

First I create one Base class to setup my Mock server, this class can be extended by all the other service test class.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring/test-portalintegration-servlet.xml" })
public class ServiceClientImplTestBase {

    private static final Logger logger = Logger.getLogger(ServiceClientImplTestBase.class);
    
    protected MockRestServiceServer mockServer;
    
    @Autowired
    private ApplicationContext appContext;
    
    @Autowired
    protected MessageServiceClient messageServiceClient;
    
    @Autowired
    protected ServiceInfo serviceInfo;
    
    @Before
    public void setUp() {
        mockServer = MockRestServiceServer.createServer((RestTemplate) appContext.getBean("restTemplate")); // (1)
        
    }
    
    protected String loadFile(String path){
        
        byte[] content;
        try {
            content = IOUtils.toByteArray(this.getClass().getClassLoader().getResourceAsStream(path));
        } catch (IOException e) {
            logger.error(String.format("failed to load file from classpath: %s", path), e);
            return null;
        }
        
        return new String(content);
    }
}

Then I create one class to test one of the service client.

public class MessageServiceClientImplTest extends ServiceClientImplTestBase{
    @Test
    public void countTest() throws Exception {
        mockServer.expect(RequestMatchers.requestTo(serviceInfo.getServiceMessageCountUrl()))
        .andExpect(RequestMatchers.method(HttpMethod.POST))
        .andRespond(ResponseCreators.withSuccess(loadFile("testdata/sample-response/message-count-success-response.json"), MediaType.APPLICATION_JSON)); // (2)

        UserRef userRef = this.getUserRef("dummy"); //this method just helps me create one dto. Nothing special.
        
        long result = messageServiceClient.count(userRef);
        
        Assert.assertEquals(1, result); // (3)
        mockServer.verify(); // (4)
    }
}

Then you can run the countTest() and it works perfect.

This example is just a very basic Mock, showing you how it works. Depends on your service and client requirement, you need to customize the expect rule to verify the functionality of your client impl.

This is really easy. I like it.

End.






12/05/2013

How to lookup port usage and release the port. (Resolve Issue: Port in-use by unknown process)

When we develop web applications, a very common problem is that sometimes we don't know what is using the current port that we need. And we cann't start our server while getting this kind of exception:


First we could look at the Task Manager to see whether we can get the process which is using this port. But what if we got nothing? Kill every suspects? It's dangerous and sometimes doesn't help at all.
Restart the computer? Ohh that'll work, that's also what I did previously but it's so out!!! It's so painful to restart everything and it's really unneccessary.

Now I get one solution, which is very easy and helpful.

For example, I want to user port "8080" but it's in use by unknown process.

First open the cmd, execute => netstat -aon|findstr "8080"
Then you'll get result like this:
  TCP    0.0.0.0:8080           0.0.0.0:0              LISTENING       87460
  TCP    [::]:8080                 [::]:0                    LISTENING       87460

Now we know who is using the port, the process with PID "87460".

Then in the cmd, we execute => taskkill /pid 87460 -f
Then you'll get result like this:
SUCCESS: The process with PID 87460 has been terminated.

That's it! Then you retart the server, everything will be fine.

12/03/2013

[MongoDB-Escape dots '.' in map key] Resolve org.springframework.data.mapping.model.MappingException: Map key foo.map.key contains dots but no replacement was configured!

Sometimes we need to save map into MongoDB. But the key of the map cannot have dots inside its keys.

If the key has dot(s), by default we'll get this kind of exception:
org.springframework.data.mapping.model.MappingException: Map key foo.bar.key contains dots but no replacement was configured! Make sure map keys don't contain dots in the first place or configure an appropriate replacement!
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.potentiallyEscapeMapKey(MappingMongoConverter.java:622)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeMapInternal(MappingMongoConverter.java:586)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.createMap(MappingMongoConverter.java:517)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:424)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:386)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:373)
    at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:257)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:373)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:451)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:386)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:373)
    at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:257)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:373)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:451)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:386)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter$3.doWithPersistentProperty(MappingMongoConverter.java:373)
    at org.springframework.data.mapping.model.BasicPersistentEntity.doWithProperties(BasicPersistentEntity.java:257)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:373)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:345)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.write(MappingMongoConverter.java:310)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.write(MappingMongoConverter.java:77)
    at org.springframework.data.mongodb.core.MongoTemplate.doSave(MongoTemplate.java:859)
    at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:806)
    at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:794)


Refer to the source code:

Spring => "MappingMongoConverter.java"
/**
     * Potentially replaces dots in the given map key with the configured map key replacement if configured or aborts
     * conversion if none is configured.
     * 
     * @see #setMapKeyDotReplacement(String)
     * @param source
     * @return
     */
    protected String potentiallyEscapeMapKey(String source) {

        if (!source.contains(".")) {
            return source;
        }

        if (mapKeyDotReplacement == null) {
            throw new MappingException(String.format("Map key %s contains dots but no replacement was configured! Make "
                    + "sure map keys don't contain dots in the first place or configure an appropriate replacement!", source));
        }

        return source.replaceAll("\\.", mapKeyDotReplacement);
    }

So the solution is configure the property mapKeyDotReplacement for bean MappingMongoConverter in the spring config file.

For example:
<bean id="mongoMoxydomainConverter" class="org.springframework.data.mongodb.core.convert.MappingMongoConverter">
        <constructor-arg index="0" ref="mongoDbFactory" />
        <constructor-arg index="1">
            <bean class="org.springframework.data.mongodb.core.mapping.MongoMappingContext" />
        </constructor-arg>
        <property name="mapKeyDotReplacement" value="\\+"/>
</bean>

What needs to be mentioned is that, the value that we set for "mapKeyDotReplacement" must follow the regular pattern's rule. If use reserved character, must use '\\' to translate it.