Rollback All DB Commits in Nested Transactions in Spring - java

I have the following service layout of nested transactions:
#Component
public class Main implements RPCInterface {
#Autowired
private ServiceA serviceA;
#Autowired
private ServiceB serviceB;
#Autowired
private ServiceC serviceC;
#Override
#Transactional (value="txManager", propagation=Propagation.REQUIRED, rollbackFor={ExceptionOne.class, ExceptionTwo.class, ExceptionThree.class})
public void outerMethod() throws ExceptionO {
try {
serviceA.methodA();
serviceB.methodB();
serviceC.methodC();
} catch (ExceptionOne e) {
throw new ExceptionO(e.getMessage, e);
} catch (ExceptionTwo e) {
throw new ExceptionO(e.getMessage, e);
} catch (ExceptionThree e) {
throw new ExceptionO(e.getMessage, e);
}
}
}
#Service
public class ServiceA implements SA {
#Autowired
private ServiceA1 serviceA1;
#Override
public void methodA() {
serviceA1.methodA1();
}
}
#Service
public class ServiceA1 implements SA1 {
#Autowired
private ServiceDBTable1 serviceDBTable1;
#Autowired
private ServiceA1A serviceA1A;
#Transactional
#Override
public void methodA1() {
serviceDBTable4.callToMapper4();
serviceA1A.methodA1A();
}
}
#Service
#Transactional (value="txManager", propagation=Propagation.REQUIRED)
public class ServiceA1A implements SA1A {
#Autowired
private ServiceDBTable2 serviceDBTable2;
#Override
public void methodA1A() {
serviceDBTable1.callToMapper1();
}
}
#Service
public class ServiceB implements SB {
#Autowired
private ServiceDBTable3 serviceDBTable3;
#Override
#Transactional (value="txManager", propagation=Propagation.REQUIRED)
public void methodB() {
serviceDBTable3.callToMapper3();
}
}
#Service
public class ServiceC implements SC {
#Override
public void methodC() throws ExceptionThree {
// code that throws ExceptionThree
}
}
I need to make all the DB calls within ServiceA and ServiceB nested calls to rollback when ServiceC#methodC() throws an exception (or any of them for that matter that throws an exception -- ServiceA or ServiceB).
I tried to make Main#outerMethod transactional with REQUIRED propagation, but it seems like the database commits are not being rolled back. I have even specified the specific classes with rollbackFor but the commits persist. Does anyone know how to fix this issue?

What I did to make it work was to migrate ServiceB.methodB() and ServiceC.methodC() calls to ServiceA.methodA(), and make methodA() #Transactional while throwing all my exceptions from methodA() and rollback based on those three exceptions (my logic actually allowed me to do that):
#Component
public class Main implements RPCInterface {
#Autowired
private ServiceA serviceA;
#Override
public void outerMethod() throws ExceptionO {
try {
serviceA.methodA();
} catch (ExceptionOne e) {
throw new ExceptionO(e.getMessage, e);
} catch (ExceptionTwo e) {
throw new ExceptionO(e.getMessage, e);
} catch (ExceptionThree e) {
throw new ExceptionO(e.getMessage, e);
}
}
}
#Service
public class ServiceA implements SA {
#Autowired
private ServiceA1 serviceA1;
#Autowired
private ServiceB serviceB;
#Autowired
private ServiceC serviceC;
#Override
#Transactional (value="txManager", propagation=Propagation.REQUIRED, rollbackFor={ExceptionOne.class, ExceptionTwo.class, ExceptionThree.class})
public void methodA() throw ExceptionOne, ExceptionTwo, ExceptionThree {
serviceA1.methodA1();
serviceB.methodB();
serviceC.methodC();
}
}
#Service
public class ServiceA1 implements SA1 {
#Autowired
private ServiceDBTable1 serviceDBTable1;
#Autowired
private ServiceA1A serviceA1A;
#Transactional
#Override
public void methodA1() {
serviceDBTable4.callToMapper4();
serviceA1A.methodA1A();
}
}
#Service
#Transactional (value="txManager", propagation=Propagation.REQUIRED)
public class ServiceA1A implements SA1A {
#Autowired
private ServiceDBTable2 serviceDBTable2;
#Override
public void methodA1A() {
serviceDBTable1.callToMapper1();
}
}
#Service
public class ServiceB implements SB {
#Autowired
private ServiceDBTable3 serviceDBTable3;
#Override
#Transactional (value="txManager", propagation=Propagation.REQUIRED)
public void methodB() {
serviceDBTable3.callToMapper3();
}
}
#Service
public class ServiceC implements SC {
#Override
public void methodC() throws ExceptionThree {
// code that throws ExceptionThree
}
}

