一文带你掌握SpringMVC扩展点RequestBodyAdvice和ResponseBodyAdvice如何使用及实现原理

1.概述

Spring MVC是当今web项目系统开发后端技术框架的不二之选,也是Spring全家桶中非常重要的一个成员,它的设计思想和实现套路都是很值得我们学习的,所以今天我们就再来看看Spring MVC框架中预留的两个钩子也就是扩展点:RequestBodyAdviceResponseBodyAdvice。之前在总结详解@ControllerAdvice的使用及其实现原理一文中就有提到这两个扩展类,它们需要配合@ControllerAdvice一起使用

1.1 RequestBodyAdvice

RequestBodyAdviceSpring MVC 框架中的一个接口,允许在 HTTP 请求的请求体(request body)被反序列化为 Java 对象之前进行拦截和修改。它为开发者提供了一个钩子,可以在请求处理过程中插入自定义的逻辑,例如对请求体进行预处理、验证或日志记录

1.1.1 源码定义如下:
public interface RequestBodyAdvice {

	boolean supports(MethodParameter methodParameter, Type targetType,
			Class<? extends HttpMessageConverter<?>> converterType);

	HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

	Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

	@Nullable
	Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

}

  • supports():该方法用于确定是否应该应用这个 RequestBodyAdvice 实例。返回 true 表示支持当前的转换。
  • beforeBodyRead():在读取请求体之前调用,可以用于包装或修改传入的 HttpInputMessage
  • afterBodyRead():在请求体被读取并转换为 Java 对象之后调用,可以修改或替换读取到的对象。
  • handleEmptyBody():在请求体为空时调用,可以提供一个默认对象或处理空请求体的情况。
1.1.2 实际应用

在实际应用中,RequestBodyAdvice 可用于以下场景:

  • 日志记录:记录请求体的内容,便于调试和审计。
  • 预处理:在反序列化之前对请求体进行预处理,如解密或解码。
  • 验证:在反序列化之前对请求体进行验证,如检查 JSON 结构是否正确。
  • 默认值:处理空请求体并提供默认值。

1.2 ResponseBodyAdvice

ResponseBodyAdviceSpring MVC 框架中的一个接口,允许在响应体写入之前进行处理。它提供了一种方式,可以在 Spring MVC 将响应对象转换为 HTTP 响应体之前,对响应数据进行修改或处理。

1.2.1 源码定义如下:
public interface ResponseBodyAdvice<T> {

	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	@Nullable
	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);

}
  • supports():这个方法用于判断是否要对给定的返回类型和转换器类型应用当前的 ResponseBodyAdvice 实现。返回 true 表示要应用,返回 false 表示不应用。

  • beforeBodyWrite():这个方法在响应体写入之前调用,允许对响应数据进行修改或处理。它返回处理后的响应数据。

1.2.2 实际应用

ResponseBodyAdvice 可以用于多种应用场景,例如:

  • 数据加密:在将响应数据返回给客户端之前对其进行加密。
  • 数据格式转换:根据客户端的请求参数动态调整响应数据的格式,例如 XML 转 JSON。
  • 统一响应格式:对所有 API 接口的响应数据进行统一包装,确保一致的响应格式。
  • 响应头设置:在返回响应体之前设置或修改响应头,例如添加自定义头部信息。

2.项目实战案例

2.1 使用ResquestBodyAdvice对接口入参进行加解密和验签

之前我们在总结Spring Boot如何优雅提高接口数据安全性一文中强调了接口数据安全的重要性,特别是对外提供的open api接口,肯定是不能让接口数据裸奔的。强烈建议点击链接跳转之前总结的文章仔细看看,可以了解需求功能背景和之前的AOP切面实现,再来和今天我们使用RequestBodyAdvice的实现对比一下两者的优劣势,这样就掌握了两个知识点。

Spring Boot项目中提高接口安全的核心所在:加密和加签,加固接口参数、验证复杂度。

**加密:**对参数进行加密传输,拒绝接口参数直接暴露,这样就可以有效做到防止别人轻易准确地获取到接口参数定义和传参格式要求了。

