由于项目升级变更,开放给第三方接口参数中的时间参数由字符串类型变更为Long类型,为了第三方上传参数能像原来一样不受影响,解决的想法是在加一个过滤器,在请求体到达控制器之前,由过滤器将json对象里的时间字符串变更为时间戳。看起来简单,但实现过程中却遇到了不少问题。
首先定义一个类实现 Filter
接口:
/** * 将时间戳由字符串过滤为long */ @Component @Order(1) public class StringToLongOfTimeStampFilter implements Filter { private final static Logger logger = LoggerFactory.getLogger(StringToLongOfTimeStampFilter.class.getName()); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { logger.info(" 拦截链接" + ((HttpServletRequest) request).getRequestURI()); chain.doFilter(httpServletRequestWrapper, response); } }
接着提供过滤器:
@Autowired StringToLongOfTimeStampFilter stringToLongOfTimeStampFilter; @Bean public FilterRegistrationBean<StringToLongOfTimeStampFilter> loggingFilter() { // 添加强检器具拦截器 并配置拦截url FilterRegistrationBean<StringToLongOfTimeStampFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(stringToLongOfTimeStampFilter); registrationBean.addUrlPatterns("MandatoryInstrumentCheckApply/audit/*"); return registrationBean; }
当我们访问 MandatoryInstrumentCheckApply/audit
这个路径时,看到控制台打印信息,就完成了过滤器的配置。但我在这里就遇到了问题,因为我一开始是用的单元测试进行过滤器的配置,但是一直没有看到拦截的信息,找了很久才发现原因。
单元测试用的是mocmvn模拟请求,它需要单独配置拦截器:
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .addFilter(this.stringToLongOfTimeStampFilter, "MandatoryInstrumentCheckApply/audit/*") // 配置过滤器 .apply(MockMvcRestDocumentation.documentationConfiguration(this.restDocumentation)) .build();
参考链接
注册好过滤器后,下一步就是修改 ServletRequest
里的内容了,需要找到 ServletRequest
的 json
数据并修改相应的字段值。
首先,可以通过 HttpServletRequestWrapper
包装 ServletRequest
来读取请求内容,但问题是他只提供了读取的方法,并没有提供修改的方法,解决办法是定义内部类继承 HttpServletRequestWrapper
,覆盖原本的getInputStream方法,达到修改请求体的目的。
/** * 自定义请求包装类 * 由于HttpServletRequestWrapper 没有提供重写请求体的方法 * 因此使用自定义类继承HttpServletRequestWrapper 覆盖getInputStream()方法 * 已达到重写请求体目的 * https://stackoverflow.com/questions/34155480/how-to-change-servlet-request-body-in-java-filter */ class CustomRequestWrapper extends HttpServletRequestWrapper { // 缓冲请求体数据 private byte[] rawData; // HttpServletRequest private HttpServletRequest request; private ResettableServletInputStream servletStream; public CustomRequestWrapper(HttpServletRequest request) { super(request); this.request = request; this.servletStream = new ResettableServletInputStream(); } // 重置请求体数据 public void resetInputStream(byte[] newRawData) { servletStream.stream = new ByteArrayInputStream(newRawData); } // 覆盖getInputStream() 达到修改请求体目的 @Override public ServletInputStream getInputStream() throws IOException { if (rawData == null) { rawData = IOUtils.toByteArray(this.request.getReader()); servletStream.stream = new ByteArrayInputStream(rawData); } return servletStream; } @Override public BufferedReader getReader() throws IOException { if (rawData == null) { rawData = IOUtils.toByteArray(this.request.getReader()); servletStream.stream = new ByteArrayInputStream(rawData); } return new BufferedReader(new InputStreamReader(servletStream)); } class ResettableServletInputStream extends ServletInputStream { private InputStream stream; @Override public int read() throws IOException { return stream.read(); } @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } } }
自定义的内部类中提供了 resetInputStream
方法来修改请求体,所以在过滤器中可以使用自定义的内部类来实现请求内容的更改:
public String[] filterFileds = {"plannedCheckDate", "checkDate"}; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { CustomRequestWrapper wrappedRequest = new CustomRequestWrapper( (HttpServletRequest) request); String body = IOUtils.toString(wrappedRequest.getReader()); JSONObject oldJsonObject = JSON.parseObject(body); for (String filterFiled : filterFileds) { if (oldJsonObject.get(filterFiled) != null) { Date date = new Date((String) oldJsonObject.get(filterFiled)); oldJsonObject.put(filterFiled, date.getTime()); } } wrappedRequest.resetInputStream(oldJsonObject.toString().getBytes()); chain.doFilter(wrappedRequest, response); // 修改后传递自定义的HttpServletRequestWrapper }
参考链接
本来以为大功告成,但经过潘老师提示,这样实现起始并不好,如果以后还需要在其他的路由上添加相应的字符串变时间戳的操作,就得去改源代码,并在注册拦截器里添加拦截的路由。期待的状态是添加注解 @StringDateToTimestampAnnotation(value = "过滤字段", url = "过滤路由")
,只要控制器方法上带有这个注解,就能在请求这个方法时自动的进行过滤操作,不需要多余的行为。
解决方法是在启动时扫描根包下所有带有 @Controller
注解的类,在类的方法上查找 @StringDateToTimestampAnnotation
这个注解,提取出 value
和 url
,再注册到拦截器中。
spring提供了 ClassPathScanningCandidateComponentProvider
这个类来供我们扫描,修改过滤器注册代码:
@Bean public FilterRegistrationBean<StringToLongOfTimeStampFilter> loggingFilter() { // 扫描com.mengyunzhi.measurement 包 找到所有RestController 注解的类 ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); provider.addIncludeFilter(new AnnotationTypeFilter(Controller.class)); //添加包含的过滤信息 for (BeanDefinition beanDef : provider.findCandidateComponents("com.mengyunzhi.measurement")) { Class<?> cl = null; try { cl = Class.forName(beanDef.getBeanClassName()); // 查找RestController 注解类下所有方法 包含StringDateToTimestampAnnotation 提取出路径 for (Method method : cl.getDeclaredMethods()) { if (method.isAnnotationPresent(StringDateToTimestampAnnotation.class)) { StringDateToTimestampAnnotation annotation = method.getAnnotation(StringDateToTimestampAnnotation.class); // 提取路由和过滤字段 stringToLongOfTimeStampFilter.pushPathAndFiles(annotation.url(), annotation.value()); } } } catch (ClassNotFoundException e) { e.printStackTrace(); } } // 添加强检器具拦截器 并配置拦截url FilterRegistrationBean<StringToLongOfTimeStampFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(stringToLongOfTimeStampFilter); for (String url : stringToLongOfTimeStampFilter.getFilterPath()) { registrationBean.addUrlPatterns(url); } return registrationBean; }
在注册拦截器时,先扫描根包下带有 @Controller
的类,再找到带有 @StringDateToTimestampAnnotation
注解的方法,提取出 value
和 url
保存到过滤器中,注册的时候再从过滤器中获得所有要过滤的路径。
参考链接:
Component Scan for custom annotation on Interface
深入Spring:自定义注解加载和使用
Spring find annotated classes
问题是解决了,但是注解里的 url
实为冗余字段,因为它的值可以从 @RequestMapping
和 @GetMapping
、 @PutMapping
里就能获取到,应该把它去除。
重新再查找资料,发现spring可以从 RequestMappingHandlerMapping
里获取到映射路由与处理方法的集合,可以直接从此集合中获取注解值与映射路由,这样一来,也不用扫描类了,因为可以直接使用spirng的扫描结果。
@Autowired private RequestMappingHandlerMapping requestMappingHandlerMapping; @Bean public FilterRegistrationBean<StringToLongOfTimeStampFilter> loggingFilter() { Map<RequestMappingInfo, HandlerMethod> map = this.requestMappingHandlerMapping.getHandlerMethods(); for (RequestMappingInfo requestMappingInfo: map.keySet()) { HandlerMethod handlerMethod = map.get(requestMappingInfo); StringDateToTimestampAnnotation annotation; if ((annotation = handlerMethod.getMethodAnnotation(StringDateToTimestampAnnotation.class)) != null) { // 提取路由和过滤字段 for (String path: requestMappingInfo.getPatternsCondition().getPatterns()) { stringToLongOfTimeStampFilter.pushPathAndFiles(path, annotation.value()); } } } // 添加强检器具拦截器 并配置拦截url FilterRegistrationBean<StringToLongOfTimeStampFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(stringToLongOfTimeStampFilter); for (String url : stringToLongOfTimeStampFilter.getFilterPath()) { registrationBean.addUrlPatterns(url); } return registrationBean; }
本以为大功告成,但是它居然路由不匹配。。之前提供的路由是 MandatoryInstrumentCheckApply/audit/*
,它能够成功匹配 MandatoryInstrumentCheckApply/audit/2
,然而映射的路由是 MandatoryInstrumentCheckApply/audit/{id}
,提供到过滤器时居然不匹配。我其实在想,spring为什么能将 MandatoryInstrumentCheckApply/audit/{id}
和 MandatoryInstrumentCheckApply/audit/2
匹配上呢,如果修改过滤器路由匹配规则是不是就能匹配上了?然而找了半天也找不着修改过滤器路由匹配规则的办法。也不知道是没找着还是思路错了,只能暂时搁浅了。说到底,还是自己的能力太浅薄,对 spring
了解太浅。