自定义注解实现SpringAOP

怎么自定义一个注解?

先来看一个自定义注解的示例,如下

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {

    /**
     * 内容描述
     */
    String value() default "";

}

可以看到,@SysLog 主要由 @Target、@Retention、@Documented 三个元注解组成。定义该注解只能用在方法上,可以被保留到运行期。

Java 的注解分为元注解和自定义注解,元注解是专门用来描述注解的注解类。在 JDK 中只有 4 个元注解,分别是

@Target //描述注解的使用范围。可以用在包、类、成员变量、局部变量、成员方法、构造方法、注解类等,支持的类型都定义在 ElementType 枚举类中

@Retention //描述注解保留的时间范围。
    RetentionPolicy 枚举类中有三种策略,
    SOURCE // 源文件保留
    CLASS  // 编译期保留(默认)
    RUNTIME // 运行期保留

@Documented //描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息

@Inherited //如果某个类使用了被 @Inherited 修饰的注解,则其子类将自动具有该注解

所以除了这四个元注解之外所有的注解都是自定义注解,像我们在 Spring、SpringBoot 开发中使用的 @Controller、@Service、@Mapper、@Resources…等这些注解其实都是自定义注解,只不过 Spring 框架已经帮我们封装好了,不需要我们再去手动封装了。


execution 表达式

Spring 官网中 Spring AOP 切入点表达式支持以下的 AspectJ 切入方式:

execution:用于匹配方法执行的连接点。这是在使用Spring AOP时要使用的主要切入点指示符。

within:将匹配限制为某些类型内的连接点。

this:在bean引用(Spring AOP代理)是给定类型的实例的情况下,将匹配限制为连接点。

target:在目标对象是给定类型的实例的情况下,将匹配限制为连接点。

args:将匹配限制为连接点,其中参数是给定类型的实例。

@target:在执行对象的类具有给定类型的注释的情况下,将匹配限制为连接点。

@args:限制匹配的连接点,其中传递的实际参数的运行时类型具有给定类型的注释。

@within:将匹配限制为具有给定注释的类型内的连接点。

@annotation:将匹配限制在连接点的对象具有给定注解的连接点处。

bean(BeanName):匹配限制为指定命名的 Bean

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

其中除了返回类型模式、方法名模式和参数模式外,其它的修饰符模式、异常模式是可选的。

execution(* com.execution.test..*.*(..))
对应
execution(返回值 全包名.类名.方法名 (参数集合))

1、 execution() 是表达式的主体
2、 第一个 * 号表示返回值可以为任意类型
3、 com.execution.test 指的是 AOP 切面所切的包名
4、 包名后面的 .. 符号表示当前包及其子包,如果只有一个 . 号表示当前包下的所有类,不包含子包
5、 第二个 * 号表示类名,* 即代表所有类,也可以用 User*  这样去指定以 User 开头的类名,用 *User 指定以 User 结尾的类名
6、 最后的".*(..)" 表示任何方法名,括号表示参数,两个点表示任意参数类型

其实平常就 execution 和 @annotation 用的多点,其他的也很少用,再看看一些不同的示例:

// 指定注解类
@annotation(com.execution.test.SysLog)

// 匹配所有目标类所有类型的方法
execution(* *(..))

// 匹配所有目标类的 private 方法
execution(private * *(..))

// 匹配SysLogController所有的方法
execution(* * com.execution.test.SysLogController.*(..))

// 匹配指定的 com.execution.test 包中的所有 public 方法,并且第一个参数是 String 类型,第二个参数是 int类型,返回值是 int 类型,后面可以有任意个且类型不限的参数的方法
execution(public int com.execution.test.*(String, int, ..))

// 匹配指定类中的所有方法
within(TestService)

// 匹配以指定名字结尾的类中的所有方法
bean(*Service)

// 匹配一个接口的所有实现类中的实现方法
within(UserDao+)

// 切点表达式组合 &&、||、!
// 匹配在 com.execution.test 包及其子包下以 Service 结尾的类
within(com.execution.test..*) && within(*Service)

增强的使用示例