**加签:**对接口参数进行加签,可以有效防止接口参数被篡改和接口参数被重放恶刷。

闲话少续,这里假如你没有点击链接查看之前的需求功能背景,所以我简要叙述下之前需求功能实现:就是使用非对称加密算法RSA和对称加密算法AES对接口出入参数进行加解密。为啥使用这两种加密算法呢?

AES 是对称加密算法,优点:加密速度快;缺点:如果秘钥丢失,就容易解密密文,安全性相对比较差

RSA 是非对称加密算法 , 优点:安全 ;缺点:加密速度慢

加解密

具体步骤如下:

  1. 客户端(调用接口方)随机生成AES加解密的密钥aes key,这里的AES密钥每次调接口都需要随机生成,可以有效提高安全性。
  2. 使用aes key对接口参数requestBody进行加密,data=base64(AES(json参数))
  3. 通过RSA加密算法加密aes key,有效保证aes算法的密钥的可靠安全性 key=base64(RSA(aes key))
  4. 经过上面的步骤,得到了加密后的业务参数及密钥,这时候就可以发送请求调用接口了
  5. 服务端接收到请求之后,先通过RSA算法对key进行解密获取到ase key, 再通过aes key解密data得到真正json参数,最后映射到接口方法的参数对象上,供controller的业务方法逻辑使用。
  6. 业务方法执行完成后,对响应参数进行加密,加密流程和上面的1、2、3一样
  7. 客户端收到响应参数之后,和步骤5一样解密响应参数,就拿到了真正的数据结果了。

加签

具体流程如下

  1. 对请求参数对象paramsortMap保证参数拼接的有序性,如果接口没有参数也没有关系,这里转成一个空的sortMap
  2. 按照约定拼接生成字符串content = sortMap + nonce + timestamp
  3. 使⽤SHA1WithRSA算法及私钥对concent进⾏签名sign
  4. 服务端判断timestamp是否超过签名有效期和nonce是否重复使用
  5. 服务端和步骤2一样规则生成字符串content
  6. 使⽤SHA1WithRSA算法及公钥对concentsign进行验签

强烈建议跳转Spring Boot如何优雅提高接口数据安全性去看看可以了解的更清楚些,有流程图那些,更详细。

代码实现

注解ApiSecurity

/**
 * @author fjzheng
 * @version 1.0
 * @date 2023/5/2 11:50
 *
 * 该注解用于标识 需要经过加密或者加签来加固接口安全性的接口
 */

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

    @Alias("isSign")
    boolean value() default true;

    /**
     * 是否加签验证,默认开启
     * @return
     */
    @Alias("value")
    boolean isSign() default true;

    /**
     * 接口请求参数是否需要解密
     * @return
     */
    boolean decryptRequest() default false;

    /**
     * 接口响应参数是否需要加密
     * @return
     */
    boolean encryptResponse() default false;
}

RequestBodyAdvice实现

@RestControllerAdvice
public class RequestBodyHandlerAdvice implements RequestBodyAdvice {
    @Resource
    private ApiSecurityProperties apiSecurityProperties;
    @Resource
    private StringRedisTemplate stringRedisTemplate;


    private static final String SIGN_KEY = "X-Sign";
    private static final String NONCE_KEY = "X-Nonce";
    private static final String TIMESTAMP_KEY = "X-Timestamp";


