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);
}
}
Related
I'm just trying use Camel Reactive Stream together with Spring Boot Reactor using the following code
package com.manning.camel.reactive;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.reactive.streams.api.CamelReactiveStreamsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
/**
* A simple Camel route that triggers from a timer and calls a bean and prints to system out.
* <p/>
* Use <tt>#Component</tt> to make Camel auto-detect this route when starting.
*/
#RestController
public class MySpringBootRouter extends RouteBuilder {
#Autowired
private ProducerTemplate template;
#Autowired
private CamelReactiveStreamsService crss;
#GetMapping
public Mono<String> sayHi() {
template.asyncSendBody("direct:works", "Hi");
//return Mono.from(crss.fromStream("greet", String.class));
return Mono.from(crss.fromStream("greet", String.class));
}
#Override
public void configure() {
from("direct:works")
.log("Fired")
.to("reactive-streams:greet");
}
}
After run the code
java.lang.IllegalStateException: The stream has no active subscriptions
After a long time, solved the error, as can be noticed the Router Class logic was changed a little
#Slf4j
#Service
#AllArgsConstructor
public class MyService {
final CamelContext context;
#PostConstruct
public void consumerData() {
var rCamel = CamelReactiveStreams.get(context);
var numbers = rCamel.fromStream("numbers", Integer.class);
Flux.from(numbers).subscribe(e -> log.info("{}", e));
}
}
#Component
#NoArgsConstructor
public class MyRouter extends RouteBuilder {
// Injects the Subscriber
#Autowired MyService service;
#Override
public void configure() {
//onException(ReactiveStreamsNoActiveSubscriptionsException.class)
// .continued(true);
from("timer://reactiveApp?fixedRate=true&period=2s")
.transform(method(Random.class, "nextInt(100)"))
//.log("${body}");
.to("direct:message");
from("direct:message")
//.log("${body}")
.to("reactive-streams:numbers");
}
}
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)
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
}
}
I wanted to play around with the different types of bean scopes. So I wrote a test environment which should generate a random number so I could see if a bean had changed. My test setup does not work and I can not explain what I found out.
I'm using Spring Boot 2.13 with the Spring Framework 5.15.
Following setup:
Main class:
package domain.webcreator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class WebcreatorApplication {
public static void main(String[] args) {
SpringApplication.run(WebcreatorApplication.class, args);
}
}
Beans class:
package domain.webcreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Random;
#Configuration
public class Beans {
#Bean
public Random randomGenerator() {
return new Random();
}
}
Scoper class:
package domain.webcreator;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.util.Random;
#Service
#Scope("singleton")
public class Scoper {
private Random rand;
public Scoper(Random rand) {
this.rand = rand;
}
public int getNumber(int max) {
return rand.nextInt(max);
}
}
Index Controller
package domain.webcreator.controller;
import domain.webcreator.Scoper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
public class IndexController {
#GetMapping("/")
#ResponseBody
#Autowired
public String indexAction(Scoper scoper) {
return String.valueOf(scoper.getNumber(50));
}
}
My problem is, that I get an NPE while calling scoper.getNumber(50).
This is strange because when debugging, a Random bean is generated and passed to the scoper constructor.
Later on, in the controller, the rand property is null.
What am I doing wrong?
You're trying to apply #Autowired to a random method, which isn't how Spring works. Controller method parameters are for information specific to that HTTP request, not general dependencies, and so Spring is trying to create a new Scoper that is associated with the request--but it doesn't have any incoming values in the request to fill in. (I'm actually surprised you're not getting an error about no default constructor.)
Instead, pass your Scoper in a constructor.
#RestController
public class IndexController {
private final Scoper scoper;
public IndexController(Scoper scoper) {
this.scoper = scoper;
}
#GetMapping("/")
public String indexAction(Scoper scoper) {
return String.valueOf(scoper.getNumber(50));
}
}
A couple of notes:
Singleton scope is the default, and there's no need to specify it.
#RestController is preferable to repeating #ResponseBody unless you have a mixed controller class.
I try to improve my Spring knowledge by reading Spring in action 4.
When I've to to section, describing using of Qualifier annotation (3.3.2), i faced the problem.
To test this annotation in action, I wrote Dessert interface, which is implemented by 3 classes, creating in context using #Component annotation.
I also created class Taster, which "tastes" some dessert, autowired into by some qualifier.
When I run my application, using AnnotationConfigApplicationContext - everything works good. With SpringJUnit4ClassRunner - it does not. I guess I miss something in my test code, but I do not have enough knowledge to realize what.
Interface:
package bakery.intrface;
#FunctionalInterface
public interface Dessert {
void introduce();
}
Cake:
package bakery.desserts;
import bakery.intrface.Dessert;
import org.springframework.stereotype.Component;
#Component
public class Cake implements Dessert {
#Override
public void introduce() {
System.out.println("I am a cake!");
}
}
Cookie:
package bakery.desserts;
import bakery.intrface.Dessert;
import org.springframework.stereotype.Component;
#Component
public class Cookie implements Dessert {
#Override
public void introduce() {
System.out.println("I'm a cookie!");
}
}
Ice cream:
package bakery.desserts;
import bakery.intrface.Dessert;
import org.springframework.stereotype.Component;
#Component
public class IceCream implements Dessert {
#Override
public void introduce() {
System.out.println("I'm an ice cream!");
}
}
The class, consumes some bean, Taster:
package bakery;
import bakery.intrface.Dessert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
#Component
public class Taster {
private Dessert dessert;
public void taste(){
dessert.introduce();
}
#Autowired
#Qualifier("iceCream")
public void setDessert(Dessert dessert) {
this.dessert = dessert;
}
}
Configuration:
package bakery.config;
import bakery.Bakery;
import bakery.Taster;
import bakery.desserts.Cake;
import bakery.intrface.Dessert;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
#ComponentScan(basePackageClasses = Bakery.class)
public class BakeryConfig {
}
Run class:
package bakery;
import bakery.config.BakeryConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Bakery {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BakeryConfig.class);
String[] beans = context.getBeanDefinitionNames();
Taster taster = (Taster) context.getBean("taster");
taster.taste();
}
}
Test class:
package bakery;
import bakery.config.BakeryConfig;
import bakery.intrface.Dessert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.junit.Assert.assertNotNull;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = BakeryConfig.class)
public class BakeryTest {
#Autowired
Dessert dessert;
#Autowired
Taster taster;
#Test
public void contextInit(){
assertNotNull(dessert);
dessert.introduce();
}
#Test
public void tasterInit(){
assertNotNull(taster);
}
}
When I run the test, I'm getting the exception: No qualifying bean of type [bakery.intrface.Dessert] is defined: expected single matching bean but found 3: cookie,iceCream,cake.
There are 3 "Dessert" beans in your application context, you have to specify which one you want to wire.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = BakeryConfig.class)
public class BakeryTest {
#Autowired
#Qualifier("iceCream") // <===================== you must specify which bean to be wired
Dessert dessert;
#Autowired
Taster taster;
This is to be expected.
The declaration
#Autowired
Dessert dessert;
is asking for a Dessert object. Dessert is the interface, and there are three implementing classes, Cookie, IceCream, and Cake. Since you haven't made it more explicit which of those implementations you want, Spring throws an error because it can't decide what to do.
If you need this in your test, you can do one of the following:
#Autowired
#Qualifier("iceCream")
Dessert dessert;
to get only the ice cream dessert,
OR
#Autowired
List<Dessert> desserts;
to get a list containing all the implementations.