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
网友评论