Spring博文
我真的很小心了,但还是被 SpringEvent 坑了!
网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!
@ConfigurationProperties VS @Value,你觉得哪个更好用 - 掘金
一个注解就搞定接口统一返回、统一异常处理、加签、验签、加密、解密
SpringBoot3 优雅集成 Knife4j - 掘金
Spring Boot 3.x 中的 RestClient 使用案例-Spring专区论坛-技术-SpringForAll社区
Maven项目Parent,可以试试用这个代替 Spring Boot Parent
SpringBoot集成Logback终极指南:从控制台到云端的多维日志输出
Knife4j:实时接口文档的利器
Spring Boot的Docker Layer优化:缩小镜像体积并提升启动速度-Spring专区论坛-技术-SpringForAll社区
使用Prometheus和Grafana监控Spring Boot应用
Spring Boot 4 新特性详解:5大核心更新助力企业级开发
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
SpringBoot + SpringCloud Gateway + Sentinel + Redis:API 网关层的接口限流、黑名单拦截与用户认证
SpringBoot + Seata + MySQL + RabbitMQ:金融系统分布式交易对账与资金清算实战
SpringBoot + MyBatis-Plus + Elasticsearch + MySQL:电商商品搜索关键词高亮与库存实时展示
SpringBoot + RabbitMQ + MySQL + XXL-Job:物流系统运单状态定时同步与异常订单重试
本文档使用 MrDoc 发布
-
+
一个注解就搞定接口统一返回、统一异常处理、加签、验签、加密、解密
目前比较流行的架构大多都是前后端分离,后端提供服务能力,供前端调用打造业务场景功能。前后端根据定义好的协议报文进行数据交互,为了方便处理,一般报文都会是标准化格式,如下: ```json { "Code": "string", "ErrorMsg": "string", "Data": [ { "...": "...", "...": "..." } ] } ``` 上述`Data`才是每个接口返回的不一样的数据,这样标准格式,很方便调用方统一处理。但如果在每一个接口返回的地方都要做一次标准格式的处理,系统中大量存在重复代码。 下面我来给大家讲述通过Spring接口统一进行处理。 ## 认识`Spring`框架两个接口 `RequestBodyAdvice` 和 `ResponseBodyAdvice` 是`Spring`框架中的两个接口,主要用于实现全局的请求体(`RequestBody`)和响应体(`ResponseBody`)的处理,它们在`Spring MVC`中用于拦截和修改进入控制器方法的请求数据以及从控制器方法返回的响应数据。这两个接口的设计遵循了`Spring`的`AOP`(面向切面编程)思想,使得开发者可以在不改动原有业务逻辑的前提下,对数据进行统一的处理,比如日志记录、数据加密解密、结果封装等。 `RequestBodyAdvice` 是`Spring`框架提供的一个接口,主要用于拦截和处理进入控制器方法之前的`HTTP`请求体(`RequestBody`)。它是`Spring MVC`中处理请求体数据的一个灵活且强大的扩展点,允许开发者在请求体被实际的方法参数绑定之前对其进行预处理或者后处理。这对于日志记录、数据转换、安全性检查或是其他任何需要在请求体数据到达具体处理器方法前进行的操作非常有用。 `RequestBodyAdvice` 接口定义了以下四个方法: ```java 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); Object handleEmptyBody( Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType); } ``` - `supports(MethodParameter, Class<?>)`: 该方法用于判断当前的`RequestBodyAdvice` 实例是否支持给定的方法参数和目标类型。返回true表 示支持,此时`beforeBodyRead`、`afterBodyRead` 或 `handleEmptyBody` 方法会被调用;反之则不 - <`beforeBodyRead(HttpInputMessage, MethodParameter, Type, Class<?>):` 在读取请求体之前被调用,可以在这里修改输入消息,比如添加或修改HTTP头信息。 - `afterBodyRead(HttpInputMessage, MethodParameter, Type, Class<?>, Object):` 在请求体被读取并转换为对象之后被调用。开发者可以在此方法中对转换后的对象进行进一步的处 理或修改。 - `handleEmptyBody(HttpInputMessage, MethodParameter, Type, Class<?>):` 当请求体为空时被调用,允许自定义如何处理空请求体的情况。 `ResponseBodyAdvice` 是`Spring`框架提供的另一个接口,与`RequestBodyAdvice`相对应,它用于在响应体(`ResponseBody`)发送给客户端之前对其进行拦截和处理。这个接口允许开发者在控制器方法已经处理完业务逻辑并准备返回响应时,对响应内容进行修改、日志记录、数据转换等操作,而不必在每个控制器方法中重复相同的处理逻辑。 `ResponseBodyAdvice` 接口定义了以下两个方法: ```java public interface ResponseBodyAdvice<T> { boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType); T beforeBodyWrite( T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response); } ``` - `supports(MethodParameter, Class<?>):` 同`RequestBodyAdvice`中的方法,用于判断当前`ResponseBodyAdvice` 实例是否支持给定的方法参数和 返回类型。返回`true`表示将介入处理过程。 -`beforeBodyWrite(Object, MethodParameter, MediaType, Class<?>, ServerHttpRequest, ServerHttpResponse):` 在响应体写回客户端之前被调用。开发者可以在这个方法里修改响应体对象,转换其格式,或者基于响应内容做出其他处理。返回的对象将会被序列化并写入响应流中。 ## 请求结果返回统一格式封装 `supports`方法:判断是否要交给 `beforeBodyWrite` 方法执行,`ture`:需要;`false`:不需要。 `beforeBodyWrite`方法:对 `response` 进行具体的处理。 - 1)定义返回`Code`枚举类,定义好接口返回的`code`值和对应的错误描述,通过枚举类来实现。示例代码如下: ```java import lombok.AllArgsConstructor; import lombok.ToString; public enum CodeEnum { SUCCESS("001","成功"), FAIL("000","失败"), EXCEPTION("999","接口异常"); public final String code; public final String message; } ``` - 2)定义统一返回结果对象,定义一个统一返回结果对象 `ResultVO`,示例代码如下: ```java import com.holmium.framwork.core.enums.CodeEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.EqualsAndHashCode; import org.springframework.http.HttpStatus; public class ResultVO<T> { private Integer status; private String code; private String message; private T data; public ResultVO() { } public static ResultVO<?> success(Object data) { return build(CodeEnum.SUCCESS.code,CodeEnum.SUCCESS.message,data); } public static ResultVO<?> failed(String message) { return build(CodeEnum.FAIL.code,message,null); } private static ResultVO <?> build(String code,String message,Object data){ return ResultVO.builder() .status(HttpStatus.OK.value()) .code(code) .message(message) .data(data) .build(); } } ``` - 3)定义不需要统一封装返回结果注解,示例代码如下: ```java public NotResponseBodyAdvice { } ``` - 4)通过实现`ResponseBodyAdvic`接口实现统一返回,示例代码如下: ```java import com.holmium.framwork.core.response.Result; 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.servlet.mvc.method.annotation.ResponseBodyAdvice; public class ResponseBodyHandler implements ResponseBodyAdvice<Object> { public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class) || methodParameter.hasMethodAnnotation(NotResponseBodyAdvice.class)); } public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body instanceof ResultVO) { return body; } if (returnType.getGenericParameterType().equals(String.class)) { ObjectMapper objectMapper = new ObjectMapper(); try { return objectMapper.writeValueAsString(ResultVO.success(body)); } catch (JsonProcessingException e) { throw new RuntimeException(e.getMessage()); } } return ResultVO.success(body); } } ``` ## 全局统一异常处理 统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,`Http` 的状态码都要是 `200` ,尽可能由业务来区分系统的异常 自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。 ```java public class BusinessException extends RuntimeException { private String code; private String message; public BusinessException() { } public BusinessException(String message) { super(message); } public BusinessException(CodeEnum codeEnum) { this.code = codeEnum.code; this.message = codeEnum.message; } public BusinessException(String code, String message) { this.code = code; this.message = message; } } ``` ```java public class CustomizeException extends RuntimeException { public CustomizeException(String message, Throwable cause) { super(message, cause); } public CustomizeException(String message) { super(message); } } ``` ```java public class ParamException extends CustomizeException { private List<String> fieldList; private List<String> msgList; public ParamException(String message) { super(message); } public ParamException(String message, Throwable cause) { super(message, cause); } public ParamException(List<String> fieldList, List<String> msgList) throws CustomizeException { super(generatorMessage(fieldList, msgList)); this.fieldList = fieldList; this.msgList = msgList; } public ParamException(List<String> fieldList, List<String> msgList, Exception ex) throws CustomizeException { super(generatorMessage(fieldList, msgList), ex); this.fieldList = fieldList; this.msgList = msgList; } private static String generatorMessage(List<String> fieldList, List<String> msgList) throws CustomizeException { if (CollectionUtils.isEmpty(fieldList) || CollectionUtils.isEmpty(msgList) || fieldList.size() != msgList.size()) { return "参数错误"; } StringBuilder message = new StringBuilder(); for (int i = 0; i < fieldList.size(); i++) { String field = fieldList.get(i); String msg = msgList.get(i); if (i == fieldList.size() - 1) { message.append(field).append(":").append(msg); } else { message.append(field).append(":").append(msg).append(","); } } return message.toString(); } } ``` ```java public final class ServerException extends RuntimeException { private String code; private String message; public ServerException() { } public ServerException(String message) { super(message); } public ServerException(CodeEnum codeEnum) { this.code = codeEnum.code; this.message = codeEnum.message; } public ServerException(String code, String message) { this.code = code; this.message = message; } } ``` ```java public class GlobExceptionHandler { public ResultVO<?> handleBusinessException(BusinessException bx) { log.info("接口调用业务异常信息{}", bx.getMessage()); return ResultVO.failed( bx.getMessage()); } public ResultVO<?> paramExceptionHandler(ParamException ex) { log.info("接口调用异常:{},异常信息{}", ex.getMessage(), ex.getMessage()); return ResultVO.failed(ex.getMessage()); } public ResultVO<?> handle(Exception ex) { log.info("接口调用异常:{},异常信息{}", ex.getMessage(), ex.getMessage()); return ResultVO.failed(ex.getMessage()); } } ``` ## 请求响应结果加密 - 1)定义一个加密数据配置读取类,读取在配置文件中配置的加密秘钥,示例代码如下: ```java public class EncryptBodyConfig { private String aesKey; private String desKey; private Charset encoding = StandardCharsets.UTF_8; } ``` - 2)定义一个判断 类、方法、字段或方法参数是否带有指定类型(`支持的加密算法`)的注解,示例代码如下: ```java private boolean hasEncryptAnnotation(AnnotatedElement annotatedElement) { if (annotatedElement == null) { return false; } return annotatedElement.isAnnotationPresent(EncryptBody.class) || annotatedElement.isAnnotationPresent(AESEncryptBody.class) || annotatedElement.isAnnotationPresent(DESEncryptBody.class) || annotatedElement.isAnnotationPresent(RSAEncryptBody.class) || annotatedElement.isAnnotationPresent(MD5EncryptBody.class) || annotatedElement.isAnnotationPresent(SHAEncryptBody.class) || annotatedElement.isAnnotationPresent(SMEncryptBody.class); } ``` - 3)检查类、方法、字段或方法参数是否带有指定类型(`支持的加密算法`)的注解 ```java private EncryptAnnotationInfoBean getEncryptAnnotation(AnnotatedElement annotatedElement) { return EncryptAnnotationInfoBeanFactory.getEncryptAnnotationInfoBean(annotatedElement); } ``` - 4)选择加密方式并对字符串进行加密,示例代码如下: ```java private String switchEncrypt(String formatStringBody, EncryptAnnotationInfoBean infoBean) { EncryptBodyMethod method = infoBean.getEncryptBodyMethod(); if (method == null) { throw new EncryptMethodNotFoundException(); } String encrypt = EncryptBodyMethod.valueOf(method.toString()).getEncryptHandler().encrypt(infoBean, config, formatStringBody); if (config.isShowLog()) { log.info("加密前数据:{},加密后数据:{}", formatStringBody, encrypt); } return encrypt; } ``` - 5) 给`supports`方法设置返回值,根据返回值判断给定的`HTTP`消息转换器是否能支持处理这种类型的返回值,为`true`会进入下述`beforeBodyWrite`方法,否则不会,则示例代码如下: ```java public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { Class<?> declaringClass = returnType.getDeclaringClass(); if (this.hasEncryptAnnotation(declaringClass)) { encrypt =true; return true; } Method method = returnType.getMethod(); if (method != null) { Class<?> returnValueType = method.getReturnType(); if(this.hasEncryptAnnotation(method) || this.hasEncryptAnnotation(returnValueType)) { encrypt =true; return true; } } return returnType.getMethod().getReturnType() != ResponseEntity.class; } ``` - 6)用于在响应体写回客户端之前对响应内容进行修改或处理,在这个方法中调用加密方法,示例代码如下: ```java public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { log.info("Response返回前数据:{}", body); if (request.getURI().getPath().startsWith("/actuator")) { return body; } if (encrypt) { if (body == null) { return successBusiness(body); } if (body instanceof Result) { body = ((Result<?>) body).getData(); } if (body instanceof RequestBase) { ((RequestBase) body).setCurrentTimeMillis(System.currentTimeMillis()); } String str = CommonUtils.convertToStringOrJson(body, objectMapper); response.getHeaders().setContentType(MediaType.TEXT_PLAIN); Method method = returnType.getMethod(); String content = null; if (method != null) { EncryptAnnotationInfoBean methodAnnotation = this.getEncryptAnnotation(method); if (methodAnnotation != null) { return successBusiness(switchEncrypt(str, methodAnnotation)); } Class<?> methodReturnType = method.getReturnType(); EncryptAnnotationInfoBean returnTypeClassAnnotation = this.getEncryptAnnotation(methodReturnType); if (returnTypeClassAnnotation != null) { return successBusiness(switchEncrypt(str, returnTypeClassAnnotation)); } } EncryptAnnotationInfoBean classAnnotation = this.getEncryptAnnotation(returnType.getDeclaringClass()); if (classAnnotation != null) { return successBusiness(switchEncrypt(str, classAnnotation)); }else { throw new EncryptBodyFailException(); } } else { if (body instanceof ResponseEntity || returnType.hasMethodAnnotation(NotControllerResponseAdvice.class)) { return body; } else { return successBusiness(body); } } } ``` `注:可在这方法里增加需要公共处理的内容,比如:记录日志、加签等,可根据需要进行增加,统一处理。` ## 请求参数解密和验证 - 定义一个判断 类、方法、字段或方法参数是否带有指定类型(`支持的解密密算法`)的注解,示例代码如下: ```java private boolean hasDecryptAnnotation(AnnotatedElement annotatedElement) { return annotatedElement.isAnnotationPresent(DecryptBody.class) || annotatedElement.isAnnotationPresent(AESDecryptBody.class) || annotatedElement.isAnnotationPresent(DESDecryptBody.class) || annotatedElement.isAnnotationPresent(RSADecryptBody.class) || annotatedElement.isAnnotationPresent(SMDecryptBody.class); } ``` - 检查类、方法、字段或方法参数是否带有指定类型(`支持的解密算法`)的注解,示例代码如下: ```java private DecryptAnnotationInfoBean getDecryptAnnotation(AnnotatedElement annotatedElement) { return DecryptAnnotationInfoBeanFactory.getDecryptAnnotationInfoBean(annotatedElement); } ``` - 选择解密方式并对字符串进行解密,返回解密厚的 字符串。示例代码如下: ```java private String switchDecrypt(String formatStringBody, DecryptAnnotationInfoBean infoBean) { DecryptBodyMethod method = infoBean.getDecryptBodyMethod(); if (method == null) { throw new DecryptMethodNotFoundException(); } String decrypt = DecryptBodyMethod.valueOf(method.toString()).getDecryptHandler().decrypt(infoBean, config, formatStringBody); return decrypt; } ``` ```java public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { String typeName = targetType.getTypeName(); Class<?> bodyClass = Class.forName(typeName); return bodyClass.newInstance(); } ``` - 用于在处理HTTP请求的请求体(`RequestBody`)之前或之后自定义逻辑,示例代码如下: > 此方法应返回一个布尔值,指示给定的`RequestBodyAdvice`实例是否支持(或适用)于处理具有指定方法参数、目标类型和消息转换器类型的请求。如果返回`true`,则表示该`RequestBodyAdvice`的其他方法(如`beforeBodyRead`, `afterBodyRead`, `onError`)将被调用以参与到请求体处理的过程中;如果返回`false`,则表示此`Advice`不适用于当前请求处理场景,`Spring`将不会调用该`Advice`的其他逻辑。 ```java public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { if (this.hasDecryptAnnotation(methodParameter.getDeclaringClass())) { return true; } Method method = methodParameter.getMethod(); if (method != null) { if (this.hasDecryptAnnotation(method)) { return true; } Class<?>[] parameterTypes = method.getParameterTypes(); for (Class<?> parameterType : parameterTypes) { if (this.hasDecryptAnnotation(parameterType)) { return true; } } } return false; } ``` ```java public HttpInputMessage beforeBodyRead( HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { String body; try { body = IoUtil.read(inputMessage.getBody(), config.getEncoding()); } catch (Exception e) { throw new DecryptBodyFailException("Unable to get request body data," + " please check if the sending data body or request method is in compliance with the specification." + " (无法获取请求正文数据,请检查发送数据体或请求方法是否符合规范。)"); } if (body == null || StrUtil.isEmpty(body)) { throw new DecryptBodyFailException("The request body is NULL or an empty string, so the decryption failed." + " (请求正文为NULL或为空字符串,因此解密失败。)"); } Class<?> targetTypeClass; try { targetTypeClass = Class.forName(targetType.getTypeName()); } catch (ClassNotFoundException e) { throw new DecryptBodyFailException(e.getMessage()); } String decryptBody = null; DecryptAnnotationInfoBean methodAnnotation = this.getDecryptAnnotation(parameter.getMethod()); if (methodAnnotation != null) { decryptBody = switchDecrypt(body, methodAnnotation); } else if (this.hasDecryptAnnotation(targetTypeClass)) { DecryptAnnotationInfoBean classAnnotation = this.getDecryptAnnotation(targetTypeClass); if (classAnnotation != null) { decryptBody = switchDecrypt(body, classAnnotation); } } else { DecryptAnnotationInfoBean classAnnotation = this.getDecryptAnnotation(parameter.getDeclaringClass()); if (classAnnotation != null) { decryptBody = switchDecrypt(body, classAnnotation); } } if (decryptBody == null) { throw new DecryptBodyFailException( "Decryption error, " + "please check if the selected source data is encrypted correctly." + " (解密错误,请检查选择的源数据的加密方式是否正确。)"); } try { return new DecryptHttpInputMessage(IoUtil.toStream(decryptBody, config.getEncoding()), inputMessage.getHeaders()); } catch (Exception e) { throw new DecryptBodyFailException("该字符串转换为流格式异常。请检查诸如编码等格式是否正确。(字符串转换成流格式异常,请检查编码等格式是否正确。)"); } } ``` ```java public Object afterBodyRead( Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { if (body instanceof RequestBase) { Long currentTimeMillis = ((RequestBase) body).getCurrentTimeMillis(); long effective = 60 * 1000; long expire = System.currentTimeMillis() - currentTimeMillis; if (Math.abs(expire) > effective) { throw new ParamException("时间戳不合法"); } } else { throw new ParamException(String.format("请求参数类型:%s 未继承:%s", body.getClass().getName(), RequestBase.class.getName())); } return body; } ``` `注:可在这个接口类增加一些接口校验的方法,比如:验签、白名单验证、重复提交等等。` ## 最后 `Spring`框架设计了许多扩展接口,用于提供高度的灵活性和可定制性,允许开发者自定义和扩展其功能以满足特定需求。 1. `BeanFactoryPostProcessor`:允许在所有`bean`定义加载完成后但在`bean`实例化之前对`BeanFactory`进行修改。你可以通过这个接口来读取配置文件,修改`bean`定义,比如注入属性值等。 2. `ApplicationContextInitializer`:用于在`ApplicationContext`初始化之前执行自定义的初始化逻辑。这使得开发者能够在应用上下文启动之前进行配置或者准备性工作。 3. `ApplicationListener`:实现此接口的类可以监听`Spring ApplicationContext`中发布的事件。这使得你能够对特定事件作出反应,比如应用启动、停止事件等。 4. `InitializingBean`和 `DisposableBean`:这两个接口分别用于定义bean的初始化和销毁方法。`afterPropertiesSet()`方法在所有必需属性设置之后调用,`destroy()`方法在`bean`销毁时调用。 5. `SmartLifecycle`:如果你的`bean`需要参与`Spring`应用的启动和关闭的生命周期管理,比如需要延迟启动或按需启动的服务,可以实现此接口。 6. `ControllerAdvice`:虽然严格来说这不是一个扩展接口,而是一个用于全局异常处理和全局数据绑定的注解,但它提供了对所有控制器方法的集中处理能力,如异常统一处理、模型属性添加等。 7. `HandlerInterceptor`和 `WebMvcConfigurer`:这些接口用于扩展`Spring MVC`的功能,比如添加拦截器来处理请求前后逻辑,或者自定义视图解析器、消息转换器等。 8. `RequestMappingHandlerMapping`和 `RequestMappingHandlerAdapter`:可以通过扩展这些类来定制`Spring MVC`的请求映射和处理逻辑,比如改变默认的`URL`到控制器方法的映射规则。 9. `HttpMessageConverter`:实现此接口可以自定义`HTTP`消息的转换,支持不同的数据类型和媒体类型,如`JSON`、`XML`等。 10. `ResponseBodyAdvice`和 `RequestBodyAdvice`:这些接口允许你在响应体写回客户端之前或读取请求体之后进行处理,适合做日志记录、数据转换或安全检查等。 `后续我讲用 系列文章来讲述spring的一些常用的扩展接口,以及使用场景。`
admin
2024年7月28日 07:11
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码