Java之Spring AOP入门到精通【IDEA版】(一篇文章精通系列)

x33g5p2x  于2021-10-17 转载在 Java  
字(25.7k)|赞(0)|评价(0)|浏览(1336)

一、设计模式-代理模式

代理模式:给某一个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理
就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下,客户不想或者不
能够直接引用一个对象,代理对象可以在客户和目标对象直接起到中介的作用。

客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象,而仅仅持有一个被代理对象的接口,这时候代理对象不能够创建被代理对象,被代理对象必须有系统的
其他角色代为创建并传入。

为什么要使用代理模式呢?
第一,它有间接的特点,可以起到中介隔离作用。
就好比在租房的时候,房东可能不在本地,而短期内又不能赶回来,此时中介的出场,就作为房东的代理实现和我们签订承租合同。而我们和房东之间就没有耦合了。

第二,它有增强的功能。还以租房为例,我们首先考虑的是找一个靠谱的中介,由中介
给我们提供房源信息,并且告诉我们房屋的细节,合同的细节等等。
当然我们也可以自己去
找一些个人出租的房屋,但是在这之中,我们要了解租赁合同,房屋验收,租金监管等情
况,这无疑对我们是繁重的工作。
而有了中介作为我们的代理中间人,他把了解到的信息告诉我们,我们一样可以租到房子,而不必做那些繁重的工作。

二、AOP思想及实现原理

1、AOP思想

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方
式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2、实现原理

在上面的概念中描述出aop的实现原理是基于动态代理技术实现的。下面是针对动态代
理的一些介绍:
特点: 字节码随用随创建,随用随加载
分类: 基于接口的动态代理,基于子类的动态代理
作用: 不修改源码的基础上对方法增强

(1)基于接口的动态代理:

提供者是:JDK官方
使用要求:被代理类最少实现一个接口。
涉及的类:Proxy
创建代理对象的方法:newProxyInstance

方法的参数:
ClassLoader:类加载器。用于加载代理对象的字节码的。和被代理对象使用相同的类加载器。固定写法。
Class[]:字节码数组。用于给代理对象提供方法。和被代理对象具有相同的方法。

被代理类是一个普通类:被代理类对象.getClass().getInterfaces();

被代理类是一个接口:new Class[]{被代理了.class}它也是固定写法InvocationHanlder:要增强的方法。此处是一个接口,我们需要提供它的实现类。通常写的是匿名内部类。增强的代码谁用谁写。

基于子类的动态代理

提供者是:第三方cglib包,在使用时需要先导包(maven工程导入坐标即可)

使用要求:被代理类不能是最终类,不能被final修饰

涉及的类:Enhancer

创建代理对象的方法:create

方法的参数:

Class:字节码。被代理对象的字节码。可以创建被代理对象的子类,还可以获取被代理对象的类加载器。
Callback:增强的代码。谁用谁写。通常都是写一个接口的实现类或者匿名内部类。

Callback中没有任何方法,所以我们一般使用它的子接口:MethodInterceptor

3、Spring中AOP的术语

Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,指的是方法,因为spring只支持方法类型的连接点。

Pointcut(切入点):所谓切入点是指我们要对哪些Joinpoint进行拦截的定义。

Advice(通知/增强):所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。

Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, 可以在运行期为类动态地添加一些方法或Field。

Target(目标对象):代理的目标对象。

Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

Proxy(代理):一个类被AOP织入增强后,就产生一个结果代理类。

Aspect(切面):是切入点和通知(引介)的结合。

三、Spring注解驱动AOP开发入门

1、写在最前

a.Spring的aop是基于ioc的。所以需要有spring的ioc基础。(本篇内容不对ioc进行讲解)
b.本章节我们只是对aop的使用做基本功能展示,目的是为了以此讲解aop中的注解和执行原理分析。

2、注解驱动入门案例介绍

需求:实现在执行service方法时输出执行日志。(除了业务层外,表现层和持久层也可以实现)

3、案例实现

  • 工程搭建

  • 引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itbluebox</groupId>
    <artifactId>spring-aop</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.45</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <!--导入yaml文件解析器坐标-->
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>1.23</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
    </dependencies>
</project>
  • 创建User实体类

package cn.itbluebox.pojo;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {

    private String id;
    private String username;
    private String password;
    private String email;
    private Date birthday;
    private String gender;
    private String mobile;
    private String nickname;

    public User() {
    }

    public User(String id, String username, String password, String email, Date birthday, String gender, String mobile, String nickname) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.birthday = birthday;
        this.gender = gender;
        this.mobile = mobile;
        this.nickname = nickname;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", email='" + email + '\'' +
                ", birthday=" + birthday +
                ", gender='" + gender + '\'' +
                ", mobile='" + mobile + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}
  • 业务层接口

