SpringBoot如何使用RequestBodyAdvice进行统一参数处理

SpringBoot RequestBodyAdvice参数处理

在实际项目中 , 往往需要对请求参数做一些统一的操作 , 例如参数的过滤 , 字符的编码 , 第三方的解密等等 , Spring提供了RequestBodyAdvice一个全局的解决方案 , 免去了我们在Controller处理的繁琐 .

RequestBodyAdvice仅对使用了@RqestBody注解的生效 , 因为它原理上还是AOP , 所以GET方法是不会操作的.

package com.xbz.common.web;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;

/**
 * @title 全局请求参数处理类
 * @author Xingbz
 * @createDate 2019-8-2
 */
@ControllerAdvice(basePackages = "com.xbz.controller")//此处设置需要当前Advice执行的域 , 省略默认全局生效
public class GlobalRequestBodyAdvice implements RequestBodyAdvice {

    /** 此处如果返回false , 则不执行当前Advice的业务 */
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
//        return methodParameter.getMethod().isAnnotationPresent(XXApiReq.class);
        return false;
    }

    /**
     * @title 读取参数前执行
     * @description 在此做些编码 / 解密 / 封装参数为对象的操作
     *
     *  */
    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return new XHttpInputMessage(inputMessage, "UTF-8");
    }

    /**
     * @title 读取参数后执行
     * @author Xingbz
     */
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return inputMessage;
    }

    /**
     * @title 无请求时的处理
     */
    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}

//这里实现了HttpInputMessage 封装一个自己的HttpInputMessage
class XHttpInputMessage implements HttpInputMessage {
    private HttpHeaders headers;
    private InputStream body;

    public XHttpInputMessage(HttpInputMessage httpInputMessage, String encode) throws IOException {
        this.headers = httpInputMessage.getHeaders();
        this.body = encode(httpInputMessage.getBody(), encode);
    }

    private InputStream encode(InputStream body, String encode) {
        //省略对流进行编码的操作
        return body;
    }

    @Override
    public InputStream getBody() {
        return body;
    }

    @Override
    public HttpHeaders getHeaders() {
        return null;
    }
}

Spring默认提供了接口的抽象实现类RequestBodyAdviceAdapter , 我们可以继承这个类按需实现 , 让代码更简洁一点

package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.lang.reflect.Type;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice {

	@Override
	public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType)
			throws IOException {
		return inputMessage;
	} 

	@Override
	public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
			Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {

		return body;
	}

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

Springboot 对RequestBody的值进行统一修改的几种方式

背景

最近在项目中遇到需要统一对Request请求中的某一个自定义对象的属性进行统一修改的需求。

考虑了几种实现方式,现在记录一下。由于原项目过于复杂,自己写几个demo进行记录。

解决方式

方式一:利用filter进行处理

大坑:

​ 如果你想要改变加了RequestBody注解的数据,无论如何你都要通过getInputStream()方法来获取流来拿到对应的参数,然后更改。在不经过拿取流的情况下,spring的RequestBody注解也是通过getInputStream()方法来获取流来映射为request对象。

但是如果你想要的统一的进行修改,也必须经过getInputStream()来首先拿到stream然后才能进行修改。但此时stream被消费之后,就会关闭。

然后你的controller中的参数就拿不到对象,报错如下。

I/O error while reading input message; nested exception is java.io.IOException: Stream closed

可以通过创建并使用自定义的的HttpServletRequestWrapper来避免这种情况。

步骤一:编写自定义HttpServletRequestWrapper

package com.example.testlhf.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.testlhf.entity.Student;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
/**
 * @Description TODO
 * @Author yyf
 * @Date 2020/10/29 12:48
 * @Version 1.0
 **/
