Bootstrap

【微服务】springboot 自定义注解+反射+aop实现动态修改请求参数

目录

一、前言

二、动态修改接口请求参数的场景

2.1 动态修改请求参场景汇总

2.1.1 数据格式标准化

2.1.2 安全需要

2.1.3 参数校验与默认值设定

2.1.4 数据隐私保护

2.1.5 适配不同客户端

2.1.6 统计与监控

2.1.7 高级功能特性

三、springboot 使用过滤器和拦截器动态修改接口请求参数

3.1 使用过滤器动态修改请求参数

3.1.1 自定义 HttpServletRequest 包装类 RequestWrapper

3.1.2 自定义 HttpServletResponse 包装类 ResponseWrapper

3.1.3 自定义过滤器 ParamModifyFilter

3.1.4 测试接口

3.2 使用拦截器动态修改请求参数

3.2.1 自定义 HttpServletRequest 包装类 RequestWrapper

3.2.2 自定义CustomInterceptor

3.2.3 自定义过滤器

3.2.4 请求参数对象

3.2.5 测试接口

3.2.6 效果测试

四、springboot使用反射+aop实现动态修改请求参数

4.1 实现思路

4.2 代码实现过程

4.2.1 导入aop依赖

4.2.2 自定义注解

4.2.3 请求对象参数

4.2.4 自定义aop实现类

4.2.5 测试接口

4.2.6 测试效果展示

4.2.7 扩展补充点

五、写在文末


一、前言

在日常使用springboot的微服务项目开发中,可能会遇到这样的业务场景,针对某些到达服务端的接口请求参数,需要做预处理,比如对请求参数中的合法性、安全性进行过滤,再比如说,某些接口的业务,需要在请求到达接口之前进行特殊的赋值操作等,类似的业务场景还有很多,本文将分享如何基于自定义注解和aop的方式实现尽可能通用的解决方案。

二、动态修改接口请求参数的场景

2.1 动态修改请求参场景汇总

动态修改接口的请求参数在多种场景下可能是必要的,下面汇聚了一些常见的场景:

2.1.1 数据格式标准化

在不同客户端之间可能存在不同的数据格式,服务器需要统一处理这些格式差异。例如:

  • 日期时间格式统一:客户端提交的日期时间格式不一致,服务器需要统一为某种格式。

  • 枚举类型映射:客户端可能使用不同的枚举值表示相同的状态,服务器需要将这些不同的值映射为内部使用的统一枚举值。

2.1.2 安全需要

为了增强安全性,可能需要对请求参数进行加密、哈希或其他形式的安全处理:

  • 敏感信息加密:对于用户的密码、银行卡号等敏感信息,在传输前进行加密处理。

  • 防止注入攻击:对字符串类型的参数进行转义处理,防止 SQL 注入、XSS 攻击等安全问题。

2.1.3 参数校验与默认值设定

在请求参数到达业务逻辑层之前,对参数进行预处理,确保参数的有效性和一致性:

  • 参数校验:检查参数是否符合预期格式,如手机号、邮箱地址等。

  • 默认值设定:某些参数如果没有提供,则为其设置默认值。

2.1.4 数据隐私保护

在涉及用户隐私数据的情况下,可能需要对某些敏感字段进行脱敏处理:

  • 脱敏处理:例如电话号码、身份证号等信息部分替换为星号或其他字符。

2.1.5 适配不同客户端

不同客户端(如移动应用、Web 应用等)可能有不同的数据需求或格式偏好:

  • 适配不同客户端:根据客户端类型动态调整返回的数据格式或内容。

  • 多语言支持:根据客户端的语言偏好动态调整请求参数中的语言标识。

2.1.6 统计与监控

为了统计或监控的目的,可能需要在请求中附加额外的信息:

  • 添加跟踪信息:例如在请求中加入唯一标识符,方便后续的日志分析。

  • 记录来源信息:记录请求来源的 IP 地址、客户端类型等信息。

2.1.7 高级功能特性