package cn.itbluebox.service;

import cn.itbluebox.pojo.User;

public interface UserService {

    /* 保存用户 */
    void save(User user);

}
  • 业务层接口实现类

package cn.itbluebox.service.impl;

import cn.itbluebox.pojo.User;
import cn.itbluebox.service.UserService;
import org.springframework.stereotype.Service;

@Service("userService")
public class UserServiceImpl implements UserService {
    public void save(User user) {
        System.out.println("保存用户信息:"+user);
    }
}
  • 日志工具类

package cn.itbluebox.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogUtil {

    /* Pointcut通用切入点表达式 execution允许 */
    @Pointcut("execution(* cn.itbluebox.service.impl.*.*(..))")
    private void pt1(){}

    /* 前置通知 */
    @Before("pt1()")
    public void beforeLog(){
        System.out.println("执行切入点方法前记录日志");
    }
    /* 后置通知 */
    @AfterReturning("pt1()")
    public void afterReturningLog(){
        System.out.println("正常执行切入点方法后记录日志");
    }

    /* 异常通知 */
    @AfterReturning("pt1()")
    public void afterThrowingLog(){
        System.out.println("执行切入点方法产生异常后记录日志");
    }

    /* 最终通知 */
    @After("pt1()")
    public void afterLog(){
        System.out.println("无论切入点方法执行是否异常都记录日志");
    }
    /* 环绕通知 */
    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
            //1.定义返回值
        Object rtValue = null;
        try{
            //前置通知
            System.out.println("执行切入点方法前记录日志");
            //2.获取方法执行所需的参数
            Object[] args = pjp.getArgs();
            //3.执行切入点方法
            rtValue = pjp.proceed(args);
            //后置通知
            System.out.println("正常执行切入点方法后记录日志");
        }catch (Throwable t){
            //异常通知
            System.out.println("执行切入点方法产生异常后记录日志"+t);
        }finally {
            //最终通知
            System.out.println("无论切入点方法执行是否有异常都记录日志");
        }
        return rtValue;
    }

}
  • 创建配置类:config.SpringConfiguration

package cn.itbluebox.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("cn.itbluebox")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
  • 创建测试类:

package cn.itbluebox.test;

import cn.itbluebox.config.SpringConfiguration;
import cn.itbluebox.pojo.User;
import cn.itbluebox.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringAOPTest {

    public static void main(String[] args) {

        //1、获取容器
        AnnotationConfigApplicationContext
                ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        //2、获取Bean对象
        UserService userService = ac.getBean("userService", UserService.class);

        //3、准备数据
        User user = new User();
        user.setId("1");
        user.setUsername("test");
        user.setNickname("张三");
        //4、执行方法
        userService.save(user);
    }

}

运行测试类

4、可以将切入点精确到方法

  • 在接口当中创建update方法

public void update(User user) {
        System.out.println("保存用户信息:"+user);
    }
  • 修改LogUtil

@Pointcut("execution(* cn.itbluebox.service.impl.UserServiceImpl.save(..))")
  • 运行测试类

  • 修改SpringAOPTest

package cn.itbluebox.test;

import cn.itbluebox.config.SpringConfiguration;
import cn.itbluebox.pojo.User;
import cn.itbluebox.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringAOPTest {

    public static void main(String[] args) {

        //1、获取容器
        AnnotationConfigApplicationContext
                ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        //2、获取Bean对象
        UserService userService = ac.getBean("userService", UserService.class);

        //3、准备数据
        User user = new User();
        user.setId("1");
        user.setUsername("test");
        user.setNickname("张三");
        //4、执行方法
        userService.update(user);
    }

}
  • 运行测试类(没有切入)

四、AOP常用注解分析

1、用于开启注解AOP支持

(1)@EnableAspectJAutoProxy
1)源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
	/** * Indicate whether subclass‐based (CGLIB) proxies are to be created as opposed * to standard Java interface‐based proxies. The default is {@code false}. */
	boolean proxyTargetClass() default false;
	/** * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal} * for retrieval via the {@link org.springframework.aop.framework.AopContext} class. * Off by default, i.e. no guarantees that {@code AopContext} access will work. * @since 4.3.1 */
	boolean exposeProxy() default false;
}
2)说明

