How do I capture all exceptions of Spring/Spring Boot 2.x? - java

I built my Spring boot 2.x application using this demo:
https://spring.io/guides/gs/spring-boot/
The problem I'm having is that when there's an exception in Spring/Spring boot, it is printed to standard output. I don't want that. I want to capture them and do other processing such as logging them. I can capture the exceptions of my code but I can't capture the exceptions of Spring/Spring boot. Therefore, how do I capture all exceptions of Spring/Spring Boot 2.x so I can handle them? Is there an easy way to do this, like a generic exception catcher? Can someone show me some code?
My Code:
1. Example.java
package example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
#SpringBootApplication
public class Example extends SpringBootServletInitializer {
public static void main(String[] args) throws Exception {
SpringApplication.run(Example.class, args);
}
}
2. ExampleController.java
package example;
import org.springframework.stereotype.Controller;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.ModelAndView;
#Controller
public class ExampleController {
#GetMapping ("/example")
public ModelAndView example()
{
MyData data= new MyData();
data.setName("Example");
data.setVersion("1.0");
ModelAndView model = new ModelAndView("page");
model.addObject("page", data);
return model;
}
}
3. GeneralExceptionHandler.java
package example;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
#ControllerAdvice
class GeneralExceptionHandler {
#ExceptionHandler(Exception.class)
public void handleException() {
System.out.println("Exception handler");
}
}
4. MyData.java
package example;
import lombok.Data;
#Data
public class MyData {
private String name;
private String version;
}
5. page.jsp
<!DOCTYPE html>
<%# taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
<form:form id="myForm" class="form-horizontal"
modelAttribute="page">
<form:label id="name" class="control-label" path="name" for="name">
${page.name}</form:label>
<!-- Error introduced on purpose to force exception. "version123" should be just "version" -->
<form:label id="version" class="control-label" path="version" for="version">
${page.version123}</form:label>
</form:form>
</html>

You can use #ExceptionHandler annotation to catch a specific Exception, to do that you can simply annotate a method inside your controller with #ExceptionHandler and provide it with a specific exception, example :
#ExceptionHandler(DataIntegrityViolationException.class)
public void handleException(){
// do some thing here
}
The limite of this way of doing is that it will handle only exceptions thrown by the #RequestMapping where the #ExceptionHandler is declared. To avoid this limitation you can use a Controller advice which allows you to use exactly the same exception handling techniques but apply them across the whole application, example using controller advice:
#ControllerAdvice
class GeneralExceptionHandler {
#ExceptionHandler(DataIntegrityViolationException.class)
public void handleException() {
// Do some thing here
}
}
hint : if you want to catch all checked exceptions you can use #ExceptionHandler(Exception.class)