某些高级功能可能需要特殊的参数处理机制:

  • 批处理请求:将多个请求合并为一个请求,减少网络开销。

  • 异步请求处理:对异步请求进行特殊处理,如设置回调地址。

三、springboot 使用过滤器和拦截器动态修改接口请求参数

如果将这个问题当作一个需求来看,在正式开始实现之前,建议全面的深入的思考一下你的解决方案是否合理,比如说:

  • 是为了解决某个特定的接口修改请求参数?

  • 针对某一类业务涉及到的所有接口均需要实现请求参数的修改?

  • 还是某一类参数涉及的接口需要修改呢?

其实不同的场景分类,实际在解决问题时使用的技术,以及技术实现的复杂程度、通用性等方面也是大不一样的,下面列举了在springboot开发中针对动态修改请求参数这个需求的常用实现思路。

3.1 使用过滤器动态修改请求参数

过滤器或拦截器在微服务的开发中可以说应用的场景非常多,利用过滤器或拦截器可以在请求到达接口之前做一些请求预处理相关的操作,比如拦截非法请求,参数XSS校验,对请求IP进行审计、拦截、限流等,也可以进行全局的会话凭证的校验等,针对请求参数的预处理或修改请求参数,也可以作为一个考虑和选择的方案,下面看具体的代码实现过程。

3.1.1 自定义 HttpServletRequest 包装类 RequestWrapper

在 Servlet 中,原始的 HttpServletRequest 对象中的请求流(即请求体)只能读取一次。这是因为 HTTP 协议是基于流的协议,服务器在读取请求流时会将其消耗掉,一旦读取完毕,就无法再次读取。当 Servlet 容器读取完请求流后,会将请求的内容解析并储存在相应的属性中,如请求参数、请求头信息等。在后续的处理过程中,Servlet 可以从这些属性中获取请求内容,而不必再次读取请求流。因此,我们需要自定义 RequestWrapper 将请求流保存下来,并提供方法来多次读取请求体的内容。

package com.congge.filter.v2;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
 * HttpServletRequest 包装类,允许在 Servlet 中多次读取请求体内容
 * 重写了 getInputStream()方法和 getReader() 方法,返回可以多次读取的流。
 */
public class OwnRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    /**
     * 构造 RequestWrapper 对象
     *
     * @param request 原始 HttpServletRequest 对象
     * @param context 请求体内容
     */
    public OwnRequestWrapper(HttpServletRequest request, String context) {
        super(request);
        this.body = context.getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 重写 getInputStream 方法,返回经过包装后的 ServletInputStream 对象
     *
     * @return 经过包装后的 ServletInputStream 对象
     */
    @Override
    public ServletInputStream getInputStream() {
        return new ServletInputStreamWrapper(new ByteArrayInputStream(body));
    }

    /**
     * 重写 getReader 方法,返回经过包装后的 BufferedReader 对象
     *
     * @return 经过包装后的 BufferedReader 对象
     */
    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
    }

    /**
     * 私有内部类,用于包装 ServletInputStream 对象
     */
    private static class ServletInputStreamWrapper extends ServletInputStream {
        private final ByteArrayInputStream inputStream;

        /**
         * 构造函数,传入待包装的 ByteArrayInputStream 对象
         *
         * @param inputStream 待包装的 ByteArrayInputStream 对象
         */
        public ServletInputStreamWrapper(ByteArrayInputStream inputStream) {
            this.inputStream = inputStream;
        }

        /**
         * 重写 read 方法,读取流中的下一个字节
         *
         * @return 读取到的下一个字节,如果已达到流的末尾,则返回-1
         */
        @Override
        public int read() {
            return inputStream.read();
        }

        /**
         * 覆盖 isFinished 方法,指示流是否已完成读取数据
         *
         * @return 始终返回 false,表示流未完成读取数据
         */
        @Override
        public boolean isFinished() {
            return false;
        }

        /**
         * 重写 isReady 方法,指示流是否准备好进行读取操作
         *
         * @return 始终返回 false,表示流未准备好进行读取操作
         */
        @Override
        public boolean isReady() {
            return false;
        }

        /**
         * 重写 setReadListener 方法,设置读取监听器
         *
         * @param readListener 读取监听器
         */
        @Override
        public void setReadListener(ReadListener readListener) {

        }
    }
}