作用:

表示开启spring对注解aop的支持。

它有两个属性,分别是指定采用的代理方式和
是否暴露代理对象,通过AopContext可以进行访问。

从定义可以看得出,它引入AspectJAutoProxyRegister.class对象,该对象是基于注解@EnableAspectJAutoProxy注册一个AnnotationAwareAspectJAutoProxyCreator,该对象通过调用

AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

注册一个aop代理对象生成器。
关于AnnotationAwareAspectJAutoProxyCreator请参

属性:
proxyTargetClass:指定是否采用cglib进行代理。默认值是false,表示使用jdk的代理。
exposeProxy: 指定是否暴露代理对象,通过AopContext可以进行访问。

使用场景: 当我们注解驱动开发时,在需要使用aop实现某些功能的情况下,都需要用到此注解。

3)示例

package cn.itbluebox.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("cn.itbluebox")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}

2、用于配置切面的

(1)@Aspect
1)源码
/** * Aspect declaration * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Aspect {
/** * Per clause expression, defaults to singleton aspect * <p/> * Valid values are "" (singleton), "perthis(...)", etc */
public String value() default "";
}
2)说明

作用:声明当前类是一个切面类。

属性:
value:默认我们的切面类应该为单例的。但是当切面类为一个多例类时,指定预处理的切入点表达式。用法是perthis(切入点表达式)。

它支持指定切入点表达式,或者是用@Pointcut修饰的方法名称(要求全限定方法名)

使用场景:此注解也是一个注解驱动开发aop的必备注解。

3)示例
@Component
@Scope("prototype")//注意:通常情况下我们的切面类是不需要多例的。
@Aspect(value="execution(* cn.itbluebox.service.impl.*.*(..))")
public class LogUtil {
	/** * 用于配置当前方法是一个前置通知 */
	@Before("execution(* cn.itbluebox.service.impl.*.*(..))")
	public void printLog(){
		System.out.println("执行打印日志的功能");
	}
}

3、用于配置切入点表达式的

(1)@Pointcut
1)源码
/** * Pointcut declaration * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Pointcut {
	/** * The pointcut expression * We allow "" as default for abstract pointcut */
	String value() default "";
	/** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the pointcut are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */
	String argNames() default "";
}
2)说明

作用:
此注解是用于指定切入点表达式的。
属性:
value:用于指定切入点表达式。

argNames:用于指定切入点表达式的参数。参数可以是execution中的,也可以是args中的。通常情况下不使用此属性也可以获得切入点方法参数。

使用场景:在实际开发中,当我们的多个通知需要执行,同时增强的规则确定的情况下,就可以把切入点表达式通用化。此注解就是代替xml中的<aop:pointcut>标签,实现切入点表达式的通用化。

3)示例

/* Pointcut通用切入点表达式 execution允许 */
    @Pointcut("execution(* cn.itbluebox.service.impl.UserServiceImpl.save(..))")
    private void pt1(){}

4、用于配置通知的

(1)@Before
1)源码
/** * Before advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
	/** * The pointcut expression where to bind the advice */
	String value();
	/** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */
	String argNames() default "";
}
2)说明

作用:
被此注解修饰的方法为前置通知。前置通知的执行时间点是在切入点方法执行之
前。
属性:
value:用于指定切入点表达式。可以是表达式,也可以是表达式的引用。
argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称
一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:在实际开发中,我们需要对切入点方法执行之前进行增强, 此时就用到了前置通
知。
在通知(增强的方法)中需要获取切入点方法中的参数进行处理时,就要配合切入点表达
式参数来使用。

3)示例
/** * 前置通知 */
@Before(value = "pt1(user)",argNames = "user")
public void beforeLog(User user){
	System.out.println("执行切入点方法前记录日志"+user);
}
(2)@AfterReturning
1)源码
/** * After returning advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning {
/** * The pointcut expression where to bind the advice */
	String value() default "";
	/** * The pointcut expression where to bind the advice, overrides "value" when specified */
	String pointcut() default "";
	/** * The name of the argument in the advice signature to bind the returned value to */
	String returning() default "";
	/** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */
	String argNames() default "";
}
2)说明

作用:
用于配置后置通知。
后置通知的执行是在切入点方法正常执行之后执行。
需要注意的是,由于基于注解的配置时,spring创建通知方法的拦截器链时,后置
通知在最终通知之后,所以会先执行@After注解修饰的方法。