    /**
     *
     * @param methodParameter 包含控制器方法的参数信息
     * @param targetType  目标类型,即请求体将要转换成的 Java 类型
     * @param converterType 将要使用的消息转换器的类型
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(ApiSecurity.class)
                || AnnotatedElementUtils.hasAnnotation(methodParameter.getDeclaringClass(), ApiSecurity.class);
    }

    /**
     * 接口入参解密
     * @param inputMessage 包含 HTTP 请求的头和体
     * @param parameter  包含控制器方法的参数信息
     * @param targetType  目标类型,即请求体将要转换成的 Java 类型
     * @param converterType  将要使用的消息转换器的类型
     * @return   返回新的流
     * @throws IOException
     */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
                                           Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        ApiSecurity apiSecurity = getApiSecurity(parameter);
        // 判断接口参数是否需要解密
        boolean decryptRequest = apiSecurity.decryptRequest();
        if (!decryptRequest) {
            return inputMessage;
        }
        InputStream inputStream = inputMessage.getBody();
        String body = IoUtil.read(inputStream, StandardCharsets.UTF_8);
        if (StringUtils.isBlank(body)) {
            throw new BizException("请求参数body不能为空");
        }
        // 加密传参格式固定为ApiSecurityParam
        ApiSecurityParam apiSecurityParam = JSON.parseObject(body, ApiSecurityParam.class);
        // 通过RSA私钥解密获取到aes秘钥
        String aesKey = RSAUtil.decryptByPrivateKey(apiSecurityParam.getKey(), apiSecurityProperties.getRsaPrivateKey());
        // 通过aes秘钥解密data参数数据,即真正实际的接口参数
        String data = AESUtil.decrypt(apiSecurityParam.getData(), aesKey);

        // 加密传参ApiSecurityParam可以接收签名参数,这里把签名参数放到header里面,方便在后面afterBodyRead中验签
        HttpHeaders headers = inputMessage.getHeaders();
        String timestamp = apiSecurityParam.getTimestamp();
        if (StringUtils.isNotBlank(timestamp)) {
            headers.set(TIMESTAMP_KEY, timestamp);
        }
        String nonce = apiSecurityParam.getNonce();
        if (StringUtils.isNotBlank(nonce)) {
            headers.set(NONCE_KEY, nonce);
        }
        String sign = apiSecurityParam.getSign();
        if (StringUtils.isNotBlank(sign)) {
            headers.set(SIGN_KEY, sign);
        }

        // 使用解密后的数据构造新的读取流, Spring MVC后续读取解析转换为接口
        return new PtcHttpInputMessage(headers, data);
    }

    /**
     * 验签
     * @param body  已转换的 Java 对象,表示请求体的数据
     * 其余参数和上面的{@link #beforeBodyRead(HttpInputMessage, MethodParameter, Type, Class)} 一样
     */
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
                                Class<? extends HttpMessageConverter<?>> converterType) {
        ApiSecurity apiSecurity = getApiSecurity(parameter);
        boolean isSign = apiSecurity.isSign();
        if (!isSign) {
            return body;
        }
        // 验证签名sign
        verifySign(inputMessage.getHeaders(), body);
        return body;
    }

    /**
     * 和 {@link #afterBodyRead(Object, HttpInputMessage, MethodParameter, Type, Class)}一样
     * 只是这里处理body为空的这种情况,比如当body位空时,返回一个默认对象啥的
     */
    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
                                  Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return null;
    }

    ApiSecurity getApiSecurity(MethodParameter methodParameter) {
        ApiSecurity apiSecurity = methodParameter.getMethodAnnotation(ApiSecurity.class);
        return apiSecurity;
    }

    void verifySign(HttpHeaders headers, Object body) {
        // 如果请求参数是加密传输的,先从ApiSecurityParam获取签名和时间戳放到headers里面这里读取。
        // 如果请求参数是非加密即明文传输的,那签名参数只能放到header中
        String sign = headers.getFirst(SIGN_KEY);
        if (StringUtils.isBlank(sign)) {
            throw new BizException("签名不能为空");
        }

        String nonce = headers.getFirst(NONCE_KEY);
        if (StringUtils.isBlank(nonce)) {
            throw new BizException("唯一标识不能为空");
        }

        String timestamp = headers.getFirst(TIMESTAMP_KEY);
        if (StringUtils.isBlank(timestamp)) {
            throw new BizException("时间戳不能为空");
        }
        try {
            long time = Long.valueOf(timestamp);
            // 判断timestamp时间戳与当前时间是否超过签名有效时长(过期时间根据业务情况进行配置),如果超过了就提示签名过期
            long now = System.currentTimeMillis() / 1000;
            if (now - time > apiSecurityProperties.getValidTime()) {
                throw new BizException("签名已过期");
            }
        } catch (Exception e) {
            throw new BizException("非法的时间戳");
        }

        // 判断nonce
        boolean nonceExists = stringRedisTemplate.hasKey(NONCE_KEY + nonce);
        if (nonceExists) {
            //请求重复
            throw new BizException("唯一标识nonce已存在");
        }

        // 验签
        SortedMap sortedMap = SignUtil.beanToMap(body);
        String content = SignUtil.getContent(sortedMap, nonce, timestamp);
        boolean flag = RSAUtil.verifySignByPublicKey(content, sign, apiSecurityProperties.getRsaPublicKey());
        if (!flag) {
            throw new BizException("签名验证不通过");
        }

        stringRedisTemplate.opsForValue().set(NONCE_KEY+ nonce, "1", apiSecurityProperties.getValidTime(),
                TimeUnit.SECONDS);
    }
}

