美文网首页
Spring boot 在项目中的应用(一)

Spring boot 在项目中的应用(一)

作者: 咦咦咦萨 | 来源:发表于2018-08-05 20:29 被阅读0次

    1. 介绍

    1.1 项目背景

    上周有一个紧急的项目,一个前置系统,需求是接收请求报文,验证解析后,对指定内容签名,再组织响应报文返回。

    1.2 技术点

    • Spring boot
    • RESTFul服务参数校验
    • 全局异常处理
    • 项目启动事件
    • swagger集成
    • 项目jar部署,优雅停机
    • Prometheus使用
    • Grafana使用

    2. 项目搭建

    2.1 项目依赖

    • Spring boot
      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.14.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
      </parent>
    
    • lombok
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
        </dependency>
    
    • fastjson
        <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjson</artifactId>
          <version>1.2.5</version>
        </dependency>
    

    2.2 集成Swagger

    • 2.2.1 加入项目依赖
        <dependency>
          <groupId>io.springfox</groupId>
          <artifactId>springfox-swagger2</artifactId>
          <version>2.7.0</version>
        </dependency>
    
        <dependency>
          <groupId>io.springfox</groupId>
          <artifactId>springfox-swagger-ui</artifactId>
          <version>2.7.0</version>
        </dependency>
    
    • 2.2.2 配置资源,解决web访问404
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    /**
     * web配置,解决swagger找不到文件404
     **/
    @Configuration
    public class WebConfig extends WebMvcConfigurerAdapter {
    
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
    
            registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
        }
    
    }
    
    
    • 2.2.3 开启swagger注解
    @EnableSwagger2
    public class TestController{}
    

    访问http://yourIp:port/swagger-ui.html即可。

    2.3 RESTFul服务

    • requestBean 请求实体
    public class RequestBean{
        private String centerId;
        // 其他参数……
    
        /**
         * Bean 转 JSON,转换名称
         */
        @JSONField(name = "center_id")
        public String getCenterId(){
            return this.centerId;
        }
    
        /**
         * JSON 转 Bean,转换名称
         */
        @JSONField(name = "center_id")
        public void setCenterId(String centerId){
            this.centerId = centerId;
        }
    }
    
    • controller
    @EnableSwagger2
    @RestController
    @Slf4j
    public class AgentController {
    
        /**
         * 签名服务
         *
         * @param requestBean 请求报文实例
         * @param bindingResult 参数校验结果
         */
        @ApiOperation("签名服务")  // swagger注解,设置API显示名称
        @PostMapping(value = "/sign")
        public ResponseBean sign(@Validated @RequestBody RequestBean requestBean,
            BindingResult bindingResult) {
            log.debug("接收到报文:{}", requestBean);
            // 处理异常情况
            if (bindingResult.hasErrors()) {
                // 处理异常情况
                return errorHandler(requestBean);
            }
            // 处理正常情况
            return normalHandler(requestBean);
        }   
    }
    
    

    2.3.1 使用hibernate-validator对请求参数校验

    • 请求实体校验,以RequestBean为例
    public class RequestBean{
        // 非空校验,演示用
        @NotBlank(message = "center_id不能为空")
        // 自定义注解,值约束。详见下文实现方法
        @ValueConstraint(allowedValues = {"center_001"}, message = "允许值:center_001")
        private String centerId;
        // 其他参数……
    
        /**
         * Bean 转 JSON,转换名称
         */
        @JSONField(name = "center_id")
        public String getCenterId(){
            return this.centerId;
        }
    
        /**
         * JSON 转 Bean,转换名称
         */
        @JSONField(name = "center_id")
        public void setCenterId(String centerId){
            this.centerId = centerId;
        }
    }
    
    
    • 自定义校验注解,约束传入之范围
    /**
     * 自定义校验实现类
     */
    public class ValueConstraintValidator implements ConstraintValidator<ValueConstraint, String> {
    
        private String[] validateValues;
    
        @Override
        public void initialize(ValueConstraint valueConstraint) {
            validateValues = valueConstraint.allowedValues();
        }
    
        @Override
        public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
            if (null != validateValues && validateValues.length > 0) {
                for (String value : validateValues) {
                    if (value.equals(s)) {
                        return true;
                    }
                }
            }
            return false;
        }
    }
    

    注解类

    @Target({ElementType.FIELD, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = ValueConstraintValidator.class)
    public @interface ValueConstraint {
    
        String[] allowedValues();
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        String message();
    }
    

    完成实体的约束注解后,如下图,通过@Validated注解对@RequestBody请求参数进行校验,通过BindingResult获取校验结果。


    controller.png

    2.3.2 解决HttpServletRequest inputStream只能读取一次的问题

    如果参数校验失败,我们可以通过BindingResult获取到失败结果,但是拿不到请求内容(可能有其他办法,但是我没有研究),如果通过过滤器拦截到request请求并且读取request body数据进行输出,在通过拦截器交给Spring去处理的时候,controller中通过@RequestBody获取JSON参数的接口抛出“Required request body is missing”的错误,这是因为HttpServletRequest inputStream只能读取一次。

    解决这个问题的办法就是通过集成HttpServletRequestWrapper,读区inputStream后进行缓存,然后将内容再写回去。

    public class ContentCachingRequestWrapper extends HttpServletRequestWrapper{
        // 缓存
        private byte[] body;
    
        private BufferedReader reader;
    
        private ServletInputStream inputStream;
    
        public ContentCachingRequestWrapper(HttpServletRequest request) throws IOException{
            super(request);
            loadBody(request);
        }
    
        // 读出数据后再写回
        private void loadBody(HttpServletRequest request) throws IOException{
            body = IOUtils.toByteArray(request.getInputStream());
            inputStream = new RequestCachingInputStream(body);
        }
    
        // 读区缓存方法,不能调用getInputStream()方法
        public byte[] getBody() {
            return body;
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (inputStream != null) {
                return inputStream;
            }
            return super.getInputStream();
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            if (reader == null) {
                reader = new BufferedReader(new InputStreamReader(inputStream, getCharacterEncoding()));
            }
            return reader;
        }
    
        private static class RequestCachingInputStream extends ServletInputStream {
    
            private final ByteArrayInputStream inputStream;
    
            public RequestCachingInputStream(byte[] bytes) {
                inputStream = new ByteArrayInputStream(bytes);
            }
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
    
            @Override
            public boolean isFinished() {
                return inputStream.available() == 0;
            }
    
            @Override
            public boolean isReady() {
                return true;
            }
    
            @Override
            public void setReadListener(ReadListener readListener) {
                throw new RuntimeException("Cannot handler readListener");
            }
    
    
        }
    
    }
    

    对应的filter

    @WebFilter(filterName = "LogFilter", urlPatterns = "/*")
    @Slf4j
    public class LogFilter implements Filter {
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            log.info("LogFilter init .....");
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
            FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
            ContentCachingRequestWrapper cachingRequestWrapper = new ContentCachingRequestWrapper(currentRequest);
    
            if(HttpMethod.POST.name().equalsIgnoreCase(currentRequest.getMethod())) {
                log.info("接收到POST请求: IP: {}, 路径: {}, 内容: {}", IPUtils.getIp(cachingRequestWrapper), cachingRequestWrapper.getRequestURI(), new String(cachingRequestWrapper.getBody()));
            }
            filterChain.doFilter(cachingRequestWrapper, servletResponse);
        }
    
        @Override
        public void destroy() {
            log.info("LogFilter destroy .....");
        }
    }
    
    

    2.3.3 全局异常处理

    @RestControllerAdvice
    @Slf4j
    public class GlobalExceptionHandler {
    
        @ExceptionHandler({HttpMessageNotReadableException.class, JSONException.class})
        @ResponseStatus(HttpStatus.BAD_REQUEST)
        @ResponseBody
        public FailResponseBean handlerBindException(Exception e) {
            log.error("参数校验异常", e);
            // 统一异常报文格式
            return ParamUtils.buidFailResponseBean(ErrorMessage.PARAM_PARSE_ERROR);
        }
    }
    

    参考&扩展:
    https://my.oschina.net/serge/blog/1094063
    https://blog.csdn.net/Swollow_/article/details/79942305

    相关文章

      网友评论

          本文标题:Spring boot 在项目中的应用(一)

          本文链接:https://www.haomeiwen.com/subject/wfopvftx.html