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不适用的一些情况。