属性:
value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
pointcut:它的作用和value是一样的。
returning:指定切入点方法返回值的变量名称。它必须和切入点方法返回值名称一
致。
argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称
一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:此注解是用于配置后置增强切入点方法的。被此注解修饰方法会在切入点方法正常执行之后执行。
在我们实际开发中,像提交事务,记录访问日志,统计方法执行效率等等都可以利用后置通知实现。

User findById(String id);

public User findById(String id) {
        System.out.println("切入点方法开始执行。。。");
        User user = new User();
        user.setId(id);
        user.setUsername("itbluebox");
        user.setNickname("蓝盒子");
        return user;
    }
  • 修改LogUtil

/* 后置通知 */
    @AfterReturning(value = "execution(* cn.itbluebox.service.impl.*.* (..))&&args(param)",
            returning = "user")
    public void afterReturningLog(String param,Object user){
        System.out.println("-----------------------");
        System.out.println("正常执行切入点方法后记录日志,切入点方法的参数 是:"+param);
        System.out.println("正常执行切入点方法后记录日志,切入点方法的返回值 是:"+user);
        System.out.println("-----------------------");
    }
  • 修改测试类

package cn.itbluebox.test;

import cn.itbluebox.config.SpringConfiguration;
import cn.itbluebox.pojo.User;
import cn.itbluebox.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringAOPTest {

    public static void main(String[] args) {

        //1、获取容器
        AnnotationConfigApplicationContext
                ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        //2、获取Bean对象
        UserService userService = ac.getBean("userService", UserService.class);

        //3、准备数据
        User user = new User();
        user.setId("1");
        user.setUsername("test");
        user.setNickname("张三");
        //4、执行方法
        userService.findById("1");
    }

}

(3)@AfterThrowing
1)源码
/** * After throwing advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
	/** * The pointcut expression where to bind the advice */
	String value() default "";
	/** * The pointcut expression where to bind the advice, overrides "value" when specified */
	String pointcut() default "";
	/** * The name of the argument in the advice signature to bind the thrown exception to */
	String throwing() default "";
	/** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */
	String argNames() default "";
}
2)说明

作用:
用于配置异常通知。
属性:

value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。

pointcut:它的作用和value是一样的。

throwing:指定切入点方法执行产生异常时的异常对象变量名称。它必须和异常变量名称一致。

argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称一致。
通常不指定也可以获取切入点方法的参数内容。

使用场景:用此注解修饰的方法执行时机是在切入点方法执行产生异常之后执行。

3)示例

package cn.itbluebox.service.impl;

import cn.itbluebox.pojo.User;
import cn.itbluebox.service.UserService;
import org.springframework.stereotype.Service;

@Service("userService")
public class UserServiceImpl implements UserService {
    public void save(User user) {
        System.out.println("保存用户信息:"+user);
    }
    public void update(User user) {
        System.out.println("保存用户信息:"+user);
    }

    public User findById(String id) {
        System.out.println("切入点方法开始执行。。。");
        User user = new User();
        user.setId(id);
        user.setUsername("itbluebox");
        user.setNickname("蓝盒子");
        int i = 1/0;
        return user;
    }
}

(3)@After
1)源码
/** * After finally advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
	/** * The pointcut expression where to bind the advice */
	String value();
	/** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */
	String argNames() default "";
}
2)说明

作用:
用于指定最终通知。
属性:
value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
argNames:用于指定切入点表达式参数的名称。它要求和切入点表达式中的参数名称
一致。通常不指定也可以获取切入点方法的参数内容。
使用场景:最终通知的执行时机,是在切入点方法执行完成之后执行,无论切入点方法执行是
否产生异常最终通知都会执行。所以被此注解修饰的方法,通常都是做一些清理操作。

3)示例
/** * 最终通知 */
@After(value = "execution(* cn.itbluebox.service.impl.*.*(..))")
public void afterLog(){
	System.out.println("无论切入点方法执行是否有异常都记录日志");
}
(4)@Around
1)源码
/** * Around advice * * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a> */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
	/** * The pointcut expression where to bind the advice */
	String value();
	/** * When compiling without debug info, or when interpreting pointcuts at runtime, * the names of any arguments used in the advice declaration are not available. * Under these circumstances only, it is necessary to provide the arg names in * the annotation ‐ these MUST duplicate the names used in the annotated method. * Format is a simple comma‐separated list. */
	String argNames() default "";
}
2)说明

作用:用于指定环绕通知。

