热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

springboot开发中遇到的问题

微服务迁移中遇到的一点问题,自定义HttpServletRequestWrapper解决读取请求体后后续报错

上周接手了一个微服务迁移的项目,即从原来的腾讯云迁移到阿里云的EDAS平台,这个项目非常的简单技术层面基本是没有什么可说的,接口也不多,而且都是使用请求阿里获取一些数据。但是随着和测试以及甲方的对接有了一些新的变化。

本来测试环境和生产是两套代码,区别在于测试环境是有数据库的,而生产环境没有数据库的(至于为啥生产环境不能加一个数据库….这个说来话长)。测试环境需要根据接口请求参数来判断是直接从数据库返回数据还是调用阿里接口获取数据后返回。原来的测试环境代码逻辑其实也简单,就是添加了一个拦截器获取请求参数,然后根据这个参数去查询数据库,数据库有结果直接返回,没有则需要调用阿里接口获取结果并返回。对接之后需求变了,首先测试环境和生产环境代码是一套,生产依然没有数据库,这就引起了一个很尴尬的问题(下面会详细说),还有就是本来测试环境的请求参数是一个额外添加的RequestParam
,现在没有这个参数了,需要根据接口的具体请求参数来进行判断,关键部分接口的参数是在RequestBody
…..本着尽量不改动原有代码的情况下,我决定继续使用原来的拦截器,接下来就是遇到了几个问题。

一、RequestBody丢失问题

第一个问题就是在拦截器我需要获取用户请求参数,这里可能会有一个疑问,拦截器不是能够拿到HttpServletRequest
吗,直接获取就行了呀,这么想确实没问题,但是如果是POST请求,且请求参数在RequestBody
中就会出现问题,看下面拦截器的preHandle
方法代码:

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info(">>>> requestInterceptor preHandle method start <<<<");
        String jsonRequest = HttpRequestUtil.getRequestBody(request);
        String requestParam = "";
        if (StringUtils.isNotBlank(jsonRequest)) {
            Map<String,String> requestMap = new Gson().fromJson(jsonRequest,Map.class);
            requestParam = requestMap != null ? requestMap.get("username") : "";
        } else {
            requestParam = request.getParameter("username");
        }
        if (StringUtils.isNotBlank(requestParam)) {
            User user = userRepository.findByUsername(requestParam);
            if (user != null) {
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
                response.getWriter().write(new Gson().toJson(user));
                return false;
            }
        }
        return true;
    }

上面的代码中使用一个工具类读取HttpServletRequest
的请求体,代码如下:

@Slf4j
public class HttpRequestUtil {

    public static String getRequestBody(HttpServletRequest request) {

        StringBuffer stringBuffer = new StringBuffer();
        try (ServletInputStream servletInputStream = request.getInputStream()){
            String line = null;
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(servletInputStream));
            while ((line = bufferedReader.readLine()) != null) {
                stringBuffer.append(line);
            }

        } catch (IOException e) {
            log.error(">>>> error occurred while get request inputStream, error message={} <<<<",e.getMessage());
            e.printStackTrace();
        }
        return stringBuffer.toString();
    }
}

如果在拦截器中获取到了相应的参数是没问题的,但是一旦preHandle方法返回true,即将HttpServletRequest
向后传递,那么就会出现问题:
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed

原因就是在拦截器已经读取了请求体中的内容,这时候请求的流中已经没有了数据,开始我只是以为是HttpRequestUtil中关闭流的问题,后面修改以后还是不行,报错信息是:
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity

是因为请求体丢失,也就是说HttpServletRequest
请求体中的内容一旦读取就不不存在了,所以直接读取是不行的。后面网上看到一种方案,就是使用一个自定义的包装类来实现,因此自定义一个包装类CustomRequestWrapper
继承HttpServletRequestWrapper
,代码如下:

public class CustomRequestWrapper extends HttpServletRequestWrapper {

    private byte[] requestBody;

    public CustomRequestWrapper(HttpServletRequest request) {
        super(request);
        requestBody = HttpRequestUtil.getRequestBody(request).getBytes();
    }

    public byte[] getRequestBody() {
        return requestBody;
    }
    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
        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(getInputStream()));
    }
}

