springAOP,过滤器、拦截器、切面的使用

面试过程中,经常会问别人AOP,或者作为被面试者,被别人问AOP。AOP是什么,面向切面编程,都能扯上个一二三来。AOP的概念已经烂大街了,搞后端开发的都知道。

一旦问道你有没有用到过AOP,或者自己实现AOP,大部分面试者的答案都很标准:

  1. 打印日志有用过,再追问什么日志,支支吾吾说不清楚。
  2. 事务用过,事务spring已经给你设计、封装的明明白白,和你有什么关系。

所以只要回答这两种答案,这个知识点就不会给分了。

我理解在开发过程中常用的AOP有三种,过滤器、拦截器、切面注解,针对这三个东西,分别写一下在我自己搭建的框架中的一些用法。

一、过滤器

过滤器其实不是springboot提供的组件或特性,我们在搞jsp、servlet的时候就经常会用到过滤器。关于过滤器和拦截器的关系、区别,就不介绍了,网上很多很好的文章,我认为,使用了springboot,绝大多数场景都应该使用拦截器,因为拦截器可以获取到spring管理的bean,过滤器就不行。什么时候使用过滤器呢,从网上抄代码,正好他用的过滤器。

介绍一个我的框架中使用的过滤器,就是输出日志,日志的内容是打印request的请求地址、参数,response的返回值。这个过滤器的使用又恰恰使用的spring提供的过滤器,嘲讽啊,直接上代码。

1、定义过滤器

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;

@Slf4j
public class LoggingFilter extends OncePerRequestFilter {

    private static final String REQUEST_PREFIX_NAME = "Request: ";
    private static final String RESPONSE_PREFIX_NAME = "Response: ";
    private static final int MAXLENGTH = 1000;
    private static final String ATTACH_RESPONSE_HEADER = "Content-disposition";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 包装Request 和 Response
        //request = new RequestWrapper(request);
        response = new ResponseWrapper(response);
        // 打印请求参数
        request = printRequestLog(request);
        // 执行过滤器链
        filterChain.doFilter(request, response);
        //打印返回参数
        printResponseLog(response);
    }

    // 打印请求参数
    protected HttpServletRequest printRequestLog(HttpServletRequest request) throws IOException {
        StringBuilder msg = new StringBuilder();
        msg.append(REQUEST_PREFIX_NAME);

        // 打印request种关键信息
        HttpSession session = request.getSession(false);
        if (session != null) {
            msg.append("sessionId = ").append(session.getId()).append("; ");
        }
        if (request.getMethod() != null) {
            msg.append("method = ").append(request.getMethod()).append("; ");
        }
        if (request.getContentType() != null) {
            msg.append("contentType = ").append(request.getContentType()).append("; ");
        }
        msg.append("uri = ").append(request.getRequestURI());
        if (request.getQueryString() != null) {
            msg.append('?').append(request.getQueryString());
        }
        // 判断是否文件上传,图片上传,视频等
        if (!isMultipart(request) && !isBinaryContent(request)) {
            request = new RequestWrapper(request);
            msg.append("; payload = ").append(((RequestWrapper) request).getRequestBodyString(request));
        }
        log.info(msg.toString());
        return request;
    }

    // 打印返回参数
    private void printResponseLog(HttpServletResponse response) {
        StringBuilder msg = new StringBuilder();
        msg.append(RESPONSE_PREFIX_NAME);

        // 判断是否请求静态资源或者文件等
        if (response instanceof ResponseWrapper && !isMultipart(response) && !isBinaryContent(response)) {
            ResponseWrapper responseWrapper = (ResponseWrapper) response;
            msg.append("; result = ")
                    .append(new String(responseWrapper.toByteArray(), Charset.defaultCharset()));
        }else {
            Collection<String> headers = response.getHeaders(ATTACH_RESPONSE_HEADER);
            if(!CollectionUtils.isEmpty(headers)){
                msg.append(headers.iterator().next());
            }
        }
        if(msg.length()> MAXLENGTH)
        {
            log.info(msg.substring(0, MAXLENGTH));
        }else{
            log.info(msg.toString());
        }

    }

    private boolean isMultipart(HttpServletResponse response) {
        return response.containsHeader(ATTACH_RESPONSE_HEADER);
    }

    private boolean isBinaryContent(HttpServletResponse response) {
        if (response.getContentType() == null) {
            return false;
        }
        return response.getContentType().startsWith("image")
                || response.getContentType().startsWith("video")
                || response.getContentType().startsWith("audio");
    }

    private boolean isBinaryContent(HttpServletRequest request) {
        if (request.getContentType() == null) {
            return false;
        }
        return request.getContentType().startsWith("image")
                || request.getContentType().startsWith("video")
                || request.getContentType().startsWith("audio");
    }

    private boolean isMultipart(HttpServletRequest request) {
        return request.getContentType() != null
                && request.getContentType().startsWith("multipart/form-data");
    }

