Spring-boot AOP advice with CompletableFuture - spring-boot

I try to log with AOP for a CompletableFuture controller. #Before advice is working OK. But with #AfterReturning it is not working correctly in the exception case. I also tried with #AfterThrowing, which is not working either. When an exception occurs, my #AfterReturning advice also is not triggered and #AfterThrowing is never reached.
How can I use an AOP advice with exceptions in this case?
JController:
#RestController
public class JController extends BaseExceptionHandler {
#GetMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public CompletableFuture<BaseResponse> search() {
final CompletableFuture result = asyncProcessor.process(request);
return result;
}
}
BaseExceptionHandler:
public class BaseExceptionHandler {
#ExceptionHandler(Exception.class)
public ResponseEntity handleException(final Exception exception) {
return new ResponseEntity<>(new ErrorResponse(Message.INTERNAL_SERVER_ERROR, StatusCode.UNKNOWN_ERROR), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
AOP Class
#AfterReturning(value = "execution(* com.xxx.xxx.controller.*.*(..))", returning = "result")
public void outgoingSuccess(final JoinPoint joinPoint, final CompletableFuture result) {
LOGGER.debug("After Returning method: " + joinPoint.getTarget().getClass().getSimpleName());
}
#AfterThrowing("execution(* com.xxx.xxx.controller.*.*(..))")
public void outgoingError(final JoinPoint joinPoint) {
LOGGER.debug("After Throwing method: " + joinPoint.getTarget().getClass().getSimpleName());
}

Related

#ControllerAdvice even by setting the highest precedense for RestControllers not working as it should

I am using SpringBoot 5.
I want to catch all exception thrown from RestController and display customize message format.
I have simplified the situation like below:
The RestController
#RestController
#RequestMapping("/test")
public class TestRestController {
#Autowired
private TestService testService;
#GetMapping("/{id}")
public ResponseEntity<?> findById(#PathVariable int id) {
Test test = testService.find(id);
if(department!=null){
throw CustomException();
}
return new ResponseEntity<>(test, HttpStatus.OK);
}
}
The ControllerAdvice Exception handler:
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RestExceptionHandler.class);
#ExceptionHandler(value= {CustomException.class})
public ResponseEntity<ErrorDetail> handleCustomException(CustomException exception,
HttpServletRequest request) {
ErrorDetail errorDetail = new ErrorDetail();
errorDetail.setTimeStamp(Instant.now().getEpochSecond());
errorDetail.setStatus(HttpStatus.NOT_FOUND.value());
errorDetail.setTitle("Resource Not Found");
errorDetail.setDetail(exception.getMessage());
errorDetail.setDeveloperMessage(exception.getClass().getName());
return new ResponseEntity<>(errorDetail, null, HttpStatus.NOT_FOUND);
}
}
The problem it is that RestExceptionHandler is not working, it is not catching the exception and returning the modified error message format. It seem my RestExceptionControllerClass is not overriding the GlobalExceptionHandler. I don't know why this is happening because I have marked the RestExceptionHandler with the highest precedense. I will appriciate any guidence to debug this problem.
#ControllerAdvice
public class RestExceptionHandler {
#ResponseStatus(HttpStatus.NOT_FOUND)
#ResponseBody
#ExceptionHandler(CustomException.class)
public ErrorDetail handleCustomException(CustomException exception) {
ErrorDetail errorDetail = new ErrorDetail();
errorDetail.setTimeStamp(Instant.now().getEpochSecond());
errorDetail.setTitle("Resource Not Found");
errorDetail.setDetail(exception.getMessage());
errorDetail.setDeveloperMessage(exception.getClass().getName());
return errorDetail;
}
}
Refer this link to know more about exception handling for REST API
https://www.baeldung.com/exception-handling-for-rest-with-spring
Change your RestExceptionHandler class like below
#RestControllerAdvice
public class RestExceptionHandler {
#ResponseStatus(HttpStatus.NOT_FOUND)
#ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorDetail> handleCustomException(CustomException exception) {
ErrorDetail errorDetail = new ErrorDetail();
errorDetail.setTimeStamp(Instant.now().getEpochSecond());
errorDetail.setTitle("Resource Not Found");
errorDetail.setDetail(exception.getMessage());
errorDetail.setDeveloperMessage(exception.getClass().getName());
return new ResponseEntity<>(errorDetail, null, HttpStatus.NOT_FOUND);
}
}
And you also need to extends RuntimeException in your CustomException class
The problem was that another exception was thrown before my CustomException. In the service call , there was part of code that threw an exception that i did not expect. So my RestExceptionHandler couldn't catch the exception because it didn't have a method to handle that exception and so the GlobalExceptionHandler was handling that exception. After fixing the code and made sure that the CustomExeption was thrown everything worked as it should. The RestExceptionHandler handled exception and printed the custom message.

