Spring深入解析之AOP
AOP:Aspect Oriented Programming 面向切面编程
OOP:Object Oriented Programming 面向对象编程
面向切面编程:基于OOP基础之上新的编程思想,OOP面向的主要对象是类,而AOP面向的主要对象是切面,在处理日志、安全检查、权限认证、事务管理等方面有非常重要的作用。AOP是Spring中重要的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC做补充。通俗点说的话就是在不修改原有代码的情况下,增强跟主要业务没有关系的公共功能代码到之前写好的方法中的指定位置。这种编程的方式叫AOP
AOP的底层用的代理,代理是一种设计模式
静态代理
弊端:需要为每一个被代理的类创建一个“代理类”,虽然这种方式可以实现,但是成本太高
动态代理(AOP的底层是用的动态)
jdk动态代理 :必须保证被代理的类实现了接口
cglib动态代理 :不需要接口
在学aop前,我们先来手写一个公共的JDk动态代理
CalculatorProxy.class
public class CalculatorProxy {
/**
* 为传入的参数对象创建一个动态代理对象
* @param calculator 被代理对象
* @return
*/
public static Calculator getProxy(final Calculator calculator){
//被代理对象的类加载器
ClassLoader loader = calculator.getClass().getClassLoader();
//被代理对象的接口
Class<?>[] interfaces = calculator.getClass().getInterfaces();
//方法执行器,执行被代理对象的目标方法
InvocationHandler h = new InvocationHandler() {
/**
* 执行目标方法
* @param proxy 代理对象,给jdk使用,任何时候都不要操作此对象
* @param method 当前将要执行的目标对象的方法
* @param args 这个方法调用时外界传入的参数值
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//利用反射执行目标方法,目标方法执行后的返回值
// System.out.println("这是动态代理执行的方法");
Object result = null;
try {
System.out.println(method.getName()+"方法开始执行,参数是:"+ Arrays.asList(args));
result = method.invoke(calculator, args);
System.out.println(method.getName()+"方法执行完成,结果是:"+ result);
} catch (Exception e) {
System.out.println(method.getName()+"方法出现异常:"+ e.getMessage());
} finally {
System.out.println(method.getName()+"方法执行结束了......");
}
//将结果返回回去
return result;
}
};
Object proxy = Proxy.newProxyInstance(loader, interfaces, h);
return (Calculator) proxy;
}
}
上述代码感觉已经非常完美了,但是要说明的是,这种动态代理的实现方式调用的是jdk的基本实现,如果需要代理的目标对象没有实现任何接口,那么是无法为他创建代理对象的,这也是致命的缺陷。而在Spring中我们可以不编写上述如此复杂的代码,只需要利用AOP,就能够轻轻松松实现上述功能,当然,Spring AOP的底层实现也依赖的是动态代理。那么我们接下来学习一下AOP是怎么实现的
AOP的核心概念及术语
切面(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。 在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以@Aspect注解(@AspectJ 注解方式)来实现。
连接点(Join point): 在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
通知(Advice): 在切面的某个特定的连接点上执行的动作。通知有多种类型,包括“around”, “before” and “after”等等。通知的类型将在后面的章节进行讨论。 许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
切点(Pointcut): 匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切点语义。
引入(Introduction): 声明额外的方法或者某个类型的字段。Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现 IsModified接口, 以便简化缓存机制(在AspectJ社区,引入也被称为内部类型声明(inter))。
目标对象(Target object): 被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。
AOP代理(AOP proxy):AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用AspectJ编译器)、类加载时或运行时中完成。 Spring和其他纯Java AOP框架一样,是在运行时完成织入的。
AOP的通知类型
前置通知(Before advice): 在连接点之前运行但无法阻止执行流程进入连接点的通知(除非它引发异常)。
后置返回通知(After returning advice):在连接点正常完成后执行的通知(例如,当方法没有抛出任何异常并正常返回时)。
后置异常通知(After throwing advice): 在方法抛出异常退出时执行的通知。
后置通知(总会执行)(After (finally) advice): 当连接点退出的时候执行的通知(无论是正常返回还是异常退出)。
环绕通知(Around Advice):环绕连接点的通知,例如方法调用。这是最强大的一种通知类型,。环绕通知可以在方法调用前后完成自定义的行为。它可以选择是否继续执行连接点或直接返回自定义的返回值又或抛出异常将执行结束。
知道了AOP的概念,那我们具体的通过AOP来实现一下
1、pom依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
2、配置切面类
@Aspect//声明为切面
@Component//将切面交给spring去管理
public class LogUtil {
/*
设置下面方法在什么时候运行
@Before:在目标方法之前运行:前置通知
@After:在目标方法之后运行:后置通知
@AfterReturning:在目标方法正常返回之后:返回通知
@AfterThrowing:在目标方法抛出异常后开始运行:异常通知
@Around:环绕:环绕通知
当编写完注解之后还需要设置在哪些方法上执行,使用表达式
execution(访问修饰符 返回值类型 方法全称)
*/
// 前置通知
@Before("execution(* cn.test.service..*.*(..))")
public static void before(){
System.out.println("方法前");
}
// 后置通知
@After("execution(* cn.test.service..*.*(..))")
public static void after(){
System.out.println("方法后");
}
// 后置异常通知
@AfterThrowing("execution(* cn.test.service..*.*(..))")
public static void afterException(){
System.out.println("方法异常");
}
// 后置返回通知
@AfterReturning("execution(* cn.test.service..*.*(..))")
public static void afterEnd(){
System.out.println("方法返回");
}
}
3、开启基于注解的aop的功能
如果SpringBoot需要在启动类上加@EnableAspectJAutoProxy
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
">
<context:component-scan base-package="cn.test"></context:component-scan>
<!--开启注解-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
4、测试
public class MyTest {
public static void main(String[] args){
ApplicationContext context = new ClassPathXmlApplicationContext("aop.xml");
Calculator bean = context.getBean(Calculator.class);
bean.XXX();
}
}
5、当然我们也可以获取方法的详细信息进行输出
@Aspect//声明为切面
@Component//将切面交给spring去管理
public class LogUtil {
// 前置通知
@Before("execution( public int cn.test.inter.MyCalculator.*(int,int))")
public static void start(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法开始执行,参数是:"+ Arrays.asList(args));
}
// 后置返回通知
@AfterReturning(value = "execution( public int cn.test.inter.MyCalculator.*(int,int))",returning = "result")
public static void stop(JoinPoint joinPoint,Object result){
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法执行完成,结果是:"+result);
}
// 后置异常通知
@AfterThrowing(value = "execution( public int cn.test.inter.MyCalculator.*(int,int))",throwing = "exception")
public static void logException(JoinPoint joinPoint,Exception exception){
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法出现异常:"+exception);
}
//环绕通知
@Around("execution( public int cn.test.inter.MyCalculator.*(int,int))")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint){
Object[] args = proceedingJoinPoint.getArgs();
String name = proceedingJoinPoint.getSignature().getName();
Object proceed = null;
try {
System.out.println("环绕前置通知:"+name+"方法开始,参数是"+Arrays.asList(args));
//利用反射调用目标方法,就是method.invoke()
proceed = proceedingJoinPoint.proceed(args);
System.out.println("环绕返回通知:"+name+"方法返回,返回值是"+proceed);
} catch (Throwable e) {
System.out.println("环绕异常通知"+name+"方法出现异常,异常信息是:"+e);
}finally {
System.out.println("环绕后置通知"+name+"方法结束");
}
return proceed;
}
}
注意:pring AOP的动态代理方式是jdk自带的方式,容器中保存的组件是代理对象com.sun.proxy.$Proxy对象,如果我们getBean()不是一个接口,而是一个具体类,那么会走CGLIB的代理,代理对象是 EnhancerBySpringCGLIB类型
总结:环绕通知的执行顺序是优于普通通知的,具体的执行顺序如下
环绕前置-->普通前置-->目标方法执行-->环绕正常结束/出现异常-->环绕后置-->普通后置-->普通返回或者异常。
但是需要注意的是,如果出现了异常,那么环绕通知会处理或者捕获异常,普通异常通知是接收不到的,因此最好的方式是在环绕异常通知中向外抛出异常。
接下来,我们就来具体分析一下AOP切入点表达式
Spring AOP支持使用以下AspectJ切点标识符(PCD),用于切点表达式:
- execution: 用于匹配方法执行连接点。 这是使用Spring AOP时使用的主要切点标识符。 可以匹配到方法级别 ,细粒度
- within: 只能匹配类这级,只能指定类, 类下面的某个具体的方法无法指定, 粗粒度
- this: 匹配实现了某个接口:this(com.xyz.service.AccountService)
- target: 限制匹配到连接点(使用Spring AOP时方法的执行),其中目标对象(正在代理的应用程序对象)是给定类型的实例。
- args: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中变量是给定类型的实例。 AOP) where the arguments are instances of the given types.
- @target: 限制与连接点的匹配(使用Spring AOP时方法的执行),其中执行对象的类具有给定类型的注解。
- @args: 限制匹配连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注解。
- @within: 限制与具有给定注解的类型中的连接点匹配(使用Spring AOP时在具有给定注解的类型中声明的方法的执行)。
- @annotation:限制匹配连接点(在Spring AOP中执行的方法具有给定的注解)。
具体说一下常用的三种表达式
1、execution
细粒度的切入点表达式,可以以方法为单位定义切入点规则
语法:execution(修饰符 返回值类型 包名.类名.方法名(参数类型,参数类型…))
execution(* cn.test.service..*.*(..))
xml配置:
<aop:pointcut expression="execution(void cn.test.service.UserServiceImpl.addUser(java.lang.String))" id="pc1"/>
访问修饰符:可不写 可以匹配任何一个访问修饰符
返回值:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名,如果被切入的方法返回值不一样可以使用*代表所有的方法值都能匹配
包名:cn.*,cn.任意名字,但是只能匹配一级。cn.test.. 代表cn.test.service包下的所有类
类名:可以写*,代表任何名字的类名。也可以模糊匹配 *ServiceImpl
方法名:可以写*,代表任何方法。也可以模糊匹配 *add
参数:如果是jdk自带类型可以不用写完整限定名,如果是自定义类型需要写上完整限定名。如果需要匹配任意参数 可以写:..
2、within
通过类名进行匹配 粗粒度的切入点表达式
within(cn.test.service.UserServiceImpl)
within(cn.test.service.*):匹配指定包下所有的类,注意,只匹配当前包,不包括当前包的子孙包
within(cn.test.*.*):匹配包
within(cn.test..*):..*匹配符,匹配指定包下及其子孙包下的所有的类
xml配置:<aop:pointcut expression="within(cn.test.service.UserServiceImpl)"/>
3、annotation(自定义接口,在方法上指定可获取自定义name)
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemServiceLog {
String description() default "";
}
在方法上加@SystemControllerLog(description = "新增商品")
@Before("annotation(systemServiceLog)")
public static void before(SystemControllerLog systemServiceLog){
System.out.println("方法前,参数"+systemServiceLog.description);
}
表达式的抽取
如果在实际使用过程中,多个方法的表达式是一致的话,那么可以考虑将切入点表达式抽取出来:
a、随便生命一个没有实现的返回void的空方法
b、给方法上标注@Potintcut注解
@Pointcut("execution(* cn.test..*.*(..))")
public void myPoint(){}
@Before("myPoint()")
//方法体
基于Schema的AOP配置
之前我们讲解了基于注解的AOP配置方式,下面我们说一下基于xml的配置方式,虽然在现在的企业级开发中使用注解的方式比较多,但是你不能不会,因此需要简单的进行配置。
1、将所有的注解都进行删除
2、添加配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
">
<context:component-scan base-package="cn.test"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<bean id="logUtil" class="cn.test.util.LogUtil2"></bean>
<bean id="securityAspect" class="cn.test.util.SecurityAspect"></bean>
<bean id="myCalculator" class="cn.test.inter.MyCalculator"></bean>
<aop:config>
<aop:pointcut id="globalPoint" expression="execution(public int cn.test.inter.MyCalculator.*(int,int))"/>
<aop:aspect ref="logUtil">
<aop:pointcut id="mypoint" expression="execution(public int cn.test.inter.MyCalculator.*(int,int))"/>
<aop:before method="start" pointcut-ref="mypoint"></aop:before>
<aop:after method="end" pointcut-ref="mypoint"></aop:after>
<aop:after-returning method="stop" pointcut-ref="mypoint" returning="result"></aop:after-returning>
<aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"></aop:after-throwing>
<aop:around method="myAround" pointcut-ref="mypoint"></aop:around>
</aop:aspect>
</aop:config>
</beans>