一、前言
在日常项目开发中,涉及到很多需要动态修改rest接口返回参数的场景,比如对接口中的字段统一脱敏,对接口中的某些字段进行二次加密处理,或者对某些特别的字段根据安全审计要求进行二次处理,甚至需要动态的在接口中增加额外的参数等,诸如此类的场景不胜枚举,本篇将介绍如何在springboot项目对接口返回结果进行动态修改。
二、动态修改接口返回结果实现方案总结
在springboot框架下,基于框架现有提供的技术组件,有很多种实现方式,下面分别展开来说。
2.1 使用反射动态修改返回结果参数
2.1.1 认识反射
Java的反射机制是在运行状态中,对于任意一个类,都能够获得这个类的所有属性和方法;对于任意一个对象,都能够调用它的任何方法和属性。这种动态获取类的信息以及动态调用方法的功能称为Java语言的反射(reflection)机制。
2.1.2 反射的作用
通过反射机制就能在程序运行时发现该对象和类的真实信息,利用这个机制,可以动态修改类对象中的参数信息,比如运行过程中对象参数的值。
2.1.3 反射相关的类
反射中常会涉及到下面几个概念
- Class类- 代表类的实体,在运行的Java应用程序中表示类和接口
- Field类- 代表类的成员变量/字段
- Method类- 代表类的方法
- Constructor类- 代表类的构造方法
2.1.4 反射实现接口参数动态修改实现思路
完整的实现思路如下:
- 获取接口返回值;
- 拿到上一步返回值中的结果集对象- 拆解结果集,通过反射,获取结果集中的对象实例,解析其中的字段- 获取字段的名称,字段的返回值- 根据业务需求,对指定的字段结果进行修改
伪代码如下:
public void modifyResult(List<T> result,String... params){
1、解析结果集
2、反射获取结果集实例
3、获取并解析结果集实例中的字段信息
4、结合入参,动态修改字段值,并重新设置到实例对象中
}
2.2 使用@ControllerAdvice 注解动态修改返回结果参数
2.2.1 注解作用
@ControllerAdvice 是 Spring Framework 提供的一个注解,它用于定义一个全局的异常处理器或跨切面行为(cross-cutting concern)。这个注解可以用来集中处理控制器中的一些公共关注点,如全局异常处理、数据绑定初始化等。主要作用如下:
全局异常处理
@ControllerAdvice 可以用来定义一个全局的异常处理器。当你在控制器中抛出了一个未被捕获的异常时,你可以定义一个带有 @ExceptionHandler 注解的方法来处理这个异常。这样可以避免在每个控制器或方法中重复定义相同的异常处理逻辑。
统一数据绑定初始化
除了异常处理外,@ControllerAdvice 还可以用来初始化数据绑定,这可以通过使用 @ModelAttribute 注解来实现。这种方法常用于在所有控制器方法调用前预先设置一些模型属性。
统一前置或后置处理
@ControllerAdvice 结合 @InitBinder 注解还可以用来定义全局的绑定初始化器和数据格式化器。此外,还可以使用 @ModelAttribute 注解来定义在所有控制器方法之前执行的前置处理方法,或者使用 @ModelAttribute 注解的方法来填充模型属性。
2.2.2 实现思路
使用@ControllerAdvice 注解实现接口返回值参数动态修改的思路如下:
- 解析返回结果;
- 反射获取结果中的对象实例;
- 修改对象参数;
补充说明:
如果仅仅是为了在返回的结果集增加参数,或者对某些固定参数进行处理,可以忽略反射这一步的操作
2.3 使用AOP动态修改返回结果参数
aop是一种很好的解决公共业务场景下通用问题的实现思路,像本次的需求,修改接口参数一般并不局限于某个具体接口,而是在很多场景下都可能用到,因此使用AOP来解决也是一种很好切入点,具体来说,主要实现思路如下:
- 自定义注解;- 注解中的参数可根据实际需要添加,比如可以添加需要修改的参数名称,修改后的类型等;
- 为需要修改结果集参数的接口添加上一步的自定义注解;
- 自定义AOP实现类,解析接口的参数,解析返回结果,利用反射技术,将结果集中需要修改的参数重新赋值;
三、动态修改接口返回结果操作实践
基于上面探讨的几种实现方案,接下来通过实战案例代码分别演示说明。
3.1 前置准备
创建一个springboot工程,并导入如下必要的依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.2 使用反射实现结果集参数动态修改
参考下面的操作步骤。
3.2.1 自定义反射工具类
完整的代码如下,实现思路:
- 方法接收一个泛型的对象T,和一组待修改的参数;
- 使用反射技术实例化对象T,拿到实例对象的字段信息;
- 循环遍历字段Field对象列表,然后进行参数值的重新赋值;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
public class ResultHandler {
public <T> void setUserInfo(T t, String... params) {
if (Objects.isNull(t)) {
log.error("t 参数为空");
return;
}
List<String> modifyParams = Arrays.stream(params).toList();
Class<? extends Object> tClass = t.getClass();
Field[] fields = tClass.getDeclaredFields();
Arrays.stream(fields)
.filter(item ->
modifyParams.contains(item.getName())
)
.collect(Collectors.toList()).
forEach(field -> {
field.setAccessible(true);
String fieldName = field.getName();
Object value = null;
try {
value = field.get(t);
field.set(t, value + "_change");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
}
3.2.2 测试接口继承工具类
如果你的接口需要动态修改返回值参数,可以继承上述工具类,如下:
@RestController
public class AviatorController extends ResultHandler{
//localhost:8081/aop/post/test
@PostMapping("/aop/post/test")
public UserRequest testPost(@RequestBody(required = false) UserRequest userRequest) {
System.out.println("进入接口");
setUserInfo(userRequest,"name");
return new UserRequest(userRequest.getName(),userRequest.getAddress());
}
}
在上面的接口中,在最终返回数据之前,调用工具类中的方法,传入返回值吗,剩下的交给工具类中的方法处理即可,启动工程之后,测试一下接口,可以看到,返回值中的name参数就被修改了
3.3 使用@ControllerAdvice实现结果集参数动态修改
3.3.1 @ControllerAdvice 简单介绍
@ControllerAdvice 是 Spring Framework 提供的一个注解,用于定义一个全局异常处理器或者跨切面的增强功能。它是一个特殊的切面(AOP Aspect),可以用于处理控制器(@Controller 或 @RestController)中的异常、数据绑定错误、模型属性预填充以及其他跨切面的关注点。@ControllerAdvice 注解通常用在需要对多个控制器进行统一处理的场景中,比如全局异常处理、数据验证失败处理、模型属性预填充等。
3.3.2 @ControllerAdvice 主要作用
@ControllerAdvice 主要有如下作用:
- 全局异常处理- 可以捕获所有控制器中抛出的异常,并提供统一的处理逻辑。- 使开发者能够集中处理异常,而不是在每个控制器中重复编写相同的异常处理代码。
- 数据绑定错误处理- 当数据绑定失败时,可以捕获
BindException
或MethodArgumentNotValidException
等异常,并进行统一处理。- 便于对前端传来的数据进行统一的校验和错误提示。 - 模型属性预填充- 可以在请求处理之前预先填充模型属性,比如当前时间、用户信息等。- 使得控制器方法更加简洁,减少重复代码。
- 跨切面关注点- 可以用来处理一些横切关注点,比如日志记录、安全检查等。- 通过 @ModelAttribute 或者自定义注解来实现。
3.3.3 @ControllerAdvice 用法
全局异常处理
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = {NullPointerException.class})
public ResponseEntity<Object> handleNullPointerException(NullPointerException ex) {
// 处理空指针异常
return ResponseEntity.status(400).body("Null pointer exception occurred: " + ex.getMessage());
}
@ExceptionHandler(value = {IllegalArgumentException.class})
public ResponseEntity<Object> handleIllegalArgumentException(IllegalArgumentException ex) {
// 处理非法参数异常
return ResponseEntity.status(400).body("Illegal argument exception occurred: " + ex.getMessage());
}
}
数据绑定错误处理
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class DataBindingExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
Map<String, String> errors = new HashMap<>();
result.getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
}
模型属性预填充
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
@ControllerAdvice
public class ModelAttributePrePopulator {
@ModelAttribute("currentUser")
public String getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication.getName();
}
}
3.3.4 @ControllerAdvice实现结果集参数动态修改
自定义一个类实现ResponseBodyAdvice接口,如下:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class DataChangeAdvice implements ResponseBodyAdvice {
static ObjectMapper objectMapper = new ObjectMapper();
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
Map res = new HashMap();
res.put("code",200);
//如果返回值是String,直接放到Result里
if (body instanceof String) {
res.put("data",(String) body);
return res;
}
//如果返回值是标准返回格式,就不需要再次封装了
//如果不加这个判断,异常的结果会被封装两次
else if (body instanceof Map) {
return body;
}
String dataStr = null;
try {
dataStr = objectMapper.writeValueAsString(body);
res.put("data",dataStr);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return res;
}
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
}
运行工程之后,测试一下上面的接口,可以看到,原本的接口返回值根据业务的需要重新做了修改
基于上述的改造,还可以继续扩展,比如通过自定义注解,在接口上面添加自定义注解,然后再在返回值中解析自定义注解,并根据实际的需要重新对注解中的参数进行修改。
3.4 使用自定义注解+AOP实现接口参数动态修改
在之前分享的一篇文章中,我们使用AOP+自定义注解的方式实现了请求参数的动态修改,使用这个方式是否也可以对接口返回的参数进行修改呢?
3.4.1 实现思路
参考下面的实现思路进行实现
- 自定义注解,- 属性主要包括:待修改的结果参数名称,修改的格式等;
- 自定义AOP类,对于那些标注了上述自定义注解的接口进行拦截;- 使用环绕通知的方式;
- 在AOP执行方法中调用point.proceed()获取目标方法的执行结果;- 在结果中,使用反射,结合解析到的自定义注解,从而动态修改接口的参数值;
3.4.2 自定义注解
自定义一个注解,用于接口中待修改的参数进行标注
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModifyResponseParams {
Param[] value() default {};
String dataFormat() default "";
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public static @interface Param {
String name();
String value() default "";
}
}
3.4.3 自定义AOP类
aop中的业务逻辑即可结合上面的实现思路进行理解,参考如下完整的示例代码,实现逻辑也是按照上述的实现思路进行构建
package com.congge.aop;
import com.congge.controller.R;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Aspect
@Component
@Order(1)
@Slf4j
public class ResponseParamModifierAspect {
@Around("@annotation(modifyResponseParams)")
public Object modifyRequestParams(ProceedingJoinPoint point, ModifyResponseParams modifyResponseParams) throws Throwable {
List<String> modifyParams = new ArrayList<>();
for (ModifyResponseParams.Param param : modifyResponseParams.value()) {
modifyParams.add(param.name());
}
Object t = point.proceed();
if (Objects.isNull(t)) {
log.error("接口返回结果为空");
return t;
}
//获取返回结果集并解析
R<?> r = (R<?>) t;
Object data = r.getData();
if (data instanceof List<?>) {
List<?> list = (List<?>) data;
list.forEach(item ->{
Class<?> tClass = item.getClass();
Field[] fields = tClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
if(modifyParams.contains(fieldName)){
try {
Object value = field.get(item);
field.set(item, value + "_change");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
});
}else {
Object t1 = data;
Class<? extends Object> tClass = t1.getClass();
Field[] fields = tClass.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
if(modifyParams.contains(fieldName)){
try {
Object value = field.get(t1);
field.set(t1, value + "_change");
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
return t;
}
}
3.4.4 测试接口一
在测试接口上面添加上述自定义注解,对需要修改的参数在注解中进行标注
@ModifyResponseParams(value = {
@ModifyResponseParams.Param(name = "address"),
@ModifyResponseParams.Param(name = "name")
})
@PostMapping("/aop/modify/v1")
public R testModifyV1(@RequestBody(required = false) UserRequest userRequest) {
System.out.println("进入接口");
return R.ok(new UserRequest(userRequest.getName(),userRequest.getAddress()));
}
使用接口工具调用一下,可以看到接口返回值中的两个参数被修改了
3.4.5 测试接口二
这一次,返回一个集合
@ModifyResponseParams(value = {
@ModifyResponseParams.Param(name = "address"),
@ModifyResponseParams.Param(name = "name")
})
@PostMapping("/aop/modify/v2")
public R testModifyV2(@RequestBody(required = false) UserRequest userRequest) {
System.out.println("进入接口");
List<UserRequest> userRequests =
Arrays.asList(new UserRequest(userRequest.getName(), userRequest.getAddress()));
return R.ok(userRequests);
}
使用接口工具调用一下,可以看到接口返回值中的两个参数被修改了
四、插件化封装
有了上面的实践之后,为了减少后续遇到类似的场景时的多次重复编码,可以考虑将上述AOP的实现方案使用springboot的starter机制进行插件化封装,参考如下操作步骤。
4.1 操作过程
4.1.1 创建maven工程
工程目录结构如下
4.1.2 导入依赖
主要包括下面几个核心依赖
<dependencies>
<!-- Spring框架基本的核心工具 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--阿里 FastJson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
4.1.3 代码迁移
将上一小节中的几个核心实现类拷贝过来到aop包下(略)
4.1.4 配置自动装配文件
在resources目录下,参考工程结构,创建配置文件 spring.factories ,将AOP的完整类路径名称配置进去
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.congge.aop.ResponseParamModifierAspect
4.1.5 使用maven命令安装jar包
执行mvn install 命令,将工程的jar安装到本地仓库中
4.2 功能测试
4.2.1 导入上一步的依赖
在需要的工程pom中导入上一步的依赖jar的maven坐标
<dependency>
<groupId>com.congge</groupId>
<artifactId>aop_com</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
4.2.2 接口改造
原本的接口代码逻辑保持不变,只需要将自定义注解改为上一步的注解即可
4.2.3 接口测试
启动工程之后再次调用上述接口,接口返回值参数被修改了,说明插件包中的逻辑正常生效了
五、写在文末
本文通过案例和操作详细介绍了如何在微服务项目中实现对接口返回值的参数修改,在实际应用中,可以结合案例中的思路以及自身的需求场景进行深度的拓展,希望对看到的同学有用,本篇到此结束,感谢观看。
版权归原作者 小码农叔叔 所有, 如有侵权,请联系我们删除。