You can use AOP with pointcut on all public method in Controller.
I use BaseController as base class for all my Controllers.
#RestController
#RequestMapping(value = "/api/application")
public class ApllicationController extends ApiController {
//class body with method
}
Then add AOP handling all exceptions throw from public methods in Controller.
#Aspect
#Configuration
public class AOP_ApiController {
#Pointcut("execution (public * *(..))")
private void anyPublicMethod() {}
#Pointcut("execution (* api.ApiController+.*(..))")//here package and class name as base class
private void methodOfApiController() {}
#AfterThrowing(value="methodOfApiController() && anyPublicMethod()", throwing="exception")
public void afterThrowingExceptionFromApiController(JoinPoint joinPoint, Exception exception) throws Exception {
ApiController controller = getController(joinPoint);
String methodName=getMethodName(joinPoint);
logException(exception, controller, methodName);
throw exception;
}
private ApiController getController(JoinPoint joinPoint) {
return (ApiController) joinPoint.getTarget();
}
private String getMethodName(JoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
private void logException(Exception ufeException, ApiController controller, String methodName) {
//log exception as you want
}
}

Related

X-Ray trace doesn't shows inner method call

I'm new to aws x-ray and trying to use x-ray with AOP based approach in a springboot application. I was able to get the traces in the aws console, but traces doesn't show inner method call method2() details. Am I missing anything here.
Controller class
import com.amazonaws.xray.spring.aop.XRayEnabled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/xray")
#XRayEnabled
public class XrayController {
#GetMapping(value = "/method1")
public String method1() {
return method2();
}
public String method2() {
return "Hello";
}
}
Aspect Class
import com.amazonaws.xray.entities.Subsegment;
import com.amazonaws.xray.spring.aop.BaseAbstractXRayInterceptor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Map;
#Aspect
#Component
public class XRayInspector extends BaseAbstractXRayInterceptor {
#Override
protected Map<String, Map<String, Object>> generateMetadata(ProceedingJoinPoint proceedingJoinPoint, Subsegment subsegment) {
return super.generateMetadata(proceedingJoinPoint, subsegment);
}
#Override
#Pointcut("#within(com.amazonaws.xray.spring.aop.XRayEnabled) && (bean(*Controller) || bean(*Service) || bean(*Client) || bean(*Mapper))")
public void xrayEnabledClasses() {}
}
When I hit http://localhost:8080/xray/method1 endpoint,
AWS Xray Console doesn't show method2() details
As I later understood with the use of M. Deinum's comment AOP prevents you to wrap a function if it is in the caller is in the same class of the callee.
Read more on AOP https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-understanding-aop-proxies
You can use a work around using below self injection
public class Test {
#Autowire
private Test test;
public void method1(){
.........
test.method2();
...........
}
public void method2(){
...............
...............
}
}
notice here we call the method 2 by test.method2(); instead of this.method2()
I referred below answer also for this solution Spring AOP not working for method call inside another method

#ControllerAdvice annotated class is not catching the exception thrown in the service layer

I'm trying to centralize the error handling in my spring boot app. Currently i'm only handling one potential exception (NoSuchElementException), this is the controller advice:
import java.util.NoSuchElementException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
#ControllerAdvice
public class ExceptionController {
#ExceptionHandler(NoSuchElementException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public DispatchError dispatchNotFound(NoSuchElementException exception) {
System.out.println("asdasdasd");
return new DispatchError(exception.getMessage());
}
}
And here's the service which throws the exceptions:
import java.util.List;
import com.deliveryman.deliverymanapi.model.entities.Dispatch;
import com.deliveryman.deliverymanapi.model.repositories.DispatchRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
#Service
public class DaoService {
#Autowired
DispatchRepository dispatchRepo;
public Dispatch findByShipmentNumber(long shipmentNumber) {
return dispatchRepo.findById(shipmentNumber).orElseThrow();
}
public List<Dispatch> findByUser(String user, String status) {
if(status == null) {
return dispatchRepo.findByOriginator(user).orElseThrow();
} else {
return dispatchRepo.findByOriginatorAndStatus(user, status).orElseThrow();
}
}
public Dispatch createDispatch(Dispatch dispatch) { //TODO parameter null check exception
return dispatchRepo.save(dispatch);
}
}
The problem is that once I send a request for an inexistent resource, the json message shown is the spring's default one. It should be my custom json error message (DispatchError).
Now, this is fixed by adding a #ResponseBody to the exception handler method but the thing is that I was using an old code of mine as reference, which works as expected without the #ResponseBody annotation.
Can someone explain me why this is happening?
Either annotate your controller advice class with #ResponseBody
#ControllerAdvice
#ResponseBody
public class ExceptionController {
...
or replace #ControllerAdvice with #RestControllerAdvice.
Tested and verified on my computer with source from your controller advice.
From source for #RestControllerAdvice
#ControllerAdvice
#ResponseBody
public #interface RestControllerAdvice {
...
Hence, #RestControllerAdvice is shorthand for
#ControllerAdvice
#ResponseBody
From source doc for #ResponseBody
Annotation that indicates a method return value should be bound to the
web response body. Supported for annotated handler methods.
Alternative using #ControllerAdvice only:
#ControllerAdvice
public class ExceptionHandlerAdvice {
#ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<DispatchError> dispatchNotFound(NoSuchElementException exception) {
return new ResponseEntity<>(new DispatchError(exception.getMessage()), HttpStatus.NOT_FOUND);
}
}
I do have a theory on what's going on in your old app. With the advice from your question, and the error handler below, I can create a behaviour where the DispatchError instance appears to be returned by advice (advice is executed), but is actually returned by error controller.
package no.mycompany.myapp.error;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
#RestController
#RequiredArgsConstructor
public class ErrorHandler implements ErrorController {
private static final String ERROR_PATH = "/error";
private final ErrorAttributes errorAttributes;
#RequestMapping(ERROR_PATH)
DispatchError handleError(WebRequest webRequest) {
var attrs = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE));
return new DispatchError((String) attrs.get("message"));
}
#Override
public String getErrorPath() {
return ERROR_PATH;
}
}
Putting an implementation of ErrorController into classpath, replaces Spring's BasicErrorController.
When reinforcing #RestControllerAdvice, error controller is no longer in effect for NoSuchElementException.
In most cases, an ErrorController implementation that handles all errors, in combination with advice exception handlers for more complex exceptions like MethodArgumentNotValidException, should be sufficient. This will require a generic error DTO like this
package no.mycompany.myapp.error;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.Map;
#Data
#NoArgsConstructor
#JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiError {
private long timestamp = new Date().getTime();
private int status;
private String message;
private String url;
private Map<String, String> validationErrors;
public ApiError(int status, String message, String url) {
this.status = status;
this.message = message;
this.url = url;
}
public ApiError(int status, String message, String url, Map<String, String> validationErrors) {
this(status, message, url);
this.validationErrors = validationErrors;
}
}
For ErrorHandler above, replace handleError with this
#RequestMapping(ERROR_PATH)
ApiError handleError(WebRequest webRequest) {
var attrs = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.of(ErrorAttributeOptions.Include.MESSAGE));
return new ApiError(
(Integer) attrs.get("status"),
(String) attrs.get("message"), // consider using predefined message(s) here
(String) attrs.get("path"));
}
Advice with validation exception handling
package no.mycompany.myapp.error;
import org.springframework.http.HttpStatus;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
#RestControllerAdvice
public class ExceptionHandlerAdvice {
private static final String ERROR_MSG = "validation error";
#ExceptionHandler(MethodArgumentNotValidException.class)
#ResponseStatus(HttpStatus.BAD_REQUEST)
ApiError handleValidationException(MethodArgumentNotValidException exception, HttpServletRequest request) {
return new ApiError(
HttpStatus.BAD_REQUEST.value(),
ERROR_MSG,
request.getServletPath(),
exception.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
// mergeFunction handling multiple errors for a field
(firstMessage, secondMessage) -> firstMessage)));
}
}
Related config in application.yml
server:
error:
include-message: always
include-binding-errors: always
When using application.properties
server.error.include-message=always
server.error.include-binding-errors=always
When using Spring Data JPA, consider using the following setting for turning off a second validation.
spring:
jpa:
properties:
javax:
persistence:
validation:
mode: none
More information on exception handling in Spring:
https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc (revised April 2018)
https://www.baeldung.com/exception-handling-for-rest-with-spring (December 31, 2020)