属性:
value:用于指定切入点表达式,可以是表达式,也可以是表达式的引用。
argNames:用于指定切入点表达式参数的名称。
它要求和切入点表达式中的参数名称一致。通常不指定也可以获取切入点方法的参数内容。

使用场景:环绕通知有别于前面介绍的四种通知类型。它不是指定增强方法执行时机的,而是
spring为我们提供的一种可以通过编码的方式手动控制增强方法何时执行的机制。

3)示例
/** * 环绕通知 */
@Around("execution(* com.itheima.service.impl.*.*(..))")
public Object arountPrintLog(ProceedingJoinPoint pjp){
	//1.定义返回值
	Object rtValue = null;
	try{
		//前置通知
		System.out.println("执行切入点方法前记录日志");
		//2.获取方法执行所需的参数
		Object[] args = pjp.getArgs();
		//3.执行切入点方法
		rtValue = pjp.proceed(args);
		//后置通知
		System.out.println("正常执行切入点方法后记录日志");
	}catch (Throwable t){
		//异常通知
		System.out.println("执行切入点方法产生异常后记录日志");
	}finally {
		//最终通知
		System.out.println("无论切入点方法执行是否有异常都记录日志");
	}
	return rtValue;
}

五、用于扩展目标类的

1、@DeclareParents

(1)源码
/** * Declare parents mixin annotation */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DeclareParents {
	/** * The target types expression */
	String value();
	/** * Optional class defining default implementation * of interface members (equivalent to defining * a set of interface member ITDs for the * public methods of the interface). */
	Class defaultImpl() default DeclareParents.class;
	// note ‐ a default of "null" is not allowed,
	// hence the strange default given above.
}
(2)说明

作用:
用于给被增强的类提供新的方法。(实现新的接口)

属性:
value:用于指定目标类型的表达式。当在全限定类名后面跟上+时,表示当前类及其子类

defaultImpl:指定提供方法或者字段的默认实现类。

使用场景:当我们已经完成了一个项目的某个阶段开发,此时需要对已完成的某个类加入一些新的方法,我们首先想到的是写一个接口,然后让这些需要方法的类实现此接口,但是如果目标类非常复杂,牵一发而动全身,改动的话可能非常麻烦。
此时就可以使用此注解,然后建一个代理类,同时代理该类和目标类。

(3)示例

package cn.itbluebox.service;

import cn.itbluebox.pojo.User;

public interface ValidateService {

    boolean checkUser(User user);

}

package cn.itbluebox.service.impl;

import cn.itbluebox.pojo.User;
import cn.itbluebox.service.ValidateService;

public class ValidateServiceImpl implements ValidateService {
    @Override
    public boolean checkUser(User user) {
        if(user.getNickname().contains("孙子")){
            return false;
        }
        return true;
    }

}
  • 创建MyPointcut

package cn.itbluebox.pointcut;

import org.aspectj.lang.annotation.Pointcut;

public class MyPointcut {

    /** * 用于定义通用的切入点表达式 */
    @Pointcut(value = "execution(* cn.itbluebox.service.impl.*.*(..))")
    public void pointcut1(){}
}
  • AOP切面类的配置

package cn.itbluebox.utils;

import cn.itbluebox.pojo.User;
import cn.itbluebox.service.ValidateService;
import cn.itbluebox.service.impl.ValidateServiceImpl;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LogUtil {

    @DeclareParents(value =
            "cn.itbluebox.service.UserService+", defaultImpl =
            ValidateServiceImpl.class)
    private ValidateService validateService;

    /** * 用于配置当前方法是一个前置通知 */
    @Before(value = "cn.itbluebox.pointcut.MyPointcut.pointcut1() && args(user) && this(validateService)")
    public void printLog(User user, ValidateService validateService) {
//第二种触发方式
        boolean check = validateService.checkUser(user);
        if (check) {
            System.out.println("执行打印日志的功能");
        } else {
            throw new IllegalStateException("名称非法");
        }
    }

    /* Pointcut通用切入点表达式 execution允许 */
    @Pointcut("execution(* cn.itbluebox.service.impl.*.*(..))")
    private void pt1() {
    }

    /* 前置通知 */
    @Before("pt1()")
    public void beforeLog() {
        System.out.println("执行切入点方法前记录日志");
    }

    /* 后置通知 */
    @AfterReturning(value = "execution(* cn.itbluebox.service.impl.*.* (..))&&args(param)",
            returning = "user")
    public void afterReturningLog(String param, Object user) {
        System.out.println("-----------------------");
        System.out.println("正常执行切入点方法后记录日志,切入点方法的参数 是:" + param);
        System.out.println("正常执行切入点方法后记录日志,切入点方法的返回值 是:" + user);
        System.out.println("-----------------------");
    }

    /* 异常通知 */
    @AfterReturning("pt1()")
    public void afterThrowingLog() {
        System.out.println("执行切入点方法产生异常后记录日志");
    }

    /* 最终通知 */
    @After("pt1()")
    public void afterLog() {
        System.out.println("无论切入点方法执行是否异常都记录日志");
    }

    /* 环绕通知 */
    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        //1.定义返回值
        Object rtValue = null;
        try {
            //前置通知
            System.out.println("执行切入点方法前记录日志");
            //2.获取方法执行所需的参数
            Object[] args = pjp.getArgs();
            //3.执行切入点方法
            rtValue = pjp.proceed(args);
            //后置通知
            System.out.println("正常执行切入点方法后记录日志");
        } catch (Throwable t) {
            //异常通知
            System.out.println("执行切入点方法产生异常后记录日志" + t);
        } finally {
            //最终通知
            System.out.println("无论切入点方法执行是否有异常都记录日志");
        }
        return rtValue;
    }

}
  • 修改SpringAOPTest

