此文接上篇拦截器实现增删改操作的日志管理;由于上篇文章讲述得较为简略,这里做下详细说明。
项目的数据库设计有表:
menu
用来前端管理平台的树形菜单渲染,而element
(表中有属性url:用来对应访问的接口的url)用来呈现前端的各个接口按钮;也每个角色有对应的meun
和element
,menu
与element
是一对多的关系,此时实现日志管理就有如下较为简便的操作了,原理大致是:通过访问的url获取element
与menu
,插入日志表中,看起来很简单,但由于个人较差,遇到了不少阻碍。
本文只提供思路,不提供完整代码实现。
首先是拦截器:
/**
* @Author fuzihao
* @Date 2019/8/27 16:11
*/
@Slf4j
public class OperationLogInterceptor extends HandlerInterceptorAdapter {
private ILogService logService = (ILogService) SpringContextUtil.getBean("logServiceImpl");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if(handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
String methodType = request.getMethod();
if(StringUtils.equalsIgnoreCase(HttpMethodEnum.POST.name(), methodType)
|| StringUtils.equalsIgnoreCase(HttpMethodEnum.DELETE.name(), methodType)
|| StringUtils.equalsIgnoreCase(HttpMethodEnum.PUT.name(), methodType)){
insertLog(request,method);
}
}
return true;
}
/**
* 根据http请求类型插入日志记录
* @param request
* @param handlerMethod
*/
private void insertLog(HttpServletRequest request,HandlerMethod handlerMethod) throws Exception{
//即通过request获取url,而通过访问的url,来获取操作的element和menu,用以插入日志表
getMenuOperation(request);
//同时,项目还需要获取RequestBody注解的参数(即put、post请求等的参数,由于不在url上,需要手动获取)
//注意:这里埋下了雷
String requestBody = new BodyReaderHttpServletRequestWrapper(request).getBodyString();
}
}
}
拦截器也在WebConfig
里配置,上篇文章也提到了,这里不做赘述。
public String getBodyString(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString().trim();
}
此时启动服务,当post
、put
请求时,报错;org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing。
其实原因也很简单,post
、put
请求中的body
参数其实是以流形式存在的,而httpServletRequest
流中数据只能读取一次
(具体原因:源自httpServletRequest流数据只能读取一次原因),
java InputStream read方法内部有一个,postion,标志当前流读取到的位置,每读取一次,位置就会移
动一次,如果读到最后,InputStream.read方法会返回-1,标志已经读取完了,如果想再次读取,可以调
用inputstream.reset方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读
了。
当然,能否reset是有条件的,它取决于markSupported,markSupported() 方法返回是否可以mark/reset
我们再回头看request.getInputStream
request.getInputStream返回的值是ServletInputStream,查看ServletInputStream源码发现,没有重写
reset方法,所以查看InputStream源码发现marksupported 返回false,并且reset方法,直接抛出异常。
综上所述,在request.getinputstream读取一次后position到了文件末尾,第二次就读取不到数据,由于无
法reset(),所以,request.getinputstream只能读取一次。
我们在拦截器中,在getBodyString
操作中提前读取流,对参数进行处理,再次跳转到controller
的时候,流已经读取不到了,就会报Required request body is missing 异常。
所以,我们需要通过Wrapper
复制流,并将流继续写出去。
因此,我们HttpServletRequestWrapper
包装HttpServletRequest
。
BodyReaderHttpServletRequestWrapper
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
String sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
}
public String getBodyString(){
return new String(body,Charset.forName("UTF-8"));
}
/**
* 获取请求Body
* @param request
* @return
*/
private String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = cloneInputStream(request.getInputStream());
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
/**
* Description: 复制输入流
*
* @param inputStream
* @return
*/
public InputStream cloneInputStream(ServletInputStream inputStream) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
}
catch (IOException e) {
e.printStackTrace();
}
InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return byteArrayInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
同时,需要添加一个过滤器,用以读取流后将流继续写出去。
public class ChannelFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 防止流读取一次后就没有了, 所以需要将流继续写出去
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);
filterChain.doFilter(requestWrapper, servletResponse);
}
@Override
public void destroy() {
}
}
最后对所有的url添加此过滤器
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean registFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new ChannelFilter());
registration.addUrlPatterns("/*");
registration.setName("channelFilter");
registration.setOrder(1);
return registration;
}
}
此处解释下FilterRegistrationBean
:
Springboot中会使用FilterRegistrationBean来注册Filter,Filter是Servlet规范里面的,属于容器范围,
Springboot中我们没有web.xml,那Springboot中,Filter是如何交给Servlet容器的呢?
在springboot添加过滤器有两种方式:
1、通过创建FilterRegistrationBean的方式(建议使用此种方式,统一管理,且通过注解的方式若不是本地调试,如果在filter中需要增加cookie可能会存在写不进前端情况)同时可以为filter设置排序值,让spring在注册web filter之前排序后再依次注册。
2、通过注解@WebFilter的方式
这里使用的是第一种方式。
至此,就完成了我们的日志管理操作。