SpringBoot实践之(十四)— AOP拦截器 Aspect的使用

  • Post author:
  • Post category:其他


常用用于实现拦截的有:Filter、HandlerInterceptor、MethodInterceptor

第一种Filter属于Servlet提供的,后两者是Spring提供的,HandlerInterceptor属于Spring MVC项目提供的,用来拦截请求,在MethodInterceptor之前执行。

实现一个HandlerInterceptor可以实现接口HandlerInterceptor,也可以继承HandlerInterceptorAdapter类,两种方法一样。这个不在本文范围,具体使用之前已经写过SpringBoot的(SpringMVC的使用一样,区别只是配置),可以参考:

http://blog.csdn.net/catoop/article/details/50501696?locationNum=2&fps=1

MethodInterceptor是AOP项目中的拦截器,它拦截的目标是方法,即使不是Controller中的方法。

实现MethodInterceptor拦截器大致也分为两种,一种是实现MethodInterceptor接口,另一种利用Aspect的注解或配置。

关于实现MethodInterceptor接口的这种方法,还需要在配置文件中做配置,在SpringMVC中使用还可以,在SpringBoot中使用起来似乎没有那么方便。

本文主要还是说Aspect注解方式,个人觉得这种方法才比较灵活,与配置与工程整个代码都没有耦合(你添加一个类,做几个注解就可以用了,无需在其他地方再做什么),更易应用。

首先导入相关包

首先为你的SpringBoot项目添加maven依赖,让其支持aop(其实就是自动引入aop需要的一些jar)

在pom.xml中添加依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

或者在build.gradle中添加

dependencies {
    //支持AOP
    compile('org.springframework.boot:spring-boot-starter-aop')
}

然后创建Aspect测试类 TestAspect:

package com.shanhy.sboot.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect // FOR AOP
@Order(-99) // 控制多个Aspect的执行顺序,越小越先执行
@Component
public class TestAspect {

    @Before("@annotation(test)")// 拦截被TestAnnotation注解的方法;如果你需要拦截指定package指定规则名称的方法,可以使用表达式execution(...),具体百度一下资料一大堆
    public void beforeTest(JoinPoint point, TestAnnotation test) throws Throwable {
        System.out.println("beforeTest:" + test.name());
    }

    @After("@annotation(test)")
    public void afterTest(JoinPoint point, TestAnnotation test) {
        System.out.println("afterTest:" + test.name());
    }

}

再添加一个自定义的注解类TestAnnotation:

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestAnnotation {
    String name() default "默认注解信息";
    String getFieldValue() default "getField";
    String setFieldValue() default "setField";
}

然后创建一个TestAOPController 验证一下:

import com.great.annotation.TestAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestAOPController {

    @TestAnnotation(name="abc")
    @RequestMapping("/show")
    public String show() {
        return "OK";
    }

    @RequestMapping("/show2")
    public String show2() {
        return "OK2";
    }
}

此时我们访问show请求,就会被拦截,控制台会打印输出。

这里写图片描述

注意:

1、在application.properties中也不需要添加spring.aop.auto=true,因为这个默认就是true,值为true就是启用@EnableAspectJAutoProxy注解了。

2、你不需要手工添加 @EnableAspectJAutoProxy 注解。

3、当你需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,这个默认值是false,不然默认使用的是标准Java的实现。


其实aspectj的拦截器会被解析成AOP中的advice,最终被适配成MethodInterceptor

,这些都是Spring自动完成的,如果你有兴趣,详细的过程请参考springAOP的实现。

关于集中拦截方法的区别总结:

HandlerInterceptoer拦截的是请求地址,所以针对请求地址做一些验证、预处理等操作比较合适。当你需要统计请求的响应时间时MethodInterceptor将不太容易做到,因为它可能跨越很多方法或者只涉及到已经定义好的方法中一部分代码。

MethodInterceptor利用的是AOP的实现机制,在本文中只说明了使用方式,关于原理和机制方面介绍的比较少,因为要说清楚这些需要讲出AOP的相当一部分内容。在对一些普通的方法上的拦截HandlerInterceptoer就无能为力了,这时候只能利用AOP的MethodInterceptor。

Filter是Servlet规范规定的,不属于spring框架,也是用于请求的拦截。但是它适合更粗粒度的拦截,在请求前后做一些编解码处理、日志记录等。

实现AOP的切面主要有以下几个要素:

使用@Aspect注解将一个java类定义为切面类