这里就是使用RequestBodyAdvice实现接口入参加解密和验签的,你可以和之前文章中使用aspectJ切面的实现,两者谁更优雅。

加密入参的数据格式:

@Data
public class ApiSecurityParam {

    /**
     * 应用id
     */
    private String appId;

    /**
     * RSA加密后的aes秘钥,需解密
     */
    private String key;

    /**
     * AES加密的json参数
     */
    private String data;

    /**
     * 签名
     */
    private String sign;

    /**
     * 时间戳
     */
    private String timestamp;

    /**
     * 请求唯一标识
     */
    private String nonce;

}

来看看调接口示例:

  @PostMapping("/security")
  @ApiSecurity(encryptResponse = true, decryptRequest = true)
  public User testApiSecurity(@RequestBody User user) {
       System.out.println(user);
       return user;
   }

执行输出如下:

可以看到对接口入参正常解密然后进行逻辑处理,最后对接口返回结果做了加密处理,这是在ResponseBodyAdvice中实现的,稍后我们会来分析的。

调整下请求测试接口,然后返回结果不加密直接返回明文,让我们来看看效果:

  @PostMapping("/security")
  @ApiSecurity(decryptRequest = true)
  public User testApiSecurity(@RequestBody User user) {
       System.out.println(user);
       return user;
   }

执行如下所示:

可以看出正常返回了明文结果,我们从接口方法定义知道就是简单地入参加密的user解密之后返回,验证无误。这里还展示返回结果的统一封装格式code, msg, data,这也是通过ResponseBodyAdvice实现的。有没有一种呼之欲出、按耐不住的感觉呀,有的话那么我们接下来就来看看它吧。

2.2 使用ResponseBodyAdvice对返回结果统一封装格式和加密

关于这个知识点之前就有总结过了:Spring Boot如何优雅实现结果统一封装和异常统一处理。可跳转查看,这里结合结果加密返回要求再来看看吧:

@RestControllerAdvice
@Slf4j
public class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> {
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private ApiSecurityProperties apiSecurityProperties;


    /**
     * 判断类或者方法是否使用了 @ResponseResultBody
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseResultBody.class)
                || returnType.hasMethodAnnotation(ResponseResultBody.class)
                || AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ApiSecurity.class)
                || returnType.hasMethodAnnotation(ApiSecurity.class);
    }

    /**
     * 当类或者方法使用了 @ResponseResultBody 就会调用这个方法
     * 如果返回类型是string,那么springmvc是直接返回的,此时需要手动转化为json
     * 因为当body都为null时,下面的非加密下的if判断参数类型的条件都不满足,如果接口返回类似为String,
     * 会报错com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String
     */
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        Method method = returnType.getMethod();
        Class<?> returnClass = method.getReturnType();
        Boolean enable = apiSecurityProperties.getEnable();
        ApiSecurity apiSecurity = method.getAnnotation(ApiSecurity.class);
        if (Objects.isNull(apiSecurity)) {
            apiSecurity = method.getDeclaringClass().getAnnotation(ApiSecurity.class);
        }
        if (enable && Objects.nonNull(apiSecurity) && apiSecurity.encryptResponse() && Objects.nonNull(body)) {
            // 只需要加密返回data数据内容
            if (body instanceof ResponseVO) {
                body = ((ResponseVO) body).getData();
            }
            JSONObject jsonObject = encryptResponse(body);
            body = jsonObject;
        } else {
            if (body instanceof String || Objects.equals(returnClass, String.class)) {
                String value = objectMapper.writeValueAsString(ResponseVO.success(body));
                return value;
            }
            // 防止重复包裹的问题出现
            if (body instanceof ResponseVO) {
                return body;
            }
        }
        return ResponseVO.success(body);
    }

    JSONObject encryptResponse(Object result) {
        String aseKey = AESUtil.generateAESKey();
        String content = JSONObject.toJSONString(result);
        String data = AESUtil.encrypt(content, aseKey);
        String key = RSAUtil.encryptByPublicKey(aseKey, apiSecurityProperties.getRsaPublicKey());
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("key", key);
        jsonObject.put("data", data);
        return jsonObject;
    }
}

该实现类实现逻辑大概是这样的:判断controller的接口方法或者类上有没有标注了注解@ResponseResultBody(控制返回结果统一格式)或者@ApiSecurity(控制接口参数加解密)。有的话进入方法#beforeBodyWrite()执行封装格式或者加密返回处理逻辑

碍于篇幅问题,涉及到一些非核心逻辑类,比如说工具类啥的这里就不一一展示,完整代码请移步仓库:https://github.com/plasticene/plasticene-boot-starter-parent

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

3.实现原理

当 Spring MVC 接收到一个 HTTP 请求时,流程大致如下:

  1. DispatchServlet 接收到请求。
  2. HandlerMapping 找到对应的处理器(Controller)。
  3. HandlerAdapter 调用对应的处理器方法。
  4. HttpMessageConverter 将请求体转换为处理器方法的参数对象。
  5. 处理器方法 执行并返回结果。
  6. HttpMessageConverter 将处理器方法的返回结果转换为响应体。
  7. 返回响应

大致执行流程图如下所示:

接下来我们通过上面场景示例调试代码来看看RequestBodyAdviceResponseBodyAdvice底层是怎么实现的

执行代码你会发现是AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters()方法在调用RequestBodyAdvice定义的相关方法

主要是在HttpMessageConverter对requestBody的前后做一些处理和body为空的时候做处理。

#getAdvice()返回是RequestResponseBodyAdviceChain:

#getMatchingAdvice(parameter, RequestBodyAdvice.class)方法获取所有的RequestBodyAdvice进行遍历执行对应的方法

private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
   // 获取RequestBodyAdvice类型的advice(此advice是我们定义实现RequestBodyAdvice接口的类)
    List<Object> availableAdvice = getAdvice(adviceType);
   if (CollectionUtils.isEmpty(availableAdvice)) {
      return Collections.emptyList();
   }
   List<A> result = new ArrayList<>(availableAdvice.size());
   for (Object advice : availableAdvice) {
      if (advice instanceof ControllerAdviceBean) {
         ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
         if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
            continue;
         }
         // 返回的是我们定义的Advice,即根据Bean的名称从BeanFactory中获取Bean对象
         advice = adviceBean.resolveBean();
      }
      // 判断这个类是否是RequestBodyAdvice类型,如果不是就不会加到结果集,所以这就是我们实现RequestBodyAdvice的原因
      if (adviceType.isAssignableFrom(advice.getClass())) {
         result.add((A) advice);
      }
   }
   return result;
}

这里的#getAdvice():

	private List<Object> getAdvice(Class<?> adviceType) {
		if (RequestBodyAdvice.class == adviceType) {
			return this.requestBodyAdvice;
		}
		else if (ResponseBodyAdvice.class == adviceType) {
			return this.responseBodyAdvice;
		}
		else {
			throw new IllegalArgumentException("Unexpected adviceType: " + adviceType);
		}
	}

看看this.requestBodyAdvice:

这里的RequestResponseBodyAdviceChain构造方法参数List<Object> requestResponseBodyAdvice是不是有点眼熟,不错就是在详解@ControllerAdvice的使用及其实现原理一文实现原理分析中提到的核心处理器适配器RequestMappingHandlerAdapter中收集生成的

