IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化成组件。
AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)
AOP是对OOP的补充延伸。
AOP底层使用的就是动态代理来实现的。
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。
spring6里程碑版本的仓库
依赖:spring context依赖、junit依赖、log4j2依赖
log4j2.xml文件放到类路径下。
一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务
这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。
如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:
使用AOP可以很轻松的解决以上问题。
总结AOP:将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
1.连接点 Joinpoint:描述的是位置
在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。
2.切点 Pointcut:本质上就是方法
在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
3.通知 Advice:本质上就是增强代码
通知又叫增强,就是具体你要织入的代码。
通知包括:
4.切面 Aspect
切点 + 通知就是切面。
5.织入 Weaving
把通知应用到目标对象上的过程。
6.代理对象 Proxy
一个目标对象被织入通知后产生的新对象。
7.目标对象 Target
被织入通知的对象。
代码体现:
public class UserService{
public void do1(){
System.out.println("do 1");
}
public void do2(){
System.out.println("do 2");
}
public void do3(){
System.out.println("do 3");
}
public void do4(){
System.out.println("do 4");
}
public void do5(){
System.out.println("do 5");
}
// 核心业务方法
public void service(){
try {
//连接点 Joinpoint
// 对于do1()来说 前置通知
do1();//切点 Pointcut (1)前置通知和后置通知都有叫环绕通知 (2)这一整个叫做切面(切点+通知)
// 对于do1()来说 后置通知
//连接点 Joinpoint
do2();//切点 Pointcut
//连接点 Joinpoint
do3();//切点 Pointcut
//连接点 Joinpoint
do5();//切点 Pointcut
//连接点 Joinpoint
}catch (Exception e){
//连接点 Joinpoint 异常通知
}finally {
//连接点 Joinpoint 最终通知
}
}
}
切点表达式用来定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
返回值类型:
全限定类名:
方法名:
形式参数列表:
异常:
例如:
表示service包下所有的类中以delete开始的所有方法
execution(public * com.mall.service.*.delete*(..))
所有类的所有方法
execution(* *(..))
mall包下所有的类的所有的方法
execution(* com.mall..*(..))
Spring对AOP的实现包括以下3种方式:
实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式。而实际开发基本使用注解方式,最重点的是注解方式的学习。
什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)
AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
使用Spring+AspectJ的AOP除了还需要引入aop依赖和aspects依赖:
而如果使用Maven则只需引入context依赖就会自动关联spring-aop依赖,没有使用Maven则需要把aop的jar包引入项目。
<!--spring aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
Spring配置文件中添加context命名空间和aop命名空间:
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
首先需要创建spring.xml文件:
true:表示强制使用CGLIB动态代理
false:表示,接口使用JDK动态代理,类使用CGLIB动态代理
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--添加组件扫描-->
<context:component-scan base-package="com.aop.annotation.service"/>
<!--
开启aspectj自动代理
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
创建目标类和目标方法,纳入spring管理:
@Service
public class UserService {//目标类
public void login(){//目标方法
System.out.println("正在登录。。。");
}
public void logout(){
System.out.println("正在退出。。。");
}
}
创建切面类:纳入spring管理,除此之外,需要加一个@Aspect注解,通知Spring框架这个类是一个切面类,到时候这个切面类会编写通知,添加切点表达式
@Component
@Aspect
public class LogAspect {//切面=切点+通知
}
通知类型包括:
@Before注解代表该通知为前置通知,注解里面需要写切点表达式
在LogAspect 切面类添加前置通知:
//前置通知
@Before("execution(* com.aop.annotation.service..*(..))")
public void beforeAdvice(){
System.out.println("前置通知。。。增强代码。。。");
}
测试程序:
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
System.out.println("---------------------------------");
userService.logout();
}
@AfterReturning代表该通知为后置通知,注解里面需要写切点表达式
在LogAspect 切面类添加后置通知:
//后置通知
@AfterReturning("execution(* com.aop.annotation.service..*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知。。。增强代码。。。");
}
再次运行测试程序:
@Around代表该通知为环绕通知,注解里面需要写切点表达式
环绕通知会收到一个ProceedingJoinPoint参数,用来执行目标方法。
在LogAspect 切面类添加环绕通知:
@Around("execution(* com.aop.annotation.service..*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前面代码
System.out.println("前环绕。。。");
//执行目标
joinPoint.proceed();
//后面代码
System.out.println("后环绕。。。");
}
再次运行测试程序:
可见环绕是最大的通知,在前置之前,在后置之后。
@AfterThrowing代表该通知为异常通知,注解里面需要写切点表达式
为了更好演示,重新创建一个目标类和目标方法:该类抛一个异常
@Service
public class OrderService {
public void generate(){
System.out.println("正在生成订单。。。");
throw new RuntimeException("异常");
}
}
在LogAspect 切面类添加异常通知:
@AfterThrowing("execution(* com.aop.annotation.service..*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知。。。");
}
修改测试程序:
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
System.out.println("---------------------------------");
userService.logout();
System.out.println("---------------------------------");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
运行程序:发现发生异常后,后环绕和后置通知已经没了,这是因为执行方法的时候抛异常之后不会再往下执行通知
@After代表该通知为最终通知,注解里面需要写切点表达式
在LogAspect 切面类添加最终通知:
@After("execution(* com.aop.annotation.service..*(..))")
public void afterAdvice(){
System.out.println("最终通知。。。");
}
再次运行测试程序:
可见最终通知一定会执行,不管有没有异常,最终通知,在后置之后,后环绕之前,由此可见环绕通知确实是最大的通知
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.。
JoinPoint是在Spring调用的切面方法时候自动传过来的,我们可以直接使用
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中, 添加了两个方法.
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
在LogAspect切面类添加注解:
@Order(2)
把OrderService目标类的异常注释掉:
@Service
public class OrderService {
public void generate(){
System.out.println("正在生成订单。。。");
//throw new RuntimeException("异常");
}
}
再定义一个切面类:优先级设置为1,使用一下JoinPoint
/**
* 安全切面
*/
@Component
@Aspect
@Order(1)//数字越小优先级越高
public class SecurityAspect {
@Before("execution(* com.aop.annotation.service..*(..))")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("安全的前置通知。。。");
Signature signature = joinPoint.getSignature();//获取目标方法的签名
System.out.println(signature.getName());
}
}
测试程序:
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
如果把优先级换一下,再次运行:
回顾之前的切点表达式,发现切点表达式是重复的
这样的缺点是:
怎么解决,可以使用通用切点表达式:@Pointcut注解将切点表达式单独的定义出来,在需要的位置引入即可。
在LogAspect 切面类添加一个方法使用@Pointcut注解:
方法只是个标记,方法名随意,方法体不需要写任何代码,只需要使用一个@Pointcut注解标注
//通用切点表达式
@Pointcut(value = "execution(* com.aop.annotation.service..*(..))")
public void execution_use(){
}
怎么引用,只需要在通知注解中写该方法即可,例如:
//@After("execution(* com.aop.annotation.service..*(..))")
@After("execution_use()")
这个通用表达式配置,只要一配置,任何切面类都可以使用,例如刚才的安全切面类,可以这样写。
/**
* 安全切面
*/
@Component
@Aspect
@Order(3)//数字越小优先级越高
public class SecurityAspect {
//@Before("execution(* com.aop.annotation.service..*(..))")
@Before("com.aop.annotation.service.LogAspect.execution_use()")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("安全的前置通知。。。");
/*
这个JoinPoint是在Spring调用的这个方法时候自动传过来的
我们可以直接使用
*/
Signature signature = joinPoint.getSignature();//获取目标方法的签名
System.out.println(signature.getName());
}
}
再次运行test的测试程序:
就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:
@Configuration //代替Spring配置文件
@ComponentScan("com.aop.annotation.service")//代替注解扫描
@EnableAspectJAutoProxy(proxyTargetClass = true)//代替开启aspectj自动代理,
public class SpringConfig {
}
测试程序:
@Test
public void testNoXML(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
第一步:编写目标类
/**
* 目标类
*/
public class UserService {
public void login(){
System.out.println("正在登录。。。");
}
}
第二步:编写切面类,并且编写通知
/**
* 切面
*/
public class TimerAspect {
//通知
public void arountAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前环绕
long begin = System.currentTimeMillis();
joinPoint.proceed();//执行目标
//后环绕
long end = System.currentTimeMillis();
System.out.println("耗时:"+ (end-begin));
}
}
第三步:编写spring配置文件
<?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 http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置bean-->
<bean id="userService" class="com.spring.aspectj.service.UserService"></bean>
<bean id="timerAspect" class="com.spring.aspectj.service.TimerAspect"></bean>
<!--aop配置-->
<aop:config>
<!--切点表达式-->
<aop:pointcut id="mypointcut" expression="execution(* com.spring.aspectj.service..*(..))"/>
<!--切面-->
<aop:aspect ref="timerAspect">
<!--哪个通知就配哪个-->
<aop:around method="arountAdvice" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
</beans>
测试程序:
@Test
public void testXML(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}