I don't understand why #Transactional doesn't work in reactive. After saving to the repository, I throw an error. But the data still appears in the database.
Spring v 5.3.10
My controller
#GetMapping("/test/save")
fun saveListNotification(): Flux<Notification> {
return service.saveListNotification(listOf(
...
))
}
My service
#Transactional
fun saveListNotification(listOf: List<Notification>): Flux<Notification> {
return Flux.fromIterable(listOf)
.flatMap { notificationRepository.save(it) }
.doOnNext {
if (it.rawJsonHash.equals("4")) throw Exception()
}
}
My repository
interface NotificationRepository : ReactiveCrudRepository<Notification?, UUID?>
I solved my problem
change on the #Transactional(rollbackFor = [Exception::class])
Because #Transactional default will be rolling back on RuntimeException and Error
Related
I am developing a Microservice application in SpringBoot. I am using Spring Cloud gateway there,now since Spring Cloud Gateway uses WebFlux module so,I want to extract username and password inside ServerAuthenticationConverter. But unfortunately flow is getting stuck on subscribe() method.
#Component
public class MyConverter implements ServerAuthenticationConverter {
#Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
String token = exchange.getRequest().getHeaders().getFirst("token");
Map<String,String> credentialMap = new HashMap<>();
if(StringUtils.containsIgnoreCase(exchange.getRequest().getPath().toString(),"/login")){
exchange.getFormData().subscribe(data -> {
for(Map.Entry<String,List<String>> mapEntry : data.entrySet()) {
for (String value : mapEntry.getValue()) {
credentialMap.put(mapEntry.getKey(),value);
log.info("key=" + mapEntry.getKey() + "|value=" + mapEntry.getValue());
}
}
});
User user = new User(credentialMap.get("username"),credentialMap.get("password"));
return Mono.justOrEmpty(new UsernamePasswordAuthenticationToken(user,credentialMap.get("password"), List.of(new SimpleGrantedAuthority("ADMIN"))));
}
else{
if(StringUtils.isNotBlank(token)){
if(StringUtils.contains(token,"Bearer")){
return Mono.justOrEmpty(new MyToken(AuthorityUtils.NO_AUTHORITIES,token.substring(7)));
}else{
return Mono.justOrEmpty(new MyToken(AuthorityUtils.NO_AUTHORITIES,token));
}
}
}
}
throw new IllegalArgumentException("Invalid Access");
}
}
But after printing log statement within subscribe method program flow is getting halted,no exception.
I think subscribe() method is causing some thread level issue.Can someone figureout the problem????
I am trying to use Spring retry for my Spring web application (with JPA, hibernate).
Currently, I have a long transaction chain (marked functions as #Transactional) in my application, intuitively:
start transction -> A() -> B() -> C() -> D() - E() -> commit/rollback
now I want to have D() to be retried if any concurrency failure occurred (marked D() with #retryable), but remain A(), B(), C() in current states
I failed, the function D() is not retried at all and just throwed a concurrency failure error (eg. ObjectOptimisticLockingFailureException)
For me, if I want to do such things in database, I have to make a new transaction try catch block with a cursor while loop to handle retries. I wonder is there a simple way I can handle this "partial" transaction retry in Spring?
An example code would be:
#RestController
public DimensionController()
{
...
#Autowired
private TblDimensionService dimensionService;
...
#PutMapping(...)
public ResponseEntity<TblDimensionDTO> update(#Valid #RequestBody TblDimensionDTO dimensionDTO)
{
...
dimensionService.update(dimensionDTO);
...
}
}
#Transactional //transaction in service level
#Service
public TblDimensionService()
{
...
#Autowired
private RetryService retryService;
...
public TblDimensionDTO update(TblDimensionDTO dimensionDTO) throws InterruptedException
{
if (dimensionDTO.getId() == null)
{
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "not found");
}
TblDimension dimension = findByIdOrElseThrow(dimensionDTO.getId()); //call another function to retrieve entity by id with JPA repository
dimension = retryService.updateEntity(dimension, dimensionDTO);
return tblDimensionMapper.toDto(dimension);
}
...
}
#Transactional //transaction in service level
#Service
public RetryService()
{
...
#Autowired
private TblDimensionRepository dimensionRepository;
...
//I want to only retry this part
//but this retry does not work
#Retryable(value = {ConcurrencyFailureException.class})
public TblDimension updateEntity(TblDimension dimension, TblDimensionDTO dimensionDTO) throws InterruptedException
{
Thread.sleep(3000);
dimension.setHeight(dimension.getHeight() + 1);
Thread.sleep(3000);
return dimensionRepository.save(dimension);
}
...
}
How to intercept and handle errors globally in WebFlux when they are being thrown from WebFilter chain?
It is clear how to handle errors thrown from controllers: #ControllerAdvice and #ExceptionHandler help great.
This approach does not work when an exception is thrown from WebFilter components.
In the following configuration GET /first and GET /second responses intentionally induce exceptions thrown. Although #ExceptionHandler methods handleFirst, handleSecond are similar, the handleSecond is never called. I suppose that is because MyWebFilter does not let a ServerWebExchange go to the stage where GlobalErrorHandlers methods could be applied.
Response for GET /first:
HTTP 500 "hello first" // expected
HTTP 500 "hello first" // actual
Response for GET /second:
HTTP 404 "hello second" // expected
HTTP 500 {"path": "/second", "status": 500, "error": "Internal Server Error" } // actual
#RestController
class MyController {
#GetMapping("/first")
String first(){
throw new FirstException("hello first");
}
}
#Component
class MyWebFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
var path = swe.getRequest().getURI().getPath();
if (path.contains("second")){
throw new SecondException("hello second")
}
}
}
#ControllerAdvice
class GlobalErrorHandlers {
#ExceptionHandler(FirstException::class)
ResponseEntity<String> handleFirst(FirstException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.message)
}
#ExceptionHandler(SecondException::class)
ResponseEntity<String> handleSecond(SecondException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.message)
}
}
Three steps are required to get full control over all exceptions thrown from application endpoints handling code:
Implement org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
Annotate with #ControllerAdvice (or just #Component)
Set #Priority less than 1 to let the custom handler run before the default one (WebFluxResponseStatusExceptionHandler)
The tricky part is where we get an instance implementing
ServerResponse.Context for passing to
ServerResponse.writeTo(exchange, context). I did not find the final
answer, and comments are welcome. In the internal Spring code they always create a new instance of context for each writeTo invocation,
although in all cases (I've manged to find) the context instance is immutable.
That is why I ended up using the same ResponseContextInstance for all responses.
At the moment no problems detected with this approach.
#ControllerAdvice
#Priority(0) /* should go before WebFluxResponseStatusExceptionHandler */
class CustomWebExceptionHandler : ErrorWebExceptionHandler {
private val log = logger(CustomWebExceptionHandler::class)
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
log.error("handled ${ex.javaClass.simpleName}", ex)
val sr = when (ex) {
is FirstException -> handleFirst(ex)
is SecondException -> handleSecond(ex)
else -> defaultException(ex)
}
return sr.flatMap { it.writeTo(exchange, ResponseContextInstance) }.then()
}
private fun handleFirst(ex: FirstException): Mono<ServerResponse> {
return ServerResponse
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("first")
}
private fun handleSecond(ex: SecondException): Mono<ServerResponse> {
return ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue("second")
}
private object ResponseContextInstance : ServerResponse.Context {
val strategies: HandlerStrategies = HandlerStrategies.withDefaults()
override fun messageWriters(): List<HttpMessageWriter<*>> {
return strategies.messageWriters()
}
override fun viewResolvers(): List<ViewResolver> {
return strategies.viewResolvers()
}
}
}
I'm looking at some existing code and wanted to know what happen's in the following scenario with Spring's #Transactional annotation? Consider the following example:
A POST request hits a #Controller annotated with #Transactional:
#ResponseBody
#Transactional
#RequestMapping(value="/send", method=RequestMethod.POST)
public void send(#RequestBody Response response) {
try {
DBItem updatedDbItem = repository.updateResponse(response);
if (updatedDbItem == null){
//some logging
}
} catch(Exception ex) {
//some logging
}
}
The controller calls a non #transactional repository method which sets a value and in turns calls a another #Transactional method:
#Override
public DBItem updateResponse(Response response) {
try {
DBItem dBItem = findResponseById(response.getKey());
if (dBItem != null){
dBItem.setSomeField(response.getValue());
return updateDataBaseItem(response);
}
} catch (Exception ex) {
//some logging
}
return null;
}
The following updateDataBaseItem() method is common and called from other non transactional methods as well as the above method:
#Transactional
#Override
public DBItem updateDataBaseItem(Response response){
try {
DBItem dBItem = em.merge(response);
return dBItem;
} catch (Exception ex) {
//some logging
}
return null;
}
send() => spring detect #transaction with default parameters
actually Propagation setting is REQUIRED and the spring join the exist transaction or create new if none.
repository.updateResponse(..) => No transactions params the method execute within the same transaction already exist
updateDataBaseItem(..) => calling the method in same repository , spring will not recognize the #Transaction annotation because the use of proxy mode, so this method will be executed within the same transaction
a method within the target object calling another method of the target
object, will not lead to an actual transaction at runtime even if the
invoked method is marked with #Transactional
I'm using a grails 1.3.7 and have the following code:
Grails service:
class MyClass {
static transactional = true
#Transactional(noRollbackFor = MyException.class)
public MyObject myMethod(Map map1, Boolean bl1 = false) throws MyException {
//codes
if(...){
throw new MyException("msg")
}
}
MyException:
class MyException extends Exception{
def errors = []
MyException(errors){
super(errors.toString())
this.errors = errors
}
}
When code throws an MyException, I catch the following error: Transaction rolled back because it has been marked as rollback-only
Ps. If I change static transactional = true the error not occurs.
Any Idea?
I you use annotations, you should set
static transactional=false
i.e. invalidate grails' transactional proxy, so that there is no overlap with the proxy from spring AOP
This should work:
#Transactional(noRollbackFor=[FooException, BarException])
def doSomething(...) {
...
}
But remember, if you use transactional annotations, grails automatic transactions does not work in the service where you place that it. You need to set:
#Transactional
class myService(...) {
static transactional = false
...
}