Spring Webflux ErrorHandling - #RestControllerAdvice with #ExceptionHandler or DefaultErrorAttributes?

In Spring Webflux what is the prefered way of Exception Handling?
#RestControllerAdvice comes from Spring MVC whereas DefaultErrorAttributes comes from Spring Webflux.
However, in Spring Webflux someone could use #RestControllerAdvice. What would be the advantages/disadvantages?
#RestControllerAdvice
#RestControllerAdvice
public class ControllerAdvice
{
#ExceptionHandler(Throwable.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
public Mono<Map<String, Object>> exceptions(Throwable e)
{
return Mono.just(Map.of("message", "bad"));
}
}
Extend DefaultErrorAttributes
#Component
public class ErrorAttributes extends DefaultErrorAttributes
{
#Override
public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace)
{
var ex = getError(request);
var attributes = new LinkedHashMap<String, Object>();
attributes.put("status", HttpStatus.BAD_REQUEST.value());
attributes.put("message", "bad");
return attributes;
}
}
I want to stay in the reactive world, so I tend more towards DefaultErrorAttributes (which plays well with DefaultErrorWebExceptionHandler in Webflux). However, in #RestControllerAdvice I could also use Mono.just(...).
It is same. Like WebMvc.
#RestControllerAdvice
public class ControllerAdvice {
#ExceptionHandler(AnyException.class)
public Mono<EntityResponse<YourModel>> example(AnyException exception) {
return EntityResponse.fromObject(new YourModel()).status(HttpStatus.NOT_FOUND).build();
}
}
In Spring Webflux in case functional routes declaration, you can also implement your own ExceptionHandler instead of DefaultErrorWebExceptionHandler:
class SystemErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
#Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable throwable) {
return super.handle(exchange, throwable)
// debug, process
.contextWrite(...);
}
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
// for all routs
return route(all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> error = getErrorAttributes(request, ErrorAttributeOptions.of());
Throwable t = this.getError(request);
// map exception on response
return ServerResponse.status(status).body(...);
}
}
Then use your implementation of AbstractErrorWebExceptionHandler in the spring configuration with #AutoConfigureBefore(WebFluxAutoConfiguration.class)

Spring Aspect on Converter

I created simple Aspect an annotation for measuring time of execution of annotated method.
When I annotate method of a simple Spring Bean, inject bean, and run it like bean.annotatedMethod(), everything works fine.
However, when I annotate convert() method on Spring Converter, annotation is ignored. I'm guessing the reason is that convert() is called internally by Spring's ConversionService, and somehow Aspects are not respected. Is there any way to get it to work?
Annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface LogExecTime {
}
Aspect, which I register in Spring:
#Aspect
#Component
public class LogTimeAspect {
#Around(value = "#annotation(annotation)")
public Object LogExecutionTime(final ProceedingJoinPoint joinPoint, final LogExecTime annotation) throws Throwable {
final long startMillis = System.currentTimeMillis();
try {
System.out.println("Starting timed operation");
final Object retVal = joinPoint.proceed();
return retVal;
} finally {
final long duration = System.currentTimeMillis() - startMillis;
System.out.println("Call to " + joinPoint.getSignature() + " took " + duration + " ms");
}
}
}
This works fine:
#Component
public class Operator {
#LogExecTime
public void operate() throws InterruptedException {
System.out.println("Performing operation");
Thread.sleep(1000);
}
}
#Bean
protected Void test(Operator o) {
o.operate();
return null;
}
But here, annotation is ignored:
public class SampleConverter implements Converter<SourceType, ResultType> {
#Override
#LogExecTime
public ImmutableData convert(#Nonnull ClassifiedEvent result) {
...
}
}
ConversionService conversionService;
...
conversionService.convert(source, ResultType.class));
Solved by comment of #EssexBoy, my converter was not a spring managed bean.