@Override
public void afterPropertiesSet() {
	// Do this first, it may add ResponseBody advice beans
	//  初始化 ControllerAdvice 相关
	initControllerAdviceCache();

	// 初始化 argumentResolvers 属性
	// 把获取的ControllerAdviceBean集合赋值到调用的对象中,调用RequestBodyAdvice的地方
	if (this.argumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	// 初始化 initBinderArgumentResolvers 属性
	if (this.initBinderArgumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
		this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	// 初始化 returnValueHandlers 属性
	// 把获取的ControllerAdviceBean集合赋值到调用的对象中,调用ResponseBodyAdvice的地方
	if (this.returnValueHandlers == null) {
		List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
		this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
	}
}

#initControllerAdviceCache()扫描controllerBean,然后收集RequestBodyAdviceResponseBodyAdvice到集合中,也就是放到List<Object> requestResponseBodyAdvice

private void initControllerAdviceCache() {
	if (getApplicationContext() == null) {
		return;
	}

	// <1> 扫描 @ControllerAdvice 注解的 Bean 们,并将进行排序
	List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
	AnnotationAwareOrderComparator.sort(adviceBeans);

	List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

	//  遍历 ControllerAdviceBean 数组
	for (ControllerAdviceBean adviceBean : adviceBeans) {
		Class<?> beanType = adviceBean.getBeanType();
		if (beanType == null) {
			throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
		}
		// 扫描有 @ModelAttribute ,无 @RequestMapping 注解的方法,添加到 modelAttributeAdviceCache 中
		Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
		if (!attrMethods.isEmpty()) {
			this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
		}
		// 扫描有 @InitBinder 注解的方法,添加到 initBinderAdviceCache 中
		Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
		if (!binderMethods.isEmpty()) {
			this.initBinderAdviceCache.put(adviceBean, binderMethods);
		}
		// 如果是 RequestBodyAdvice 或 ResponseBodyAdvice 的子类,添加到 requestResponseBodyAdviceBeans 中
		if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
			requestResponseBodyAdviceBeans.add(adviceBean);
		}
		if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
			requestResponseBodyAdviceBeans.add(adviceBean);
		}
	}

	// 将 requestResponseBodyAdviceBeans 添加到 this.requestResponseBodyAdvice 属性种
	if (!requestResponseBodyAdviceBeans.isEmpty()) {
		this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
	}	
}

到这里RequestBodyAdvice的实现原理和流程就搞清楚了吧,关于ResponseBodyAdvice的实现流程套路基本一样的,

来到RequestResponseBodyAdviceChain的重写方法#beforeBodyWrite()

	@Override
	@Nullable
	public Object beforeBodyWrite(@Nullable Object body, MethodParameter returnType, MediaType contentType,
			Class<? extends HttpMessageConverter<?>> converterType,
			ServerHttpRequest request, ServerHttpResponse response) {

		return processBody(body, returnType, contentType, converterType, request, response);
	}
@Nullable
private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
      Class<? extends HttpMessageConverter<?>> converterType,
      ServerHttpRequest request, ServerHttpResponse response) {

   for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
      if (advice.supports(returnType, converterType)) {
         body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
               contentType, converterType, request, response);
      }
   }
   return body;
}

#getMatchingAdvice(returnType, ResponseBodyAdvice.class)方法和上面的一样,这里获取的是ResponseBodyAdvice.class

4.总结

RequestBodyAdviceResponseBodyAdviceSpring MVC 提供的一种扩展点钩子机制。

RequestBodyAdvice允许开发者在处理 HTTP 请求体之前或之后插入自定义逻辑。它通过与 HttpMessageConverter 紧密集成,在请求体读取和转换的过程中提供了扩展点。了解其工作原理有助于在复杂的请求处理场景中实现更强大的功能,如日志记录、数据预处理加解密和签名验证等。ResponseBodyAdvice 是一个强大的工具,允许在 Spring MVC 中对响应数据进行集中处理和修改。通过自定义 ResponseBodyAdvice 实现类,可以实现响应数据的加密、格式转换、统一包装等多种功能,提升代码的可维护性和一致性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/794343.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