3.1.2 自定义 HttpServletResponse 包装类 ResponseWrapper

与请求流(即请求体)一样,原始的 HttpServletResponse 对象中的响应流(即响应体)只能写入一次。当服务器在向客户端发送响应时,会将响应流写入到网络传输通道中,一旦写入完毕,就无法再次修改或写入。因此需要通过自定义 ResponseWrapper 包装原始的 HttpServletResponse 对象并重写其输出流或者输出写方法,从而实现对响应流的修改和控制。

package com.congge.filter.v2;

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;
import java.io.PrintWriter;
import java.nio.charset.Charset;

public class ResponseWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream outputStream;
    private ServletOutputStream servletOutputStream;
    private PrintWriter writer;

    /**
     * 构造函数,传入原始的 HttpServletResponse 对象
     *
     * @param response 原始的 HttpServletResponse 对象
     */
    public ResponseWrapper(HttpServletResponse response) {
        super(response);
        this.outputStream = new ByteArrayOutputStream();
    }

    /**
     * 重写 getOutputStream 方法,返回经过包装后的 ServletOutputStream 对象
     *
     * @return 经过包装后的 ServletOutputStream 对象
     */
    @Override
    public ServletOutputStream getOutputStream() {
        if (servletOutputStream == null) {
            servletOutputStream = new ServletOutputStreamWrapper(outputStream);
        }
        return servletOutputStream;
    }

    /**
     * 重写 getWriter 方法,返回经过包装后的 PrintWriter 对象
     *
     * @return 经过包装后的 PrintWriter 对象
     */
    @Override
    public PrintWriter getWriter() {
        if (writer == null) {
            writer = new PrintWriter(getOutputStream());
        }
        return writer;
    }

    /**
     * 获取响应数据,并指定字符集
     *
     * @param charsetName 字符集名称
     * @return 响应数据字符串
     */
    public String getResponseData(String charsetName) {
        Charset charset = Charset.forName(charsetName);
        byte[] bytes = outputStream.toByteArray();
        return new String(bytes, charset);
    }

    /**
     * 设置响应数据,并指定字符集
     *
     * @param responseData 响应数据字符串
     * @param charsetName  字符集名称
     */
    public void setResponseData(String responseData, String charsetName) {
        Charset charset = Charset.forName(charsetName);
        byte[] bytes = responseData.getBytes(charset);
        outputStream.reset();
        try {
            outputStream.write(bytes);
        } catch (IOException e) {
            // 处理异常
        }
        setCharacterEncoding(charsetName);
    }

    /**
     * 私有内部类,用于包装 ServletOutputStream 对象
     */
    private static class ServletOutputStreamWrapper extends ServletOutputStream {
        private final ByteArrayOutputStream outputStream;

        /**
         * 构造函数,传入待包装的 ByteArrayOutputStream 对象
         *
         * @param outputStream 待包装的 ByteArrayOutputStream 对象
         */
        public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream) {
            this.outputStream = outputStream;
        }

        /**
         * 重写 write 方法,将指定字节写入输出流
         *
         * @param b 字节
         */
        @Override
        public void write(int b) {
            outputStream.write(b);
        }

        /**
         * 重写 isReady 方法,指示输出流是否准备好接收写入操作
         *
         * @return 始终返回 false,表示输出流未准备好接收写入操作
         */
        @Override
        public boolean isReady() {
            return false;
        }

        /**
         * 重写 setWriteListener 方法,设置写入监听器
         *
         * @param writeListener 写入监听器
         */
        @Override
        public void setWriteListener(WriteListener writeListener) {

        }
    }
}

3.1.3 自定义过滤器 ParamModifyFilter

这里的需求是:

  • 请求到达接口之前,对请求参数进行修改;

  • 在响应返回之前,对响应结果进行处理;

