封装统一返回格式

386 阅读5分钟

实现功能

在前后端分离项目的开发过程中,后端需要每次都按照格式把数据返回给前端,如果每次都需要手动自己把数据封装到返回类上面,会比较麻烦,因此可以实现统一返回数据类型处理,让手动封装变成自动封装。在这里我们的思路是通过注解的方式,如果Controller上的类加上了我们自定义的注解,就自动封装

相关知识

  • HandlerInterceptor
  • WebMvcConfigurer
  • ResponseBodyAdvice
  • RequestContextHolder

HandlerInterceptor

在不同阶段对请求的拦截处理

WebMvcConfigurer

这里用于配置需要拦截的请求

ResponseBodyAdvice

根据搜索的资料,ResponseBodyAdvice 接口是在 Controller 执行 return 之后,在 response 返回给客户端之前,执行的对 response 的一些处理,可以实现对 response 数据的一些统一封装或者加密等操作。 可以参考 ResponseBodyAdvice解析

RequestContextHolder

持有上下文的Request容器,获取HttpServletRequest对象

www.jianshu.com/p/83a872d8d…

这个接口里面提供了两个方法

方法作用
supportsboolean类型,根据返回值确定是否执行beforeBodyWrite方法
beforeBodyWrite对 response 处理的具体执行方法

基础准备工作

测试请求

我这里准备了http://localhost:8080/hello,一个简单的请求,在控制台输出文字和返回字符串

@GetMapping("/hello")
public String hello() {
    System.out.println("执行到了TestController.hello");
    return HELLO_WORLD;
}

拦截器

package com.example.myresult.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyResultInterceptor implements HandlerInterceptor {

    public MyResultInterceptor() {
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("执行到了MyResultInterceptor.preHandle");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("执行到了MyResultInterceptor.postHandle");
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("执行到了MyResultInterceptor.afterCompletion");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

拦截请求的配置类

package com.example.myresult.interceptor;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        System.out.println("执行到了WebAppConfigurer.addInterceptors");
        registry.addInterceptor(new MyResultInterceptor()).addPathPatterns("/**");
    }
}

用于对Controller返回数据进行处理的类

package com.example.myresult.interceptor;

import com.example.myresult.config.ResponseAdvice;
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.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class MyResultHandler implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        System.out.println("执行到了MyResultHandler.supports");
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        System.out.println("执行到了MyResultHandler.beforeBodyWrite");
        return body;
    }
}

注解

package com.example.myresult.annotation;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyResult {
}

测试

现在是最基础的状态,没有进行任何处理,这个时候我选择先直接调用接口,看一下上述方法的执行顺序。

  1. 在启动项目的时候就会执行WebAppConfigurer.addInterceptors()方法,并且只会执行一次,这个时候是把拦截规则进行添加,以后符合规则的请求就会被拦截处理
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.3)

2022-12-10 23:31:51.743  INFO 1276 --- [           main] c.example.myresult.MyResultApplication   : Starting MyResultApplication using Java 1.8.0_121 on PC-202211051806 with PID 1276 (D:\StudyProject\my-result\my-result\target\classes started by Administrator in D:\StudyProject\my-result\my-result)
2022-12-10 23:31:51.745  INFO 1276 --- [           main] c.example.myresult.MyResultApplication   : No active profile set, falling back to 1 default profile: "default"
2022-12-10 23:31:52.110  INFO 1276 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-12-10 23:31:52.114  INFO 1276 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-12-10 23:31:52.114  INFO 1276 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-12-10 23:31:52.166  INFO 1276 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-12-10 23:31:52.166  INFO 1276 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 401 ms
执行到了WebAppConfigurer.addInterceptors
2022-12-10 23:31:52.295  INFO 1276 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-12-10 23:31:52.299  INFO 1276 --- [           main] c.example.myresult.MyResultApplication   : Started MyResultApplication in 0.696 seconds (JVM running for 0.995)
  1. 调用接口 image.png

通过方法输出的先后顺序,可以看到这些类的执行先后,如下图所示 image.png

实现思路

知道了这个顺序之后,我们就选择其中的方法进行操作:

  1. 在进入Controlelr方法之前我们可以判断方法或者类上面有没有添加我们自定义的注解,然后进行相应的处理,也就是在preHandle方法里进行判断
  2. 如果有,我们就在preHandlerequest里面放个flag,等执行到supports如果返回的是true,的时候我们根据这个来判断是否需要对返回值进行包装
  3. supports如果返回的是true,就在beforeBodyWrite里面进行返回值的包装
  4. 走完第三步之后其实流程就完整了,后面的postHandlerafterCompletion不用处理了

具体细节如下面代码所示,可以过滤掉添加了注解又手动封装返回类型的方法、String类型(String类型需要单独处理、后续可以添加异常类,在策略模式Map里面添加即可)

代码实现

MyResultInterceptor

package com.example.myresult.interceptor;

import com.example.myresult.annotation.MyResult;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyResultInterceptor implements HandlerInterceptor {

    public MyResultInterceptor() {
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("执行到了MyResultInterceptor.preHandle");
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            val beanType = handlerMethod.getBeanType();
            val method = handlerMethod.getMethod();
            if (beanType.isAnnotationPresent(MyResult.class)) {
                request.setAttribute("needPackagingFlag",beanType.getDeclaredAnnotation(MyResult.class));
            } else if (method.isAnnotationPresent(MyResult.class)) {
                request.setAttribute("needPackagingFlag",method.getDeclaredAnnotation(MyResult.class));
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("执行到了MyResultInterceptor.postHandle");
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("执行到了MyResultInterceptor.afterCompletion");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

MyResultHandler

package com.example.myresult.interceptor;

import com.example.myresult.annotation.MyResult;
import com.example.myresult.config.ResponseAdvice;
import com.example.myresult.entity.Result;
import com.example.myresult.enums.ResultCode;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
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.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;

@Slf4j
@ControllerAdvice
public class MyResultHandler implements ResponseBodyAdvice<Object> {

    private static final Map<Class, Function<Object, Object>> STRATEGY_PATTERN_MAP = new HashMap<>(16);

    @PostConstruct
    public void strategyPatternInit() {
        STRATEGY_PATTERN_MAP.put(String.class, this::stringResultMapper);
        STRATEGY_PATTERN_MAP.put(Result.class, (body) -> body);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        System.out.println("执行到了MyResultHandler.supports");
        // 判断是否需要对返回值进行封装
        final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        final HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        MyResult myResult = (MyResult) request.getAttribute("needPackagingFlag");
        return !Objects.isNull(myResult);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        System.out.println("执行到了MyResultHandler.beforeBodyWrite");
        Function<Object, Object> result = Optional.ofNullable(body).map((b) -> STRATEGY_PATTERN_MAP.get(b.getClass())).orElse(null);
        return Objects.isNull(result) ? Result.success(body) : result.apply(body);
    }

    private Object stringResultMapper(Object body) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(Result.success(ResultCode.RC100.getMessage(), body));
        } catch (JsonProcessingException var3) {
            log.error("字符串转化失败:【{}】", var3.getMessage());
            return null;
        }
    }
}