In my REST service app, I am planning to create a #ControllerAdvice class to catch controller thrown exceptions and return ResponseEntity objects according to the error type.
But I already have a #RestController class implementing the ErrorController interface to catch all exceptions.
Do these two interfere in any manner?
In which cases will ErrorController be called when #ControllerAdvice exists?
Edit:
The ErrorController code as requested
#RestController
public class ControllerCustomError implements ErrorController{
//error json object
public class ErrorJson {
public Integer status;
public String error;
public String message;
public String timeStamp;
public String trace;
public ErrorJson(int status, Map<String, Object> errorAttributes) {
this.status = status;
this.error = (String) errorAttributes.get("error");
this.message = (String) errorAttributes.get("message");
this.timeStamp = errorAttributes.get("timestamp").toString();
this.trace = (String) errorAttributes.get("trace");
}
}
private static final String PATH = "/error";
#Value("${hybus.error.stacktrace.include}")
private boolean includeStackTrace = false;
#Autowired
private ErrorAttributes errorAttributes;
#RequestMapping(value = PATH)
ErrorJson error(HttpServletRequest request, HttpServletResponse response) {
// Appropriate HTTP response code (e.g. 404 or 500) is automatically set by Spring.
// Here we just define response body.
return new ErrorJson(response.getStatus(), getErrorAttributes(request, includeStackTrace));
}
#Override
public String getErrorPath() {
return PATH;
}
private Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return errorAttributes.getErrorAttributes(requestAttributes, includeStackTrace);
}
}
An implementation of the ErrorController is used to provide a custom whitelabel error page.
A class annotated with #ControllerAdvise is used to add a global exception handling logic for the whole application. Thus, more than one controller in your application.
If in your application there is no mapping found for a request or page then spring will fallback to the 'whitelabel error page'. And in this case it will be the custom implementation of ErrorController
Related
I have a spring-boot application without any controller classes.
How can I write exception handlers for this application. Exception handler classes annotated with #ControllerAdvice doesn't work.
If you are developing web applications, ErrroController is available.
#Controller
#RequestMapping("${server.error.path:${error.path:/error}}")
public class MyErrorController implements ErrorController {
private final ErrorAttributes errorAttributes;
public MyErrorController(final ErrorAttributes errorAttributes) {
this.errorAttributes = errorAttributes;
}
#Override
public String getErrorPath() {
return null;
}
#RequestMapping
public ResponseEntity<Map<String, Object>> error(final HttpServletRequest request) {
final WebRequest webRequest = new ServletWebRequest(request);
final Throwable th = errorAttributes.getError(webRequest);
// ...
// see also: BasicErrorController implementation
}
}
I have a scenario where is an already existing controller and the service throws exceptions which are handled via the #RestControllerAdvice.
Now i have a new class which i have introduced which invokes methods from the above service class in a batch mode. In my class i have to capture the exceptions or successes bundle them up and return. For any exceptions that occur i need to report the HTTP Status and the error message.
Could you let me know if there is any way this can be achieved?
You can create your own Exception class.
public class MyException extends Exception {
private int errorCode;
private String errorMessage;
public MyException(int errorCode, String errorMessage) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}
and you can create new MyException when occurring any exception and throw it. Then you get this exception in the #RestControllerAdvice class.
#RestControllerAdvice
public class ExceptionAdvice {
private ErrorCodeMapper errorCodeMapper;
#Autowired
public ExceptionAdvice(ErrorCodeMapper errorCodeMapper) {
this.errorCodeMapper = errorCodeMapper;
}
#ExceptionHandler(value = MyException.class)
public ResponseEntity handleGenericNotFoundException(MyException e) {
return new ResponseEntity(errorCodeMapper.getStatusCode(e.getErrorCode()));
}
}
and mapper class like below:
#Service
public class ErrorCodeMapper {
public static Map<Integer,HttpStatus> errorCodeMap = new HashMap<>();
public ErrorCodeMapper(){
errorCodeMap.put(100, HttpStatus.BAD_REQUEST);
errorCodeMap.put(101,HttpStatus.OK);
errorCodeMap.put(102,HttpStatus.BAD_REQUEST);
errorCodeMap.put(103,HttpStatus.BAD_REQUEST);
}
HttpStatus getStatusCode(int errorCode){
return errorCodeMap.get(errorCode);
}
}
You can more details to MyException and add the error message to the ResponseEntity.
For learning purposes, I have made a custom authentication system where I pass a token from the client to the server through the Authorization header.
In the server side, I'd like to know if it's possible to create in the interceptor, before the request reaches a method in the controller, an User object with the email from the token as a property, and then pass this user object to every request where I require it.
This what I'd like to get, as an example:
#RestController
public class HelloController {
#RequestMapping("/")
public String index(final User user) {
return user.getEmail();
}
}
public class User {
private String email;
}
Where user is an object that I created in the pre-interceptor using the request Authorization header and then I can pass, or not, to any method in the RestController.
Is this possible?
#Recommended solution
I would create a #Bean with #Scope request which would hold the user and then put the appropriate entity into that holder and then take from that holder inside the method.
#Component
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CurrentUser {
private User currentUser;
public User getCurrentUser() {
return currentUser;
}
public void setCurrentUser(User currentUser) {
this.currentUser = currentUser;
}
}
and then
#Component
public class MyInterceptor implements HandlerInterceptor {
private CurrentUser currentUser;
#Autowired
MyInterceptor(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
this.currentUser.setCurrentUser(new User("whatever"));
return true;
}
}
and in the Controller
#RestController
public class HelloController {
private CurrentUser currentUser;
#Autowired
HelloController(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#RequestMapping("/")
public String index() {
return currentUser.getCurrentUser().getEmail();
}
}
#Alternative solution
In case your object that you would like to have, only contains one field, you can just cheat on that and add that field to the HttpServletRequest parameters and just see the magic happen.
#Component
public class MyInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TRY ONE AT THE TIME: email OR user
//BOTH SHOULD WORK BUT SEPARATELY OF COURSE
request.setAttribute("email", "login#domain.com");
request.setAttribute("user", new User("login#domain.com"));
return true;
}
}
You can use a local thread context object as follows - which will be handling one parameter per request thread (thread safe):
public abstract class LoggedUserContext {
private static ThreadLocal<User> currentLoggedUser = new ThreadLocal<>();
public static void setCurrentLoggedUser(User loggedUser) {
if (currentLoggedUser == null) {
currentLoggedUser = new ThreadLocal<>();
}
currentLoggedUser.set(loggedUser);
}
public static User getCurrentLoggedUser() {
return currentLoggedUser != null ? currentLoggedUser.get() : null;
}
public static void clear() {
if (currentLoggedUser != null) {
currentLoggedUser.remove();
}
}
}
Then in the interceptor prehandle function:
LoggedUserContext.setCurrentLoggedUser(loggedUser);
And in the interceptor postHandler function:
LoggedUserContext.clear();
From any other place:
User loggedUser = LoggedUserContext.getCurrentLoggedUser();
I am using springboot 2.0.1.RELEASE with spring-data-rest and followed the workaround mentioned here and my Validator is still not being invoked. Here are the details:
ValidatorRegistrar: Workaround for a bug
#Configuration
public class ValidatorRegistrar implements InitializingBean {
private static final List<String> EVENTS;
static {
List<String> events = new ArrayList<String>();
events.add("beforeCreate");
events.add("afterCreate");
events.add("beforeSave");
events.add("afterSave");
events.add("beforeLinkSave");
events.add("afterLinkSave");
events.add("beforeDelete");
events.add("afterDelete");
EVENTS = Collections.unmodifiableList(events);
}
#Autowired
ListableBeanFactory beanFactory;
#Autowired
ValidatingRepositoryEventListener validatingRepositoryEventListener;
#Override
public void afterPropertiesSet() throws Exception {
Map<String, Validator> validators = beanFactory.getBeansOfType(Validator.class);
for (Map.Entry<String, Validator> entry : validators.entrySet()) {
EVENTS.stream().filter(p -> entry.getKey().startsWith(p)).findFirst()
.ifPresent(p -> validatingRepositoryEventListener.addValidator(p, entry.getValue()));
}
}
}
Validator class:
#Component("beforeSaveBidValidator")
public class BeforeSaveBidValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Bid.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Bid bid = (Bid)target;
if (!bid.getAddendaAcknowledged()) {
errors.rejectValue("addendaAcknowledged",
"addendaAcknowledged is not true");
}
}
}
Custom RestController for Bids:
#RestController
#RequestMapping(path = "/bids")
public class BidController {
private BidRepository bidRepository;
#Autowired
public BidController(
BidRepository bidRepository) {
this.bidRepository = bidRepository;
}
#PutMapping("{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
Rest Client Test Code:
Bid bid = new Bid()
...
bid.setAddendaAcknowledged(false)
Map<String, String> uriVariables = new HashMap<String, String>()
uriVariables.put("id", bid.id)
HttpHeaders headers = new HttpHeaders()
headers.setContentType(MediaType.APPLICATION_JSON)
HttpEntity<Bid> entity = new HttpEntity<>(bid, headers)
ResponseEntity<String> response = restTemplate.exchange(
"/bids/{id}", HttpMethod.PUT, entity, Bid.class, bid.id)
// Expected: response.statusCode == HttpStatus.BAD_REQUEST
// Found: response.statusCode == HttpStatus.OK
// Debugger showed that Validator was never invoked.
Any idea what I am missing?
You are trying to use your validator with custom controller, not SDR controller. In this case you can just add it to your controller with #InitBinder annotation:
#RestController
#RequestMapping("/bids")
public class BidController {
//...
#InitBinder("bid") // add this parameter to apply this binder only to request parameters with this name
protected void bidValidator(WebDataBinder binder) {
binder.addValidators(new BidValidator());
}
#PutMapping("/{id}")
public Bid update(#RequestBody #Valid Bid bid) {
return bidRepository.save(bid);
}
}
#Component annotation on your validator is not necessary as well as ValidatorRegistrar class.
How to use validators with SDR controllers you can read in my another answer.
I have created a AppErrorController that extends Boot's ErrorController in order to handle status500 errors. The example:
#Controller
public class AppErrorController implements ErrorController {
private static final Logger LOGGER = LogManager.getLogger(AppErrorController.class);
private static final String ERROR = "error";
private static final String ERROR_MESSAGE = "errorMessage";
#RequestMapping(value = "/error")
public String error(Exception e, Model model) {
LOGGER.error("500", e);
model.addAttribute(ERROR_MESSAGE, "Internal server error");
return ERROR;
}
#Override
public String getErrorPath() {
return ERROR;
}
}
I need the error to be logged. But the problem is that Exception e is always null. How to extract the actual error in order to log it?
ADDED
I have a GlobalExceptionHandler, but it never catches '500' errors
#Component
#ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger LOGGER = LogManager.getLogger(GlobalExceptionHandler.class);
private static final String ERROR = "error";
#ExceptionHandler(Exception.class)
public String handleException(Exception e) {
LOGGER.error(e);
return ERROR;
}
}
One way to catch exception from jsp layer is to define your own error-page and point it location to you controller. Then you can extract the actual cause and do with it whatever you like.
web.xml
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error</location>
</error-page>
ErrorController
#Controller
#RequestMapping("/error")
public class ErrorController {
private final Logger log = LoggerFactory.getLogger(ErrorController.class);
#RequestMapping
public String ex(HttpServletRequest request) {
Throwable throwable = (Throwable) request.getAttribute("javax.servlet.error.exception");
throwable.printStackTrace(); //print
log.error(throwable.getMessage(), throwable); // or log
// or save to db
return "error"; //and redirect to some user-friendly page
}
}