示例代码环境的版本:JDK1.8,SpringBoot 2.0.0,Spring 5.0.4。
切面类 SysLogAspect ,记得需要加上 @Component、@Aspect 这两个注解。

@Component// 交给 Spring 管理
@Aspect// 标识为切面
public class SysLogAspect {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 以所有添加了 SysLog 注解的方法为切点
     */
    @Pointcut("@annotation(com.jsd.zhaopin.web.component.SysLog)")
    public void logAspect() {
    }

    /**
     * 前置增强
     */
    @Before("logAspect()")
    public void doBefore() {
        logger.info("==========doBefore==========");
    }

    /**
     * 正常返回增强
     */
    @AfterReturning("logAspect()")
    public void doAfterReturning() {
        logger.info("==========doAfterReturning==========");
    }

    /**
     * 异常返回增强
     */
    @AfterThrowing("logAspect()")
    public void doAfterThrowing() {
        logger.info("==========doAfterThrowing==========");
    }

    /**
     * 后置通知
     */
    @After("logAspect()")
    public void doAfter() {
        logger.info("==========doAfter==========");
    }

    /**
     * 环绕增强
     */
    @Around("logAspect()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) {
		try {
            logger.info("==========doAround before==========");
            // 调用proceed()方法目标方法才会执行,这是环绕增强控制目标方法执行的关键
            Object o = joinPoint.proceed();
            logger.info("==========doAround after==========");
            return o;
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return null;
	}

}

目标方法SysLogController.test()

@RestController
@RequestMapping("/api/aop")
public class SysLogController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @SysLog("AOP测试")
    @GetMapping("test")
    public void test() {
        logger.info("==========test aop==========");
    }

}

测试结果

==========doAround before==========
==========doBefore==========
==========test aop==========
==========doAround after==========
==========doAfter==========
==========doAfterReturning==========(异常时==========doAfterThrowing==========)

可以看出,不同增强的执行顺序如下:
执行顺序
@Around 环绕增强是前面四个增强的结合体,当需要处理的数据需要强一致性的时候那就必须得使用 @Around 去实现,如果只是打印请求 URL、请求用户 IP、接口入参这些那就直接 @Before 就可以了。另外,当使用 @Around 来定义环绕增强时,形参必须要一个及以上而且第一个形参必须是 ProceedingJoinPoint 类型。

利用AOP实现日志记录

	@Around("@annotation(sysLog)")
    public Object doAround(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求用户IP
        String ip = request.getRemoteAddr();
        // 获取请求接口URL
        String url = request.getRequestURL().toString();

        // 获取签名
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        // 获取接口入参参数名称
        String argumentNames = StringUtils.join(methodSignature.getParameterNames(), ",");
        // 获取接口入参参数值
        String params = StringUtils.join(joinPoint.getArgs(), ",");

        // 获取包名+类名
        String packageName = signature.getDeclaringTypeName();
        // 获取方法名
        String methodName = signature.getName();
        // 返回被增强处理的目标对象
        Object target = joinPoint.getTarget();
        // 返回AOP框架为目标对象生成的代理对象
        Object proxy = joinPoint.getThis();

        try {
            logger.info("==========doAround before==========");
            long start = System.currentTimeMillis();
            Object proceed = joinPoint.proceed();
            long end = System.currentTimeMillis();
            // 接口执行时间,单位毫秒
            long time = end - start;
            logger.info("==========doAround after==========");
            return proceed;
        } catch (Throwable e) {
            // 异常信息
            String message = e.getMessage();
            throw e;
        }

        // 以上注释的变量可以选择性的保存到数据库中,这样就利用AOP实现了日志记录的功能
        SystemLog systemLog = new SystemLog();
        systemLog.setIp(ip);
        systemLog.setUrl(url);
        ...
        systemLogMapper.insert(systemLog);
    }

aop应用场景也很丰富,比如日志记录,认证鉴权,全局异常处理等,只要是在横向扩展一些局部或全局功能的时候就可以考虑能否使用aop。不过实际业务可能复杂多变,可按具体情况去实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值