spring boot开发中遇到的问题

上周接手了一个微服务迁移的项目,即从原来的腾讯云迁移到阿里云的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中必须要重写getInputStreamgetReader这两个方法(这两个方法都是返回的请求体),不然依然无法获取到请求体的内容。当然我觉得最好参考这个代码实现。


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

图-3.png

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

二、不同环境下服务启动

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

@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<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;
    }
}

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

图-4.png

可见启动日志中没有任何和数据库相关的内容,测试一下接口也是正常的。
但是改回spring.profiles.active=dev的时候就无法启动了,因为在启动类上我们排除了DataSourceAutoConfigurationHibernateJpaAutoConfiguration,所以必须修改启动类上的注解,即将(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

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


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

最后:自己在微信开了一个个人号:超超学堂,都是自己之前写过的一些文章,另外关注还有Java免费自学资料,欢迎大家关注。

二维码.jpg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • SpringMVC介绍 Spring web mvc 和Struts2都属于表现层的框架,它是Spring框架的一...
    day_Sunny阅读 719评论 0 0
  • IOC 控制反转容器控制程序对象之间的关系,而不是传统实现中,有程序代码之间控制,又名依赖注入。All 类的创建,...
    irckwk1阅读 868评论 0 0
  • 对于java中的思考的方向,1必须要看前端的页面,对于前端的页面基本的逻辑,如果能理解最好,不理解也要知道几点。 ...
    神尤鲁道夫阅读 738评论 0 0
  • 16. Web MVC 框架 16.1 Spring Web MVC 框架介绍 Spring Web 模型-视图-...
    此鱼不得水阅读 983评论 0 4
  • Getting Started Burp Suite 是用于攻击web 应用程序的集成平台。它包含了许多工具,并为...
    Eva_chenx阅读 28,267评论 0 14