这样成员变量requestBody
保存了请求体的内容,根据其构造函数可以看出,其先会调用父类构造,然后将HttpServletRequest
的请求体内容赋值给成员变量requestBody
这样似乎应该没问题了,毕竟赋值是调用父类构造之后进行的,只要在之后的过程中将自定义的CustomRequestWrapper
向后进行传递就行了,这么说好像没问题,但是实际上在拦截中没办法实现这点,这时候就需要引入一个Filter
,因为Filter
先执行这样能够保证在过滤的时候将HttpServletRequest
替换成我们自定义的CustomRequestWrapper
向后进行传递。定义一个Filter
,代码如下:

@Slf4j
@Component
@WebFilter(urlPatterns = {"/user/*"},filterName = "customFilter")
public class CustomFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info(">>>> customFilter init <<<<");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info(">>>> customFilter doFilter start <<<<");
        CustomRequestWrapper requestWapper = null;
        if (servletRequest instanceof HttpServletRequest) {
            requestWapper = new CustomRequestWrapper((HttpServletRequest) servletRequest);
        }

        if (requestWapper != null) {
            filterChain.doFilter(requestWapper,servletResponse);
        } else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }

    @Override
    public void destroy() {
        log.info(">>>> customFilter destroy <<<<");
    }
}

上面的代码中在自定义Filter
执行doFilter
时判断ServletRequest
是不是一个HttpServletRequest
实例,是的话,则创建一个自定义的CustomRequestWrapper
对象,并将其向后传递,这样之后的代码中我们获取到的HttpServletRequest
其实都是一个CustomRequestWrapper
对象。
这样应该就没什么问题了,接下来debug模式重启项目测试一下,我们现在过滤器看看创建的CustomRequestWrapper
和在拦截器中的HttpServletRequest
对象是不是一个,见图-1和图-2。

图-1.png


图-2.png


也就是说在自定义的过滤器之后,其实传递的都是CustomRequestWrapper
对象。这里需要说明一点,就是自定义的CustomRequestWrapper
中必须要重写getInputStream
getReader
这两个方法(这两个方法都是返回的请求体),不然依然无法获取到请求体的内容。当然我觉得最好参考这个代码实现。

