在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程
AOP是一种编程范式,隶属于软工范畴,指导开发者如何组织程序结构
AOP最早由AOP联盟的组织提出的,制定了一套规范.Spring将AOP思想引入到框架中,必须遵守AOP联盟的规范
通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型
利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
AOP采取横向抽取机制,补充了传统纵向继承体系(OOP)无法解决的重复性代码优化(性能监视、事务管理、安全检查、缓存)
将业务逻辑和系统处理的代码(关闭连接、事务管理、操作日志记录)解耦。
重复性代码被抽取出来之后,维护更加方便
1. Joinpoint(连接点) -- 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
2. Pointcut(切入点) -- 所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
3. Advice(通知/增强) -- 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知(切面要完成的功能)
4. Introduction(引介) -- 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field
5. Target(目标对象) -- 代理的目标对象
6. Weaving(织入) -- 是指把增强应用到目标对象来创建新的代理对象的过程
7. Proxy(代理) -- 一个类被AOP织入增强后,就产生一个结果代理类
8. Aspect(切面) -- 是切入点和通知的结合,以后咱们自己来编写和配置的
9. Advisor(通知器、顾问) -- 和Aspect很相似
- AspectJ是一个Java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
- 可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言。更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
- 了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。
- 对于织入这个过程,一般分为动态织入和静态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术
- ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
Spring AOP是通过动态代理技术实现的
而动态代理是基于反射设计的。(关于反射的知识,请自行学习)
动态代理技术的实现方式有两种:基于接口的JDK动态代理和基于继承的CGLib动态代理。
目标对象必须实现接口
/**
* 使用JDK的动态代理实现 它是基于接口实现的
*
*/
public static UserService getProxy(UserService service) {
// Proxy是JDK中的API类
// 第一个参数:目标对象的类加载器
// 第二个参数:目标对象的接口
// 第二个参数:代理对象的执行处理器
UserService userService = (UserService) Proxy.newProxyInstance(service.getClass().getClassLoader(),
service.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("记录日志-开始");
// 下面的代码,是反射中的API用法
// 该行代码,实际调用的是目标对象的方法
Object object = method.invoke(service, args);
System.out.println("记录日志-结束");
return object;
}
});
return userService;
}
@Test
public void testJDKProxy() {
//创建目标对象
UserService service = new UserServiceImpl();
//生成代理对象
UserService proxy = MyProxyUtils.getProxy(service);
//调用目标对象的方法
service.saveUser();
System.out.println("===============");
//调用代理对象的方法
proxy.saveUser();
}
目标对象不需要实现接口
底层是通过继承目标对象产生代理子对象(代理子对象中继承了目标对象的方法,并可以对该方法进行增强)
/**
* 使用CGLib动态代理技术实现 它是基于继承的方式实现的
*
*/
public static UserService getProxyByCgLib(UserService service) {
// 创建增强器
Enhancer enhancer = new Enhancer();
// 设置需要增强的类的类对象
enhancer.setSuperclass(UserServiceImpl.class);
// 设置回调函数
enhancer.setCallback(new MethodInterceptor() {
// MethodProxy:代理之后的对象的方法引用
@Override
public Object intercept(Object object, Method method, Object[] arg, MethodProxy methodProxy)
throws Throwable {
long start = System.currentTimeMillis();
System.out.println("记录程序开始时间..." + start);
// 因为代理对象是目标对象的子类
// 该行代码,实际调用的是目标对象的方法
// object :代理对象
Object object2 = methodProxy.invokeSuper(object, arg);
long end = System.currentTimeMillis();
System.out.println("记录程序结束时间..." + end);
System.out.println("记录程序执行总时长..." + (end - start));
return object2;
}
});
// 获取增强之后的代理对象
UserService userService = (UserService) enhancer.create();
return userService;
}
@Test
public void testCgLibProxy() {
//创建目标对象
UserService service = new UserServiceImpl();
//生成代理对象
UserService proxy = MyProxyUtils.getProxyByCgLib(service);
//调用目标对象的方法
service.saveUser();
System.out.println("===============");
//调用代理对象的方法
proxy.saveUser();
}
其实就是指的Spring + AspectJ整合,不过Spring已经将AspectJ收录到自身的框架中了
1、开发阶段(我们做的)
编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。
在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。
2、运行阶段(Spring 框架完成的)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
org.springframework
spring-aspects
5.0.7.RELEASE
aopalliance
aopalliance
1.0
UserService、UserServiceImpl,并且配置bean
package com.spring.service;
/**
* AOP中的目标对象
*
*/
public class UserServiceImpl implements UserService {
@Override
public void saveUser() {
System.out.println("添加用户");
// 抛出异常的代码
// System.out.println(1 / 0);
}
@Override
public void saveUser(String name ) {
System.out.println("添加用户 : " + name);
// 抛出异常的代码
// System.out.println(1 / 0);
}
@Override
public void updateUser() {
System.out.println("修改用户");
// 抛出异常的代码
// System.out.println(1 / 0);
}
}
package com.spring.service;
public interface UserService {
void saveUser();
void saveUser(String name);
void updateUser();
}
1. 编写通知(增强类,一个普通的类)
package com.spring.advice;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 编写通知类
*
*/
public class MyAdvice {
//演示前置通知
public void log() {
System.out.println("开始记录日志了...");
}
//演示后置通知
public void log2() {
System.out.println("开始记录日志了log2...");
}
//演示最终通知
public void log3() {
System.out.println("开始记录日志了log3...");
}
//演示异常抛出通知
public void log4() {
System.out.println("开始记录日志了log4...");
}
/**
* 环绕通知
* 场景使用:事务管理
*/
public void log5(ProceedingJoinPoint joinPoint) {
System.out.println("前置通知");
//调用目标对象的方法
try {
joinPoint.proceed();
System.out.println("后置通知");
} catch (Throwable e) {
//相当于实现异常通知
System.out.println("异常抛出配置");
e.printStackTrace();
}finally {
System.out.println("最终通知");
}
}
}
2. 配置通知,将通知类交给spring IoC容器管理
3. 配置aop切面
4. 测试
这里使用了Spring和junit集成测试,测试的是环绕通知
package com.spring.test;
import com.spring.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:applicationContext.xml")
public class TestAOP {
@Autowired
private UserService userService;
@Test
public void test() {
userService.saveUser();
System.out.println("=================");
userService.saveUser("lisi");
System.out.println("=================");
userService.updateUser();
}
}
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
execution:必须要
修饰符:可省略
返回值类型:必须要,但是可以使用通配符
包名:多级包之间使用.分割
包名可以使用*代替,多级包名可以使用多个*代替
如果想省略中间的包名可以使用..
类名:
可以使用*代替
也可以写成ServiceImpl、Service、*Service
方法名:
也可以使用*代替
也可以写成具体方法名
参数:
一个参数使用*代替
如果匹配任意参数,可以使用..代替
这个匹配的挺多的,以后可能写一篇。
通知类型(五种):前置通知、后置通知、最终通知、环绕通知、异常抛出通知。
前置通知:
执行时机:目标对象方法之前执行通知
配置文件:<aop:before method="before" pointcut-ref="myPointcut"/>
应用场景:方法开始时可以进行校验
后置通知:
执行时机:目标对象方法之后执行通知,有异常则不执行了
配置文件:<aop:after-returning method="afterReturning" pointcut-ref="myPointcut"/>
应用场景:可以修改方法的返回值
最终通知:
执行时机:目标对象方法之后执行通知,有没有异常都会执行
配置文件:<aop:after method="after" pointcut-ref="myPointcut"/>
应用场景:例如像释放资源
环绕通知:
执行时机:目标对象方法之前和之后都会执行。
配置文件:<aop:around method="around" pointcut-ref="myPointcut"/>
应用场景:事务、统计代码执行时机
异常抛出通知:
执行时机:在抛出异常后通知
配置文件:<aop:after-throwing method=" afterThrowing " pointcut- ref="myPointcut"/>
应用场景:包装异常
1. 编写切面类(注意不是通知类,因为该类中可以指定切入点)
package com.spring.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //@Aspect:标记该类是一个切面类
@Component("myAspect") // 需要将切面类交给Spring IoC容器管理
public class MyAspect {
// @Before:定义该方法是一个前置通知
/*
@Before(value = "execution(* *..*.*Service.*(..))")
public void before() {
System.out.println("注解前置通知");
}
@After(value = "execution(* *..*.*Service.*(..))")
public void after() {
System.out.println("注解最终通知");
}*/
/**
* 第二种方式,可以把表达式抽取出来
*/
//切入点表达式
private static final String str="execution(* *..*.*Service.*(..))";
@Before(value = "fn()")
public void before() {
System.out.println("注解前置通知");
}
@After(value = "fn()")
public void after() {
System.out.println("注解最终通知");
}
//使用PointCut注解,来定义一个通用切入点表达式
@Pointcut(value=str)
public void fn() {}
}
2. 配置容器,并开启aop自动代理
3. 测试
给xml方式的impl加上@Service注解即可
编写配置类即可
@Configuration
@ComponentScan(basePackages="com.spring")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}