2、将过滤器注入spring管理

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LogAutoConfiguration {

    @Bean
    @ConditionalOnProperty(name = "enable-request-logging", havingValue = "true")
    public LoggingFilter loggingFilter() {
        return new LoggingFilter();
    }
}

二、拦截器

我们的微服务框框架中,直接使用spring原生拦截器的地方还真不多,使用比较多的是三方依赖提供的拦截器,比如mybatis plus提供的分页拦截扩展,服务和服务间调用的RequestInterceptor等。找来找去,在一个独立小项目里,找到了一个拦截器的使用场景。

该项目是一个文件加解密服务,作用是三方系统将文件传递给该服务,该服务针对文件进行加解密并返回给三方系统。该服务提供了一个http的接口,三方系统调用restful接口就可以了,但是不能谁来调都能调,也是有权限控制的,这里用了一个相当简单的权限控制,给三方系统颁发一个code和密钥,拼接时间戳MD5加密来鉴权。该系统提供了加密、解密、判断密级等多个接口,不可能每个接口里加一个check吧,所以用拦截器来做最为合适。

我们常用的拦截器方法就两种,preHandle和postHandle,preHandle是controller方法执行之前,postHandle是controller方法执行之后。针对上文提到的场景,执行之前鉴权就可以了。

1、定义拦截器,并实现逻辑

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@Component
public class PathInterceptor implements HandlerInterceptor {

    @Value("${mod: 5}")
    private Long mod;

    @Value("${checkOpen: 1}")
    private String checkOpen;

    private final  static Logger logger = LoggerFactory.getLogger(PathInterceptor.class);

    @Autowired
    private LogService logService;

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
        /**
         * 获取参数
         */
        String appCode = request.getParameter("appCode");
        String time = request.getParameter("time");
        String sign = request.getParameter("sign");
        MultipartFile file = null;
        try {
            file = ((MultipartHttpServletRequest) request).getFile("file");
        }catch (Exception e)
        {
            logger.error("不是post文件请求");
        }
        String ip = IpUtil.getIpAddress(request);

        /**
         * 构建log
         */
        SysLog sysLog = new SysLog();
        sysLog.setAppCode(appCode);
        if(file != null)
        {
            sysLog.setFileName(file.getOriginalFilename());
        }

        sysLog.setIp(ip);
        sysLog.setFlag("0");

        if("0".equals(checkOpen))
        {
            return true;
        }
        String message = "";

        if(StringUtils.isEmpty(appCode) || StringUtils.isEmpty(time) || StringUtils.isEmpty(sign))
        {
            message = "appCode || time || sign 为空";
            sysLog.setMessage(message);
            logger.error(sysLog.getAppCode()+"="+message);
            logService.saveLog(sysLog);
            throw new BaseException(401,message);
        }
        String secretKey = AppService.appMap.get(appCode);
        if(StringUtils.isEmpty(secretKey))
        {
            message = "appCode不可用";
            sysLog.setMessage(message);
            logger.error(sysLog.getAppCode()+"="+message);
            logService.saveLog(sysLog);
            throw new BaseException(401,message);
        }
        String signServer =  DigestUtils.md5DigestAsHex((secretKey+time).getBytes());
        if(!sign.equals(signServer))
        {
            message = "鉴权失败";
            sysLog.setMessage(message);
            logger.error(sysLog.getAppCode()+"="+message);
            logService.saveLog(sysLog);
            throw new BaseException(401,message);
        }
        Long timeServer = new Date().getTime();
        if(Math.abs(timeServer-Long.parseLong(time))>mod*60*1000)
        {
            SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            message = format.format(new Date())+"鉴权时间超时"+time+":"+timeServer+"mode:"+mod;
            sysLog.setMessage(message);
            logger.error(sysLog.getAppCode()+"="+message);
            logService.saveLog(sysLog);
            throw new BaseException(401,message);
        }
        return true;
    }
}

