2
votes

I'm using ModelMapper 0.7.4 (latest version) in a Play 2.4.2 (latest version) framework application. Play 2.4 has an internal Google Guice dependency injection solution build into it, and our application is a manually bridged from Guice to a Spring Framework dependency injection solution, to get Play 2.4 to work with Spring. So communication flows from Play to Guice to Spring.

Things (dependency injection with Spring) seem to work fine, but when a random Java class is changed in the test development environment, Play automatically reloads the class or webapp. This reloading work fine in general, but it seems to cause problems with ModelMapper, when ModelMapper is used as a Spring Bean in this Play setup. (But couldn't reproduce the problem when bypassing the Guice-Spring bridge by manually creating a Spring container within the setup and then contacting ModelMapper as a Spring bean.)

The error is:

Caused by: org.modelmapper.ConfigurationException: ModelMapper configuration errors:

1) Failed to configure mappings

1 error
        at org.modelmapper.internal.Errors.throwConfigurationExceptionIfErrorsExist(Errors.java:241) ~[modelmapper-0.7.4.jar:na]
        at org.modelmapper.internal.ExplicitMappingBuilder.build(ExplicitMappingBuilder.java:207) ~[modelmapper-0.7.4.jar:na]
        at org.modelmapper.internal.TypeMapImpl.addMappings(TypeMapImpl.java:72) ~[modelmapper-0.7.4.jar:na]
        at org.modelmapper.internal.TypeMapStore.getOrCreate(TypeMapStore.java:101) ~[modelmapper-0.7.4.jar:na]
        at org.modelmapper.ModelMapper.addMappings(ModelMapper.java:93) ~[modelmapper-0.7.4.jar:na]
        at configs.AppConfig.modelMapper(AppConfig.java:109) ~[na:na]
        at configs.AppConfig$$EnhancerBySpringCGLIB$$b19a8688.CGLIB$modelMapper$2(<generated>) ~[na:na]
        at configs.AppConfig$$EnhancerBySpringCGLIB$$b19a8688$$FastClassBySpringCGLIB$$1f1c1728.invoke(<generated>) ~[na:na]
        at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228) ~[spring-core-4.2.0.RELEASE.jar:4.2.0.RELEASE]
        at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:318) ~[spring-context-4.2.0.RELEASE.jar:4.2.0.RELEASE]
        at configs.AppConfig$$EnhancerBySpringCGLIB$$b19a8688.modelMapper(<generated>) ~[na:na]
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_45]
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_45]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_45]
        at java.lang.reflect.Method.invoke(Unknown Source) ~[na:1.8.0_45]
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162) ~[spring-beans-4.2.0.RELEASE.jar:4.2.0.RELEASE]
        ... 64 common frames omitted
Caused by: java.lang.ClassCastException: project.entities.User$$EnhancerByModelMapper$$f1b8f0f9 cannot be cast to project.entities.User
        at configs.AppConfig$1.configure(AppConfig.java:106) ~[na:na]
        at org.modelmapper.PropertyMap.configure(PropertyMap.java:383) ~[modelmapper-0.7.4.jar:na]
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_45]
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_45]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_45]
        at java.lang.reflect.Method.invoke(Unknown Source) ~[na:1.8.0_45]
        at org.modelmapper.internal.ExplicitMappingBuilder.build(ExplicitMappingBuilder.java:195) ~[modelmapper-0.7.4.jar:na]
        ... 78 common frames omitted

This only happens on a class reload, and not when no reloading is done. Also, the problem does NOT occur when modelMapper.addMappings(aPropertyMap) is not used. The Spring AppConfig class looks like this:

@Configuration
public class AppConfig {
    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();

        // BEGIN: WITHOUT THE FOLLWOING CODE, it works fine
        PropertyMap<CreateUserFormDTO, User> userMap = new PropertyMap<CreateUserFormDTO, User>() {
            @Override
            public void configure() {
                map().setPassword(source.getPassword1());
            }
        };
        modelMapper.addMappings(userMap);
        // END


        return modelMapper;
    }
}

The ModelMapper is accessed using a plain Spring @Autowire injection. The User and CreateUserFormDTO class are just POJOs.

What could the problem be?

2

2 Answers

4
votes

The problem is exactly what Jean said. According to Spring-Dev Tools:

Applications that use spring-boot-devtools automatically restart whenever files on the classpath change. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes.

When code change occurs, ModelMapper Library is already loaded into classpath due to base classloader. According to Spring-Dev tools:

The restart technology provided by Spring Boot works by using two classloaders. Classes that do not change (for example, those from third-party jars) are loaded into a base classloader. Classes that you are actively developing are loaded into a restart classloader. When the application is restarted, the restart classloader is thrown away and a new one is created. This approach means that application restarts are typically much faster than “cold starts”, since the base classloader is already available and populated.

By default, any open project in your IDE is loaded with the “restart” classloader, and any regular .jar file is loaded with the “base” classloader.

So, we need to customize the Restart Classloader and put modelmapper.jar into restart classloader instead of base classloader.

To do so,

  1. Create a META-INF/spring-devtools.properties file in your project to load it into project's classpath.

  2. add this line in spring-devtools.properties file

    restart.include.modelmapper=/modelmapper-.*.jar
    
  3. Clean & Build the full project. Now modelmapper library will always be reloaded each time there is a file change.

Links:

  1. ModelMapper Issue: 254
  2. Customizing the Restart Classloader
3
votes

The issue here is that ModelMapper creates an in memory cache of mappers (see TypeMapStore.getOrCreate ) and creates a builder class which returns a class compatible with your model (User$$EnhancerByModelMapper$$f1b8f0f9 extends User)

All works fine as long as you don't reload. However, once you reload the play app, the app class loader is discarded, therefore a new instance of the User class is loaded in the new classloader. User$$EnhancerByModelMapper$$f1b8f0f9 still extends the previous class and fails to cast to the new class.

I don't have a solution using ModelMapper. I guess you would have to fix ModelMapper so that when a ClassCastException is thrown by a cached mapper, the TypeMapstore discards it and tries to recreate a new mapper.

In the end the project I used to work on when I commented on the classloading issues on ModelMapper replaced ModelMapper with Selma (http://www.selma-java.org/) which worked fine in our tests (My contract ended at that point and I don't know if they kept selma in the end)