@Slf4j
public class ChangeStudentNameRequestWrapper extends HttpServletRequestWrapper {
    /**
     * 存储body数据的容器
     */
    private byte[] body;
    public ChangeStudentNameRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        //接下来的request使用这个
        String bodyStr = getBodyString(request);
        body = bodyStr.getBytes(Charset.defaultCharset());
    }
    /**
     * 获取请求Body
     *
     * @param request request
     * @return String
     */
    public String getBodyString(final ServletRequest request) {
        try {
            return inputStream2String(request.getInputStream());
        } catch (IOException e) {
            log.error("", e);
            throw new RuntimeException(e);
        }
    }
    /**
     * 获取请求Body
     *
     * @return String
     */
    public String getBodyString() {
        final InputStream inputStream = new ByteArrayInputStream(body);
        return inputStream2String(inputStream);
    }
    /**
     * 将inputStream里的数据读取出来并转换成字符串
     *
     * @param inputStream inputStream
     * @return String
     */
    private String inputStream2String(InputStream inputStream) {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()));
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
        } catch (IOException e) {
            log.error("", e);
            throw new RuntimeException(e);
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    log.error("", e);
                }
            }
        }
        JSONObject jsonObject = JSONObject.parseObject(sb.toString());
        if (jsonObject != null && jsonObject.get("student") != null) {
            Student student = JSON.toJavaObject((JSON) jsonObject.get("student"), Student.class);
            log.info("修改之前的学生名称为:" + student.getName());
            student.setName("amd");
            jsonObject.put("student", student);
            return jsonObject.toJSONString();
        }
        return sb.toString();
    }
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}

步骤二:使用自定义的HttpServletRequestWrapper取代原有的

使用自定义的request取代原有的传递给过滤器链。

package com.example.testlhf.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
 * @Description TODO
 * @Author yyf
 * @Date 2020/10/29 13:20
 * @Version 1.0
 **/
@Slf4j
public class ReplaceStreamFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("StreamFilter初始化...");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
         //获取请求中的流,将取出来的字符串,再次转换成流,然后把它放入到新request对象中,
        if (request instanceof HttpServletRequest) {
            requestWrapper = new ChangeStudentNameRequestWrapper((HttpServletRequest) request);
        }
        // 在chain.doFiler方法中传递新的request对象
        if (requestWrapper == null) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }
    @Override
    public void destroy() {
        log.info("StreamFilter销毁...");
    }
}

步骤三:将过滤器注册进spring容器

package com.example.testlhf.filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
/**
 * @Description TODO
 * @Author yyf
 * @Date 2020/10/29 14:20
 * @Version 1.0
 **/
@Configuration
public class MyFilterConfig {
    /**
     * 注册过滤器
     *
     * @return FilterRegistrationBean
     */
    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>();
        registration.setFilter(replaceStreamFilter());
        registration.addUrlPatterns("/*");
        registration.setName("replaceStreamFilter");
        return registration;
    }
    /**
     * 实例化StreamFilter
     *
     * @return Filter
     */
    @Bean(name = "replaceStreamFilter")
    public Filter replaceStreamFilter() {
        return new ReplaceStreamFilter();
    }
}

看下效果:

到此使用过滤器对post请求中的参数的修改已经完毕。

方式二:使用拦截器进行处理

当我自以为可以使用拦截器前置通知进行处理时才发现,事情并不简单。

步骤一:自定义一个拦截器

如下图实现一个拦截器,preHandle中有HttpServletRequest request参数,虽然可以通过它的流获取到body中数据,但是如果将body中数据进行修改的话,其并不能传递给controller。因为request只有两个set方法。如果将要统一修改的值摄入Attribute,则还仍需从controller中拿到

步骤二:在controller中获取值

虽然用这种方式可以在request中添加统一的参数,也可以从每一个controller中获取值,但仍需要对每一个controller进行代码修改,显然这种方式并不是我们需要的。

方式三:使用切面处理

步骤一:引入aspect所需要使用的maven依赖

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.1.3.RELEASE</version>
        </dependency>

步骤二:编写自定义的前置通知以及表达

@Component
@Aspect
public class ChangeStudentNameAdvice {
    @Before("execution(* com.example.testlhf.service.impl.*.*(..))&&args(addStudentRequset)")
    public void aroundPoints(AddStudentRequset addStudentRequset) {
        addStudentRequset.getStudent().setName("amd");
    }
}

注意此处的形参需要和args括号内的字符串保持一致,否则报错。

注意此处的形参需要和args括号内的字符串保持一致,否则报错。

步骤三:开启注解@EnableAspectJAutoProxy

总结:

首先说下filter和interceptor的区别:两者之间的所依赖的环境不一致,filter作为javaWeb三大组件之一,其作用为:拦截请求,以及过滤相应。其依赖于servlet容器。但interceptor依赖于web框架,例如springmvc框架。最常见的面向切面编程AOP所使用的动态代理模式,即是使用拦截器在service方法执行前或者执行后进行一些操作。他们都可以适用于如下的场景:权限检查,日志记录,事务管理等等。当然包括,对所有的请求某些参数进行统一的修改。

比较三种方式,方式一和方式二所谓的拦截基本都是基于对http请求的拦截,filter执行在interceptor之前。虽然filter和interceptor都有类似链这种概念,但filter可以将request请求修改之后传递给后面的filter,就像电路中的串联,而interceptor的链是独立的,修改其中一个request并不会影响其他的interceptor,类似并联,不能做到只修改一处其他不用修改的方式。

简单来说方式一和方式二针对进入controller进行拦截,而后做一些操作。方式三使用的拦截的理念是针对业务方法的,在执行业务方法的前面对参数进行修改,和spring中对事务控制的实现方式类似。

思考:

虽然第一,第三种方式都可以在技术上实现针对某些方法进行统一的参数修改。但是如果将项目当做一个工程来思考的话,不同于日志打印或者事务控制这种非业务逻辑的处理,这种统一修改某些参数来完成一些操作,已严重入侵了业务逻辑。