minio在redhat7.9上面的单节点单驱动离线安装(docker)

问题 最近需要在红帽上面离线安装minio&#xff0c;并且还是要离线安装到服务器中的Docker里面去。 检查服务器磁盘 # lsblk -f NAME FSTYPE LABEL UUID MOUNTPOINT sda ├─sda1 xfs xxxxsx-xxx-xxx…

如何基于大模型开发应用接口

一、前言 针对自然语言处理方向&#xff0c;以前要开发一个情感分析、命名实体识别之列的应用是非常麻烦的&#xff0c;我们需要完成收集数据、清理数据、构建模型、训练模型、调优模型等一系列操作&#xff0c;每一步都非常耗时。如今大语言模型&#xff08;LLM&#xff09;的…

【BUG】已解决:JsonMappingException

已解决&#xff1a;JsonMappingException 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 概述&#xff1a; 没有getter方法的实体的序列化&#xff0c;并解决Jackson引发的JsonMappingException异常。 默认情况下&#xff0c;Jackson 2只会处理公有字段或具有公有get…

RK3588部署YOLOV8-seg的问题

在使用YOLOV8-seg训练出来的pt模型转为onnx的时候&#xff0c;利用以下仓库地址转。 git clone https://github.com/airockchip/ultralytics_yolov8.git 在修改ultralytics/cfg/default.yaml中的task&#xff0c;mode为model为自己需要的内容后&#xff0c; 执行以下语句 cd …

2006-2021年 291个地级市资源错配指数、劳动和资本相对扭曲指数do文件和结果

资源错配指数&#xff1a;衡量生产要素配置效率的关键指标 资源错配指数&#xff08;Misallocation Index&#xff09;是一个衡量资源配置效率的指标&#xff0c;它反映了生产要素是否得到了合理配置&#xff0c;以及是否达到了生产效率的最优状态。一个较高的资源错配指数意味…

视图库对接系列(GA-T 1400)十六、视图库对接系列(本级)通知(订阅回调)

说明 之前我们实现了订阅接口,其中有一个receiveAddr参数, 这个就是对应的回调的地址。一般情况下对应的是同一个服务。 我们推荐使用http://xxx:xxx/VIID/SubscribeNotifications接口文档 SubscribeNotificationList对象对象如下: 文档中是xml,但实际上目前使用的都是jso…

传音控股Android一面凉经(2024)