使用@Pointcut定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。

根据需要在切入点不同位置的切入内容

使用@Before在切入点开始处切入内容

使用@After在切入点结尾处切入内容

使用@AfterReturning在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理)

使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容

使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑

补充:

  1. 实例一:日志记录切面
package com.shanhy.sboot.aoplog;

import java.util.Arrays;

import javax.servlet.http.HttpServletRequest;

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.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Component
public class WebLogAspect {

    private static final Logger LOG = LoggerFactory.getLogger(WebLogAspect.class);

    @Pointcut("execution(public * com.shanhy.sboot..controller.*.*(..))")//两个..代表所有子目录,最后括号里的两个..代表所有参数
    public void logPointCut() {
    }

    @Before("logPointCut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 记录下请求内容
        LOG.info("请求地址 : " + request.getRequestURL().toString());
        LOG.info("HTTP METHOD : " + request.getMethod());
        LOG.info("IP : " + request.getRemoteAddr());
        LOG.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "."
                + joinPoint.getSignature().getName());
        LOG.info("参数 : " + Arrays.toString(joinPoint.getArgs()));

    }

    @AfterReturning(returning = "ret", pointcut = "logPointCut()")// returning的值和doAfterReturning的参数名一致
    public void doAfterReturning(Object ret) throws Throwable {
        // 处理完请求,返回内容
        LOG.info("返回值 : " + ret);
    }

    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object ob = pjp.proceed();// ob 为方法的返回值
        LOG.info("耗时 : " + (System.currentTimeMillis() - startTime));
        return ob;
    }

}

package com.shanhy.sboot.demo.controller;

import java.util.concurrent.TimeUnit;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/log")
public class WebLogTestController {

    @RequestMapping("/test")
    public String test() {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "TEST";
    }

}
  1. 实例二:登录验证切面
/**
 * Description: 登录验证AOP
 **/

@Configuration
@Aspect
@Order(4)
public class LoginAOP {

    /** 日志对象 */
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISystemUserDAO systemUserDAO;

    /**
     * 环绕切面,验证用户登录token
     *
     * @param point 切点对象
     * @return ResponseContent 返回信息
     * @throws Throwable 切面异常
     **/

