Mohicane Mohicane - 15 days ago 6
Java Question

@Transactional Services

I passed one day and a half looking for answer, but this thing is going to put me crazy!

My teammates and I are working on an project, based on springboot. I work specificaly on the administration part, which is a web administration.

There are mainly three layers on my project: Controllers which use Services which use Repositories.
I want my project work with @Transactional for the Service layer (we made some successful efforts until now to use only annotations for configuration).

But, it seems that it doesn't work: One of my service throws a RuntimeException and no rollback is done. I allready read all the proposition in the others sibling subjects. The only thing, related to my problem, that i'm not sure to do neatly is the contexts configuration. Eventhow, i'm not sure that it's really my problem.

I show you the actual configuration:

@SpringBootApplication
@EnableScheduling
@EnableTransactionManagement
public class Application extends SpringBootServletInitializer {

@Value("${ajp.port}")
private int ajpPort;

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}

@Bean
public EmbeddedServletContainerFactory servletContainer() {
TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory() {};
tomcat.addAdditionalTomcatConnectors(createConnector(ajpPort));
return tomcat;
}

@Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return container -> {

ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/static/401.html");
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/static/404.html");

container.addErrorPages(error401Page, error404Page);
};
}

@Bean
public EmailValidator emailValidator() {
return EmailValidator.getInstance();
}

private static Connector createConnector(int ajpPort) {
Connector connector = new Connector("AJP/1.3");
connector.setPort(ajpPort);
return connector;
}
}


The web config:

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

@Autowired
private RequestProcessingTimeInterceptor requestProcessingTimeInterceptor;

@Autowired
private CertificateInterceptor certificateInterceptor;

@Autowired
private ProfilesAuthorizationInterceptor profilesAuthorizationInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestProcessingTimeInterceptor);
registry.addInterceptor(certificateInterceptor);
registry.addInterceptor(profilesAuthorizationInterceptor);
}

@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}

@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setExposeContextBeansAsAttributes(true);
resolver.setPrefix("/WEB-INF/");
resolver.setSuffix(".jsp");

return resolver;
}

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/admin/css/**").addResourceLocations("/WEB-INF/admin/css/").setCachePeriod(CACHE_PERIOD);
registry.addResourceHandler("/admin/img/**").addResourceLocations("/WEB-INF/admin/img/").setCachePeriod(CACHE_PERIOD);
registry.addResourceHandler("/admin/js/**").addResourceLocations("/WEB-INF/admin/js/").setCachePeriod(CACHE_PERIOD);
registry.addResourceHandler("/admin/plugins/**").addResourceLocations("/WEB-INF/admin/plugins/").setCachePeriod(CACHE_PERIOD);
}

}


A Controler-like:

@RestController
@RequestMapping("/pathA")
public class ControlerA {

@Autowired
public ServiceA serviceA;

@RequestMapping(value = "{id}", method = RequestMethod.GET)
@ResponseStatus(HttpStatus.OK)
public A getA(@PathVariable long id) {
return serviceA.getA(id);
}

}


A Service-like (interface + implémentation):

public interface ServiceA {

A getA(long id);

}

@Service
@Transactional
public class ServiceAImpl implements ServiceA {

@Autowired
public RepositoryA repositoryA;

public A getA(long id) {
(...)
A a = repositoryA.findOne(id);
a.updatesomething(something);
repositoryA.update(a);
doOtherThing(a); //throw RuntimeException
(...)
return a;
}

}


And the Repository:

@Repository
public interface RepositoryA extends JpaRepository<A, Long> {
(...)
}


Here is the configuration of the MySQL database:

# Configuration de la base de donnée
spring.datasource.url=jdbc:mysql://localhost/name_innodb
spring.datasource.username=username
spring.datasource.password=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.testOnBorrow=true
spring.datasource.validationQuery=SELECT 1


I know that repository transaction works by default (I saw it, when a SQLException happen). But in the service layer, nothing happen (cf. the throwing exception line) ; when the exception is thrown, the update is done and not rollback. Then it mean that my @Transactional is ignored.

Edit :
I manage to get a transaction like I want, adding @Transactional on the method
getA(...)
of the Controller. It works, but it's not the place to manage Transaction.

Then my question is: How can I make it work?

Answer

Ok, after some days of brainstorming, I found!

The only reasonnable answer is to take care about your Configuration class. My problem was only a crossover configuration problem which leaded to a DispatcherServlet configuration who caused the mess.

Related Subject: For web MVC Spring app should @Transactional go on controller or service?

Edit: I add some details because it'll be hard to find some information in order to separate context. And I'm still calibrating the configuration because there's no complete and exhaustive information about all the spring's annotations.

You could create parent and child context like this:

@SpringBootApplication
@ComponentScan({"com.mycompany.service", "com.mycompany.interceptors","com.mycompany.manager"})
@PropertySource("file:config/application.properties")
public class ParentConfig{

    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .parent(ParentConfig.class)
                .child(ChildConfig1.class, ChildConfig2.class, ChildConfig3.class, ..., ChildConfigN.class)
                .run(args);
    }

    (...)

}

I'm still wondering why I must add the @PropertySource in order children are aware of property values, why "classpath:path" have not work in @PropertySource, why I have to add a static PropertySourcesPlaceholderConfigurer for using @Value in my children (before I do that, i.e without this hierarchical contexts, every context was aware of the properties)

@Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigInDev() {
    return new PropertySourcesPlaceholderConfigurer();
}

and I'm still playing with annotations in order every configuration work.