Issues integration testing a Spring MVC controller method that calls an async Spring service method

I have the following Spring MVC controller method:
#RequestMapping(value = "/sendPasswordReset", method = RequestMethod.POST, produces = "text/html")
public String sendPasswordResetInformation(#ModelAttribute #Validated({ ValidationGroups.PasswordReset.class }) PasswordResetInfo passwordResetInfo,
BindingResult bindingResult, Model model, final RedirectAttributes redirectAttributes, Locale locale) throws InterruptedException, ExecutionException {
if (preferenceService.isEmailAvailable(passwordResetInfo.getEmail())) {
bindingResult.rejectValue("email", "controller.preference.email_not_in_system");
}
if (bindingResult.hasErrors()) {
model.addAttribute("passwordResetInfo", passwordResetInfo);
return "preference/sendPasswordReset";
}
redirectAttributes.addFlashAttribute("flashMessage", messageSource.getMessage("controller.preference.password_reset_info_sent", null, locale));
Future<Void> future = preferenceService.sendPasswordResetInfo(passwordResetInfo.getEmail());//.get();//TODO is ".get()" ugly?
future.get();//NPE HERE!!
return "redirect:/preference/sendPasswordReset";
}
Here is the implementation of sendPasswordResetInfo:
#Async
#Override
public Future<Void> sendPasswordResetInfo(String email) {
Assert.hasText(email);
Member member = memberRepository.findByEmail(email);
try {
mailerService.doMailPasswordResetInfo(member);
return new AsyncResult<Void>(null);
} catch (MessagingException | MailSendException e) {
log.error("MessagingException | MailSendException", e);
throw new MailerException("MessagingException | MailSendException");
}
}
Here is how I am trying to integration test the controller method:
#Test
public void sendPasswordResetShouldHaveNormalInteractions() throws Exception {
when(preferenceService.isEmailAvailable(anyString())).thenReturn(Boolean.FALSE);
mockMvc.perform(post("/preference/sendPasswordReset")//
.param("email", VALID_EMAIL))//
.andExpect(redirectedUrl("/preference/sendPasswordReset"))//
.andExpect(flash().attributeExists("flashMessage"))//
.andExpect(flash().attributeCount(1));
verify(preferenceService).sendPasswordResetInfo(eq(VALID_EMAIL));
reset(preferenceService);
}
I systematically get a NullPointerException (in tests) in the controller method because the future object is null here:
future.get()
However the controller method runs fine when I use the app (outside of tests).
I have tried using a sync task executor as follows (to no avail):
#Profile(Profiles.TEST)
#Configuration
#EnableAsync
public class FakeAsyncConfiguration implements AsyncConfigurer {
#Override
public Executor getAsyncExecutor() {
SyncTaskExecutor taskExecutor = new SyncTaskExecutor();
return taskExecutor;
}
#Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}
My questions are:
Why is the Future object always null during the integration tests and
How can I ensure it is not null during my integration tests?

#ExceptionHandler not being invoked

After some time researching and trying different things I still cannot get my #ExceptionHandler to be invoked in my jUnit integration test. Please, help me understand why?
#RequestMapping(value = "/someURL", method = RequestMethod.POST)
public ModelAndView batchUpload(#RequestBody final String xml, #RequestParam boolean replaceAll)
throws IOException, URISyntaxException, SAXException, ParserConfigurationException, InvocationTargetException, IllegalAccessException, UnmarshallingFailureException
{
StreamSource source = new StreamSource(new StringReader(xml));
DomainClass xmlDomainClass;
try
{
xmlDomainClass = (DomainClass) castorMarshaller.unmarshal(source);
}
catch (UnmarshallingFailureException me)
{
// some logging. this gets executed
throw me;
}
.
#ExceptionHandler(Throwable.class)
public ModelAndView handleUnmarshallingExceptions(Throwable th)
{
// never executes anything in here
return new ModelAndView( /*some parameters */ );
}
Spring: RESTful controllers and error handling helped me, my problem was the lack of this:
#Component
public class AnnotatedExceptionResolver extends AnnotationMethodHandlerExceptionResolver{
public AnnotatedExceptionResolver() {
setOrder(HIGHEST_PRECEDENCE);
}
}

Resources