    @Around("execution (* com.myp.webinsight.controller..*.*(..)) && !execution(* com.myp.webinsight.controller.SystemController.loginTimeAspect(..)) " +
            "&& !execution(* com.myp.webinsight.controller.ReportController.expReport(..))")
    public ResponseContent loginCheck(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        //获取请求头中的token
        String token = request.getHeader("token");
        if (token == null) {
            return new ResponseContent(MessageType.HEADER_TOKEN_NULL);
        }

        //判断redis中用户登录组是否存在缓存对象,不存在,后台服务器重启,重新登录
        /*if (loginTokens.isEmpty()) {
            return new ResponseContent(MessageType.BACKGROUND_SERVER_RESTART);
        }*/

        //判断redis中是否存在该token,不存在,返回错误码:两种情况,用户被挤下线(判断token中userId匹配),账号被禁用或删除(token中userId不匹配)
        MSySystemUser user = RedisUtils.get(ERedisDomain.TOKEN_LOGIN_USER, token, MSySystemUser.class);
        if (user == null) {
            Set<String> loginTokens = RedisUtils.getKeys(ERedisDomain.TOKEN_LOGIN_USER); //获取redis中用户登录的所有token集合
            String requestUserId = TokenUtils.getUserID(token); //从token中获取请求的用户id
            boolean isLoginElsewhere = false; //标记是否被挤下线
            for (String existToken : loginTokens) { //判断redis中是否存在该用户的token
                String existUserID = DESUtils.decrypt(existToken).split("_")[0];
                if (existUserID.equals(requestUserId) && RedisUtils.get(ERedisDomain.USER_REQUEST_TIME, existUserID) != null) { //存在该用户id且存在请求时间
                    isLoginElsewhere = true;
                }
            }
            if (isLoginElsewhere == true) {
                return new ResponseContent(MessageType.ACCOUNT_LOGIN_ELSEWHERE); //账号在别处登录
            } else {
                MSySystemUser requestUser = systemUserDAO.get(requestUserId);
                if (requestUser == null) {
                    return new ResponseContent(MessageType.SYSTEM_ACCOUNT_DELETED); //账号被删除
                } else if ("update".equals(RedisUtils.get(ERedisDomain.USER_REQUEST_TIME, requestUserId))) {
                    return new ResponseContent(MessageType.SYSTEM_ACCOUNT_UPDATED); //账号被修改,请重新登录
                } else {
                    return new ResponseContent(MessageType.TOKEN_NOTEXIST); //非法token
                }
            }
        }

        //token存在,根据用户id获取最后请求时间,超时30分钟,清除redis中缓存,返回错误码,拦截
        /*Map<String, Object> requestTime = SystemInit.getRequestTime();
        Long lastRequestTime = (Long) requestTime.get(user.getUserId());*/
        Long lastRequestTime = Long.valueOf(RedisUtils.get(ERedisDomain.USER_REQUEST_TIME, user.getUserId()));
        if (lastRequestTime == null) {
            return new ResponseContent(MessageType.TOKEN_NOTEXIST);
        }
        long currentTime = TimeZoneUtils.getTimeMillis(); //当前时间(格林威治时区)
        if (currentTime - lastRequestTime > (Long.valueOf(PropertyReaderUtils.getProValue("token.invalid.time")))) {    //未请求超过30分钟
            RedisUtils.delete(ERedisDomain.TOKEN_LOGIN_USER, token); //清除redis中缓存
            RedisUtils.delete(ERedisDomain.USER_REQUEST_TIME, user.getUserId()); //清除用户请求时间记录
            logger.info("user {} login overtime, the last request time is {}, the current time is {}", user.getUserId(), lastRequestTime, currentTime);
            return new ResponseContent(MessageType.LOGIN_OVERTIME);
        }

        //未超时,记录本次请求时间,放行(因为未拦截登录请求,所以登录时要将登录时间记录到map<userId,Date>中)
        //requestTime.put(user.getUserId(), TimeZoneUtils.getTimeMillis());
        RedisUtils.save(ERedisDomain.USER_REQUEST_TIME, user.getUserId(), TimeZoneUtils.getTimeMillis());
        logger.info("user {} the last request at time [{}], requst method is [{}]", user.getUserId(), DateUtils.format(new Date()), point.getTarget().getClass().getName() + "." + point.getSignature().getName());
        Object proceed = point.proceed(); //执行请求体方法
        ResponseContent response = (ResponseContent) proceed; //controller层各接口返回值均为ResponseContent类型
        return response;
    }
}
  1. 实例三:异常捕获切面
/**
 * Description: 该切面主要用于捕捉后台服务抛出的未知异常
 **/
@Configuration
@Aspect
@Order(2)
public class ExceptionAOP {

    /** 日志对象 **/
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 处理controller层抛出的所有异常
     *
     * @param point 切点对象
     * @return ResponseContent 切面处理完异常后的返回结果
     */
    @Around("execution (* com.myp.webinsight.controller..*.*(..))")
    public ResponseContent handleException(ProceedingJoinPoint point) {
        try {
            Object response = point.proceed();
            return (ResponseContent) response;
        } catch (Throwable e) {
            logger.error("Internal Server Error", e);
            return new ResponseContent(MessageType.UNKNOWN);
        }
    }
}
  1. 实例四:时区转换切面
/**
 * Description: 该类主要用于系统中前后端Controller层针对部分接口转换前端时间处理
 **/
@Aspect   //定义一个切面
@Order(5)
@Configuration
public class TimeZoneChangeAspect {

