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

news2024/9/21 20:48:05

目录

一、前言

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

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项目中对接口请求参数进行动态修改,如果在实际使用中,可以基于自身的需求场景酌情使用,并做代码上的持续完善,希望对看到的同学有用,本篇到此结束,感谢观看。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql8 主从复制主从切换(超详细)

文章目录 1 主从复制1.1 实施前提1.2 主节点配置(在192.168.25.91操作)1.3 从节点配置(在192.168.25.92操作)1.4 创建用于主从同步的用户1.5 开启主从同步1.5 主从同步验证 2 主从切换2.1 实施前提2.2 主节点设置只读(在192.168.25.91操作)2.3 检查主从数据是否同步完毕(在192.…

Vue的冷门内置指令:优化与性能提升的利器

在Vue.js的广阔生态中&#xff0c;开发者们常常聚焦于那些耳熟能详的内置指令&#xff0c;如v-for用于循环渲染列表&#xff0c;v-if和v-else-if用于条件渲染等。然而&#xff0c;Vue还提供了一系列较为冷门但功能强大的内置指令&#xff0c;它们在某些特定场景下能够显著提升应…

ER模型介绍

7.1.概述&#xff1a; 1.ER模型也叫做实体关系模型&#xff0c;是用来描述现实生活中客观存在的事物、事物的属性&#xff0c;以及事物之间关系的一种数据模型。2.在开发基于数据库的信息系统的设计阶段&#xff0c;通常使用ER模型来描述信息需要和信息特性&#xff0c;帮助我…

云平台之Zabbix 监控网站

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

NMPC非线性模型预测控制经验分享与代码实例

NMPC非线性模型预测控制经验分享与代码实例 原本做完本科毕设之后就应该动笔写这一部分&#xff0c;但是做的过程中慢慢意识到自己懂的只是一点点。最近重新接触一些优化相关的问题&#xff0c;希望能够做出我认知之下比较好的解答。本人知识有限&#xff0c;难免写的有问题&am…

ClickHousez中如何定时清理过期数据库?

一、脚本清理 要在ClickHouse中自动删除过期的数据库&#xff0c;你可以使用ClickHouse的SQL命令结合外部脚本&#xff08;如Shell脚本&#xff09;和计划任务&#xff08;如cron&#xff09;来实现。下面是一个示例&#xff0c;展示如何创建一个Shell脚本来检查数据库的创建时…

1、.Net UI框架:Avalonia UI - .Net宣传系列文章

Avalonia UI是一个开源的跨平台UI框架&#xff0c;它允许开发者使用C#和XAML来创建应用程序&#xff0c;这些应用程序可以在多个平台上运行&#xff0c;包括Windows、macOS、Linux、Android和iOS。Avalonia UI的设计目标是提供一个现代化、可移植的UI框架&#xff0c;它具有类似…

C++之搜索二叉树(上)

目录 搜索二叉树的概念 搜索二叉树的操作 递归版本 二叉树的插入 二叉树的查找 二叉树的删除 非递归版本 二叉树的递归插入 二叉树的递归查找 二叉树的递归删除 在之前我们已经学习过了二叉树这一数据结构&#xff0c;本期我们将学习一种新的数据结构------搜索二…

Ubuntu服务器时间和本地时间不一致怎么解决——Linux的Local Time和RTC time

最近一直在搞大模型的相关工作&#xff0c;所以一直在用Linux服务器&#xff0c;前面的文章里也提到了&#xff0c;我用的是一台Dell PowerEdge R730xd。 但在使用中发现&#xff0c;IDRAC中的日志时间和本地时间存在时差&#xff0c;大概相关8小时。 对于技术人员&#xff0c…

UE5学习笔记19-服务器的更新频率,根骨骼旋转节点

一、服务器向客户端发送数据的频率 在Config中的DefaultEngine.ini文件添加 [/Script/OnlineSubsystemUtils.IpNetDriver] NetServerMaxTickRate60; 二、角色类中&#xff0c;角色蓝图类中在细节面板收缩net可以在界面中找到下面两个变量 NetUpdateFrequency 66.f; //净更…

代码随想录Day 32|leetcode题目:501.斐波那契数、70.爬楼梯、746.使用最小花费爬楼梯

提示&#xff1a;DDU&#xff0c;供自己复习使用。欢迎大家前来讨论~ 文章目录 动态规划理论基础一、理论基础1.1 什么是动态规划1.2 动态规划的解题步骤1.3 动态规划应该如何debug 二、题目题目一&#xff1a; 509. 斐波那契数解题思路&#xff1a;动态规划递归解法 题目二&a…

spring boot 项目 prometheus 自定义指标收集区分应用环境集群实例ip,使用 grafana 查询--方法耗时分位数指标

spring boot 项目 prometheus 自定义指标收集 auth author JellyfishMIX - github / blog.jellyfishmix.comLICENSE LICENSE-2.0 说明 网上有很多 promehteus 和 grafana 配置&#xff0c;本文不再重复&#xff0c;只介绍自定义部分。目前只介绍了分位数指标的收集和查询&a…

基于nodejs+vue+uniapp的摄影竞赛小程序

开发语言&#xff1a;Nodejs框架&#xff1a;expressuniapp数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;VS Code 系统展示 管理员登录 管理员主界面 用户管理 书籍分类管理 书籍信息管理 系统管理…

五种常见的人工智能错误以及如何避免它们?

目录 常见错误一&#xff1a;忘乎所以 常见错误二&#xff1a; 未能整合 常见错误三&#xff1a; 过于“以技术为中心”的方法 常见错误四&#xff1a;事后才考虑治理 常见错误五&#xff1a; 没有规模规划 对于肩负着为股东带来收益的重大赌注的高管来说&#xff0c;将人…

使用模块化流简化 RHEL 8 上的 NVIDIA 驱动程序部署

目录 DNF 模块化 使用预编译驱动程序 使用包管理器安装 选择模块化流 切换流 使用模块化配置文件 RHEL 的支持矩阵 概括 相关资源 NVIDIA GPU 已成为加速机器学习、高性能计算 (HPC)、内容创建工作流程和数据中心应用程序等各种工作负载的主流。对于这些企业用例&#xff0c;NV…

【个人笔记】VCS工具与命令

Title&#xff1a;VCS工具学习 一 介绍 是什么&#xff1f; VCS (Verilog Compiler Simulator) 是synopsys的verilog 仿真软件&#xff0c;竞品有Mentor公司的Modelsim、Cadence公司的NC-Verilog、Verilog—XL. VCS能够 分析、编译 HDL的design code&#xff0c;同时内置了 仿…

ubuntu环境下实现ROS 2 与 Arduino 通信

本教程为https://blog.csdn.net/2301_81924597/article/details/141757091?spm1001.2014.3001.5501的进一步拓展 ROS 2 与 Arduino 通信指南 准备工作 确保已安装 ROS 2&#xff08;本指南基于 ROS 2 Humble&#xff09;确保已安装 Arduino IDE 并能正常使用安装必要的 ROS…

系统架构师考试学习笔记第三篇——架构设计高级知识(10)系统质量属性与架构评估

本章知识点&#xff1a; 第10课时主要学习软件系统质量属性、系统架构评估以及ATAM方法评估实践等内容。 本课时内容侧重于概念知识&#xff0c;根据以往全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;。考试的出题规律&#xff0c;考查的知识点多来源于教材&a…

在Web服务应用中,如何编程使用Redis的缓存功能?包括缓存页面内容、缓存数据库查询结果、用户会话信息等代码分享

目录 一、概述 二、redis介绍 1、简介 2、Redis作为缓存的原理 &#xff08;1&#xff09;内存存储 &#xff08;2&#xff09;数据结构 &#xff08;3&#xff09;工作原理 3、Redis作为缓存的作用 三、redis缓存页面内容 1、作用 2、实现方法 3、示例代码&#x…

python07-单元测试框架unittest1-2

5 fixture 可以看作case的前置条件、后置条件 5.1 fixture的用例执行顺序 fixture分为 方法级别类级别模块级别 5.1.1方法级fixture 每个测试用例之前要调用setUp每个测试用例执行后要调用tearDowntestCase中有多少测试用例,那么setUp和tearDown就被调用多少次 def add(…