传音控股Android一面凉经(2024) 笔者作为一名双非二本毕业7年老Android, 最近面试了不少公司, 目前已告一段落, 整理一下各家的面试问题, 打算陆续发布出来, 供有缘人参考。今天给大家带来的是《传音控股Android一面凉经(2024)》。 面试职位: Android应用开发工程师(移动互联业…

Python酷库之旅-第三方库Pandas(017)

目录 一、用法精讲 41、pandas.melt函数 41-1、语法 41-2、参数 41-3、功能 41-4、返回值 41-5、说明 41-5-1、宽格式数据(Wide Format) 41-5-2、长格式数据(Long Format) 41-6、用法 41-6-1、数据准备 41-6-2、代码示例 41-6-3、结果输出 42、pandas.pivot函数 …

新旧电脑数据转移方法

随着科技的发展和电脑性能的不断提升&#xff0c;许多用户在工作和生活中都需要更换新电脑。当我们购买了一台新电脑后&#xff0c;如何将旧电脑中的数据转移到新电脑上成许多用户关注的问题。本文将详细介绍几种有效的电脑数据转移方法&#xff0c;帮助大家顺利完成数据迁移。…

5G AGV 安全演示:解决新欧盟机器指令挑战

2024 年 2 月 26 日至 2 月 29 日&#xff0c;2024 世界移动通信大会&#xff08;MWC2024&#xff09;在西班牙巴塞罗那召开。 这场以「未来先行」为主题的大会&#xff0c;吸引了大量通信领域企业和观众参加&#xff0c;现场盛况空前。 HMS Labs 在与易福门&#xff08;ifm&am…

07_Shell内置命令-declare

07_Shell内置命令-declare 一、设置变量属性 - 增加属性 取消属性 1.1、设置变量为整形变量 declare -i 变量名 #!/bin/bashage"abc" #设置变量为整形变量&#xff0c;这时候age如果不是整形&#xff0c;则置为0 declare -i age echo $age#并且设置非整形值无效, 只…

不会写提示词的,快下载这个“老六”插件(附插件)

在AI绘画的世界里&#xff0c;每一个细节都至关重要&#xff0c;面对复杂的提示词——SixGod_k插件&#xff0c;只需轻点即可获得提示词&#xff0c;sd-webui中文提示词插件、老手新手炼丹必备。 一、SixGod_k提示词的功能亮点 SixGod_k提示词不仅解决了提示词编写的难题&…

MybatisPlus 核心功能

MybatisPlus 核心功能 文章目录 MybatisPlus 核心功能1. 条件构造器1.1 QueryWrapper1.2 LambdaQueryWrapper&#xff08;推荐&#xff09;1.3 UpdateWrapper1.4 LambdaUpdateWrapper 2. 自定义SQL3. Service接口 1. 条件构造器 当涉及到查询或修改语句时&#xff0c;MybatisP…

8、matlab彩色图和灰度图的二值化算法汇总

1、彩色图和灰度图的二值化算法汇总原理及流程 彩色图和灰度图的二值化算法的原理都是将图像中的像素值转化为二值&#xff08;0或1&#xff09;&#xff0c;以便对图像进行简化或者特定的图像处理操作。下面分别介绍彩色图和灰度图的二值化算法的原理及流程&#xff1a; 1&a…

怎么录制网页上的视频?这篇文章告诉你

在数字化时代&#xff0c;网页视频已成为我们获取信息、学习知识和娱乐放松的重要途径。然而&#xff0c;有时我们可能想要保存或分享网页上的视频内容&#xff0c;却苦于没有合适的工具和方法。那么怎么录制网页上的视频呢&#xff1f;本文将详细介绍两种常见的网页视频录制方…

【基础算法总结】链表

链表 1.链表常用技巧和操作总结2.两数相加4.两两交换链表中的节点4.重排链表5.合并 K 个升序链表6.K 个一组翻转链表 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&…

一个开源完全免费的无损视频或音频的剪切/裁剪/分割/截取和视频合并工具

大家好&#xff0c;今天给大家分享一款致力于成为顶尖跨平台FFmpeg图形用户界面应用的软件工具LosslessCut。 LosslessCut是一款致力于成为顶尖跨平台FFmpeg图形用户界面应用的软件工具&#xff0c;专为实现对视频、音频、字幕以及其他相关媒体资产的超高速无损编辑而精心打造。…

LabVIEW电子水泵性能测试平台

开发了一种车用电子水泵性能测试平台&#xff0c;该平台以工控机为载体&#xff0c;利用LabVIEW开发上位机软件&#xff0c;采用PLC控制阀门和水泵等电气元件&#xff0c;通过RS485进行数据采集并传输到上位机。通过上位机与下位机的协同控制&#xff0c;实现了数据交互处理和性…

备考美国数学竞赛AMC8和AMC10:吃透1850道真题和知识点

距离接下来的AMC8、AMC10美国数学竞赛还有几个月的时间&#xff0c;实践证明&#xff0c;做真题&#xff0c;吃透真题和背后的知识点是备考AMC8、AMC10有效的方法之一。 通过做真题&#xff0c;可以帮助孩子找到真实竞赛的感觉&#xff0c;而且更加贴近比赛的内容&#xff0c;…

c语言题目之喝汽水问题

文章目录 一、题目二、思路三、代码实现3.1方法一3.1方法二 一、题目 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 二、思路 20元首先可以喝20瓶&#xff0c;此时手中有20个空瓶子两个空瓶子可以喝一瓶&#xff0c;喝完之后&#xff0c;空瓶子剩余&…