Spring核心AOP技术说明目录
1. AOP技术概念
1.1 什么是AOP?
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理。
切面:公共的,通用的,重复的功能称为切面,面向切面编程就是将切面提取出单独开发,在需要调用的方法中通过动态代理的方式进行织入.
AOP技术就是说:在外部运行方法的同时,实际上切入了通用的事务以及日志等
1.2 AOP技术的作用
AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 Spring 框架中的一个重要内容。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样, 会使主业务逻辑变的混杂不清。例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑—转账。
1.3 面向切面编程的好处
1.减少重复;
2.专注业务;
2. 使用代理模式模拟实现AOP框架
2.1 静态代理方式实现AOP框架(仅能实现一个功能)
实现样例:
使用静态代理拆分业务和业务接口,切面和实现购买图书业务,在业务实现时实现事务切入或者日志
购买图书业务接口
public interface Service {
//实现图书购买业务
void buy();
}
购买图示功能实现类:
public class BookServiceImpl implements Service {
//此类是目标对象
public void buy(){
System.out.println("图书购买业务功能实现......");
}
}
切面接口
public interface AOP {
default void before(){}
default void after(){}
default void exception(){}
}
日志实现类
public class LogImpl implements AOP{
@Override
public void before() {
System.out.println("日志开启......");
}
}
事务实现类:
public class TransImpl implements AOP{
@Override
public void before() {
System.out.println("事务开启......");
}
@Override
public void after() {
System.out.println("事务提交......");
}
@Override
public void exception() {
System.out.println("事务回滚......");
}
}
代理类:
public class Agent implements Service{
//创建目标以(业务)对象,切面对象
Service target;
AOP aop;
public Agent(Service target, AOP aop) {
this.target = target;
this.aop = aop;
}
@Override
public void buy() {
try {
//切面
aop.before();
//业务
target.buy();
//切面
aop.after();
} catch (Exception e) {
aop.exception();
}
}
}
测试:
2.2 动态代理方式实现AOP框架
实现样例:
使用动态代理优化静态代理方式实现的业务
其他地方一样,唯一不一样的就是: obj = method.invoke(target, args);,可以在测试类中传入到代理工厂不同的实现功能,在静态代理中实现的功能已经在代理类中指定:
代理工厂类:
public class ProxyFactory {
//创建动态代理对象
public static Object getProxy(Service target,AOP aop){
return Proxy.newProxyInstance(
//类加载器
target.getClass().getClassLoader(),
//目标对象实现的所有接口
target.getClass().getInterfaces(),
//代理功能的实现
new InvocationHandler() {
@Override
public Object invoke(
//代理对象
Object proxy,
//正在被调用的方法
Method method,
//目标对象的参数
Object[] args) throws Throwable {
Object obj = null;
try {
//切面
aop.before();
//业务功能实现
obj = method.invoke(target, args);
//切面
aop.after();
aop.exception();
} catch (IllegalAccessException e) {
aop.exception();
}
return obj;
}
}
);
}
}
3. AOP的通知类型以及编程术语
-
通知类型
- Before通知:在目标方法被调用前调用,涉及接口org.springframework.aop.MethodBeforeAdvice;
- After通知:在目标方法被调用后调用,涉及接口为org.springframework.aop.AfterReturningAdvice;
- Throws通知:目标方法抛出异常时调用,涉及接口org.springframework.aop.ThrowsAdvice;
- Around通知:拦截对目标对象方法调用,涉及接口为 org.aopalliance.intercept.MethodInterceptor。
-
编程术语:
- 切面(Aspect)泛指交叉业务逻辑,或是公共的,通用的业务。上例中的事务处理、日志处理就可以理解为切面。常用的切面是通知(Advice)。实际就是对主业务逻辑的一种增强。
- 连接点(JoinPoint)指可以被切面织入的具体方法。通常业务接口中的方法均为连接点。
- 切入点(Pointcut)指声明的一个或多个连接点的集合。通过切入点指定一组方法。被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
- 目标(target)对象指 将要被增强 的对象。 即包含主业 务逻辑的 类的对象。 上例中 的BookServiceImpl 的对象若被增强,则该类称为目标类,该类对象称为目标对象。当然, 不被增强,也就无所谓目标不目标了。
- 通知(Advice)表示切面的执行时间,Advice 也叫增强。上例中的 MyInvocationHandler 就可以理解为是一种通知。换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
4. AspectJ对AOP的实现
对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便, 而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。
在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
4.1 AspectJ简介
AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。
总的来说就是易学易用,在学之后可以使用注解方式轻松实现切面编程
4.2 AspectJ通知类型
AspectJ 中常用的通知有四种类型:
Ⅰ 前置通知@Before
Ⅱ 后置通知@AfterReturning
Ⅲ 环绕通知@Around
Ⅳ 最终通知@After
Ⅴ 定义切入点@Pointcut(了解)
4.3 切入点表达式[比较重要]
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
简单点就是:
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中黑色文字表示可省略部分,各部分间用空格分开。在其中可以使用以下符号:
*:表示0至任意多个字符
… 如果出现在方法的参数中,则代表任意参数
如果出现在路径中,则代表本路径及其所有的子路径
示例:
execution(public * (…)) :任意的公共方法
execution( set*(…)):任何一个以“set”开始的方法
execution(* com.xyz.service.impl..(…)):任意的返回值类型,在com.xyz.service.impl包下的任意类的任意方法的任意参数
execution(* com.xyz.service….(…)):任意的返回值类型 ,在com.xyz.service及其子包下的任意类的任意方法的任意参数
需要牢记在表达式中可以没有访问权限,和抛出异常,*和…代表的含义
4.4 AspectJ基于注解的方式实现AOP技术
4.4.1 AspectJ的前置通知@Before
在目标方法执行前切入切面功能.在切面方法中不可以获得目标方法的返回值,只能得到目标方法的签名.
在本次通知中,将所有的类都给全,包括接口类,目标实现类,以及AspectJ实现切面编程,测试类,在后面的几个通知了类中给出的是核心,其余的一样可以使用(接口以及实现类)
接口类:
public interface SomeService {
String doSome(String name ,int age);
}
接口实现类(目标业务实现这里仅用一句话带过):
@Service
public class SomeServiceImpl implements SomeService {
@Override
public String doSome(String name, int age) {
System.out.println("doSome业务功能实现......");
return "abcd";
}
}
AspectJ框架实现:
@Aspect
@Component
public class MyAspect {
/**
* 所有的切面功能都是由切面方法实现的
* 可以将各种切面都在此类中开发
*
* 前置通知的切面方法规范
* 访问权限是public
* 方法的返回值是void
* 方法名称自定义
* 方法没有参数,如果有也只有JointPoint类型
* 必须使用@Before注解来声明切入的时机是前切工功能和切入点
* 参数:value 指定切入点表达式
*
* 业务方法:
* public String doSome(String name, int age)
*/
@Before(value = "execution(public String com.lcl.s01.SomeServiceImpl.doSome(String,int))")
public void myBefore(JoinPoint joinPoint){
//使用JoinPoint可以获取目标方法的签名:权限修饰符 返回值类型 方法声明(参数)
System.out.println("切面方法中的前置功能实现......");
System.out.println("目标方法的签名是:"+joinPoint.getSignature());
System.out.println("目标方法的参数是:"+ Arrays.toString(joinPoint.getArgs()));
}
}
配置文件:
<!--基于注解的形式创建对象-->
<context:component-scan base-package="com.lcl.s01"></context:component-scan>
<!--实现绑定业务-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试:
@Test
public void testSomeService(){
ApplicationContext ac = new ClassPathXmlApplicationContext("s01/applicationContext.xml");
// SomeService someService = (SomeService) ac.getBean("someService");
/*
* 换成注解的方式获取代理对象就需要命名为someServiceImpl[驼峰命名法]
* 使用set注入的方式写入的是id
* */
SomeService someService = (SomeService) ac.getBean("someServiceImpl");
System.out.println(someService);
String s = someService.doSome("李华", 22);
System.out.println(s);
}
注意:
在实现类和AspectJ类中首先要使用注解的形式实现IoC,将对象交给容器创建,其次在AspectJ框架实现类中加入@Aspect指明此类是切面
4.4.2 @AfterReturning后置通知
后置通知是在目标方法执行后切入切面功能,可以得到目标方法的返回值.如果目标方法的返回值是简单类型(8种基本类型+String)则不可改变.如果目标方法的返回值是引用类型则可以改变.
@Aspect
@Component
public class MyAspect {
/**
* 后置通知的方法的规范
* 1)访问权限是public
* 2)方法没有返回值void
* 3)方法名称自定义
* 4)方法有参数(也可以没有参数,如果目标方法没有返回值,则可以写无参的方法,但一般会写有参,这样可以处理无参可以处理有参),这个切面方法的参数就是目标方法的返回值
* 5)使用@AfterReturning注解表明是后置通知
* 参数:
* value:指定切入点表达式
* returning:指定目标方法的返回值的名称,则名称必须与切面方法的参数名称一致.
*/
@AfterReturning(value = "execution(* com.lcl.s02.*.*(..))",returning = "obj")
public void myAfterReturning(Object obj){
System.out.println("后置通知功能实现..............");
if(obj != null){
if(obj instanceof String){
obj = obj.toString().toUpperCase();
System.out.println("在切面方法中目标方法的返回值:"+obj);
}
if(obj instanceof Student){
Student stu = (Student) obj;
stu.setName("李四");
System.out.println("在切面方法中目标方法的返回值:"+stu);
}
}
}
}
4.4.3 AspectJ框架切换JDK动态代理和CGLib动态代理说明
<aop:aspectj-autoproxy ></aop:aspectj-autoproxy> ===>默认是JDK动态代理,取时必须使用接口类型
<aop:aspectj-autoproxy proxy-target-class=“true”></aop:aspectj-autoproxy> ==>设置为CGLib子类代理,可以使用接口和实现类接
使用接口来接,永远不出错.
4.4.4 环绕通知@Around
它是通过拦截目标方法的方式 ,在目标方法前后增强功能的通知.它是功能最强大的通知,一般事务使用此通知.它可以轻易的改变目标方法的返回值.
@Aspect
@Component
public class MyAspect {
/**
* 环绕通知方法的规范
* 1)访问权限是public
* 2)切面方法有返回值,此返回值就是目标方法的返回值
* 3)方法名称自定义
* 4)方法有参数,此参数就是目标方法
* 5)回避异常Throwable
* 6)使用@Around注解声明是环绕通知
* 参数:
* value:指定切入点表达式
*/
@Around(value = "execution(* com.lcl.s03.*.*(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//前切功能实现
System.out.println("环绕通知中的前置功能实现............");
//目标方法调用
Object obj = pjp.proceed(pjp.getArgs());
//后切功能实现
System.out.println("环绕通知中的后置功能实现............");
return obj.toString().toUpperCase(); //改变了目标方法的返回值
}
}
4.4.5 最终通知@After以及AspectJ总结
无论目标方法是否正常执行,最终通知的代码都会被执行.
//最终业务功能实现
@After(value = "myCut()")
public void myAfter(){
System.out.println("最终业务功能实现......");
}
- 前置业务功能:可以获取目标方法的签名和方法
- 后置通知功能:在切入点表达式中不仅有Value,还有returning,返回的要和参数一致(目标方法),.如果目标方法的返回值是简单类型(8种基本类型+String)则不可改变.如果目标方法的返回值是引用类型则可以改变.
- 环绕业务功能:有参数ProceedingJoinPoint,表示目标方法,可以随意修改方法返回值
- 最终通知功能:没有参数和返回值,一定会被执行