2、将拦截器注入spring管理

这里需要定义拦截器的作用路径,我这里是匹配/**,也就是所有路径

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Autowired
    private PathInterceptor pathInterceptor;

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

三、切面注解

切面注解简直太强了,任何不想改原来代码的地方都可以切切切。切面注解的作用域和过滤器拦截器完全不同,不局限与web的交互层面,service层、数据连接层,还可以根据自定义注解定义切面。什么都地方都可以用,简单记录一下切面注解。

@Aspect:定义在切面实现类上的注解

@Pointcut:定义切入点

@Before:切面方法执行之前的增强

@After:切面方法执行之后的增强,不管抛异常还是正常退出都执行的增强

@AfterReturning:切面方法执行之后的增强,方法正常退出时执行

@AfterThrowing:切面方法执行之后的增强,抛出异常后执行

@Around:环绕增强,包围一个连接点的增强,可获取执行前信息,可修改执行后数据

1、demo

@Aspect
@Component
@Slf4j
public class DemoAspect {


    /**
     * 监控切入点
     */
    @Pointcut("execution(* com.*.*Mapper.*(..))")
    private void pointCutMethod() {
        // 通过类路径定义切入点
    }

 @Pointcut("@annotation(com.es.EsInterfaceLog)")
    public void annotatedMethod() {
        //通过自定义注解定义切面     
    }

    @Before("pointCutMethod()")
    public void before(JoinPoint joinPoint){
        System.out.println("this is before");
    }
    @Before("pointCutMethod(JoinPoint joinPoint)")
    public void After(){
        System.out.println("this is After");
    }

    /**
     * 声明环绕通知
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("pointCutMethod()")
    public object doAround(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("this is aroud before");
         pjp.proceed();
        System.out.println("this is aroud after");
        return object;
    }

2、打印sql执行时间

打印sql执行时间,之前mybatisplus是有提供这个插件的,后来没了。我也预研过druid的sql执行时间,要格外的集成不说,可视化页面也是相当难用,于是就自己做了一个,并设置了开关,想开启就开启,不需要打印日志就关闭,直接上代码。

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@ConditionalOnProperty(name = "logging.mapper", havingValue = "true")
@Aspect
@Component
@Slf4j
public class MapperAspect {


    @Pointcut("execution(* com.cnhtc..*.*Mapper.*(..))")
    private void pointCutMethod() {
        // 定义切入点
    }

    /**
     * 声明环绕通知
     *
     * @param pjp
     * @return
     * @throws Throwable
     */
    @Around("pointCutMethod()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.nanoTime();
        Object obj = pjp.proceed();
        long end = System.nanoTime();
        log.debug("调用Mapper方法:{},参数:{},执行耗时:{}毫秒",
                pjp.getSignature().toString(), Arrays.toString(pjp.getArgs()), (end - begin) / 1000000);
        return obj;
    }

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring拦截器过滤器AOPSpring框架中常用的三种技术,用于实现对请求的处理和控制。它们的作用和使用方式有一些不同,我会分别对它们进行简单解释。 1. 过滤器(Filter): 过滤器Java Servlet规范提供的一种技术,用于在请求到达Servlet之前或响应返回给客户端之前拦截和处理请求。过滤器可以在Web应用的整个生命周期中起作用,对所有请求都生效。常见的应用场景包括:编码转换、身份认证、日志记录等。 2. 拦截器(Interceptor): 拦截器Spring框架提供的一种技术,用于在请求到达Handler(Controller方法)之前或响应返回给客户端之前对请求进行拦截和处理。拦截器只在Spring MVC中起作用,用于实现对请求的预处理和后处理。常见的应用场景包括:身份认证、权限控制、日志记录等。 3. AOP(面向切面编程): AOP是一种编程思想,也是Spring框架提供的一种技术。通过AOP,可以将与业务逻辑无关的横切关注点(如日志、事务管理等)从业务逻辑中解耦出来,以模块化的方式进行管理。在Spring中,AOP通常通过动态代理实现,可以在方法执行前、后或抛出异常时进行一些额外的处理。AOP常用于事务管理、日志记录、性能监控等方面。 总结: - 过滤器主要用于在Servlet规范中对请求进行拦截和处理。 - 拦截器主要用于在Spring MVC中对请求进行拦截和处理。 - AOP主要用于将与业务逻辑无关的横切关注点进行解耦和管理。 希望以上解释能对你有所帮助!如果有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值