文章有参考其他博主文章:
- https://www.cnblogs.com/zhangxufeng/p/9160869.html
- https://blog.51cto.com/5914679/2092253
- https://www.cnblogs.com/xxdsan/p/6496332.html
一、AOP概述
-
AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在合适的时机将这些切面横向切入到业务流程指定的位置中。
-
利用AOP可以实现诸如:参数校验,日志记录,权限校验,事务控制等
-
AOP是处理一些横切行问题。这些横切性问题不会影响到主逻辑的实现,但是会散落到代码的各个部分,难以维护。AOP就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。
二、AOP术语
名词 | 释义 |
---|---|
连接点(Join point) | 程序执行过程中明确的点,一般是方法的调用。 Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被增强的方法 |
切入点(Pointcut) | 实际上被增强的方法称为切入点,在程序中主要体现为书写切入点表达式 |
通知(Advice) | 在特定的切入点上执行的增强处理 |
切面(Aspect) | 切面是通知和切入点的结合。说白了就是一个类里定义了切入点切入哪些方法 ,被切入的那些方法又完成了什么通知类型的增强处理 |
织入(Weaving) | 切面应用到目标对象并导致代理对象创建的过程 |
引入(Introduction) | 在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段 |
Spring AOP的五种通知类型
-
前置通知(Before):前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。注意,如果接收Join-Point,必须保证其为方法的第一个参数,否则报错。
对应Spring方法:
public void beforce()
-
后置通知(After):在目标方法执行之后执行的通知,在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。
对应Spring方法:
public void after()
-
环绕通知(Around):在目标方法执行之前和之后都可以执行额外代码的通知。在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过
ProceedingJoinPoint
来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。对应Spring方法:
public Object around(ProceedingJoinPoint jp)
-
异常通知(After-throwing):在目标方法抛出异常时执行的通知,可以配置传入
JoinPoint
获取目标对象和目标方法相关信息,但必须处在参数列表第一位,另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。对应Spring方法:
public void afterThrow(JoinPoint jp,Throwable e)
-
返回通知/最终通知(After-running):是在目标方法执行之后执行的通知。和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。另外,后置通知可以通过配置得到返回值,而最终通知无法得到。最终通知也可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。
对应Spring方法:
public void after(JoinPoint jp)
三、Spring AOP切点表达式
切点指示符是切点定义的关键字,切点表达式以切点指示符开始。开发人员使切点指示符来告诉切点将要匹配什么,有以下9种切点指示符:execution、within、this、target、args、@target、@args、@within、@annotation
1.excecution:
由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且在Spring中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。
如下是execution表达式的语法:
execution( [modifiers-pattern] [ret-type-pattern] [declaring-type-pattern] [name-pattern(param-pattern)] [throws-pattern])
格式 | 说明 |
---|---|
modifiers-pattern | 方法的可见性,如public,protected |
ret-type-pattern* | 方法的返回值类型,如int,void等 |
declaring-type-pattern | 方法所在类的全路径名,如com.spring.Aspect |
name-pattern* | 方法名类型,如buisinessService() |
param-pattern | 方法的参数类型,如java.lang.String |
throws-pattern | 方法抛出的异常类型,如java.lang.Exception |
*号表示参数不能省略
示例:
execution(public * com.test.ServiceImpl.userServiceImpl(java.lang.String,..))
2.通配符:
符号 | 说明 |
---|---|
* | *通配符,该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词 |
… | …通配符,该通配符表示0个或多个项。如果用于declaring-type-pattern中, 则表示匹配当前包及其子包,如果用于param-pattern中,则表示匹配0个或多个参数 |
示例:
// 表示匹配返回值为任意类型,在com.test.service路径下所有的类,方法参数为0个
execution(* com.test.service.*())
// 表示匹配返回值为任意类型,在com.test.service路径下以User为前缀的包下的所有类,方法参数为0个
execution(* com.test.service.User*.*())
// 表示匹配返回值为任意类型,并且是com.test.service包及其子包下的任意类的 名称为service()的方法,方法参数为0个
execution(* com.test.service..*.service())
// 表示匹配返回值为任意类型,参数类型第一个为String,后续为任意类型参数的方法
execution(* com.test.service.UserService.userLogin(java.lang.String,..))
3.within:
within表达式的粒度为类,其参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕。
within表达式的语法:
within(declaring-type-pattern)
示例:
// within表达式只能指定到类级别,如下示例表示匹配UserService中的所有方法
within(com.test.service.UserService)
// within表达式路径和类名都可以使用通配符进行匹配,比如如下表达式将匹配com.test.service包下的所有类,不包括子包中的类
within(com.test.service.*)
// 如下表达式表示匹配com.test.service包及子包下的所有类
within(com.test.service..*)
4.args
args表达式的作用是匹配指定参数类型和指定参数数量的方法,无论其类路径或者是方法名是什么。这里需要注意的是,args指定的参数必须是全路径的。
语法:
args(param-pattern)
示例:
// 表示匹配所有只有一个参数,并且参数类型是java.lang.String类型的方法
args(java.lang.String)
// 也可以使用通配符,但这里通配符只能使用..,而不能使用*
// 该切点表达式将匹配第一个参数为java.lang.String,最后一个参数为java.lang.Integer,并且中间可以有任意个数和类型参数的方法
args(java.lang.String,..,java.lang.Integer)
5.this和target
- this和target用法比较相像,this和target表达式中都只能指定类或者接口
- this表示匹配的连接点是所属的对象实例,即被切入方法的所在对象。比如有两个类A和B,并且A调用了B的某个方法,如果切点表达式为this(B),那么A的实例将会被匹配。也就是说被切入的B类方法在A中调用了,因为在A中,所以最终匹配的是A这个实例
- target表示匹配的连接点是所属的某个类型的实例(最为典型的例子就是接口和实现类),target()定位的就是指定类和它的子类。如果表达式为target(B),B类不仅会被匹配,A类也会被匹配
如何选择:
如果某个类实现了某个接口,,spring aop将使用jdk的动态代理来实现切面编程,在编写匹配这类型的目标对象的连接点表达式时要使用target指示符
public class UserDao implements BaseDao {
...
}
@Pointcut("target(com.test.dao.BaseDao)")
如果UserDao类没有实现任何接口,或者在spring aop配置属性:proxyTargetClass设为true时,Spring Aop会使用基于CGLIB的动态字节码技为目标对象生成一个子类将为代理类,这时应该使用this指示器:
@Pointcut("this(com.test.dao.UserDao)")
6.@within
前面within()表示切入指定类型的类实例,这里@within注解表示匹配带有指定注解的类
语法:
@within(annotation-type)
示例:
假设我们有自定义注解,并且在目标类上使用了自定注解
// 注解类
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface FruitAspect {
}
// 目标类
@FruitAspect
public class Apple {
public void eat() {
System.out.println("Apple.eat method invoked.");
}
}
// 切面类
@Aspect
public class MyAspect {
@Around("@within(com.business.annotation.FruitAspect)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is before around advice");
Object result = pjp.proceed();
System.out.println("this is after around advice");
return result;
}
}
上述切面表示匹配使用FruitAspect注解的类,而Apple则使用了该注解,因而Apple类方法的调用会被切面环绕
7.@annotation
@annotation注解匹配那些有指定注解的连接点
语法:
@annotation(annotation-type)
示例:
// 目标类,将FruitAspect移到了方法上
public class Apple {
@FruitAspect
public void eat() {
System.out.println("Apple.eat method invoked.");
}
}
@Aspect
public class MyAspect {
@Around("@annotation(com.business.annotation.FruitAspect)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is before around advice");
Object result = pjp.proceed();
System.out.println("this is after around advice");
return result;
}
}
这里Apple.eat()方法使用FruitAspect注解进行了标注,因而该方法的执行会被切面环绕,其执行结果如下:
this is before around advice
Apple.eat method invoked.
this is after around advice
8.@args
@args则表示使用指定注解标注的类作为某个方法的参数时该方法将会被匹配
语法:
@args(annotation-type)
示例:
// 使用注解标注的参数类
@FruitAspect
public class Apple {}
// 使用Apple参数的目标类
public class FruitBucket {
public void putIntoBucket(Apple apple) {
System.out.println("put apple into bucket.");
}
}
// 切面类
@Aspect
public class MyAspect {
@Around("@args(chapter7.eg6.FruitAspect)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is before around advice");
Object result = pjp.proceed();
System.out.println("this is after around advice");
return result;
}
}
// 测试类
public class AspectApp {
public static void main(String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("applicationContext.xml");
FruitBucket bucket = (FruitBucket) context.getBean("bucket");
bucket.putIntoBucket(new Apple());
}
}
这里FruitBucket.putIntoBucket(Apple)方法的参数Apple使用了@args注解指定的FruitAspect进行了标注,因而该方法的调用将会被环绕。结果如下:
this is before around advice
put apple into bucket.
this is after around advice
三、为目标对象引入新的方法
-
前面我们提到spring aop可以在运行期为目标类植入新的方法,那么可以通过@DeclareParents来实现
-
Spring AOP提供的
@Before
、@After
、@AfterReturning
、@AfterThrowing
、Around
只是对类的现有方法进行增强处理,并不能作为目标类引入新的方法
示例:
// 定义一个Person类
@Component
public class Person {
}
// 定义Person的子类Student
@Component
public class Student extends Person {
public void sayIdentification(){
System.out.println("我是学生。");
}
}
定义一个名为 Skill 的接口及它的实现类 SkillImpl。我们将要把 SkillImpl 的getSkill()方法添加到其他的类实例
@Component
public interface Skill {
void getSkill(String skill);
}
@Component
public class SkillImpl implements Skill {
@Override
public void getSkill(String skill) {
System.out.println(skill);
}
}
配置一个切面类,和SpringConfig配置类
@Component
@Aspect
public class AopConfig {
// 指定类全路径名和默认实现类
@DeclareParents(value = "com.san.spring.aop.Person", defaultImpl = SkillImpl.class)
public Skill skill;
}
@ComponentScan
@Configuration
@EnableAspectJAutoProxy
public class SpringConfig {
//SpringConfig 配置类
}
测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AopTest {
@Autowired
private Student student;
@Test
public void test(){
Skill skill = (Skill)student; // 通过类型转换,student对象就拥有了SkillImpl 类的方法
skill.getSkill("我会英语");
student.sayIdentification();
}
}