Since there is no code presented it is hard to know for sure.
However, transactions only work when methods are public. Private methods are not proxied and hence transaction support for them is not there.
Read through Declarative Transations - Spring Docs for more details.
Please post code if you still are struggling for getting better help.

Related

Google Guice with Junit and Mockito

how can i inject EntityManager(jpa) with Mockito?
I wanna bind Mockito.spy(UserService.class) to guice injector.
but UserService.class has EntityManager for query execution.
When installing 'JunitServiceModule' in guice injector, EntityManager is not found.
See below for error details
com.google.inject.CreationException: Unable to create injector, see the following errors:
1) Error in custom provider, java.lang.NullPointerException
while locating com.google.inject.persist.jpa.JpaPersistService
while locating javax.persistence.EntityManager
My code is below.
(The test code just made the error situation similar.)
(actually 'EntityManager' is located in UserRepository... It's just example!)
(#Transactional is belong to guice)
public class UserServiceTest {
#Inject
private UserService userService;
#Before
public void setUp() {
Injector injector = new TestBuilder().init();
injector.initMembers(this);
Mockito.doReturn(10).when(userService).getEntityCount(UserEntity.class);
}
#Test
public void test() {
assertEquals(10, userService.getEntityCount(UserEntity.class));
}
}
public class TestBuilder {
public TestBuilder() {
}
public Injector init() {
Injector injector = Guice.createInjector(
new TestDBInjectModule("test"),
new JunitServiceModule()
);
}
}
public class TestDBInjectModule extends AbstractModule {
private String unitName;
public TestDBInjectModule(String unitName) {
this.unitName = unitName;
}
#Override
protected void configure() {
install(new JpaPersistModule(unitName));
bind(JpaInitializer.class).asEagerSingleton();
}
#Singleton
private static class JpaInitializer {
#Inject
public JpaInitializer(final PersistService persistService) {
persistService.start();
}
}
}
public class JunitServiceModule extends AbstractModule {
#Override
protected void configure() {
bind(UserService.class).toInstance(Mockito.spy(UserService.class));
}
}
public class UserService {
#Inject
private EntityManager entityManager;
public UserService {} // <-- throw NullPointerException!!! since EntityManager
#Transactional
public void addUser(User user) {
return entityManager.persist(user);
}
public Number getCount() {
return entityManager.createQuery("select count(*) from user", Number.class).getSingleResult();
}
}

Mockito NullPointerException - Not recognizing repository

I'm having trouble figuring out why Mockito is throwing a NullPointerException when I'm telling the mock to return true.
Here is my JUnit Test:
public class PizzaValidatorTest {
private Pizza meatPizza;
private PizzaValidator validator = new PizzaValidator();
#MockBean
private IngredientRepository ingredientRepository;
#MockBean
private PizzaSizeRepository pizzaSizeRepository;
#Before
public void setUp() throws Exception {
meatPizza = new Pizza();
validator = new PizzaValidator();
}
#Test
public void validateValid() {
when(ingredientRepository.existsById(any())).thenReturn(true);
when(pizzaSizeRepository.existsById(any())).thenReturn(true);
assertTrue(validator.validate(meatPizza));
}
}
The PizzaValidator class is implemented below:
#Controller
public class PizzaValidator implements Validator<Pizza> {
#Autowired
IngredientRepository ingredientRepository;
#Autowired
PizzaSizeRepository pizzaSizeRepository;
#Override
public boolean validate(Pizza entity) {
return validatePizza(entity);
}
private boolean validatePizza(Pizza pizza) {
return validPizzaSize(pizza) && validIngredients(pizza);
}
private boolean validPizzaSize(Pizza pizza) {
return pizzaSizeRepository.existsById(pizza.getSizeDesc().getId());
}
private boolean validIngredients(Pizza pizza) {
for (Ingredient ingredient : pizza.getIngredients()) {
if (!ingredientRepository.existsById(ingredient.getId())) {
return false;
}
}
return true;
}
}
For some reason it seems like Mockito isn't connecting the mock repository with my class repository, but I can't figure out why. Any help is appreciated. Thanks.
You should not create the PizzaValidator using new keyword, you should #Autowire it in the test
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
public class PizzaValidatorTest {
private Pizza meatPizza;
#Autowire
private PizzaValidator validator;
#MockBean
private IngredientRepository ingredientRepository;
#MockBean
private PizzaSizeRepository pizzaSizeRepository;
#Before
public void setUp() throws Exception {
meatPizza = new Pizza();
}
#Test
public void validateValid() {
when(ingredientRepository.existsById(any())).thenReturn(true);
when(pizzaSizeRepository.existsById(any())).thenReturn(true);
assertTrue(validator.validate(meatPizza));
}
}

How to use service that inited by ApplicationListener ConfigurableApplicationContext

I use springMvc and mybatis.Copy a BaseService and BaseServiceImpl from a project.
public interface BaseService<Record, Example> {
//init mybatis mapper
void initMapper();
}
BaseServiceImpl
public abstract class BaseServiceImpl<Mapper, Record, Example> implements BaseService<Record, Example> {
public Mapper mapper;
#Override
public void initMapper() {
this.mapper = SpringContextUtil.getBean(getMapperClass());
}
public Class<Mapper> getMapperClass() {
return (Class<Mapper>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
EntityServiceImpl
#Service
#Transactional
#BaseService
public class EntityServiceImpl extends BaseServiceImpl<EntityMapper, Entity, EntityExample> implements EntityService {
}
I init BaseService use this code.
#Component
public class ApplicationContextListener implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger LOGGER = Logger.getLogger(ApplicationContextListener.class);
#Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// root application context
if(null == contextRefreshedEvent.getApplicationContext().getParent()) {
LOGGER.debug(">>>>> spring init finished <<<<<");
// call BaseService initMapper method
ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext)contextRefreshedEvent.getApplicationContext();
Map<String, Object> baseServices = applicationContext.getBeansWithAnnotation(BaseService.class);
for(Object service : baseServices.values()) {
try {
Method initMapper = service.getClass().getMethod("initMapper");
initMapper.invoke(service);
} catch (Exception e) {
LOGGER.error("init BaseService initMapper failed", e);
e.printStackTrace();
}
}
}
}
}
when project start,entityServiceImpl.initMapper method have been called
#Override
public void initMapper() {
this.mapper = SpringContextUtil.getBean(getMapperClass());
}
but when i use the entityService in conroller.Can't use the entityService which have been init by the ApplicationListener.
This is how i try to use the entityService
#Conroller
public class LoginController {
#Resource
EntityService EntityService1;
#Autowired
EntityService EntityService2;
EntityService EntityService3 = (EntityService)SpringContextUtil.getBean(EntityService.class);
}
Use Idea debug,i can find a EntityService with a not null mapper.
But in controller all of three EntityService mapper is null.
how could i use the EntityService with mapper inited by ApplicationListener ?
Spring uses chain of beanPostProcessors and it can modify your initial class by CGLIB or dynamic proxy. So this.mapper = SpringContextUtil.getBean(getMapperClass()); might not works as you expect

Autowiring in concrete classes of an abstract class than implements an interface

I am using Spring boot and I have the following Task model
public class Task {
private String name;
private TaskType type; // ManualTask, AutomatedTask
private boolean completed;
//....other fields
//getters and setters
}
A controller
#Controller
#RequestMapping("/api/task")
public class TaskController {
#Autowired
private TaskService taskService;
#GetMapping("/{taskId}/handle")
public String handle(Model model, #PathVariable("taskId") Long taskId) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
try {
Task task = taskService.handle(taskId);
model.addAttribute("task", task);
} catch (Exception e) {
return "errorpage";
}
return "successpage";
}
}
I have an interface
public interface TaskService {
Task findById(Long taskId);
Task handleTask(Long taskId) throws ClassNotFoundException, InstantiationException, IllegalAccessException;
}
An abstract class implements the interface:
#Service
public abstract class TaskServiceImpl implements TaskService {
#Autowired
private TaskRepository taskRepository;
private static final String PATH_OF_CLASS = "com.task.service.impl";
protected abstract Task doTypeSpecificTask(Long taskId);
#Override
public Task findById(Long taskId) {
return taskRepository.findById(taskId).get();
}
#Override
public Task handleTask(Long taskId) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
Task task = findById(taskId);
TaskServiceImpl service = getHandlerService(task);
return service.doTypeSpecificTask(taskId);
}
private TaskServiceImpl getHandlerService(Task task) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
String serviceClassName = PATH_OF_CLASS.concat(".").concat(task.getTaskType().getName()).concat("Service");
Class<?> serviceClass = Class.forName(serviceClassName);
if (!TaskServiceImpl.class.isAssignableFrom(serviceClass)) {
throw new RuntimeException("Service class " + serviceClassName + " did not implements " + TaskServiceImpl.class.getName());
}
Object serviceObject = serviceClass.newInstance();
TaskServiceImpl service = (TaskServiceImpl) serviceObject;
return service;
}
}
And concrete services that extend the abstract class
#Service
#Primary
public class ManualTaskService extends TaskServiceImpl {
#Autowired
private TaskRepository taskRepository;
#Autowired
private ManualTaskHandlerService manualTaskHandlerService;
#Override
protected Task doTypeSpecificTask(Long taskId) {
Task task = findById(taskId);
manualTaskHandlerService.handleManualTask(task);
task.setCompleted(true);
return taskRepository.save(task);
}
}
#Service
public class AutomatedTaskService extends TaskServiceImpl {
#Autowired
private TaskRepository taskRepository;
#Autowired
private AutomatedTaskHandlerService automatedTaskHandlerService;
#Override
protected Task doTypeSpecificTask(Long taskId) {
Task task = findById(taskId);
automatedTaskHandlerService.handleAutomatedTask(task);
task.setCompleted(true);
return taskRepository.save(task);
}
}
public interface TaskRepository extends JpaRepository<Task, Long> {
}
The ManualTaskService or AutomatedTaskService is selected dynamically based on the type of task on runtime.
Now, without the #Primary, I get the following error:
Field taskService in com.test.controller.TaskController required a single bean, but 2 were found:
- manualTaskService
- automatedTaskService
Action:
Consider marking one of the beans as #Primary, updating the consumer to accept multiple beans, or using #Qualifier to identify the bean that should be consumed
With #Primary set in ManualTaskService, doTypeSpecificTask in ManualTaskService works but in AutomatedTaskService it fails because of automatedTaskHandlerService.handleAutomatedTask(task). Also calls to taskRepository from AutomatedTaskService fail.
I've tried using #Qualifier as well as defining all #Autowired in the abstract class as protected but nothing works. What am I doing wrong?
You should have different names to each Qualifier:
#Autowired
#Qualifier("manualTaskService")
private TaskServiceImpl manualTaskService;
#Autowired
#Qualifier("automatedTaskService")
private TaskServiceImpl automatedTaskService;
Which is defined in services:
#Service("manualTaskService")
public class ManualTaskService extends TaskServiceImpl {
#Service("automatedTaskService")
public class AutomatedTaskService extends TaskServiceImpl {
I solved the issue by using the factory pattern as mentioned in this link (thanks to #user7294900 for providing the link)
I completely removed the abstract class TaskServiceImpl. Instead I created two new interfaces ManualTaskService and AutomatedTaskService both extending TaskService interface
public interface ManualTaskService extends TaskService {
}
public interface AutomatedTaskService extends TaskService {
}
Then I created a TaskServiceFactory
#Component
public class TaskServiceFactory {
#Autowired
private ManualTaskService manualTaskService;
#Autowired
private AutomatedTaskService automatedTaskService;
public TaskService getService(TaskType type) throws Exception {
switch (type) {
case MANUAL_TASK:
return manualTaskService;
case AUTOMATED_TASK:
return automatedTaskService;
default:
throw new Exception("Unrecognized task type");
}
}
}
Next I created implementations for both ManualTaskService and AutomatedTaskService
#Service
public class ManualTaskServiceImpl implements ManualTaskService {
#Autowired
private TaskRepository taskRepository;
#Autowired
private ManualTaskHandlerService manualTaskHandlerService;
#Override
public Task findById(Long taskId) {
return taskRepository.findById(taskId).get();
}
#Override
public Task handleTask(Long taskId) throws Exception {
Task task = findById(taskId);
manualTaskHandlerService.handleManualTask(task);
task.setCompleted(true);
return taskRepository.save(task);
}
}
#Service
public class AutomatedTaskServiceImpl implements AutomatedTaskService {
#Autowired
private TaskRepository taskRepository;
#Autowired
private AutomatedTaskHandlerService automatedTaskHandlerService;
#Override
public Task findById(Long taskId) {
return taskRepository.findById(taskId).get();
}
#Override
public Task handleTask(Long taskId) throws Exception {
Task task = findById(taskId);
automatedTaskHandlerService.handleAutomatedTask(task);
task.setCompleted(true);
return taskRepository.save(task);
}
}
Finally I updated the controller to get the task type from the user and then use the TaskServiceFactory to get the correct service instance based on the type
#Controller
#RequestMapping("/api/task")
public class TaskController {
#Autowired
private TaskServiceFactory taskServiceFactory;
#PostMapping("/{taskId}/handle")
public String handle(Model model, #PathVariable("taskId") Long taskId, HttpServletRequest request) throws Exception {
try {
TaskType type = TaskType.valueOf(request.getParameter("type"));
Task task = taskServiceFactory.getService(type).handleTask(taskId, request);
model.addAttribute("task", task);
} catch (Exception e) {
return "errorpage";
}
return "successpage";
}
}

Spring TransactionTemplate called from a #Transaction method

I've a particular flow calls in my Spring app.
I've a repository, and I call the method sell() as first call.
#Service
#Transactional
public class ServiceRepositoryImpl implements ServiceRepository {
#Inject
private SellRepository sellRepository;
#Override
public long sell(long id)
....
....
....
Sell sell = new Sell();
...
sellRepository.save(sell);
}
and my SellService:
#Service
public class SellRepositoryImpl implements SellRepository {
#PersistenceContext
private EntityManager entityManager;
#Inject
SellHelperRepository sellHelperRepository;
#Inject
private TransactionTemplate transactionTemplate;
#Override
public <S extends Sell> S save(S sell) throws Exception {
transactionTemplate.execute(new TransactionCallback<S>() {
#Override
public S doInTransaction(TransactionStatus status) {
try {
...
entityManager.persist(sell);
} catch (Throwable e) {
log.error("", e);
status.setRollbackOnly();
}
return vendita;
}
});
sellHelperRepository.createTickets(sell.getId());
return vendita;
}
this is my sellHelperRepository:
#Service
#Transactional
public class SellHelperRepositoryImpl implements SellHelperRepository {
#PersistenceContext
private EntityManager entityManager;
#Override
#Async
public void createTickets(long sellID) {
Sell sell = entityManager.find(Sell.class, sellID);
try{
...
Ticket t = new Ticket();
ticketService.save(t);
}catch(Throwable e){
sell.setStatus("CANCELED");
}
}
and in the end my ticketService:
#Service
#Transactional
public class TicketRepositoryImpl implements TicketRepository {
#PersistenceContext
protected EntityManager entityManager;
#Override
#Transactional(propagation = Propagation.REQUIRES_NEW)
public Biglietto save(Ticket ticket) throws Exception {
entityManager.persist(ticket);
}
The problem is that in this chain of calls when I'm on SellRepositoryImpl, the object sell is not persisted asap I'm out of the transactiontemplate but it should be! So when I enter in SellHelperRepositoryImpl and I try to search the sell it doesn't find it!
This particular structure is needed because these methods are used also from others repository.
I created many junit tests case to check if all works fine; for the rest of tests all works fine, a part for this particular chain of calls.
I think I'm missing something....
Thanks
the problem with your code is that you are using the #Async on the createTickets method , which forces Spring to execute it in the different thread with the "fresh" transaction, so you need to flush/commit first transaction which was opened in the SellRepositoryImpl class.
So you can go in three ways
Remove the #Async from the createTickets method
Change the Transaction Isolation Level from DEFAULT to READ_UNCOMMITTED in SellHelperRepositoryImpl
#Service
#Transactional(isolation=Isolation.READ_UNCOMMITTED)
public class SellHelperRepositoryImpl implements SellHelperRepository {
#PersistenceContext
private EntityManager entityManager;
#Override
#Async
public void createTickets(long sellID) {
Sell sell = entityManager.find(Sell.class, sellID);
try{
...
Ticket t = new Ticket();
ticketService.save(t);
}catch(Throwable e){
sell.setStatus("CANCELED");
}
}
Flush the manually managed transaction in save(S sell), see following code snippet.
#Override
public <S extends Sell> S save(S sell) throws Exception {
transactionTemplate.execute(new TransactionCallback<S>() {
#Override
public S doInTransaction(TransactionStatus status) {
try {
...
entityManager.persist(sell);
entityManager.getTransaction().commit();
status.flush();
} catch (Throwable e) {
log.error("", e);
status.setRollbackOnly();
}
return vendita;
}
});
sellHelperRepository.createTickets(sell.getId());
return vendita;
}

Categories

Resources