另外这里还遇到了一个关于Filter
的小问题,根据自定义的代码可以看出来我配置的过滤路径是/user/*
,但是实际在启动日志中却并不是这样的,如下:

图-3.png


可见我配置的过滤路径并没有生效,依然是过滤/*
所有请求,虽然不影响功能的实现,但是我还是觉得还是根据具体需求来比较好。网上找资料说需要在启动类使用@ServletComponentScan
注解,指定basePackages
即自定义Filter
的包名或者使用basePackageClasses
指定具体的Filter
类即可,具体原因尚不清楚。

二、不同环境下服务启动

前面介绍了对接后的变更,这里还有个令人难受的问题,那就是生产环境和测试环境不同的问题,测试环境需要使用数据库,生产没有数据库。如果直接将现在代码部署到生产环境,服务是无法启动的,因为在服务启动过程会涉及到创建数据库链接,但是没有数据源。开始的想法是能不能在测试环境配置数据源,而在生产环境不配置,但是因为spring boot
是自动配置,那么可以禁用自动配置,即在测试环境使用自定义数据源配置,而在生产环境不指定。先按照生产环境代码优先的原则,先排除掉所有和数据库相关的自动配置,比如DataSourceAutoConfiguration
HibernateJpaAutoConfiguration
,代码如下:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
@ServletComponentScan(basePackageClasses = {CustomFilter.class})
public class NacosApplication {

    public static void main(String[] args) {
        SpringApplication.run(NacosApplication.class, args);
    }

}

但是启动过程依然报错,因为创建RequestInterceptor
时需要依赖UserRepository
,但是因为排除了HibernateJpaAutoConfiguration
,所以无法创建UserRepository
,当时想着要不然直接使用JdbcTemplate
算了,虽然需要自己写sql,这样就不会涉及到JPA的内容了。后来想起其实拦截器这部分代码只在测试环境使用,那问题就容易解决了,只要自定义拦截器和注册拦截器的配置类只在测试环境下创建就可以了,修改自定义拦截器和其注册配置类,代码如下:

@Slf4j
@Configuration
@Profile("dev")
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private RequestInterceptor requestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info(">>>> registry interceptor start <<<<");
        registry.addInterceptor(requestInterceptor).addPathPatterns("/user/**");
    }
}
// 拦截器
@Slf4j
@Component
@Profile("dev")
public class RequestInterceptor implements HandlerInterceptor {

    @Autowired
    private UserRepository userRepository;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info(">>>> requestInterceptor preHandle method start <<<<");

        CustomRequestWrapper requestWrapper = new CustomRequestWrapper(request);
        String jsonRequest = new String(requestWrapper.getRequestBody(), Charset.forName("UTF-8"));

        String requestParam = "";
        if (StringUtils.isNotBlank(jsonRequest)) {
            Map requestMap = new Gson().fromJson(jsonRequest,Map.class);

            requestParam = requestMap != null ? requestMap.get("username") : "";
        } else {
            requestParam = request.getParameter("username");
        }

        if (StringUtils.isNotBlank(requestParam)) {
            User user = userRepository.findByUsername(requestParam);
            if (user != null) {
                response.setContentType("application/json");
                response.setCharacterEncoding("UTF-8");
                response.getWriter().write(new Gson().toJson(user));
                return false;
            }
        }
        return true;
    }
}

当修改spring.profiles.active=prod
时启动服务,服务终于可以正常启动,日志如下:

图-4.png

可见启动日志中没有任何和数据库相关的内容,测试一下接口也是正常的。
但是改回spring.profiles.active=dev
的时候就无法启动了,因为在启动类上我们排除了

DataSourceAutoConfiguration
HibernateJpaAutoConfiguration
,所以必须修改启动类上的注解,即将:

(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
去掉。但是生产环境确实不能有这两个自动配置项,所以改为在生产环境配置文件,在

application-prod.properties
添加以下配置,排除两个自动配置项:

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration

然后切换dev环境启动服务,启动日志如下:

图-5.png

从上图可以看出启动过程中输出了HikariPool
Hibernate
相关日志,且调用接口也返回了测试数据,说明整个服务根据配置文件切换环境的功能完成了。

其实在解决这个问题的过程中也有同事建议我使用一个内存型的数据库,但是自己对这方面了解的比较少,因此没有按照他的思路去做,不知道可不可行。另外其实就功能实现上来讲我觉得使用JdbcTemplate
而不使用JPA应该也是可行的,但是我觉得尽量不动原来的代码比较好,所以还是按照原有方案解决了。
当然,实际工作中解决方案可能不止一种,感兴趣的话可以尝试下不同的解决方案,这样也可以加深对相关知识点的了解。



推荐阅读
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
  • 云原生应用最佳开发实践之十二原则(12factor)
    目录简介一、基准代码二、依赖三、配置四、后端配置五、构建、发布、运行六、进程七、端口绑定八、并发九、易处理十、开发与线上环境等价十一、日志十二、进程管理当 ... [详细]
  • Windows下配置PHP5.6的方法及注意事项
    本文介绍了在Windows系统下配置PHP5.6的步骤及注意事项,包括下载PHP5.6、解压并配置IIS、添加模块映射、测试等。同时提供了一些常见问题的解决方法,如下载缺失的msvcr110.dll文件等。通过本文的指导,读者可以轻松地在Windows系统下配置PHP5.6,并解决一些常见的配置问题。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • PDO MySQL
    PDOMySQL如果文章有成千上万篇,该怎样保存?数据保存有多种方式,比如单机文件、单机数据库(SQLite)、网络数据库(MySQL、MariaDB)等等。根据项目来选择,做We ... [详细]
  • 2018深入java目标计划及学习内容
    本文介绍了作者在2018年的深入java目标计划,包括学习计划和工作中要用到的内容。作者计划学习的内容包括kafka、zookeeper、hbase、hdoop、spark、elasticsearch、solr、spring cloud、mysql、mybatis等。其中,作者对jvm的学习有一定了解,并计划通读《jvm》一书。此外,作者还提到了《HotSpot实战》和《高性能MySQL》等书籍。 ... [详细]
  • 先看一段错误日志:###Errorqueryingdatabase.Cause:com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransie ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
  • Activiti7流程定义开发笔记
    本文介绍了Activiti7流程定义的开发笔记,包括流程定义的概念、使用activiti-explorer和activiti-eclipse-designer进行建模的方式,以及生成流程图的方法。还介绍了流程定义部署的概念和步骤,包括将bpmn和png文件添加部署到activiti数据库中的方法,以及使用ZIP包进行部署的方式。同时还提到了activiti.cfg.xml文件的作用。 ... [详细]
author-avatar
大众化的公爵樱桃rwr_208
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有