How to throw custom exception from transactional S

2019-07-27 02:57发布

问题:

I have this spring service:

@Service
@Transactional
public class ConsorcioServiceImpl implements ConsorcioService {

    ...

    @Autowired
    private ConsorcioRepository consorcioRepository;

    @Override
    public void saveBank(Consorcio consorcio) throws BusinessException {

        try {
            consorcioRepository.save(consorcio);
        }
        catch(DataIntegrityViolationException divex) {
            if(divex.getMessage().contains("uq_codigo")) {
                throw new DuplicatedCodeException(divex);
            }
            else {
                throw new BusinessException(dives); 
            }
        }
        catch (Exception e) {
            throw new BusinessException(e);
        }
    }


}

That service uses this Spring Data repository:

@Repository
public interface ConsorcioRepository extendsCrudRepository<Consorcio, Integer> {


}

I'm calling the service from a spring controller:

@Controller
@RequestMapping(value = "/bank")
public class BancaController {

    @Autowired
    private ConsorcioService consorcioService;

    @RequestMapping(value="create", method=RequestMethod.POST)
    public ModelAndView crearBanca(@Valid BancaViewModel bancaViewModel, BindingResult bindingResult,
                                   RedirectAttributes redirectAttributes) {
        ModelAndView modelAndView;

        MessageViewModel result;
        try {

            consorcioService.saveBank(bancaViewModel.buildBanca());
            result = new MessageViewModel(MessageType.SUCESS);
            redirectAttributes.addFlashAttribute("messageViewModel", result);
            modelAndView = new ModelAndView("redirect:/banca/crear");
            return modelAndView;
        } catch (Exception e) {
            result = new MessageViewModel(MessageType.ERROR);
            modelAndView = new ModelAndView("crear-bancas");
            modelAndView.addObject("messageViewModel", result);
            return modelAndView;
        }
}

But the exception I get in the controller is: org.springframework.transaction.TransactionSystemException: Could not commit JPA transaction; nested exception is javax.persistence.RollbackException: Transaction marked as rollbackOnly instead of the DuplicatedCodeException I throw in the service. I need to identify the type of exception so I can give a custom friendly user message.

回答1:

also your DuplicatedCodeException , BusinessException should be runtime exception , or add for method saveBank :

@Transactinal(rolbackFor={BusinessException.class,DuplicatedCodeException.,class })

in other case spring will not rollback transaction.

from Spring documentation:

While the EJB default behavior is for the EJB container to automatically roll back the transaction on a system exception (usually a runtime exception), EJB CMT does not roll back the transaction automatically on an application exception (that is, a checked exception other than java.rmi.RemoteException). While the Spring default behavior for declarative transaction management follows EJB convention (roll back is automatic only on unchecked exceptions), it is often useful to customize this.



回答2:

Just add catch (TransactionSystemException tse) before catch (Exception e) branch and then extract your exception with getOriginalException().

try {
    consorcioService.saveBank(bancaViewModel.buildBanca());
    result = new MessageViewModel(MessageType.SUCESS);
    redirectAttributes.addFlashAttribute("messageViewModel", result);
    modelAndView = new ModelAndView("redirect:/banca/crear");
    return modelAndView;
} catch (TransactionSystemException tse) {
    final Throwable ex = tse.getOriginalException();
    if (ex instanceof DuplicatedCodeException) {
        // DuplicatedCodeException
    }
} catch (Exception e) {
    result = new MessageViewModel(MessageType.ERROR);
    modelAndView = new ModelAndView("crear-bancas");
    modelAndView.addObject("messageViewModel", result);
    return modelAndView;
}


回答3:

That happens because your exception is wrapped in RollbackException as Throwable cause. In it's turn RollbackException is also a cause of TransactionSystemException.

You can build global exception handler to catch and customize all exceptions as you wish:

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT) 
    @ExceptionHandler(TransactionSystemException.class)
    public ModelAndView handleDupilatedCode(HttpServletRequest req, TransactionSystemException ex) {
        // Build you exception body here
        Throwable e = ex.getOriginalException();
        if(e instanceof DuplicatedCodeException)
          // throw
        // Or build custom exception as you want
        ModelAndView mav = new ModelAndView();
        mav.addObject("exception", e);
        mav.addObject("url", req.getRequestURL());
        mav.setViewName("error");
        return mav;
    }
}

@ControllerAdvice is available since Spring 3.2

Also you can use @ExceptionHandler at the @Controller level, or create this handling in abstract-controller if you have one as a super class of all your controllers.

public class FooController {

    //...
    @ExceptionHandler({ CustomException1.class, CustomException2.class })
    public void handleException() {
        //
    }
}

There are some other approaches, for full reference please follow the:

Spring IO: Exception handling in Spring MVC

Baeldung: Exception handling with Spring