下一篇链接:实现用户访问量/功能访问量的统计信息的查询
真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的
技术没有高低之分,就看你如何去使用!(提升内功,思维的提高!多思考!)
云计算的长征之路:阿里云的这群疯子,阿里巴巴的架构演进!
request的输入流只能读取一次不能重复读取
我们在过滤器或拦截器里读取了request的输入流之后
请求走到controller 层时就会报错。
文章最后会分析不能重复读取的原因
解决办法:HttpServletRequestWrapper + Filter(过滤器)
既然ServletInputStream不支持重新读写,那我们将ServletInputStream流读出来后,用容器存储起来起来,后面就可以多次读取了。
关于HttpServletRequestWrapper类:
它并没有真正去实现HttpServletRequest的方法,而只是在方法内又去调用HttpServletRequest的方法
所以我们可以通过继承该类并实现想要重新定义的方法,就可以达到包装原生HttpServletRequest对象的目的。
RequestWrapper是我们定义的一个容器,将ServletInputStream输入流里面的数据存储到这个容器里
这个容器可以是数组或集合,然后我们重写里面的getInputStream方法
每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。
package com.hut.weekcp.server.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
/**
* @author zxp
* @program wrapper-demo
* @description 包装HttpServletRequest,目的是让其输入流可重复读,解决了在过滤器或拦截器里读取了request的输入流之后,请求走到controller层时就会报错的问题
* @create 2021-1-22
**/
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
/**
* 缓存下来的HTTP body
*/
private byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = StreamUtils.copyToByteArray(request.getInputStream());
}
/**
* 重新包装输入流
* @return
* @throws IOException
*/
@Override
public ServletInputStream getInputStream() throws IOException {
InputStream bodyStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bodyStream.read();
}
/**
* 下面的方法一般情况下不会被使用,如果你引入了一些需要使用ServletInputStream的外部组件,可以重点关注一下。
* @return
*/
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
InputStream bodyStream = new ByteArrayInputStream(body);
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
在过滤器doFilter()方法里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象
然后每次接口请求的输入流,都会用RequestWrapper容器存储了
package com.hut.weekcp.server.filter;
import com.hut.weekcp.server.utils.RequestWrapper;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @author zxp
* @program wrapper-demo
* @description 替换HttpServletRequest
* @create 2021-1-22
**/
@Slf4j
public class ReplaceStreamFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("StreamFilter初始化...");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
chain.doFilter(requestWrapper, response);
}
@Override
public void destroy() {
log.info("StreamFilter销毁...");
}
}
在配置类中对上述过滤器进行注册,要对哪些路径进行过滤
package com.hut.weekcp.server.config;
import com.hut.weekcp.server.filter.ReplaceStreamFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
/**
* @author zxp
* @program wrapper-demo
* @description 过滤器配置类
* @create 2021-1-22
**/
@Configuration
public class FilterConfig {
/**
* 注册过滤器
* @return FilterRegistrationBean
*/
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(replaceStreamFilter());
registration.addUrlPatterns("/*");
registration.setName("streamFilter");
return registration;
}
/**
* 实例化StreamFilter
* @return Filter
*/
@Bean(name = "replaceStreamFilter")
public Filter replaceStreamFilter() {
return new ReplaceStreamFilter();
}
}
业务功能:实现用户访问量/功能访问量的统计
过滤器解决输入流不能重复读取的问题后
我们就可以获取参数token(密钥),得到用户信息和对应的接口请求地址
从而统计用户访问量和各功能访问量了
下面是我用redis实现用户访问量/功能访问量的统计的业务规则
afterCompletion在整个请求完成之后,才拦截的,提供接口响应速度
package com.hut.weekcp.server.interceptor;
import com.hut.weekcp.server.entity.UrlModuleDO;
import com.hut.weekcp.server.entity.VisitorsModule;
import com.hut.weekcp.server.iservice.IVisitService;
import com.hut.weekcp.server.mapper.VisitMapper;
import com.hut.weekcp.server.utils.ConstantUtil;
import com.hut.weekcp.server.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
/**
* @author zxp
* @program wrapper-demo
* @description 签名拦截器
* @create 2021-1-22
**/
@Slf4j
@Component
public class VisitorInterceptor implements HandlerInterceptor {
@Autowired
IVisitService visitService;
@Autowired
VisitMapper mapper;
@Autowired
private RedisUtil redisUtil;
@Autowired
//@Qualifier:表明我们自定义的RedisTemplate才是我们所需要的
@Qualifier(value = "redisTemplates")
private RedisTemplate redisTemplates;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Calendar cal = Calendar.getInstance();
String day = cal.get(Calendar.DATE)+"";
String month = (cal.get(Calendar.MONTH) + 1)+"";
String year = cal.get(Calendar.YEAR)+"";
String hour = cal.get(Calendar.HOUR_OF_DAY)+"";
//redisTemplates注入为空
if (redisTemplates == null) {
return;
}
//获取请求地址
String Url = request.getRequestURI();
UrlModuleDO urlModuleDO = visitService.getUrlModule(Url);
//请求地址能够映射的到,才放到redis中,映射不到的地址,说明不重要
if (urlModuleDO != null) {
//在请求发生后执行,token一定有效,不用验证
String token = request.getParameter("token");
Integer userId = visitService.getUserId(token);
//将当前接口的访问信息存入数据库,redis只记录用户访问了哪些功能,后续再访问同样的功能
//redis用户访问信息不变(每24小时做一次用户访问量持久化,提供给后台),mysql会记录每一次用户访问信息(方便后续大数据分析)
VisitorsModule visitorsModule = new VisitorsModule();
visitorsModule.setUserId(userId);
visitorsModule.setModuleId(urlModuleDO.getId());
visitorsModule.setFunction(urlModuleDO.getFunction());
visitorsModule.setUrl(urlModuleDO.getUrl());
visitorsModule.setYear(year);
visitorsModule.setMonth(month);
visitorsModule.setDay(day);
visitorsModule.setHour(hour);
//mysql会记录每一次用户访问信息
mapper.setVisitorsModule(visitorsModule);
//redis记录用户访问功能
Object object = redisTemplates.opsForHash().get(ConstantUtil.Redis.HASH_VISITOR_TNTERCEPTOR, ConstantUtil.Redis.HASH_VISITOR_TNTERCEPTOR_PREX + userId);
List<String> ListFunctions =new ArrayList();
if (object == null) {
ListFunctions.add(urlModuleDO.getFunction());
redisTemplates.opsForHash().put(ConstantUtil.Redis.HASH_VISITOR_TNTERCEPTOR, ConstantUtil.Redis.HASH_VISITOR_TNTERCEPTOR_PREX + userId, ListFunctions);
} else {//集合不为空,遍历集合,不存在该功能,就添加
ListFunctions = (List<String>)object;
for (String function : ListFunctions) {
if (function.equals(urlModuleDO.getFunction())) {
return;
}
}// VisitorInterceptor VisitorInterceptor:+用户id
ListFunctions.add(urlModuleDO.getFunction());
redisTemplates.opsForHash().put(ConstantUtil.Redis.HASH_VISITOR_TNTERCEPTOR, ConstantUtil.Redis.HASH_VISITOR_TNTERCEPTOR_PREX + userId, ListFunctions);
}
}
}
}
里面注册了好几个拦截器,大家可以删除多余的。
如:token(密钥)拦截器 ;swagger拦截器
package com.hut.weekcp.server.config;
import com.hut.weekcp.server.interceptor.VisitorInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import com.hut.weekcp.server.interceptor.HutTokenInterceptor;
import com.hut.weekcp.server.interceptor.TokenInterceptor;
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Autowired
TokenInterceptor tokenInterceptor;
@Autowired
HutTokenInterceptor hutTokenInterceptor;
@Autowired
VisitorInterceptor visitorInterceptor;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/api/**")
.excludePathPatterns("/api/token/**")
.excludePathPatterns("/api/view/**")
.excludePathPatterns("/api/hut/**")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
registry.addInterceptor(hutTokenInterceptor).addPathPatterns("/api/hut/**")
.excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
registry.addInterceptor(visitorInterceptor).addPathPatterns("/api/talk/**","/api/hut/**","/api/attention/**","/api/cim/match/**");
super.addInterceptors(registry);
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/*
配置 restful delete put风格
*/
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter",
name = {"enable"},
matchIfMissing = true)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter(){
return new OrderedHiddenHttpMethodFilter();
}
}
我们先来看看为什么HttpServletRequest的输入流只能读一次
当我们调用getInputStream()方法获取输入流时得到的是一个InputStream对象
而实际类型是ServletInputStream,它继承于InputStream。
InputStream的read()方法内部有一个postion,标志当前流被读取到的位置
每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了
如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,然而mark默认是0,所以就不能从头再读了。
调用reset()方法的前提是,已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。
然而InputStream默认不实现reset(),并且markSupported()默认也是返回false,这一点查看其源码便知:
我们再来看看ServletInputStream,可以看到该类也没有重写mark(),reset()以及markSupported()方法: