统一增加缓存的实现(一)

450 阅读5分钟

实现缓存逻辑有2种方式:

    1. 每个接口单独控制缓存逻辑
    1. 统一控制缓存逻辑

传统的缓存实现方案

  • 在测试时添加一个这样的开关更加灵活,测试完后上线不需要修改代码,只需要在配置文件中修改即可

完成校验的逻辑


@Component
//拦截器
public class RedisCacheInterceptor implements HandlerInterceptor {

    private static ObjectMapper mapper = new ObjectMapper();

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Value("${tanhua.cache.enable}")
    private Boolean enable;

    //请求执行之前执行这个,true就放行,false就拦截.
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!enable) {
            //未开启缓存
            return true;
        }

        String method = request.getMethod();
        //缓存是缓存查询到的结果,如果严格按照result风格的话,查询都是get请求.
       	if (!StringUtils.equalsAnyIgnoreCase(method, "GET", "POST")) {
       	//if (!StringUtils.equalsAnyIgnoreCase(method, "GET")) {
            //非GET的请求不进行缓存处理
            return true;
        }

        // 通过缓存做命中,查询redis,redisKey ?  组成:md5(请求的url + 请求参数)
        String redisKey = createRedisKey(request);
        String data = this.redisTemplate.opsForValue().get(redisKey);
        if (StringUtils.isEmpty(data)) {
            // 缓存未命中
            return true;
        }

        // 将data数据进行响应
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.getWriter().write(data);

        return false;
    }

    public static String createRedisKey(HttpServletRequest request) throws
            Exception {
        String paramStr = request.getRequestURI();
        //获取所有的请求参数
        Map<String, String[]> parameterMap = request.getParameterMap();
        if (parameterMap.isEmpty() && request.getMethod().equalsIgnoreCase("POST")) {
            //请求体的数据只能读取一次拿到token信息,需要进行包装Request进行解决.
            //get请求体里面没有东西.
            paramStr += IOUtils.toString(request.getInputStream(), "UTF-8");
        } else {
            paramStr += mapper.writeValueAsString(request.getParameterMap());
        }
        //路径+参数+token,拼接后太长了所以我们再用md5加密一下.
        String authorization = request.getHeader("Authorization"); //我们的token,因为不同的用户请求结果也是不一样的.
        if (StringUtils.isNotEmpty((authorization))) {
            paramStr += "_" + authorization;
        }
        //md5加密
        return "SERVER_DATA_" + DigestUtils.md5Hex(paramStr);
    }
    

响应结果写入到缓存

前面已经完成了缓存命中的逻辑,那么在查询到数据后,如果将结果写入到缓存呢?

1.通过拦截器可以实现吗?

不可以,因为MVC拦截器在
完成之后已经完成了视图的渲染,这个时候如果是post请求,请求参数在请求体里面,这个时候prehandler读取了RequestInputStream内容的话.这个时候请求体内容消失了,不可以拿到响应的结果了.所以我们需要通过ResponseBodyAdvice进行实现。ResponseBodyAdvice是Spring提供的高级用法,会在结果被处理前进行拦截,拦截的逻辑自己实现,这样就可以实现拿到结果数据进行写入缓存的操作了。 三个地方可以处理:

  • Pre(请求到达目标对象之前)
  • post(目标方法执行完返回给浏览器之前)
  • compl(当请求返回到了页面,页面完成展示之后执行)

2.如果第一次没有命中,从数据库里查询出来后,我们希望post把数据存储到redis里面怎么存?

我们直接返回一个对象,没有放到request域对象里.所以拿到的时ModelAndView里面没有值.

对一个类中的方法进行增强有几种方式

  1. 动态代理 被代理的对象要是接口(自动生成接口的实现类)或者是接口的实现类
  2. 装饰者模式 装饰者类和被装饰者类要实现同一个接口 装饰者类要持有被装饰者类的引用
  3. 继承 的条件是什么 我们要能够控制这个的构造方法.

tomcat在请求过来的时候会在请求过来的时候创建对象,我们现在就是要修改获取HttpServerletRequest对象. d

下面其实就是我们装饰者模式中的增强类,但是装饰者模式有一个最大的缺点,那就是增强的时候需要重写所有的方法,太过于繁琐.所以提供了一个模板类,这个模板类把通用的东西都写完了实现完了.当你想要修改某个方法,就可以继承模板类想要改哪个方法就重写哪个方法.

/**
 * 包装HttpServletRequest
 */
public class MyServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;  //就是ServletInputStream里面的东西

    /**
     * Construct a wrapper for the specified request.
     *
     * @param request The request to be wrapped
     */
    public MyServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        body = IOUtils.toByteArray(super.getInputStream()); //在构造中往body里赋值
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

	//增强的方法
    @Override
    public ServletInputStream getInputStream() throws IOException {
    //获取的时候把body接收,原来只能获取一次body,现在定义为成员变量不管获取多少次都会有值.
        return new RequestBodyCachingInputStream(body);  
    }

    private class RequestBodyCachingInputStream extends ServletInputStream {
        private byte[] body;
        private int lastIndexRetrieved = -1;
        private ReadListener listener;

        public RequestBodyCachingInputStream(byte[] body) {
            this.body = body;
        }

        @Override
        public int read() throws IOException {
            if (isFinished()) {
                return -1;
            }
            int i = body[lastIndexRetrieved + 1];
            lastIndexRetrieved++;
            if (isFinished() && listener != null) {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                    throw e;
                }
            }
            return i;
        }

        @Override
        public boolean isFinished() {
            return lastIndexRetrieved == body.length - 1;
        }

        @Override
        public boolean isReady() {
            // This implementation will never block
            // We also never need to call the readListener from this method, as this method will never return false
            return isFinished();
        }

        @Override
        public void setReadListener(ReadListener listener) {
            if (listener == null) {
                throw new IllegalArgumentException("listener cann not be null");
            }
            if (this.listener != null) {
                throw new IllegalArgumentException("listener has been set");
            }
            this.listener = listener;
            if (!isFinished()) {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                }
            } else {
                try {
                    listener.onAllDataRead();
                } catch (IOException e) {
                    listener.onError(e);
                }
            }
        }

        @Override
        public int available() throws IOException {
            return body.length - lastIndexRetrieved - 1;
        }

        @Override
        public void close() throws IOException {
            lastIndexRetrieved = body.length - 1;
            body = null;
        }
    }
}

这是把自己写的增强后的request对象替换掉默认的request对象,这样我们的增强类才能生效.这个时候的request变成了我们的MyServletRequest,而不是tomcat帮我们生成的request.

/**
 * 替换Request对象
 */
@Component
public class RequestReplaceFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (!(request instanceof MyServletRequestWrapper)) {
             System.out.println("替换了原有的request对象");
            request = new MyServletRequestWrapper(request);
        }
        filterChain.doFilter(request, response);
    }
}
  1. 请求过来先经过RequestReplaceFilter的一层包装,包装完成后我们的request对象在获取inputstream的时候发生了改变,这样请求就能到达我们的拦截器.
  2. 拦截器需要生效的话,原来在mvc阶段我们用配置文件写interceptor,但是现在我们需要用WebMvcConfigurer,调用addInterceptors,把我们的揽件其添加到springMVC的架构中,设置所有的请求都会经过拦截器的处理.
@Configuration
public class WebConfig implements WebMvcConfigurer {

   @Autowired
   private RedisCacheInterceptor redisCacheInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
   }
}