真正的解决方式要么在请求的源头就做好参数设置,要么通过配置文件在需要使用的地方来进行某些参数的赋值。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 解决没有@RunWith 和 @SpringBootTest注解或失效问题

    导入别人的项目 或者 自己想创建一个测试类 经常会遇见了这个问题没有@RunWith 和 @SpringBootTest注解或失效 网上搜了搜 全是我下面的第一个解决方案 第二个才是重点 解决方案 1 添加依赖 如果 你是springboot项目 pom文件中添加 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</

  • springboot接口如何多次获取request中的body内容

    1. 概述 在使用springboot开发接口时,会将参数转化为Bean,用来进行参数的自动校验.同时也想获取request中原始body报文进行验签(防止报文传输过程中被篡改). 因为通过将bean再转化为字符串后,body里面的报文格式.字段顺序会发生改变,就会导致验签失败.因此只能通过request来获取body里面的内容. 既想接口自动实现参数校验,同时又想获取request中的原始报文,因此我们可以通过在controller中的restful方法中,写入两个参数,获取多次request

  • 使用@SpringBootTest注解进行单元测试

    概述 @SpringBootTest注解是SpringBoot自1.4.0版本开始引入的一个用于测试的注解.基本用法如下: 1. 添加Maven依赖 <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <parent> <groupId>org.springframework.boot</gro

  • SpringBoot使用@SpringBootTest注解开发单元测试教程

    概述 @SpringBootTest注解是SpringBoot自1.4.0版本开始引入的一个用于测试的注解.基本用法如下: 1.添加依赖: <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.

  • SpringBoot2.x 集成腾讯云短信的详细流程

    一.腾讯云短信简介 腾讯云短信(Short Message Service,SMS)沉淀腾讯十多年短信服务技术和经验,为QQ.微信等亿级平台和10万+客户提供快速灵活接入的高质量的国内短信与国际/港澳台短信服务. 国内短信验证秒级触达,99%到达率. 国际/港澳台短信覆盖全球200+国家/地区,稳定可靠. 单次短信的业务请求流程如下所示: 短信由签名和正文内容组成,发送短信前需要申请短信签名和正文内容模板.短信签名是位于短信正文前[]中的署名,用于标识公司或业务.短信签名需要审核通过后才可使用.

  • SpringBoot如何使用RequestBodyAdvice进行统一参数处理

    SpringBoot RequestBodyAdvice参数处理 在实际项目中 , 往往需要对请求参数做一些统一的操作 , 例如参数的过滤 , 字符的编码 , 第三方的解密等等 , Spring提供了RequestBodyAdvice一个全局的解决方案 , 免去了我们在Controller处理的繁琐 . RequestBodyAdvice仅对使用了@RqestBody注解的生效 , 因为它原理上还是AOP , 所以GET方法是不会操作的. package com.xbz.common.web;

  • 关于springboot的接口返回值统一标准格式

    目录 一.目标 二.为什么要对springboot的接口返回值统一标准格式? 第一种格式:response为String 第二种格式:response为Objct 第三种格式:response为void 第四种格式:response为异常 三.定义response的标准格式 四.初级程序员对response代码封装 步骤1:把标准格式转换为代码 步骤2:把状态码存在枚举类里面 步骤3:加一个体验类 五.高级程序员对response代码封装 步骤1:采用ResponseBodyAdvice技术来实

  • springboot断言异常封装与统一异常处理实现代码

    目录 步骤 1.异常类 2.统一异常处理配置类 3.断言类 4.使用 补充:异常处理器说明 handleServletException handleBindException handleValidException handleBusinessException.handleBaseException handleException 总结 步骤 1.异常类 package com.walker.dianping.common.exceptions; import lombok.Data; @

  • SpringBoot实现动态控制定时任务支持多参数功能

    由于工作上的原因,需要进行定时任务的动态增删改查,网上大部分资料都是整合quertz框架实现的.本人查阅了一些资料,发现springBoot本身就支持实现定时任务的动态控制.并进行改进,现支持任意多参数定时任务配置 实现结果如下图所示: 后台测试显示如下: github 简单demo地址如下: springboot-dynamic-task 1.定时任务的配置类:SchedulingConfig import org.springframework.context.annotation.Bean

  • Springboot使用@Valid 和AOP做参数校验及日志输出问题

    项目背景 最近在项目上对接前端的的时候遇到了几个问题 1.经常要问前端要请求参数 2.要根据请求参数写大量if...else,代码散步在 Controller 中,影响代码质量 3.为了解决问题1,到处记日志,导致到处改代码 解决方案 为了解决这类问题,我使用了@Valid 做参数校验,并使用AOP记录前端请求日志 1.Bean实体类增加注解 对要校验的实体类增加注解,如果实体类中有List结构,就在List上加@Valid @Valid注解 注解 备注 @Null 只能为null @NotNu

  • springboot中不能获取post请求参数的解决方法

    问题描述 最近在做微信小程序,用的spring boot做后端,突然发现客户端发送post请求的时候服务端接收不到参数.问题简化之后如下: 微信小程序端: 在页面放一个按钮进行测试 <!--index.wxml--> <view class="container"> <button catchtap='testpost'>点击进行测试</button> </view> 绑定一个函数发送post请求 //index.js //获

  • SpringBoot @ControllerAdvice 拦截异常并统一处理

    在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler.@InitBinder.@ModelAttribute,并应用到所有@RequestMapping中.参考:@ControllerAdvice 文档 一.介绍 创建 MyControllerAdvice,并添加 @ControllerAdvice注解. package com.sam.demo.controller; import org.springframework.ui

  • 详解SpringBoot配置文件启动时动态配置参数方法

    序言 当我们要同时启用多个项目而又要使用不同端口或者变换配置属性时,我们可以在配置文件中设置${变量名}的变量来获取启动时传入的参数,从而实现了动态配置参数,使启用项目更加灵活 例子 server: port: ${PORT:50101} #服务端口 spring: application: name: xc‐govern‐center #指定服务名 eureka: client: registerWithEureka: true #服务注册,是否将自己注册到Eureka服务中 fetchReg

  • SpringBoot在RequestBody中使用枚举参数案例详解

    前文说到 优雅的使用枚举参数 和 实现原理,本文继续说一下如何在 RequestBody 中优雅使用枚举. 本文先上实战,说一下如何实现.在 优雅的使用枚举参数 代码的基础上,我们继续实现. 确认需求 需求与前文类似,只不过这里需要是在 RequestBody 中使用.与前文不同的是,这种请求是通过 Http Body 的方式传输到后端,通常是 json 或 xml 格式,Spring 默认借助 Jackson 反序列化为对象. 同样的,我们需要在枚举中定义 int 类型的 id.String

  • SpringBoot实战之高效使用枚举参数(原理篇)案例详解

    找入口 对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑.我们从这里出发,一层一层向里扒. 跟着代码深入,我们会找到org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的

随机推荐