I have a ResponseBodyAdvice bean which wraps the returning objects from rest controller with a specific form:
public class Response<T> {
private T data; //the data is from the rest controller
private int code;
private String message;
}
#RestControllerAdvice
public class ResponseAdviceConfig implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Response) {
return body;
} else {
return new Response<>(body);
}
}
}
#Data
public class User {
#JsonView(SummaryView.class)
private String account;
#JsonView(SummaryView.class)
private String avatar;
private String realname;
#JsonView(SummaryView.class)
private String nickname;
private String password;
public interface SummaryView {
}
}
#RestController
#RequestMapping("/v1/user")
public class UserAPI {
#GetMapping("/follows/list")
#JsonView(User.SummaryView.class)
public List<User> followsList(#RequestParam String account){
return userService.followsList(account);
}
}
But the problem is that the #JsonView annotated method in controller doesn't work anymore and the final response just a empty object {}. I guess that this ResponseBodyAdvice bean may conflict with the internal JsonViewResponseBodyAdvice. I debuged but without result.
Related
I have an interface that's not aware of its implementations (module-wise):
public interface SomeInterface {
}
and an enum implementing it:
public enum SomeInterfaceImpl {
}
I have a #RestController:
#RequestMapping(method = RequestMethod.GET)
public List<SomeClass> find(#RequestParam(value = "key", required = false) SomeInterface someInt,
HttpServletResponse response){
}
Although Jackson is aware that it should deserialize SomeInterface as SomeInterfaceImpl as follows:
public class DefaultSomeInterfaceDeserializer extends JsonDeserializer<SomeInterface> {
#Override
public SomeInterface deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return parser.readValuesAs(SomeInterfaceImpl.class).next();
}
}
and:
#Configuration
public class MyJacksonConfiguration implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof Jackson2ObjectMapperBuilder){
Jackson2ObjectMapperBuilder builder = (Jackson2ObjectMapperBuilder) bean;
builder.deserializerByType(SomeInterface.class, new DefaultSomeInterfaceDeserializer());
}
return bean;
}
}
and successfully serializes and deserializes SomeInterface as SomeInterfaceImpl in the #RequestBody, it doesn't seem to have any effect on mapping SomeInterface to SomeInterfaceImpl with #RequestParam. How do I overcome this?
Having a Converter in the application context as M.Denium suggested does the job:
#Component
public class SomeInterfaceConverter implements Converter<String, SomeInterface> {
#Override
public SomeInterface convert(String value) {
return new SomeInterfaceImpl(value);
}
}
I want to be able to modify the POJO returned by a RestController method (or endpoint) before it gets serialized into the HttpServletResponse as a stream of data. But I want to be able to do it outside the controller method code (as a middleware).
I have tried to do it using a HandlerInterceptor but I do not have access there to the POJO. I have also tried using AOP but the Pointcut was never called.
#RestController
public class TestController {
#GetMapping("/test")
public Resource<User> getTest() {
Resource<User> resource = new Resource<>();
resource.setData(new User("test user"));
return resource;
}
#Builder
#Getter
#Setter
#AllArgsConstructor
static class User {
private String username;
}
#Getter
#Setter
#AllArgsConstructor
#NoArgsConstructor
static class Resource<T> {
private T data;
private Set<String> errors;
}
}
I want to be able to add a list of errors (if needed) to the Resource returned by the Controller after the controller performs its own logic and returns.
To change the object after returned from #RestController method but before it is written to the HTTP response, you can implement ResponseBodyAdvice and declared it as #ControllerAdvice bean:
#ControllerAdvice
public static class Foo implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//body is the object return from the #RestController method
//Cast it and modify it accordingly.
if(body instanceof Resource) {
Resource res = (Resource)body;
//Modify it .... blablablba
}
return body;
}
}
I want to create a standard response using #ControllerAdvice
I have written a Custom Annotation for setting status code and message.
I am getting class cast exception in
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class);
String statusCode = annotation.status();
String message = annotation.message();
ResponseHeader<Object> responseHeader = new ResponseHeader(statusCode,message,body);
System.out.println(responseHeader);
return responseHeader;
}
when I am using JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class);
Here is my classes that I have created -
#RestControllerAdvice
public class RestResponseHandler implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return (AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null ||
returnType.getMethodAnnotation(ResponseBody.class) != null);
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
JsonFilter annotation = returnType.getMethodAnnotation(JsonFilter.class);
String statusCode = annotation.status();
String message = annotation.message();
ResponseHeader<Object> responseHeader = new ResponseHeader(statusCode,message,body);
System.out.println(responseHeader);
return responseHeader;
}
public class CommentController {
#RequestMapping(value = "/repos", method = RequestMethod.GET)
#JsonFilter(status="0",message="done")
public #ResponseBody String repos() throws IOException {
return null;
}
}
#Target(ElementType.METHOD)
#Documented
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonFilter {
String status() default "0";
String message() ;
}
public class ResponseHeader<Object> {
private String code;
private String message;
private Object body;
public ResponseHeader(String code, String message,Object body) {
super();
this.code = code;
this.message = message;
this.body = body;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
public Object getBody() {
return body;
}
Need to override the supports method.
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
List<Annotation> annotations = Arrays.asList(returnType.getMethodAnnotations());
return annotations.stream().anyMatch(annotation -> annotation.annotationType().equals(JsonFilter.class));
}
How can I achive that the #ResponseBody (in my case a class of type SomePojoInterface) is automatically validated (lets say through JSR-303 validation). Nice to have would be, that in case of a validation-failure the handler would throw an Exception which can be handled in some #ControllerAdvice annotated class.
My code so far.
#RestController
public class MyRestController {
#GetMapping(value = "validate", produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
protected SomePojo validateResponse() {
return new SomePojo();
}
}
#ControllerAdvice
class GlobalControllerExceptionHandler {
#ResponseStatus(HttpStatus.XXX)
#ExceptionHandler(MyResponseValidationException.class)
public void handleResponseValidationException() {
// ...
}
}
public class SomePojo implements SomePojoInterface {
#NotNull
private String someValue;
// getter / setter
}
If you have annotated your class SomePojo, then:
#GetMapping(value = "validate", produces = MediaType.APPLICATION_JSON_VALUE)
protected SomePojo validateResponse() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
SomePojo somePojo = new SomePojo(null);
Set<ConstraintViolation<Car>> constraintViolations = validator.validate(somePojo);
// Other stuff
}
#Valid annotation is for request. More examples from their docs. I am not sure what all you want to validate
I managed to achieve this through the #RestControllerAdvice.
#RestControllerAdvice
public class RestPostProcessingAdvice implements ResponseBodyAdvice<SomePojoInterface> {
#Inject
private Validator validator;
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if(doSomeChecksIfEligiable(returnType, converterType)) {
return true;
}
return false;
}
#Override
public SomePojoInterface beforeBodyWrite(SomePojoInterface body, MethodParameter returnType,
MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(body);
if(constraintViolations.size() > 0) {
response.setStatusCode(HttpStatus.XXX);
LOG.fatal("Sorry, I'm sending crap");
}
return body;
}
}
Be aware that throwing an Exception and catching it in an #ExceptionHandler that is sending the same (mofified) object out in the #ResponseBody could lead to an endless loop, since the object will be checked again this #RestControllerAdvice.
For example, here's a method which returns a User:
#RequestMapping(method = GET, value = "/user")
public User getUser() {
return new Users();
}
For some reasons, the client expect an other type
class CommonResponse<T> {
int code;
T data;
}
So I need to convert all return value from T(User for this e.g.) to CommonResponse<T> before it handled by the MessageConverter.
Cause there're many request hanlders should be modified, is there any way to write the convert data just once?
Finally I find ResponseBodyAdvice to do such work.
Here the sample code:
#RestControllerAdvice
public class CommonAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.getDeclaringClass().getPackage().getName().startsWith("foo.bar.demo");
// you can change here to your logic
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
return new CommonResponse<Object>().setCode(200).setData(body);
}
}
you need to add/configure your custom converter. so that your custom converter is executed before others
#EnableWebMvc
#Configuration
#ComponentScan({ "org.app.web" })
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
messageConverters.add(createCustomConverter());
super.configureMessageConverters(converters);
}
private HttpMessageConverter<Object> createCustomConverter() {
....
}
}