    @Around("execution(* com.myp.webinsight.controller..*.*TimeAspect(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //比较后台时间与web端时间大小
        int serviceBigInt = TimeZoneUtils.compareWebToService();
        //排序工具类
        SortByTimeFormat sortByTimeFormat = new SortByTimeFormat();
        //日期格式定义
        Pattern pattern = Pattern.compile("(\\d{4})-(\\d{1,2})-(\\d{1,2}) (\\d{2}):(\\d{2}):(\\d{2})");

        Object object = null;
        //获取目标方法的参数信息
        Object[] obj = joinPoint.getArgs();
        Object[] objNew = new Object[obj.length];
        if (null != obj && obj.length > 0 && serviceBigInt != 0) {
            for (int i = 0; i < obj.length; i++) {
                JSONObject jsonObjNew = new JSONObject();
                Class tempClass = obj[i].getClass();
                String postStr = JSON.toJSONString(obj[i], SerializerFeature.WriteMapNullValue);
                Matcher matcherPost = pattern.matcher(postStr);
                ArrayList<String> postTimeList = new ArrayList<String>(0);
                while (matcherPost.find()) {
                    String post = matcherPost.group(1) + "-" + matcherPost.group(2) + "-" + matcherPost.group(3) + " " + matcherPost.group(4) + ":" + matcherPost.group(5) + ":" + matcherPost.group(6);
                    postTimeList.add(post);
                }
                //根据前后台时间那个大做排序处理
                if (null != postTimeList && postTimeList.size() > 0) {
                    Collections.sort(postTimeList, sortByTimeFormat);//正序排序,越小的值越在前面
                    if (serviceBigInt == 1) {
                        Collections.reverse(postTimeList);//倒叙排序
                    }
                    for (String oldTime : postTimeList) {
                        String newTime = TimeZoneUtils.webConvertToServiceTime(oldTime);
                        postStr = postStr.replace(oldTime, newTime);
                    }
                    objNew[i] = JSON.parseObject(postStr, tempClass);
                } else {
                    objNew[i] = obj[i];
                }
            }
            object = joinPoint.proceed(objNew);
        } else {
            object = joinPoint.proceed(obj);
        }

        //返回值中修改时区时间
        ResponseContent newObject = (ResponseContent) object;
        if (serviceBigInt != 0) {
            Object oldMess = newObject.getMess();
            String changeNew = JSON.toJSONString(oldMess, SerializerFeature.WriteMapNullValue);
            Matcher matcherReturn = pattern.matcher(changeNew);
            ArrayList<String> returnSTimeList = new ArrayList<String>(0);
            while (matcherReturn.find()) {
                String ret = matcherReturn.group(1) + "-" + matcherReturn.group(2) + "-" + matcherReturn.group(3) + " " + matcherReturn.group(4) + ":" + matcherReturn.group(5) + ":" + matcherReturn.group(6);
                returnSTimeList.add(ret);
            }
            //根据前后台时间那个大做排序处理
            if (null != returnSTimeList && returnSTimeList.size() > 0) {
                Collections.sort(returnSTimeList, sortByTimeFormat);//正序排序,越小的值越在前面
                if (serviceBigInt == -1) {
                    Collections.reverse(returnSTimeList);//倒叙排序
                }
                for (String oldTime : returnSTimeList) {
                    String newTime = TimeZoneUtils.serviceConvertToWebTime(oldTime);
                    changeNew = changeNew.replace(oldTime, newTime);
                }
            }
            JSONObject newMess = JSON.parseObject(changeNew);
            newObject.setMess(newMess);
        }
        //返回时间转换后的返回值
        return newObject;

    }

}

/**
 * CreateDate: 16:22 2017/12/26
 * Author: wenliang
 * Description: 该类主要用于时间格式的List排序
 **/
class SortByTimeFormat implements Comparator<String> {
    public int compare(String StartTime, String endTime) {
        int returnFlag = 0;
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            Calendar calendar = Calendar.getInstance();

            Date web = sdf.parse(StartTime);
            calendar.setTime(web);
            Long webL = calendar.getTimeInMillis();

            Date service = sdf.parse(endTime);
            calendar.setTime(service);
            Long serviceL = calendar.getTimeInMillis();
            if ((serviceL - webL) > 0l) {
                returnFlag = -1;
            } else if ((serviceL - webL) == 0l) {
                returnFlag = 0;
            } else {
                returnFlag = 1;
            }
        } catch (Exception e) {
            // TODO: handle exception
        }
        return returnFlag;
    }


}
  1. 实例五:返回前端国际化信息切面
/**
 * Description: 该类主要用于国际化返回消息码
 **/
@Configuration
@Aspect
@Order(1)
public class ResponseAOP {

    @Autowired
    private MessageSource messageSource;

    @AfterReturning(value = "execution (* com.myp.webinsight.controller..*.*(..))", returning = "response")
    public ResponseContent i18ResponseDesc(ResponseContent response) {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            //Locale locale = request.getLocale();
            String language = request.getHeader("language");
            Locale locale = new Locale(language);
            String code = response.getCode();
            if (code != MessageType.SUCCESS.getCode()) {
                String i18Nkey = MessageType.getMessageTypeEnum(code).generateI18Nkey();
                String i18Message = messageSource.getMessage(i18Nkey, null, locale);
                response.setError(i18Message);
            }
            return response;
        } catch (Exception e) {
            return response;
        }
    }
}



版权声明:本文为luckykapok918原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。