前言
最近公司在做一个数据展示项目,很多数据都是存放在每日库里,然而数据库分库查询的性能很低,所以想到用拦截器和AOP对数据接口做一个统一的缓存处理。
Redis缓存的Key初步设计为请求的uri,value为数据的list集合。
1、自定义拦截器拦截请求
拦截器拦截请求,先查询Redis是否有该接口对应的缓存,若没有则放行,查询数据库后将数据存入Redis并返回,若存在则直接从缓存读出数据。
当currentPage为1时,则认为该接口的查询条件改变,表示缓存数据无效,需要查询数据库。
/**
* @description: 缓存拦截器
* @author: panyu
* @create: 2019-04-25 16:22
**/
@Component
public class RedisInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 返回json对象
// 获取请求路径
String path = request.getServletPath();
List data = (ArrayList)redisUtil.get("/xxx"+path);
System.out.println(path);
// 从Request中读出数据流,并解决只能读一次导致Controller接收不到数据的问题
JSONObject requestData = JSON.parseObject(new BodyReaderHttpServletRequestWrapper(request).getSessionStream());
// 获取当前页,当前页为1时,即更改了查询条件,从数据库查询
Integer currentPage = (Integer)requestData.get("currentPage");
Integer pageSize = (Integer)requestData.get("pageSize");
PrintWriter out = null;
JSONObject jsonObject = null;
try {
jsonObject = new JSONObject();
if (data != null && currentPage != 1) {// 如果缓存中存在数据,则直接查出返回
int dataSize = data.size();
if(dataSize < pageSize) {// 如果数据条数小于页面大小
data = data.subList((currentPage - 1) * pageSize, dataSize);
} else {
if(currentPage * pageSize <= dataSize) {// 如果不是最后一页,则显示 页面大小 条
data = data.subList((currentPage - 1) * pageSize, currentPage * pageSize);
} else {// 如果是最后一页,则不显示 页面大小 条
data = data.subList((currentPage - 1) * pageSize, (currentPage - 1) * pageSize + (dataSize % currentPage));
}
}
jsonObject.put("list", data);
jsonObject.put("itotal", dataSize);
jsonObject.put("success", true);
// 解决中文乱码
response.setCharacterEncoding("utf-8");
response.setContentType("text/json;charset=UTF-8");
out = response.getWriter();
out.print(jsonObject);
return false;
}
} catch (Exception e) {
e.printStackTrace();
jsonObject.put("success", false);
jsonObject.put("errorMassage", "缓存读取异常");
return false;
} finally {
if(out != null) {
out.close();
}
}
// 如果不存在缓存,则放行,进行查询后将数据存入数据库
return true;
}
}
**注意:**此处有一个问题,由于该项目请求参数为JSON格式,然而Request中的数据流只能读出一次,在拦截器中读取出请求参数后,会导致Controller获取不到Request Body。而又由于@RequestBody注解获取输出参数的方式也是根据流的方式获取的。
解决方案:先读取流,再把流写入。此处摘自
SpringBoot拦截器或过滤器中使用流读取参数后,controller中注解读取不到参数
1.1、自定义Filter读取流,再把流写入
/**
* @description: 解决Request数据流只能读一次的问题
* @author: panyu
* @create: 2019-04-28 11:42
**/
@Component
@WebFilter(filterName="myFilter",urlPatterns="/*")
public class MyFilter 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() {
}
}
1.2、重写HttpServletRequestWrapper
/**
* @description: 保存流
* @author: panyu
* @create: 2019-04-28 11:18
**/
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
private final String sessionStream;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
}
public String getSessionStream() {
return sessionStream;
}
/**
* 获取请求Body
*
* @param request
* @return
*/
public 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: 复制输入流</br>
*
* @param inputStream
* @return</br>
*/
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) {
}
};
}
}
1.3、在拦截器或过滤器中获取请求参数
// 从Request中读出数据流,并解决只能读一次导致Controller接收不到数据的问题
JSONObject requestData = JSON.parseObject(new BodyReaderHttpServletRequestWrapper(request).getSessionStream());
2、AOP对Controller层方法进行增强
当执行Controller,则表示不存在缓存,需要查询数据库。
对Controller层的方法定义切入点,在方法执行完后判断是否存在list参数(即需要做缓存的数据),若存在则将数据存入Redis并将数据返回。
/**
* @description: Redis缓存
* @author: panyu
* @create: 2019-04-25 17:40
**/
@Aspect
@Configuration//定义一个切面
public class RedisAspect {
@Autowired
private RedisUtil redisUtil;
@Pointcut("execution(* com.inheritech.gps_data.web.controller..*(..))")
public void excudeService() {}
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
String url = request.getRequestURL().toString();
String method = request.getMethod();
String uri = request.getRequestURI();
String queryString = request.getQueryString();
Object[] args = pjp.getArgs();
String params = "";
//获取请求参数集合并进行遍历拼接
Map paramsMap = null;
if(args.length>0){
if("POST".equals(method)){
Object object = args[0];
paramsMap = getKeyAndValue(object);
params = JSON.toJSONString(paramsMap);
}else if("GET".equals(method)){
params = queryString;
}
}
System.out.println("请求开始===地址:"+uri);
System.out.println("请求开始===类型:"+method);
System.out.println("请求开始===参数:"+params);
// result的值就是被拦截方法的返回值
JSONObject result = (JSONObject)pjp.proceed();
List datas = (ArrayList)result.get("list");
if(datas != null) {
redisUtil.set(uri, datas, 1800);
int dataSize = datas.size();
Integer currentPage = (Integer)paramsMap.get("currentPage");
Integer pageSize = (Integer)paramsMap.get("pageSize");
if(dataSize < pageSize) {// 如果数据条数小于页面大小
result.put("list", datas.subList((currentPage - 1) * pageSize, dataSize));
} else {
if(currentPage * pageSize <= dataSize) {// 如果不是最后一页,则显示 页面大小 条
result.put("list", datas.subList((currentPage - 1) * pageSize, currentPage * pageSize));
} else {// 如果是最后一页,则不显示 页面大小 条
result.put("list", datas.subList((currentPage - 1) * pageSize, (currentPage - 1) * pageSize + (dataSize % currentPage)));
}
}
}
return result;
}
public static Map<String, Object> getKeyAndValue(Object obj) {
Map<String, Object> map = new HashMap<>();
// 得到类对象
Class userCla = (Class) obj.getClass();
/* 得到类中的所有属性集合 */
Field[] fs = userCla.getDeclaredFields();
for (int i = 0; i < fs.length; i++) {
Field f = fs[i];
f.setAccessible(true); // 设置些属性是可以访问的
Object val = new Object();
try {
val = f.get(obj);
// 得到此属性的值
map.put(f.getName(), val);// 设置键值
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
}