共计 7204 个字符,预计需要花费 19 分钟才能阅读完成。
由于项目升级变更,开放给第三方接口参数中的时间参数由字符串类型变更为 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(request, 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
了解太浅。