AOP是什么
AOP(Aspect Orient Programming)
,面向切面编程,把原先的静态思考转换成动态的程序运行过程,可以在于运行期间的动态代理实现,属于Spring
世界的重要一员,降低了代码的耦合度,提高了程序的可冲用性,提高了开发效率,属于面向对象变成的一种补充。
以一个常见的事务与业务混合编写的代码为例子,问题就在于耦合度过高,除了关心业务代码外还要关心事务代码的编写(或其他切面)
public void action(){
//业务与事务耦合度太高
try {
System.out.println("开始事务");
System.out.println("进行诸多业务执行");
System.out.println("事务提交");
} catch (Exception e) {
System.out.println("事务回滚");
}
}
稍微改进一下,通过子类代理的方式(装饰者模式)
//要被代理的类
class Action {
public void action(){
System.out.println("进行诸多业务执行");
}
}
//代理类
class Proxy extends Action{
@Override
public void action(){
try{
System.out.println("开始事务");
super.action(); //开始业务
System.out.println("事务提交");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
目前这种方式的进行了业务和事务的分离,父类中只有纯净的业务代码
再改进下,使用静态代理拆分业务和切面,业务和切面都实现相同的接口,接口是核心
//定义接口
class interface Action{
void action();
}
//业务接口的实现
interface ActionImpl{
@Override
public void action(){
System.out.println("进行诸多业务执行");
}
}
//最终的代理类
class Proxy implements Action{
private Action target;
//定义接口类型的成员变量,通过构造方法传入目标代理对象
public Proxy(Action target){
this.traget = target;
}
@Override
public void action(){
try{
System.out.println("事务开始");
target.action(); //调用被代理的目标方法
System.out.println("事务提交");
}catch(Exception e){
System.out.println("事务回滚");
}
}
}
这个实现的好处在于,只要传入的目标对象实现了Action
接口,那么就可以被代理,灵活切换目标代理对象,切面仍然可以继续和业务结合工作,缺点是切面是"死的"
,切面可能还有很多比如日志,权限等等
再改进一下,让切面也可以灵活切换
定义切面的接口,分别抽象出业务执行前,业务执行后,业务异常时三种切入时机
// java8开始提供了接口默认实现的特性,想实现哪个时机的切面方法更灵活,不用全实现
interface Aop{
default before(){}
default after(){}
default exception(){}
}
class LogAop implements Aop{
@Override
public void before(){
System.out.println("业务执行前日志");
}
@Override
public void after(){
System.out.println("业务执行后日志");
}
@Override
public void exception(){
System.out.println("发生异常时日志");
}
}
class Proxy implements Action{
private Action target;
private Aop aop;
//定义接口类型的成员变量,通过构造方法传入目标代理对象
//再定义一个成员变量类型为Aop类型,通过构造方法传入切面
public Proxy(Action target,Aop aop){
this.traget = target;
this.aop = aop;
}
@Override
public void action(){
try{
aop.before(); //切面方法调用
target.action(); //调用被代理的目标方法
aop.after(); //切面方法调用
}catch(Exception e){
aop.exception(); //切面方法调用
}
}
}
现在代理对象可以灵活的传入切面对象和业务对象,代理就能正常的按照预期进行工作,由于代理对象也实现了业务接口,所以可以进行俄罗斯套娃进行多个切面组织
但仍然不够灵活,目前的业务是写死的action,预期应该要可以代理目前对象的任意方法,进行动态代理的改造
定义一个代理工厂返回代理对象
class ProxyFactor{
public static Object getInstance(Action target,Aop aop){
//JDK提供的代理需要提供三个参数,类加载器,目标对象实现的所有接口,代理功能具体实现类
return Proxy.newProxyInstance(
target.getClass().getClassLoader, //类加载器
target.getClass().getInterfaces(), //所有实现的接口集合
(agent, method, args) -> { //三个参数分别为被生成的代理对象
//切记这里不可调用agent的任何方法,否则将死循环
// 这个函数的返回就是代理对象调用方法的返回值
Object result = null;
try{
aop.before();
result = method.invoke(target,args);
aop.after();
}catch(Exception e){
aop.exception();
}
return result;
} //这里使用lambda表达式实现匿名内部类InvocationHandler
);
}
}
利用JDK
提供的动态代理,便完成了接口与切面的灵活整合,这种方式的优点是方便,不需要依赖任何第三方库,缺点是功能受限,只能用于接口,需要处理类,则需要使用CGLIB
库进行字节码增强
一些AOP的常用术语
- 切面:重复,公共,通用的功能称为切面,例如日志,事务,权限
- 连接点:目标方法,因为在目标方法中要实现目标方法的功能和功能
- 切入点(
Pointcut
):指定切入点,多个连接点构成切入点,切入点可以是一个目标方法,也可以是类中的所有的方法,可以是某个包下的所有类中的方法 - 目标对象:操作谁,谁就是目标对象
- 通知(
Adive
):指定切入的时机,是在目标方法执行前还是执行后还是出现异常后,还是环绕目标方法切入切面功能
Spring原生就提供了Aop的实现
常用的有
Beofre
通知:在目标方法调用前调用(org.springframework.aop.MethodBeforeAdvice
)After
通知:在目标方法被调用后调用(org.springframework.aop.AfterReturnAdvice
)Throws
通知:目标方法抛出异常时调用(org.springframework.aop.ThrowsAdvice
)Around
通知:拦截对目标对象方法的调用(org.aopalliance.intercept.MethodInterceptor
)
以xml配置的形式使用Spring提供的IOC
模拟一个服务接口
public interface Service{
void doSomething();
}
// 对接口进行实现
public class UserServiceImpl implements Service{
@Override
public void doSomething(){
System.out.println("do something!");
}
}
再定义一个bean
,去实现MethodBeforeAdvice
和AfterReturningAdvice
接口
public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("系统日志:" + sf.format(new Date()) + " :方法名为->" + method.getName() +
" 方法参数为->" + Arrays.toString(objects));
}
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) {
System.out.println("系统日志:方法调用结束");
}
}
在Spring
的xml
中配置bean
,主要在于org.springframework.aop.framework.ProxyFactoryBean
按属性注入
<bean id="logAdvice" class="cn.mgl.aop.LogAdvice"></bean>
<bean id="serviceTarget" class="cn.mgl.aop.UserServiceImpl"></bean>
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="interfaces" value="cn.mgl.aop.Service"></property>
<property name="interceptorNames">
<list>
<value>logAdvice</value>
</list>
</property>
<property name="target" ref="serviceTarget"></property>
</bean>
使用时
ClassPathXmlApplicationContext ct = new ClassPathXmlApplicationContext("classpath:application.xml");
Service service = (Service)ct.getBean("userService");
service.doSomething();
执行后
AspectJ
优秀的面向切面的框架,扩展了Java语言,提供了强大的切面功能,在Spring
中使用AOP
时一般使用AspectJ的实现方式,支持注解式开发。
AspectJ
中常用的通知
- 前置通知,调用前执行
@Before
- 后置通知,调用后执行
@After
- 环绕通知,一般用于事务
@Around
- 最终通知,不管成功失败都执行
@After
- 定义切入点
@Pointcut
Aspect切入点公式
规范的公式为execution(访问权限 方法返回值 方法声明(参数) 异常类型)
简化后的公式为execution(方法返回值 方法声明(参数))
特殊符号
*
表示通配符,任意个字符..
两个点- 如果出现在方法的参数中,表示任意参数
- 如果出现在路径中,则表示本路径及其所有的子路径
实例 | 作用 |
---|---|
execution(public * *(..)) | 任意的公共方法 |
execution(* set*(..) | 任意的setter的方法 |
execution(* com.abc.service.*.*(..)) | 任意返回值,com.abc.service 包下中所有类,任意方法名,任意参数 |
execution(* *..service.*.*(..)) | 任意目录下的service包中的所有类,任意方法名,任意参数 |
execution(* *.service.*.*(..)) | 任意子包(注意层级)为service的所有类,任意方法名,任意参数 |
Spring将AspectJ整合到了自己的框架中,在pom.xml中引入AspectJ
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
定义一个配置Bean
这里的EnableAspectJAutoProxy
注解很重要,只有添加后,Spring
才会去扫描所有带@Aspect
注解的切面对象,同时需要满足对象是Bean
,所有一般@Aspect
和@Component
配合使用,否则无效,通过扫描@Aspect
定义的切面类,再从切入点找到目标类的目标方法,再由通知类型找到切入时间点。其中有一个proxyTargetClass
属性,默认是false
,false
则使用JDK
的动态代理,true
则使用CGLIB
。顺提一下,在SpringBoot2.0
后,自动装配的配置中,默认为true
,可以通过配置文件的spring.aop.proxy-target-class
修改
@Configuration
@ComponentScan("cn.mgl.aop")
@EnableAspectJAutoProxy
public class MainConfig {
}
这里的切面表达式指定了一个全限定类名的任意方法任意参数,通知类型为@Before
所有的通知方法(通知方法的方法名不重要)都有一个参数类型为JoinPoint
,意味切面表达式本身,可以通过对象获取目标调用方法的方法名,方法参数,目标对象
@Aspect
@Component
public class LogAspect {
@Before("execution(* cn.mgl.aop.service.UserServiceImpl.*(..))")
public void beforeAOP(JoinPoint jp) {
System.out.println("前置通知:");
System.out.println("调用方法的签名:" + jp.getSignature());
System.out.println("调用方法的参数:" + Arrays.toString(jp.getArgs()));
}
}
检验切面织入是否成功
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(MainConfig.class);
Service userService = (Service) ctx.getBean("userService");
userService.doSomething();
调用后
@AfterReturning 后置通知
在目标方法执行之后执行,由于是执行之后执行,所以可以获取目标方法的返回值,该主要有一个returning
属性,用于指定接受方法返回值的变量名,该注解除了JoinPoint
参数外,还可以有一个接受返回值的参数,最好是Object
类型,因为返回值的类型可能是任意类型
@AfterReturning(value = "execution(* cn.mgl.aop.service.UserServiceImpl.*(..))", returning = "result")
public void afterAOP(JoinPoint jp, Object result) {
//result要与注解中的定义一致
System.out.println("方法的返回值:" + result);
}
@Around 环绕通知
会在目标方法执行之前以及执行之后执行,被注解为环绕怎强的方法要有返回值,Object
类型,并且方法可以包含一个ProceedingJoinPoint
类型的参数,对象带有一个proceed
方法,作用是执行目标方法,若目标方法有返回值,则该函数的返回值就是目标方法的返回值,最后环绕方法再将返回值return
,实质上就是对方法的一次拦截处理。
把模拟的Service
接口和实现的doSomething
方法返回值改为Object
并返回null
,接着使用@Around
增强方法
@Around("execution(* *..UserServiceImpl.*(..))")
public Object aroundAOP(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("环绕增强开始:");
Object result= pjp.proceed(); //一定要调用,不然相当于目标方法没有执行过,等价于动态代理中的method.invoke()
System.out.println("环绕增强结束");
return "123456";
}
最后看看输出结果,本来返回null
的结果被篡改成功
@After 最终通知
无论目标方法是否抛出异常都会被执行,类似于try catch finally
的finally
@Around("execution(* *..UserServiceImpl.*(..))")
public void finallyAOP() {
System.out.println("最终通知已执行");
}
这个过于简单便不演示了,可自行测试,最终通知的执行时机在环绕通知结束之前
@Pointcut 定义切入点别名
当通知增强方法使用的切入点表达式相同时,编写和维护比较麻烦,可以通过@Pointcut
定义一个表达式别名,后面使用该表达式时只需要写在execution
属性写入方法名即可,一般带@Pointcut
的修饰符用private
,因为没有别的实际作用
// 使用别名
@After("aliasCut()")
public void finallyAOP() {
System.out.println("最终通知已执行");
}
// 定义别名
@Pointcut("execution(* *..UserServiceImpl.*(..))")
private void aliasCut() {
}
总体而言,AspectJ
功能比Spring AOP
更加强大和易用
完 :)