package cn.itbluebox.test;

import cn.itbluebox.config.SpringConfiguration;
import cn.itbluebox.pojo.User;
import cn.itbluebox.service.UserService;
import cn.itbluebox.service.ValidateService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class SpringAOPTest {

    public static void main(String[] args) {

        //1、获取容器
        AnnotationConfigApplicationContext
                ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        //2、获取Bean对象
        UserService userService = ac.getBean("userService", UserService.class);

        //3、准备数据
        User user = new User();
        user.setId("1");
        user.setUsername("test");
        user.setNickname("孙子");
        //4、执行方法
        userService.save(user);

    }

}

2、@EnableLoadTimeWeaving

(1)源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(LoadTimeWeavingConfiguration.class)
public @interface EnableLoadTimeWeaving {
	/** * Whether AspectJ weaving should be enabled. */
	AspectJWeaving aspectjWeaving() default AspectJWeaving.AUTODETECT;
	/** * AspectJ weaving enablement options. */
	enum AspectJWeaving {
	/** * Switches on Spring‐based AspectJ load‐time weaving. */
	ENABLED,
	/** * Switches off Spring‐based AspectJ load‐time weaving (even if a * "META‐INF/aop.xml" resource is present on the classpath). */
	DISABLED,
	/** * Switches on AspectJ load‐time weaving if a "META‐INF/aop.xml" resource * is present in the classpath. If there is no such resource, then AspectJ * load‐time weaving will be switched off. */
	AUTODETECT;
	}
}
(2)说明

作用:
用于切换不同场景下实现增强。
属性:
aspectjWeaving:是否开启LTW的支持。
ENABLED 开启LTW
DISABLED 不开启LTW
AUTODETECT 如果类路径下能读取到META‐INF/aop.xml文件,则开启LTW,否则关闭
使用场景:在Java 语言中,从织入切面的方式上来看,存在三种织入方式:编译期织入、类加载期织入和运行期织入。

编译期织入是指在Java编译期,采用特殊的编译器,将切面织入到Java类中;而类加载期织入则指通过特殊的类加载器,在类字节码加载到JVM时,织入切面;运行期织入则是采用CGLib工具或JDK动态代理进行切面的织入。

AspectJ提供了两种切面织入方式,第一种通过特殊编译器,在编译期,将AspectJ语言编写的切面类织入到Java类中,可以通过一个Ant或Maven任务来完成这个操作;第二种方式是类加载期织入,也简称为LTW(Load Time Weaving)

(3)示例
**
* @author 黑马程序员
* @Company http://www.itheima.com
*/
//@Component
@Aspect
public class LoadTimeWeavingAspect {
	/** * 增强方法 * @param pjp * @return * @throws Throwable */
	@Around("pointcut()")
	public Object profile(ProceedingJoinPoint pjp) throws Throwable {
		//1.创建秒表对象
		StopWatch sw = new StopWatch(getClass().getSimpleName());
		try {
			//2.记录执行
			sw.start(pjp.getSignature().getName());
			//3.调用切入点方法并返回
			return pjp.proceed();
		} finally {
			//4.停止计时
			sw.stop();
			//5.输出
			System.out.println(sw.prettyPrint());
		}
	}
	/** * 切入点表达式 */
	@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
	public void pointcut() {
	}
}

相关文章

最新文章

更多