SpringBoot拦截器与过滤器 实现用户访问量/功能访问量的统计

SpringBoot拦截器与过滤器 实现用户访问量/功能访问量的统计

  • 前言
  • 1、过滤器 解决输入流不能重复读取问题
    • 1.1定义RequestWrapper容器
    • 1.2 定义ReplaceStreamFilter 过滤规则
    • 1.3 过滤器配置类FilterConfig
  • 2、拦截器 实现我们的业务功能
    • 2.1 定义VisitorInterceptor 拦截业务规则
    • 2.2 拦截器配置类WebMvcConfig
    • 2.3 业务功能对应的的mysql表
  • 3、HttpServletRequest的输入流只能读取一次的原因

前言

下一篇链接:实现用户访问量/功能访问量的统计信息的查询

真正在公司中的实践:NoSQL + RDBMS 一起使用才是最强的

技术没有高低之分,就看你如何去使用!(提升内功,思维的提高!多思考!)

云计算的长征之路:阿里云的这群疯子,阿里巴巴的架构演进!

1、过滤器 解决输入流不能重复读取问题

request的输入流只能读取一次不能重复读取
我们在过滤器或拦截器里读取了request的输入流之后
请求走到controller 层时就会报错。
文章最后会分析不能重复读取的原因


解决办法:HttpServletRequestWrapper + Filter(过滤器)
既然ServletInputStream不支持重新读写,那我们将ServletInputStream流读出来后,用容器存储起来起来,后面就可以多次读取了。

1.1定义RequestWrapper容器

关于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()));
    }
}

1.2 定义ReplaceStreamFilter 过滤规则

在过滤器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销毁...");
    }
}

1.3 过滤器配置类FilterConfig

在配置类中对上述过滤器进行注册,要对哪些路径进行过滤

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();
    }
}

2、拦截器 实现我们的业务功能

业务功能:实现用户访问量/功能访问量的统计


过滤器解决输入流不能重复读取的问题后
我们就可以获取参数token(密钥),得到用户信息和对应的接口请求地址
从而统计用户访问量和各功能访问量了

2.1 定义VisitorInterceptor 拦截业务规则

下面是我用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);
            }
        }
    }
}

2.2 拦截器配置类WebMvcConfig

里面注册了好几个拦截器,大家可以删除多余的。
如: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();
    }
}

2.3 业务功能对应的的mysql表

SpringBoot拦截器与过滤器 实现用户访问量/功能访问量的统计_第1张图片

3、HttpServletRequest的输入流只能读取一次的原因

我们先来看看为什么HttpServletRequest的输入流只能读一次
当我们调用getInputStream()方法获取输入流时得到的是一个InputStream对象
而实际类型是ServletInputStream,它继承于InputStream。

SpringBoot拦截器与过滤器 实现用户访问量/功能访问量的统计_第2张图片

InputStream的read()方法内部有一个postion,标志当前流被读取到的位置
每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了
如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,然而mark默认是0,所以就不能从头再读了。
调用reset()方法的前提是,已经重写了reset()方法,当然能否reset也是有条件的,它取决于markSupported()方法是否返回true。
然而InputStream默认不实现reset(),并且markSupported()默认也是返回false,这一点查看其源码便知:

SpringBoot拦截器与过滤器 实现用户访问量/功能访问量的统计_第3张图片
我们再来看看ServletInputStream,可以看到该类也没有重写mark(),reset()以及markSupported()方法:
SpringBoot拦截器与过滤器 实现用户访问量/功能访问量的统计_第4张图片

你可能感兴趣的:(SpringBoot,spring,boot,过滤器,filter,interceptor)