Custom platform transaction manager in spring - spring

I'm trying to implement custom transactional cache in a spring boot application. I've created my own implementation of AbstractPlatformTransactionManager and some unit tests, which show transactions are working as expected. However the real application ignores my transaction manager - it`s methods are never called. What I'm doing wrong? Thank you.
Transaction manager implementation:
#Component
public class CacheTransactionManager extends AbstractPlatformTransactionManager{
#Override
protected Object doGetTransaction() throws TransactionException {
...
}
...
}
Cache transaction configuration:
#Configuration
#EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class CacheTransactionConfiguration {
#Bean(name = "cacheTransactionManager")
public PlatformTransactionManager cacheTransactionManager() {
return new CacheTransactionManager();
}
}
Custom transactional annotation (I've tried also without this, but no difference):
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Transactional(value = "cacheTransactionManager", rollbackFor = Exception.class)
public #interface CacheTransactional {
}
Cache service:
#Component
public class CacheService {
#CacheTransactional
public void add(Object o){
...
}
}
Working JUnit test:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = TestApplication.class)
#Configuration
#EntityScan(...)
#IntegrationTest
#TransactionConfiguration(defaultRollback = false)
public class CacheTransactionManagerTest {
#Autowired
private CacheService cacheService;
#Test
#CacheTransactional
public void transactionTest(){
cacheService.add(new Object());
}
}
Not working wicket application main class (ignores cacheTransactionManager):
#Configuration("MyApplication")
#EnableAutoConfiguration
#EntityScan(...)
#EnableJpaRepositories(...)
#EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
#ComponentScan(...)
#ImportResource({...})
public class MyApplication extends AuthenticatedWebApplication {
...
}
My env: Java 8, Spring Boot 1.2.1, Spring 4.1.4, Spring data JPA 1.7.2, Hibernate 4.3.7, Apache Tomcat 8.0.15, Wicket 6.17.0

I found out some new facts:
when I remove AdviceMode.ASPECTJ from #EnableTransactionManagement on CacheTransactionConfiguration, transactions begin to work, but propagation of transaction is ignored - nested call from one #CacheTransactional method to another #CacheTransactional methods always creates new transaction. Same behavior in JUnit test and real application.
when AdviceMode.ASPECTJ is on CacheTransactionConfiguration setted, but I remove #CacheTransactional annotation from junit test, transaction stops working also in juint (in test body is a #CacheTransaction method called, so there should be a transaction created).
application log contains this entry:
o.s.c.a.ConfigurationClassBeanDefinitionReader isOverriddenByExistingDefinition:290 - Skipping bean definition for [BeanMethod:name=cacheTransactionManager,declaringClass=x.y.z.CacheTransactionConfiguration]: a definition for bean 'cacheTransactionManager' already exists. This top-level bean definition is considered as an override.
So I can get this working, but without propagation...

For propagation, you need to tell Spring's #Transactional what propagation mode to apply. You can define several tx annotations, each inherit from #Transactional, but with a different propagation mode.

Related

How to avoid a second Instantiation of a spring bean in child test context

I created an Embedded Sftp server bean for my integration tests, i hooked the startup and the shutdown of the server respectively with the afterPropertiesSet and destroy life cycles
public class EmbeddedSftpServer implements InitializingBean, DisposableBean {
//other class content
#Override
public void afterPropertiesSet() throws Exception {
//Code for starting server here
}
#Override
public void destroy() throws Exception {
//Code for stopping server here
}
}
here my config class
#TestConfiguration
public class SftpTestConfig {
#Bean
public EmbeddedSftpServer embeddedSftpServer() {
return new EmbeddedSftpServer();
}
//Other bean definitions
}
Now when i inject the bean in my test classes like the following :
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = SftpTestConfig .class)
class ExampleOneIT {
#Autowired
private EmbeddedSftpServer embeddedSftpServer;
}
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = SftpTestConfig .class)
class ExampleTwoIT {
#Autowired
private EmbeddedSftpServer embeddedSftpServer;
}
#SpringBatchTest
#ContextConfiguration(classes = SftpTestConfig .class)
class ExampleThreeIT {
#Autowired
private EmbeddedSftpServer embeddedSftpServer;
}
And i run all the test classes simultaneously, i found out that for the test classes annotated with #ExtendWith(SpringExtension.class), it's the same context that is used (which is understandable since i guess spring cache it) and therefore the bean lifecycle methods are not executed again, but to my surprise, for the class annotated with #SpringBatchTest i noticed that the life cycle hooks of the bean are executed again! Which is a behavior that is not convenient since i want the application context to start the server one time for all tests and close it at the end of those tests (which is the case if i use only #ExtendWith(SpringExtension.class) for all my test classes).
N.B. : I need to use #SpringBachTest for my ExampleThreeIT test class.
I think you are hitting this issue: https://github.com/spring-projects/spring-batch/issues/3940 which has been fixed in v4.3.4/4.2.8. Upgrading to one of these versions should fix your issue.

ConstraintValidator dependency injection leads to ValidationException when being validated at class level

I've encountered an unexpected behaviour when using dependency injection in a ConstraintValidator which is getting evaluated at class level.
Entity class:
#Entity
#ValidDemoEntity
public class DemoEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
Validation annotation:
#Target({ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = {DemoEntityValidator.class})
public #interface ValidDemoEntity {
String message() default "{some.demo.validator.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Validator:
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity> {
private DemoEntityRepository demoEntityRepository;
public DemoEntityValidator(DemoEntityRepository demoEntityRepository) {
this.demoEntityRepository = demoEntityRepository;
}
#Override
public void initialize(ValidDemoEntity constraintAnnotation) {
}
#Override
public boolean isValid(DemoEntity demoEntity, ConstraintValidatorContext constraintValidatorContext) {
return true;
}
}
Test class:
#SpringBootTest
public class ValidatorInstantiationTest {
private Validator validator;
#Before
public void setUp() throws Exception {
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}
#Test
public void shouldInitiateAndCallDemoEntityValidator() {
DemoEntity demoEntity = new DemoEntity();
validator.validate(demoEntity);
}
}
Validating the entity leads to:
javax.validation.ValidationException: HV000064: Unable to instantiate ConstraintValidator: com.example.demo.DemoEntityValidator.
and further down the stack trace:
Caused by: java.lang.NoSuchMethodException: com.example.demo.DemoEntityValidator.<init>()
which indicates that Hibernate tried to initiate the the class instead of letting Spring take care of that.
The strange thing about this is that dependency injection works fine for validations applied on field level.
The code is available at GitHub.
The exception says that there is no default constructor because Hibernate Validator tries to instantiate your validator.
You have to use Spring.
1 Make your validator a Spring Bean:
#Component
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity> {
2 Inject the Spring provided validator and use the SpringRunner for executing your tests:
#SpringBootTest
#RunWith(SpringRunner.class)
public class ValidatorInstantiationTest {
#Autowired
private Validator validator;
#Test
public void shouldInitiateAndCallDemoEntityValidator() {
DemoEntity demoEntity = new DemoEntity();
validator.validate(demoEntity);
}
}
1 Make your validator a Spring Bean
This site states:
The Spring framework automatically detects all classes which implement the ConstraintValidator interface. The framework instantiates them and wires all dependencies like the class was a regular Spring bean.
Which clearly works for validations applied on field level.
Nevertheless I've updated the code.
DemoEntityValidator is now a Spring component:
#Component
public class DemoEntityValidator implements ConstraintValidator<ValidDemoEntity, DemoEntity>
I've changed the test to:
#SpringBootTest
#RunWith(SpringRunner.class)
public class ValidatorInstantiationTest {
#Autowired
private DemoEntityRepository demoEntityRepository;
#Test
public void shouldInitiateAndCallDemoEntityValidator() {
DemoEntity demoEntity = new DemoEntity();
demoEntityRepository.save(demoEntity);
}
}
To make the usecase clearer, but the test still leads to the same exception.
Adding an empty constructor to the class DemoEntityValidator disables the error.
I think you answer is here:
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring
You need to declare a LocalValidatorFactoryBean in your configuration class and it will just work.
From the documentation:
By default, the LocalValidatorFactoryBean configures a
SpringConstraintValidatorFactory that uses Spring to create
ConstraintValidator instances. This lets your custom
ConstraintValidators benefit from dependency injection like any other
Spring bean.
And an example from the same place:
import javax.validation.ConstraintValidator;
public class MyConstraintValidator implements ConstraintValidator {
#Autowired;
private Foo aDependency;
...
}
And this is how I declared that bean in a #Configuration annotated class:
/**
* Provides auto-wiring capabilities for validators Checkout: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring
*/
#Bean
public LocalValidatorFactoryBean validatorFactoryBean() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(validationMessageSource());
return bean;
}
There's nothing wrong with your validator class. I got this working by making two changes to the test configuration:
1. Run test with Spring
In order to have Spring manage your beans, you need to run your test with a test runner that sets up Spring. You can specify the test runner class using junit's #RunWith-annotation:
#RunWith(SpringRunner.class)
#SpringBootTest
public class ValidatorInstantiationTest { ... }
2. Inject a Spring managed validator bean
Since you're using Spring Boot, you can inject a Spring managed validator – it's already configured. This way, Spring will handle the initiation of your DemoEntityValidator.
#RunWith(SpringRunner.class)
#SpringBootTest
public class ValidatorInstantiationTest {
#Autowired
private Validator validator;
...
}
This is all that is needed. You should not annotate your DemoEntityValidator with #Component or similar.
Note that you need to provide Spring with a data source, since SpringRunner will set up a context based on your Spring Boot setup (I'm guessing it includes spring-boot-starter-data-jpa in your case). The easiest way to get going is just to put an in-memory DB such as h2 on the classpath.

Is there a way to include a spring component in a WebMvcTest

Given production code classes:
#RestController
#RequiredArgsConstructor
public class MyController {
private final MyValidator validator;
// annotations relating to request mapping excluded for brevity
public void test(#Valid #RequestBody final MyParams params) {
// do stuff
}
#InitBinder
#SuppressWarnings("unused")
protected void initBinder(final WebDataBinder binder) {
binder.setValidator(validator);
}
}
and
#Component
#RequiredArgsConstructor
public class MyValidator implements Validator {
...
#Override
public void validate(final Object target, final Errors errors) {
// custom validation
}
}
and finally test code:
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
// tests
}
I encounter the error:
NoSuchBeanDefinitionException: No qualifying bean of type 'MyValidator' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
I think the error is fair enough. I've annotated the test as a WebMvcTest, which I believe has excluded #Component beans. This is intentional and desired (from the perspective that I am only wanting to test the "web layer", not the whole context - it just so happens I need a component which is related/used only in the controllers)
My question, therefore, is: how can one explicitly include a component like a validator in the test context for a web test?
My environment is java version "10.0.2" 2018-07-17, spring boot 1.5.16.RELEASE.
There are two ways to solve this.
Using #SpringBootTest and #AutoConfigureMvc instead of #RunWith(SpringRunner.class) and #WebMvcTest.
#SpringBootTest
#AutoConfigureMvc
public class MyControllerTest {
}
Creating a #TestConfiguration class that injects the 'MyValidator' bean as:
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#TestConfiguration
static class TestConfig {
#Bean
MyValidator getMyValidator(){
return new MyValidator();
}
}
// tests
}
More on this can be found here : https://mkyong.com/spring-boot/spring-boot-how-to-init-a-bean-for-testing/
There are two ways to test the web layer
first.
#RunWith(SpringRunner.class)
#SpringBootTest
public class MyControllerTest {
#Autowired
private MyController myController;
}
The #SpringBootTest annotation tells Spring Boot to go and look for a
main configuration class (one with #SpringBootApplication for
instance), and use that to start a Spring application context.
A nice feature of the Spring Test support is that the application
context is cached in between tests, so if you have multiple methods in
a test case, or multiple test cases with the same configuration, they
only incur the cost of starting the application once. You can control
the cache using the #DirtiesContext annotation.
Secondly, if you want to use the #WebMvcTest(MyController.class)
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#MockBean
private MyValidator validator;
}
But this validator is a fake, so you have to customize it for testing.
See this link for more details https://spring.io/guides/gs/testing-web/
I cannot recommend it as a standard practice but if you do need an instance of a dependency in your Web MVC tests (for example in legacy code) you can add them into the spring context using #SpyBean annotation.
Real methods of that class will be called during the test and you can verify them if needed similarly to the beans annotated with #MockBean
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#SpyBean
private MyValidator validator
}

How to test domain event in Spring Boot #DataJpaTest?

I'm using Spring Data JPA domain event as described in https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.domain-events. The event listener is marked with #Service. It is working perfectly when I run it, but I can't make it works when testing it using #DataJpaTest. If I replaced this with #SpringBootTest, the test run perfectly.
I know that #DataJpaTest will not load #Service. But even if I add #Import(MyService.class), this will still not work. My question how do I test domain event with #DataJpaTest without loading the full context as in #SpringBootTest?
It turned out that #SpringBootTest added #Transactional to the test. This causes the domain event listener to be not executed since it is still in transaction.
This is my solution.
// TestConfig
#TestConfiguration
public class TestConfig {
#Bean
public MyService myService() {
return new MyService()
}
}
// Domain Event Test
#RunWith(SpringRunner.class)
#Import({TestConfig.class})
#Transactional
#DataJpaTest
public class DomainEventTest {
#Autowired
private TestRepository repository;
public void domainEventTest() {
Entity entity = new Entity();
repository.save(entity);
}
}

Spring Data JPA Transaction - No Transaction in progress - Spring Data Neo4j

I think i'm missing something obvious. Iam trying to make a entity persist into a database via a JUnit Test case, however it doesnt seem to be persisting due to no active transaction.
Configuration:
#Configuration
#EnableTransactionManagement
public class TransactionConfig {
#Inject
private EntityManagerFactory entityMangerFactory;
#Bean
public JpaTransactionManager transactionManager(){
return new JpaTransactionManager(entityMangerFactory);
}
TestCase:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = { Application.class })
#ActiveProfiles(CommonConstants.SPRING_PROFILE_TEST)
#IntegrationTest
#WebAppConfiguration
public class UserRepositoryTest {
#Inject
UserRepository userRepo;
#Test
#Rollback(false)
#Transactional("transactionManager")
public void addUser() {
User user = BootstrapDataPopulator.getUser();
userRepo.save(user);
System.out.println(user.getId()); //Successfully outputs the id generate by hibernate
assertNotNull(user.getId());
}
}
^This test case executed successfully however I do not see any entiites persisted in the database as expected.
When I change the from userRepo.save(user) to userRepo.saveAndFlush(user) I get the following exception:
javax.persistence.TransactionRequiredException: no transaction is in progress
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.checkTransactionNeeded(AbstractEntityManagerImpl.java:1171)
at org.hibernate.jpa.spi.AbstractEntityManagerImpl.flush(AbstractEntityManagerImpl.java:1332)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Spring Boot AutoConfiguration Report: http://dumptext.com/YcGaR3Wf
Names of all Spring Beans Initialized: http://dumptext.com/jp9O6l8v
I am using Spring Data Neo4j (SDN) in my application as well. SDN comes with a default class Neo4jConfiguration which has:
#Bean(name = {"neo4jTransactionManager","transactionManager"})
#Qualifier("neo4jTransactionManager")
public PlatformTransactionManager neo4jTransactionManager() throws Exception {
return new JtaTransactionManagerFactoryBean(getGraphDatabaseService()).getObject();
}
The "transactionManager" overrides the bean defined in my TransactionConfig class. Hence the reason no Entity transaction was in progress. I stopped using the SDN class Neo4jConfiguration. This resolved my issue.

Resources