AOP
面向切面编程 Aspect Oriented Programming
切面:切入点+通知
给容器中的组件进行增强,AOP其实做的事情就是将委托类组件替换为代理组件,取出的组件就是代理组件。AOP做的是更精细的增强,并不是所有的组件里的方法都做增强,而是做筛选,开发人员提供筛选条件。
切入点 → 筛选容器中的组件中要被增强的方法
通知 → 做什么样的增强
编程术语
Target:目标类、委托类、被代理类
Proxy:代理类
PointCut:切入点
Advice:通知
Aspect:切面
JoinPoint:连接点,增强过程中的点。通过JoinPoint可以拿到一些参数(委托类对象、代理类对象、方法、参数)
Weaver:织入
案例
案例1使用工厂方法注册代理
FactoryBean
@Component
public class ServiceFactoryBean implements FactoryBean<UserService> {
@Qualifier("userServiceImpl")
@Autowired
UserService userService; //维护一个成员变量,用于增强
@Override
public UserService getObject() throws Exception {
Object proxy = Enhancer.create(UserServiceImpl.class, new InvocationHandler() {
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
System.out.println("使用FactoryBean的动态代理进行增强");
method.invoke(userService, objects);
return null;
}
});
return (UserService) proxy;
}
@Override
public Class<?> getObjectType() {
return UserService.class;
}
}
这里一定要注意,因为我们的动态代理也会实现委托类的接口,所以在取出委托类的组件时要按照类型+id来取值,因为此时实现了接口,那就相当于有了两个这个类型的组件
@Autowired
@Qualifier("serviceFactoryBean")
UserService userServiceProxy;
@Test
public void test3(){
userServiceProxy.demo();
}
SpringAOP
Spring提供了一个生成动态代理对象的ProxyFactoryBean
通过getObject方法来生成代理对象,那么需要指定委托类对象,也需要指定增强
通知要实现接口 → MethodInterceptor → invoke
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation var1) throws Throwable;
}
实现接口在导包的时候,一定要注意导的是org.aopalliance下的包
@Component
public class UserServiceInter implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("AOP的动态代理");
Object proceed = methodInvocation.proceed(); //执行委托类的方法
System.out.println("AOP的动态代理");
return proceed;
}
}
通知需要做的事情类似于InvocationHandler
1.委托类方法的执行
2.增强
通过配置文件来注册AOP的对象,target表示的是委托类,interceptorNames表示的就是通知的组件名
<!--ProxyFactoryBean已经编译好了-->
<bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--委托类组件-->
<property name="target" ref="userServiceImpl"/>
<!--interceptor是什么,长什么样 → 通知-->
<property name="interceptorNames" value="customInterceptor"/>
</bean>
这里在取出的时候也要注意动态代理会实现委托类的接口,就会多产生一个该类型的实例,就不能使用取出类型的方法取出。
只有是在容器中注册一个代理类才会出现这种情况,如果没有在容器中注册,就不会。
AspectJ
Pointcut
切入点表达式→匹配的方法→判断方法是否在增强范围内→筛选方法进行增强
execution
大的范围指定,指定满足特征的方法
表达式的规范:execution(修饰符 返回值 包名、类名、方法名(形参))
例如:public void com.cskaoyan.service.UserService.sayHello();
1.能否省略
2.能否通配
3.特殊的用法
修饰符
可以省略→直接省略不写就代表任意的修饰符
返回值
不能省略,但可以使用通配符*来表示返回值可以是任意值
如果是Pojo类,那就需要写一个全限定类名,这里面也可以用通配符
例如:com.cskaoyan.bean.User→com.caskaoyan.*.User
包名、类名、方法名
只能省略一部分:包名的第一级和方法名不能省略(头尾不能省略),中间的任意一部分都可以省略
使用…进行省略,这里可以先写出完整的,然后中间的部分删去不写即可,只要用了…就表示省略,如果有多级的,那么也只需要使用一次…
public void com.cskaoyan.service.UserServiceImpl.sayHello();
中间加黑的部分都可以省略
例如:
execution(void com…service.UserServiceImpl.sayHello())
execution( void com.cskaoyan…UserServiceImpl.sayHello()😉
execution( void com.cskaoyan…sayHello()😉
可以使用通配符*,任意位置都可以使用
- execution(void *.cskaoyan…sayHello())
- execution(void com.cskaoyan…say*())
形参
public void com.cskaoyan.service.UserServiceImpl.sayHello()
可以省略不写→表示无参方法
可以使用通配符
*表示任意类型的单个字符
…表示任意参数(类型任意,数量任意)
同样的,如果是pojo类作为形参,那么需要写全限定类名
增强service层的任意方法:
execution(* com.cskaoyan.service…*(…))
前面的第一个…表示的是省略中间的包,*表示的任意方法名,方法参数的…表示的是任意参数
使用
需要引入依赖:aspectjweaver
<!--aspectjweaver → groupid为org.aspectj-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
引入aop的schema约束
<aop:config>
<aop:pointcut id="mypointcut" expression="execution(* com.cskaoyan.service..*(..))"/>
</aop:config>
@annotation
指定要增强的具体的方法,注解的形式
@annotation(自定义的注解的全限定类名)
首先需要自定义一个注解,自定义的注解写在容器中的组件的哪个方法上,哪个方法就被划入了增强的范围
@Target(ElementType.METHOD) //注解可以出现在方法上
@Retention(RetentionPolicy.RUNTIME) //注解生效时间:runtime运行时
public @interface CountTime {
}
<aop:pointcut id="mypointcut2" expression="@annotation(com.cskaoyan.anno.CountTime)"/>
Advisor
Advisor:pointcut + advice (实现MethodInterceptor接口)
<aop:config>
<aop:pointcut id="myPointcut" expression="execution(* com.fh.service.*(..))"/>
<aop:pointcut id="myPointcut2" expression="@annotation(com.fh.anno.CountTime)"/>
<aop:advisor advice-ref="countTimeAdvice" pointcut-ref="myPointcut2"/>
</aop:config>
@Component
public class CountTimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = methodInvocation.proceed();
long end = System.currentTimeMillis();
System.out.println(methodInvocation.getMethod().getName() + "执行时间:" + (end - start));
return proceed;
}
}
pointcut属性:切入点,可以直接写execution表达式,也可以用注解
advisor:advice-ref表示的是通知的全限定类名,pointcut-ref表示指定的切入点
进行增强后,取出的组件就是增 强后的代理组件,动态织入将委托类替换为代理类;此外,也需要看代理的对象是否在容器中注册,只有注册了才会起冲突。AspectJ中的没有注册,采取的是动态织入的方式。
Aspect
Aspect = pointcut + advice(提供好一些包含时间属性的通知)
具体的增强还是需要自己来实现
时间属性:相对于委托类方法的执行时间,是一个相对时间,只需要关注相对委托类的时间就行,不需要关注通知之间的时间。
- Before :在委托类方法之前执行
- Around:一部分在之前,一部分在之后,类似于InvocationHandler,会使用invoke来调用委托类的方法
- After:在委托类方法执行之后执行,类似于finally,不管发生什么情况,对应的方法一定会执行
- AfterThrowing:在抛出异常后执行,类似于catch
- AfterRetruning:在委托方法执行之后执行,可以获得执行的return的结果,如果抛出异常则不会执行
1.如何将通知和方法结合
将方法写在切面类中,作为通知方法
2.切面组件
1.注册组件
2.标记切面组件
@Component
public class CustomAspect {
//before通知方法,方法名任意
public void mybefore() {
System.out.println("Before通知:正道的光1.0");
}
//after通知方法,方法名任意
public void myafter() {
System.out.println("After通知:照在大地上1.0");
}
//around通知方法 → 类似于InvocationHandler的invoke、类似于MethodInterceptor的invoke
//要包含委托类方法的执行和增强两部分
//around方法的返回值作为代理对象的执行结果 → 返回值为Object类型
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("around的前半部分");
//委托类方法的执行 → ProceedingJoinPoint
Object proceed = proceedingJoinPoint.proceed();//执行的是委托类的方法
//增强
System.out.println("around的后半部分");
return proceed + " niupi";
}
//afterReturning可以获得委托类方法的执行结果,意味着能够接收到这个值 → 形参中以Object接收
//形参名自己定义
public void afterReturning(Object returnValue) {
System.out.println("afterReturning通知:" + returnValue);
}
//afterThrowing通知 抛出异常的时候执行 → 可以获得你抛出的异常对象
//使用Exception或Throwable来接收
public void afterThrowing(Throwable e) {
System.out.println("afterThrowing通知:" + e.getMessage());
}
}
<aop:aspect ref="customAspect"/>
如何将通知和切入点结合
使用配置文件:
<aop:config>
<aop:pointcut id="mypointcut" expression="execution(* com.cskaoyan.service..*(..))"/>
<aop:pointcut id="mypointcut2" expression="@annotation(com.cskaoyan.anno.CountTime)"/>
<aop:aspect ref="customAspect">
<!--子标签-->
<!--指定方法为什么通知方法、通知是啥切入点-->
<!--pointcut属性:直接写切入点表达式-->
<!--pointcut-ref属性:引用了切入点的id-->
<aop:before method="mybefore" pointcut-ref="mypointcut"/>
<aop:after method="myafter" pointcut-ref="mypointcut"/>
<aop:around method="around" pointcut-ref="mypointcut"/>
<!--returning属性:after-returning通知方法中哪一个Object类型的形参来接收委托类方法的执行结果-->
<aop:after-returning method="afterReturning" pointcut-ref="mypointcut" returning="returnValue"/>
<!--throwing属性:afterThrowing通知方法中哪一个Exception或Throwable类型的形参来接收委托类方法抛出的异常-->
<aop:after-throwing method="afterThrowing" pointcut-ref="mypointcut" throwing="e"/>
</aop:aspect>
</aop:config>
如果委托类方法抛出异常
Around通知的后半部分和AfterReturning通知执行不到
After通知是可以执行到的,它类似于finally
共有的属性:method、pointcut(-ref)
特有属性:afterReturning → returning、AfterThrowing → throwing ,让通知方法的形参能够接收到一些值
aop:pointcut的作用范围
aop:pointcut标签可以写在aop:aspect标签里,这时候就表示的是局部变量
<aop:config>
<!--全局变量:整个aop:config标签里都可以使用-->
<aop:pointcut id="mypointcut" expression="execution(* com.cskaoyan.service..*(..))"/>
<aop:aspect ref="customAspect">
<!--局部变量:只能在当前aop:aspect标签里使用-->
<aop:pointcut id="mypointcut3" expression="execution(* com.cskaoyan.service..*(..))"/>
</aop:aspect>
</aop:config>
JoinPoint连接点
通过连接点,可以获得增强过程中的一些值:委托类对象、代理对象、方法、
提供这些值可以让业务更加灵活
写在Before通知或者Around通知的形参中
public interface ProceedingJoinPoint extends JoinPoint {
//意味着委托类方法的参数不做变化
Object proceed() throws Throwable;
//意味着委托类方法传入你的参数
Object proceed(Object[] var1) throws Throwable;
}
在around通知中,可以直接使用ProceedingJoinPoint的实例来获得对应的值
before通知里的使用
//before通知方法,方法名任意
public void mybefore(JoinPoint joinPoint) {
System.out.println("Before通知:正道的光1.0");
Object proxy = joinPoint.getThis(); //proxy代理对象
Object target = joinPoint.getTarget();//target委托类对象
//拿到方法
Signature signature = joinPoint.getSignature();
String name = signature.getName(); //Signature的name就是方法名
//委托类方法执行的参数
Object[] args = joinPoint.getArgs();
}
around通知里的使用
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//比如说,我们将传给委托类方法的参数拿到
Object[] args = proceedingJoinPoint.getArgs();
//如果执行的是sayHello5我们做一个个性化的处理
if ("sayHello5".equals(proceedingJoinPoint.getSignature().getName())) {
//个性化的处理
//原来传入的参数是songge → 现在给改一下 改成songge and ligenli
args[0] += " and ligenli";
}
System.out.println("around的前半部分");
//委托类方法的执行 → ProceedingJoinPoint
//Object proceed = proceedingJoinPoint.proceed();//执行的是委托类的方法的参数
Object proceed = proceedingJoinPoint.proceed(args);//执行的是委托类的方法,参数被修改
//增强
System.out.println("around的后半部分");
return proceed + " niupi";
}
通过around可以对参数进行修改,更加灵活了,返回值就是最终的方法执行的结果
注解使用aspectj
需要做的事
- aop:pointcut标签配置了切入点以及对应的切入点表达式
- 指定组件为切面组件
- 指定切面组件中的方法为一个什么样的通知方法
- 将方法和切入点绑定起来
打开AspectJ的注解开关
<aop:aspectj-autoproxy/>
注册切面类
在组件上增加 一个@Aspect注解,将当前组件标记为切面组件;注意@Component不能省
@Aspect
@Component
public class CustomAspect {
}
切入点配置
以方法的形式存在:体现出切入点的id和表达式
- id:方法名作为切入点的id
- 表达式:@Pointcut的value属性
@Pointcut("execution(* com.cskaoyan.service..*(..))")
public void mypointcut() {//只需要这个方法的方法名
}
指定方法为什么通知方法
在切面类中的方法上增加上通知注解:@Before、@After、@Around、@AfterReturning、@AfterThrowing
将方法与切入点绑定
利用通知注解的value属性
- 直接写切入点表达式
- 引用切入点对应的方法
//@Before("execution(* com.cskaoyan.service..*(..))") //直接写切入点表达式
@Before("mypointcut()") //引用方法名时要有一对括号
public void mybefore(JoinPoint joinPoint) {
}
//afterReturning可以获得委托类方法的执行结果,意味着能够接收到这个值 → 形参中以Object接收
//形参名自己定义
@AfterReturning(value = "mypointcut()",returning = "returnValue")
public void afterReturning(Object returnValue) {
System.out.println("afterReturning通知:" + returnValue);
}
//afterThrowing通知 抛出异常的时候执行 → 可以获得你抛出的异常对象
//使用Exception或Throwable来接收
@AfterThrowing(value = "mypointcut()",throwing = "e")
public void afterThrowing(Throwable e) {
System.out.println("afterThrowing通知:" + e.getMessage());
}