How to test custom ExceptionHandlers for annotations referenced by #ControllerAdvice?

I am working on a service broker based on the Spring Cloud Open Service Broker framework. I'm using a custom ExceptionHandler which inherits from ServiceBrokerExceptionHandler to adjust the HTTP status code for certain situations on all the ServiceBrokerRestControllers:
#ControllerAdvice(annotations = ServiceBrokerRestController.class)
public class MyServiceBrokerExceptionHandler extends ServiceBrokerExceptionHandler {
#ExceptionHandler({MyCustomException.class})
#ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorMessage handleException(MyCustomException ex) {
return this.getErrorResponse(ex);
}
}
Now I want to test it but I don't really understand how. Since the handler is applied to all controllers annotated with #ServiceBrokerRestController I don't really care about a certain controller bean let aside a certain method. I just want to "mock" some kind of ServiceBrokerRestController and given one of its methods throws a MyCustomException the web layer should return with 500 - Internal Server Error. Do I have to create a ServiceBrokerRestController dummy for that or is there a better way?
It should be an simple integration as follows. You can create a dummy controller in your test package that just throws the exceptions that you want to test.
Dummy Controller:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class DummyExceptionTestController {
#GetMapping("/my-exception")
public void customException() {
throw new MyCustomException("My Error Message 1");
}
}
The Integration test:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
#AutoConfigureMockMvc
#SpringBootTest(classes = DemoApplication.class)
public class ExceptionHandlerIT {
#Autowired
private MockMvc mockMvc;
#Test
public void testMyException() throws Exception {
mockMvc.perform(get("/my-exception"))
.andExpect(status().isBadRequest())
.andExpect(content().string("My Error Message 1"));
}
}
Exception handler and the Exception:
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
#ControllerAdvice
public class MyServiceBrokerExceptionHandler {
#ExceptionHandler({MyCustomException.class})
public ResponseEntity<String> handleException(MyCustomException ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
}
public class MyCustomException extends RuntimeException {
public MyCustomException(String msg) {
super(msg);
}
}

Adding a new parameter to a Class which is instantiated by Spring

I have this class, which is part of a SpringBoot Application
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.springboot.tutorial.upwork.service.WelcomeService;
#RestController
public class WelcomeController {
#Autowired
private WelcomeService service;
// http://localhost:7777/welcome
#RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
// http://localhost:7777/welcome/bla
#RequestMapping("/welcome/{msg}")
public String welcome(#PathVariable("msg") String message) {
return service.retrieveWelcomeMessage(message);
}
}
Now i need to add a variable to hold some data, which will be available to class methods. How can i do it since an instance of this class is created by Spring?

Spring - Validation with #Valid is never called

Trying to simply validate a field of my bean and instead of doing it manually wanted to check out Spring Validation, but didn't have much luck as of now.
In short:
Validation with #Valid annotation seems to never be called when I call a method of my #RestController
My code:
pom.xml (for the validation part)
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
Spring is version 4.1.1
Validator
package mypackage;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
public class UtenteValidator implements Validator{
public UtenteValidator() {
// TODO Auto-generated constructor stub
}
#Override
public boolean supports(Class<?> clazz) {
return UtenteLite.class.equals(clazz);
}
//validation test
#Override
public void validate(Object target, Errors errors) {
UtenteLite user = (UtenteLite) target;
if(user.getName != "foo") {
errors.rejectValue("name", "name not correct");
}
}
}
Controller
package myPackage;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/users")
public class UsersController {
public UsersController() {
}
//tried to put #InitBinder, but no luck
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new UtenteValidator());
}
#ResponseBody
#RequestMapping(value="", method=RequestMethod.PUT)
public <T> ResponseEntity<T> aggiornaUtente(#RequestBody #Valid UtenteLite utente, BindingResult result)
{
ResponseEntity<T> responseEntity=null;
return responseEntity;
}
}
Te BindingResult result object shows always zero errors and the validate, supports or initBinder methods are never called.
Found this tutorial that reads:
When #InitBinder methods get called?
The #InitBinder annotated methods
will get called on each HTTP request if we don't specify the 'value'
element of this annotation.
WebDataBinder argument is specific to a model attribute. That means
each time a model attribute is created by Spring this method will get
called with a new instance of WebDataBinder.
So I tried to change my controller method to this adding a #ModelAttribute and NOW the validation code gets called BUT the requestBody object (the "utente" object) is empty, so validation always fails because the fields are all nulls:
#ResponseBody
#RequestMapping(value="", method=RequestMethod.PUT)
public <T> ResponseEntity<T> aggiornaUtente(#RequestBody #Valid #ModelAttribute("utente") UtenteLite utente, BindingResult result)
{
...
}
The utente method parameter is passed with a JSON as the body of the request.
Ok,
after several tries I succeded in producing a working solution just by adding the hibernate-validation artifact reference in my pom.xml.
I wrongly supposed the hibernate-validator was mandatory only if I was usig validation annotations on the beans properties (like #NotNull, #Pattern, etc..)
So only by adding this snippet I was able to solve my problem (hope this will spare a few hours of work to someone else):
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.1.3.Final</version>
</dependency>
The complete code now is:
Validator
package mypackage;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
public class UtenteValidator implements Validator{
public UtenteValidator() {
// TODO Auto-generated constructor stub
}
#Override
public boolean supports(Class<?> clazz) {
return UtenteLite.class.equals(clazz);
}
//validation test
#Override
public void validate(Object target, Errors errors) {
UtenteLite user = (UtenteLite) target;
if(user.getName != "foo") {
errors.rejectValue("name", "name not correct");
}
}
}
Controller
package myPackage;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/users")
public class UsersController {
public UsersController() {
}
//tried to put #InitBinder, but no luck
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new UtenteValidator());
}
#ResponseBody
#RequestMapping(value="", method=RequestMethod.PUT)
public <T> ResponseEntity<T> aggiornaUtente(#RequestBody #Valid UtenteLite utente)
{
ResponseEntity<T> responseEntity=null;
return responseEntity;
}
}

Categories

Resources