spring对controller返回值进行额外处理—翻译code
1.使用原生filter过滤器
filter会在映射方法的前后执行,是一个栈的调用过程,类似于spring 的aop执行链,把本身链的引用(包含有必要的上下文信息)传到具体的链中某一个执行策略中,在这个策略可以随意对整个链进行操作。
思路:ServletResponse本身是无法获取返回内容的,所以必须对response进行劫持(代理),重写其中的写入内容方法,在filter调用返回时,获取返回内容,再进行额外的处理,写入到response中。
学习了几篇博客,写出的简易demo如下:
ResponseContentWrapper类:
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ResponseContentWrapper extends HttpServletResponseWrapper {
ByteArrayOutputStream buffer;
ServletOutputStream out;
public ResponseContentWrapper(HttpServletResponse response) {
super(response);
buffer = new ByteArrayOutputStream();
out = new BufferOutPutStream(buffer);
}
public byte[] getContent() {
return buffer.toByteArray();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return out;
}
@Override
public void flushBuffer() throws IOException {
out.flush();
}
static class BufferOutPutStream extends ServletOutputStream {
ByteArrayOutputStream buffer;
public BufferOutPutStream(ByteArrayOutputStream buffer) {
this.buffer = buffer;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setWriteListener(WriteListener listener) {
}
@Override
public void write(int b) throws IOException {
buffer.write(b);
}
}
}
DataTranslateFilter类:
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class DataTranslateFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//这里面会执行其他的filter,并最终执行对应的映射方法
//栈的调用过程,最终会返回到这里
//类似于spring 的aop执行链,都是把本身链的引用传到具体的链中某一个具体策略中,在这个策略可以随意对之进行操作。
ResponseContentWrapper wrapper = new ResponseContentWrapper((HttpServletResponse) servletResponse);
filterChain.doFilter(servletRequest, wrapper);
byte[] content = wrapper.getContent();
String str=null;
/*
在这里对获取的内容做处理并用str指向对应结果
*/
ServletOutputStream outputStream = servletResponse.getOutputStream();
outputStream.write(str.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
}
}
配置filter到spring容器中,则按照框架要求的配置方法即可,即xml或者@bean
不建议使用此方法,filter做通用的、与具体返回值无关的处理比较合适,例如字符类型的转换、访问相关的控制等,不适用于返回值的特殊处理,很大原因是因为返回的内容是一个流,关于返回值本身的信息获取不到。
2.使用@ControllerAdvice对Controller切面
在spring的官方网站中看了下ControllerAdvice注解的介绍,这个注解中包含有@Component子注解,spring会自动将之加入到容器中。@ControllerAdvice修饰的类会对spring中管理的所有的Controller生效,常用的用法是结合@ExceptionHandler、@InitBinder和@ModelAttribute使用,做一些全局异常处理、参数转化为实体和访问和新加参数等工作。
@ControllerAdvice可以通过包路径、类、注解来确定作用范围。官网的示例:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
在本业务范围中,@ControllerAdvice要结合ResponseBodyAdvice接口使用,这个接口是springmvc提供的一个对返回内容作处理的钩子,实现这个接口的support和beforeBodyWrite方法即可,代码如下:
DataTranslateAdvice类:
import com.dengcl.datatranslate.codeannotation.SexCode;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ControllerAdvice
public class DataTranslateAdvice implements ResponseBodyAdvice<Object> {
private static final List<Class<?>> BASIC_OBJECT = new ArrayList<>();
static {
BASIC_OBJECT.add(Boolean.class);
BASIC_OBJECT.add(Character.class);
BASIC_OBJECT.add(Integer.class);
BASIC_OBJECT.add(Long.class);
BASIC_OBJECT.add(Integer.class);
//and so on
}
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
//判断是否匹配,应该根据一些注解或者类,来区分是否需要走beforeBodyWrite
//这儿简单判定下是否有ResponseBody注解
return returnType.hasMethodAnnotation(ResponseBody.class) || returnType.getDeclaringClass().getAnnotation(RestController.class) != null;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//Object 可能是各种类型,这儿需要根据不同类型做各种不同的策略
if (body == null) {
return null;
}
if (BASIC_OBJECT.contains(body.getClass())) {
return body;
}
//这儿没有导入json包,先用map暂存一下
Map<String, Object> result = new HashMap<>();
Field[] declaredFields = body.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
try {
declaredField.setAccessible(true);
Object value = declaredField.get(body);
result.put(declaredField.getName(), value);
if (declaredField.getAnnotation(SexCode.class) != null && value != null) {
result.put(declaredField.getName() + "_translate", (Integer) value == 1 ? "男" : "女");
}
} catch (IllegalAccessException e) {
}
}
return result;
}
}
DataTranslateController类:
import com.dengcl.datatranslate.entity.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("dataTranslate")
public class DataTranslateController {
@GetMapping("str")
public User getStr() {
User user = new User();
user.setAddress("月亮之湾");
user.setAge(20);
user.setName("风箫");
user.setSex(1);
return user;
}
}
User类:
public class User {
@SexCode
private Integer sex;
private String name;
private Integer age;
private String address;
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
还有注解SexCode:
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface SexCode {
}
测试结果如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3vvIqbX6-1647860412221)(/Users/conglindeng/Library/Application Support/typora-user-images/image-20220321154640177.png)]
总结:在controller返回结果后,此刻需要将数据写入到responsebody中,springmvc提供了钩子,让开发这可以在写入到response之前,对body进行额外的处理,通过ControllerAdvice注解适配到spring工厂中发挥作用。
3.使用Spring提供的切面
此方法实际和方法2大同小异,只不过使用到了spring提供的不同能力,方法2为模板方法的落地实现,方法3则为更通用的代理实现,两者本质都是在controller执行前后做一些额外处理。
前置知识:aop相关的概念和配置等,这个简单看一下就理解了,不做过多赘述,直接上代码。DataTranslateAspect类:
import com.dengcl.datatranslate.codeannotation.SexCode;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
@Aspect
@Component
public class DataTranslateAspect {
@Around("execution(* com.dengcl.datatranslate.controller.*.*(..))")
public Object dataTranslate(ProceedingJoinPoint joinPoint) throws Throwable {
Object proceed = joinPoint.proceed();
Field[] declaredFields = proceed.getClass().getDeclaredFields();
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
for (Field declaredField : declaredFields) {
try {
declaredField.setAccessible(true);
if (declaredField.getAnnotation(SexCode.class) != null && declaredField.get(proceed) != null) {
Object value = declaredField.get(proceed);
request.setAttribute(declaredField.getName() + "_translate", (Integer) value == 1 ? "男" : "女");
}
} catch (IllegalAccessException e) {
}
}
return proceed;
}
}
这里面有个与方法2不一样的点是:方法2是可以直接对返回值做处理的,放入到response中是没有类型要求的,此时可以直接添加想要的值、修改为需要的类型,返回即可。
此方法不能直接在返回值上处理并返回的原因是:aop增强是对方法内的逻辑做额外的处理,不管如何增强、有多少增强,最终方法的返回类型要满足方法本身要求的类型。
总结:建议使用方法2中spring本身提供的钩子实现对返回结果的处理。方法1太古老,需要造很多轮子,方法3可以补充方法2不适用的一些情况。