请求参数进行修改,利用过滤器的实现思路如下:

  • 获取请求体内容;

  • 修改请求体内容;

  • 将修改后的请求对象替换原来请求对象,以便后续接口获取修改后的参数;

对响应结果的参数进行修改,利用过滤器的实现思路如下:

  • 获取响应数据;

  • 对响应数据进行处理;

  • 将修改后的数据作为最终结果返回;

最后,为了确保每个请求在请求时只会被过滤一次,这里可以通过继承 OncePerRequestFilter 来定义自己的过滤器,代码如下:

package com.congge.filter.v2;

import com.alibaba.fastjson.JSONObject;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@WebFilter(urlPatterns = "/aop/*",filterName = "myFilter")
public class ParamModifyFilter extends OncePerRequestFilter {

    static Map<String, Map> urlParamMap = new HashMap();

    static {
        Map paramMap = new HashMap();
        paramMap.put("name","mike");
        paramMap.put("address","guangzhou");
        urlParamMap.put("/aop/post/test",paramMap);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 1. 从 HttpServletRequest 对象中获取请求体内容
        String requestBody = getRequestBody(httpServletRequest);
        // 2. 解析请求体内容为JSON对象
        JSONObject jsonBody = JSONObject.parseObject(requestBody);
        // 3. 修改请求体内容
        String requestURI = httpServletRequest.getRequestURI();
        if(urlParamMap.containsKey(requestURI)){
            Map paramMap = urlParamMap.get(requestURI);
            paramMap.forEach((key,val)->{
                if(jsonBody.containsKey(key)){
                    jsonBody.put(String.valueOf(key),paramMap.get(key));
                }
            });
        }

        // 4. 包装 HttpServletRequest 对象为自定义的 RequestWrapper 对象,以便后续的处理
        OwnRequestWrapper requestWrapper = new OwnRequestWrapper(httpServletRequest, jsonBody.toJSONString());
        // 5. 包装 HttpServletResponse 对象为自定义的 ResponseWrapper 对象,以便后续的处理
        ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);
        // 6. 调用下一个过滤器或 Servlet
        filterChain.doFilter(requestWrapper, responseWrapper);
        // 7. 获取响应数据
        String responseData = responseWrapper.getResponseData(StandardCharsets.UTF_8.name());
        // 8. 解析响应数据为JSON对象
        JSONObject jsonData = JSONObject.parseObject(responseData);
        // 9. 在这里可以对响应数据进行处理
        jsonData.put("responseNewKey", "responseNewValue");
        // 10. 将修改后的 JSON 对象转换为字符串
        responseData = jsonData.toJSONString();
        // 11. 将修改后的 JSON 对象设置为最终的响应数据
        responseWrapper.setResponseData(responseData, StandardCharsets.UTF_8.name());
        // 12. 将响应数据写入原始的响应对象,解决响应数据无法被多个过滤器处理问题
        OutputStream outputStream = httpServletResponse.getOutputStream();
        outputStream.write(responseData.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
    }

    /**
     * 获取请求体内容。
     *
     * @param request HttpServletRequest对象
     * @return 请求体内容
     * @throws IOException 如果读取请求体内容时发生I/O异常
     */
    private String getRequestBody(HttpServletRequest request) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    }
}

补充说明:

在这段代码中,我们通过自定义过滤器的方式,拦截指定类型的接口,并获取接口中的参数,并对特定的接口中的请求参数进行修改,同时,也对接口执行完成之后的返回值进行修改

3.1.4 测试接口

添加一个测试接口

    @PostMapping("/aop/post/test")
    public Object testPost(@RequestBody UserRequest userRequest) {
        System.out.println("进入接口");
        String myParam1 = userRequest.getName();
        String myParam2 = userRequest.getAddress();
        System.out.println(myParam1 + ":" + myParam2);
        return new UserRequest(myParam1,myParam2);
    }

如果没有过滤器的情况下,接口的响应如下:

如果上述的过滤器生效之后,得到的响应结果如下,入参被修改了,同时返回结果也被修改了

3.2 使用拦截器动态修改请求参数

拦截器在使用上和过滤器有点类似,也是在请求到达接口之前生效,下面直接上代码,参照代码注释进行理解

3.2.1 自定义 HttpServletRequest 包装类 RequestWrapper

因为HttpServletRequest对象的body数据只能get,不能set,即不能再次赋值。而我们的需求是需要给HttpServletRequest赋值,所以需要定义一个HttpServletRequest实现类:customHttpServletRequestWrapper,这个实现类可以被赋值来满足我们的需求。

package com.congge.filter.v3;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
    // 保存request body的数据
    private String body;
    // 解析request的inputStream(即body)数据,转成字符串
    public CustomHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        body = stringBuilder.toString();
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
    public String getBody() {
        return this.body;
    }
    // 赋值给body字段
    public void setBody(String body) {
        this.body = body;
    }
}

3.2.2 自定义CustomInterceptor

拦截请求,获取接口方法相关信息(方法名,参数,返回值等)。从而实现统一的给request body动态赋值,实现思路如上所述,具体的实现代码如下:

package com.congge.filter.v3;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.util.ClassUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.yaml.snakeyaml.util.ArrayUtils;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Slf4j
public class CustomInterceptor implements HandlerInterceptor {

    static Map<String, Map> urlParamMap = new HashMap();

    static {
        Map paramMap = new HashMap();
        paramMap.put("userName","jerry");
        urlParamMap.put("/create",paramMap);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        pushUserInfo2Body(request, handlerMethod);
        return true;
    }

    private void pushUserInfo2Body(HttpServletRequest request, HandlerMethod handlerMethod) throws Exception{

        //获取请求参数
        String queryString = request.getQueryString();
        log.info("请求参数:{}", queryString);

        //获取请求body
        byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
        String body = new String(bodyBytes, request.getCharacterEncoding());
        CustomHttpServletRequestWrapper requestWrapper = (CustomHttpServletRequestWrapper) request;
        JSONObject jsonBody = JSONObject.parseObject(body);

        //执行参数修改
        String requestURI = request.getRequestURI();
        if(urlParamMap.containsKey(requestURI)){
            Map paramMap = urlParamMap.get(requestURI);
            paramMap.forEach((key,val)->{
                if(jsonBody.containsKey(key)){
                    jsonBody.put(String.valueOf(key),paramMap.get(key));
                }
            });
        }
        requestWrapper.setBody(JSON.toJSONString(jsonBody));
    }
}

3.2.3 自定义过滤器

自定义UserInfoFilter 过滤器,实现Filter接口,该类的作用是,在进入拦截器之前,将request中的参数封装到自定义的CustomHttpServletRequestWrapper中,以便后续在拦截器中可以对请求参数进行修改;

package com.congge.filter.v3;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;

@Slf4j
public class UserFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        CustomHttpServletRequestWrapper customHttpServletRequestWrapper = null;
        try {
            HttpServletRequest req = (HttpServletRequest)request;
            customHttpServletRequestWrapper = new CustomHttpServletRequestWrapper(req);
        }catch (Exception e){
            log.warn("customHttpServletRequestWrapper Error:", e);
        }
        chain.doFilter((Objects.isNull(customHttpServletRequestWrapper) ? request : customHttpServletRequestWrapper), response);
    }
}

3.2.4 请求参数对象

UserInfoParam

import lombok.Data;

@Data
public class UserInfoParam {
    private Long userId;
    private String userName;
}

3.2.5 测试接口

import lombok.Data;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TemplateController {

    @PostMapping(value = "/create")
    public Object create(@RequestBody RequestParam param) {
        return param;
    }
}

@Data
class RequestParam{
    private Long templateId;
    private String userId;
    private String userName;
}

3.2.6 效果测试

如下请求参数,预期请求接口之后返回的参数被修改掉

四、springboot使用反射+aop实现动态修改请求参数

通过上面的介绍,我们实现了使用过滤器或拦截器对接口请求参数的动态修改效果,整体来说,也是可以满足很多场景下的使用,而且具备一定的通用性,但是认真思考的同学可能会发现,这两种方式的实现,是基于对接口请求的前置操作,在一些高并发场景下,这种方式多少会带来一定的性能上的损耗,并且不够灵活,而且定制化程度不够高,接下来再介绍另一种实现方案,即采用自定义注解+aop的方式实现对特定接口请求参数的修改。

4.1 实现思路

整体实现思路如下:

  • 自定义接口中要修改的参数;

  • 在需要修改的接口上添加自定义注解,补充需要修改的参数;

  • 自定义aop实现,解析自定义注解,利用反射技术动态修改指定的字段,并修改为特定的值;

4.2 代码实现过程

4.2.1 导入aop依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

4.2.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 ModifyRequestParams {

    Param[] value();

    String requestClassName();

    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public static @interface Param {
        String name();
        String value();
    }

}

4.2.3 请求对象参数

后续在接口中,将会对里面的参数进行动态修改

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRequest {

    private String name;

    private String address;

}

4.2.4 自定义aop实现类

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
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.Map;

@Aspect
@Component
@Order(1) // 设置优先级,数值越小优先级越高
public class RequestParamModifierAspect {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Pointcut("@annotation(com.congge.aop.ModifyRequestParams)")
    public void pointParam(){

    }

    @Around("@annotation(modifyRequestParams)")
    public Object modifyRequestParams(ProceedingJoinPoint point, ModifyRequestParams modifyRequestParams) throws Throwable {
        Object result = null;
        String fullClassName = modifyRequestParams.requestClassName();
        Object[] argsArray = point.getArgs();
        List<String> modifyParams = new ArrayList<>();
        for (ModifyRequestParams.Param param : modifyRequestParams.value()) {
            modifyParams.add(param.name());
        }
        String paramStr = objectMapper.writeValueAsString(argsArray[0]);
        Map<String, Object> map = JSON.parseObject(paramStr, Map.class);
        for(Object param : argsArray){
            Class<?> clazz = param.getClass();
            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                String name = field.getName();
                if(modifyParams.contains(name)){
                    field.setAccessible(true);
                    //原来的值
                    Object oldVal = map.get(name);
                    String newVal = oldVal + "_change";
                    field.set(param, newVal);
                }
            }
        }
        result = point.proceed();
        return result;
    }
}

4.2.5 测试接口

添加一个测试接口,接口中使用自定义的注解,如果你的业务中,需要修改更多的请求参数,只需要在注解中添加即可,需要注意的是,注解中的请求参数名称和对象中定义的要保持一致

    //localhost:8081/aop/post/test
    @PostMapping("/aop/post/test")
    @ModifyRequestParams(value = {
            @ModifyRequestParams.Param(name = "address", value = "newValue1"),
            @ModifyRequestParams.Param(name = "name", value = "newValue2")
    },requestClassName = "com.congge.aop.UserRequest")
    public UserRequest testPost(@RequestBody(required = false) UserRequest userRequest) {
        System.out.println("进入接口");
        String myParam1 = userRequest.getName();
        String myParam2 = userRequest.getAddress();
        System.out.println(myParam1 + ":" + myParam2);
        return new UserRequest(myParam1,myParam2);
        //return "Received: " + myParam1 + ", " + myParam2;
    }

4.2.6 测试效果展示

请求一下上述接口,可以看到参数已经被修改了

4.2.7 扩展补充点

基于上述的实现,在实际业务中,还可以扩展出更丰富的场景,比如为那些默认的为空的参数赋初值,为时间字段根据时区动态赋值等,可以在上面的代码中继续完善。

五、写在文末

本文通过详细的案例操作演示了如何在springboot项目中对接口请求参数进行动态修改,如果在实际使用中,可以基于自身的需求场景酌情使用,并做代码上的持续完善,希望对看到的同学有用,本篇到此结束,感谢观看。

;