AOP(Aspect Oriented Programming)
本文旨在将清楚AOP是什么,以及Spring AOP具体如何使用,有何注意事项。
1.概念
- AOP:全称是 Aspect Oriented Programming即:面向切面编程。
- 在软件业,AOP意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
- AOP既是一种技术也是一种编程思想。
- AOP的核心是动态代理。
2.生活中的例子(代理模式)
我们先来看一个iphone:
这里不是打广告(~~~)。
在APPLE早期,大概1977年的时候,APPLE刚刚起步,规模不是很大,对于APPLE来说,它有一个总工厂,它主要负责一下这些事务:
对这个APPLE这个总工厂来说,它需要在保证APPLE的核心产品和服务标准的情况下,进行生产产品,研发产品,销售,售后四大主要的业务。
随着APPLE的不断壮大,它的各个业务都变得异常庞大,单单靠APPLE总工厂来处理这些业务的话,已经吃不消了,所以,它需要对现在的这个业务体制进行改革。
改革的方式是:请来代理商,让它们来对产品进行销售和售后,自己只负责生产产品和研发,但是它们也必须保证各个方面符合APPLE的标准。
随着时间的推移,代理商也不满足于销售和售后了,于是,它可以在原本APPLE总工厂没有这项服务的前提下,提供一些个性化服务,比如,碎屏险,贴膜,手机壳。
现在,你需要购买APPLE的任何产品,只需要在代理商那去买,可以同样享受APPLE的服务,而且还会有个性化的服务。
以上的这个例子就是代理模式的思想。
我们来看一下什么是代理模式:
-
代理模式(英语:Proxy Pattern)是程序设计中的一种设计模式。
-
代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
-
通俗一点来理解代理模式:本来由A做的事情,现在由B进行代理,并且可以对A的方法进行增强。以前访问一个对象需要通过A,现在只需要通过B就可以进行对该对象的访问。
3.代理模式的实现
在Java中,使用代理的方式有两种,静态代理和动态代理:
- 静态代理是在程序运行前就已经存在代理类的字节码文件,代理类和原始类的关系在运行前就已经确定。
- 动态代理模式:动态代理类的源码是在程序运行期间通过JVM反射等机制动态生成,代理类和委托类的关系是运行时才确定的。
- 动态代理的特点:字节码随用随创建,随用随加载。
下面是两种方式的代码演示,仅演示了需要代理的部分。
静态代理实现:
首先确定一下接口:
public interface IAppleCore {
//销售
void sale();
//售后
void saleAfter();
}
然后是工厂类,需要实现接口:
public class AppleFactory implements IAppleCore {
@Override
public void sale() {
System.out.println("销售了产品");
}
@Override
public void saleAfter(){
System.out.println("进行了售后服务");
}
}
然后是代理类,同样需要实现接口:
public class AppleProxy implements IAppleCore {
private IAppleCore target=new AppleFactory();
@Override
public void sale() {
System.out.println("代理已开启");
target.sale();
System.out.println("代理结束");
}
@Override
public void saleAfter() {
System.out.println("代理已开启");
target.saleAfter();
System.out.println("代理结束");
}
}
最后main方法测试一下:
public class Client {
public static void main(String[] args) {
//使用代理对象
IAppleCore proxy=new AppleProxy();
//使用代理对象执行方法
proxy.sale();
proxy.saleAfter();
}
}
测试截图:
动态代理实现-JDK(基于接口):
-
动态代理的核心是使用反射创建对象。
-
动态代理用到了JDK官方提供的
Proxy
类。 -
使用jdk生成的动态代理的前提是目标类必须有实现的接口。
-
在这个代理的过程中JDK动态生成了一个类去实现接口。
-
newProxyInstance
方法参数说明:ClassLoader
:类加载器,用于加载被代理对象的字节码,与被代理对象使用相同的类加载器。Claa[]
:字节码数组,让代理对象和被代理对象有相同的方法。InvocationHandler
:提供增强的代码,通常采用匿名内部类,谁使用,谁写此类。
接口和工厂类与上面一致,不同的是代理类:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class AppleProxy{
private Object target;
public AppleProxy(IAppleCore target) {
this.target = target;
}
public Object getProxyInstance(){
Object proxy= Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
//增强的方法
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName=method.getName();
Object result=null;
if("saleAfter".equals(methodName)){
System.out.println("检测到售后方法,开始代理");
result=method.invoke(target,args);
System.out.println("代理成功");
}else{
result=method.invoke(target,args);
}
return result;
}
}
);
return proxy;
}
}
最后测试一下:
public class Client {
public static void main(String[] args) {
IAppleCore target=new AppleFactory();
//代理对象
IAppleCore proxy=(IAppleCore)new AppleProxy(target).getProxyInstance();
System.out.println("target:"+target.getClass());
System.out.println("proxy:"+proxy.getClass());
proxy.sale();
proxy.saleAfter();
}
}
测试截图:
动态代理实现-Cglib(基于子类):
-
使用JDK动态代理需要目标类必须有实现的接口。如果需要去代理一个普通的类,需要使用Cglib提供的
Enhancer
类。 -
使用Cglib的话,被代理类不能被
final
修饰,既需要可以创建子类 -
Cglib是以动态生成的子类继承目标的方式实现,在运行期动态的在内存中构建一个子类。
-
create
方法参数:Class
:指定被代理对象的字节码。Callback
:提供增强的方法。
代码如下:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class Client {
public static void main(String[] args) {
final IAppleCore target=new AppleFactory();
IAppleCore proxy=(IAppleCore) Enhancer.create(target.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object oproxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
String methodName=method.getName();
Object result=null;
if("saleAfter".equals(methodName)){
System.out.println("检测到售后方法,开始代理");
result=method.invoke(target,args);
System.out.println("代理成功");
}else{
result=method.invoke(target,args);
}
return result;
}
});
System.out.println("target:"+target.getClass());
System.out.println("proxy:"+proxy.getClass());
proxy.sale();
proxy.saleAfter();
}
}
4.AOP再理解
在明白了上述代理,动态代理的远离和优势之后,对于AOP就好理解了,因为AOP的核心思想就是动态代理。
我们先来看一下官方的一些术语:
- 连接点(Joinpoint): 指那些被拦截到的点,一个类的一个方法可以称为一个连接点,Spring只支持方法类型的连接点。
- 切入点(Pointcut): 指我们要对哪些 Joinpoint进行拦截的定义,也可以说是拦截的条件。
- 增强 / 通知(Advice): 拦截到 Joinpoint之后所要做的事情就是通知,增强是置入目标连 接点的一段代码,分为:前置通知,后置通知,异常通知,最终通知,环绕通知。
- 切面(Aspect): 切面由切点和增强组成。
- 目标对象(Target): 代理的目标对象。
- 引介(Introduction): 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或 Field。
- 织入(Weaving) : 是指把增强应用到目标对象来创建新的代理对象的过程。
- 代理(Proxy) : 一个类被 AOP织入增强后,就产生一个结果代理类。
看了这些术语后,可能关于代理的部分好理解了,但是,切面与这些切入点,连接点怎么处理呢?
我们来看一个例子,假如你现在需要对数据库里面的信息做一个操作:将所有男生的财产增加100万,那么对于的关系应该是下面这样的。
- 连接点是所有可能被选中的候选的那些点,如图中数据库中的数据。
- 切入点是一个选中目标对象的条件语句,如图中的sql语句。
- 通知是对选中目标对象进行增强,如图中的增加资金。
- 切面包含选择对象的条件和增强的方法,也就是切入点和通知。
- 织入是指这个增强的过程。
- 目标对象被选中去增强的对象,也就是被代理的对象。
总结一下对AOP的理解,AOP其实就是面对这个切面来编写程序,选择需要增强的对象(切入点),然后增强(通知)。
5.Spring AOP的简单配置
- AOP是Spring的两大特性之一,Spring对AOP提供了近乎完美的支持。
在配置Spring AOP前,先把需要使用到的类配置到相关的IOC容器中。
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置srping的Ioc,把service对象配置进来-->
<bean id="accountService" class="cservice.impl.ATServiceImpl"></bean>
<!-- 配置一个日志类 -->
<bean id="logger" class="utils.Loggers"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
-
<aop:config>
表示开始进行Spring AOP的配置。 -
aop:aspect
表示配置切面- 属性
id
:切面提供一个唯一标识。 - 属性
ref
:指定通知类bean的id。
- 属性
6.Spring AOP 通知类型与切入点表达式
- 在
aop:aspect
标签的内部需要使用对应标签来配置通知的类型。
通知的类型:
-
aop:before
:用于配置前置通知。指定增强的方法在切入点方法之前执行。method
:用于指定通知类中的增强方法名称。ponitcut-ref
:用于指定切入点的表达式的引用。poinitcut
:用于指定切入点表达式。
-
aop:after-returning
:用于配置后置通知。切入点方法正常执行之后。它和异常通知只能有一个执行。属性同上。 -
aop:after-throwing
:用于配置异常通知。切入点方法执行产生异常后执行。它和后置通知只能执行一个 。属性同上。 -
aop:after
:于配置最终通知。无论切入点方法执行时是否有异常,它都会在其后面执行。属性同上。 -
aop:around:
用于配置环绕通知。它是 spring框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。属性同上。- spring框架为我们提供了一个接口:
ProceedingJoinPoint
,它可以作为环绕通知的方法参数。在环绕通知执行时,spring框架会为我们提供该接口的实现类对象,我们可以直接使用。 - 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
- spring框架为我们提供了一个接口:
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("后置");
return rtValue;
}catch (Throwable t){
System.out.println("异常");
throw new RuntimeException(t);
}finally {
System.out.println("最终");
}
}
切入点表达式:
-
在书写切入点表达式前需要加上关键字
execution(表达式)
。 -
可以单独配置标签(写在当前切面标签内也可以写在切面外,不过要写在切面之前),也可以直接写在通知标签内,单独配置标签方式如下:
<aop:pointcut id="pt01" expression="excution(* com.atfwus.service.impl.*.*(..))"></aop:pointcut>
-
标准切入点表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
例如:public void com.atfwus.service.impl.ATServiceImpl.proxy()
-
省略访问修饰符:
例如:void com.atfwus.service.impl.ATServiceImpl.proxy()
-
返回值可以使用通配符,表示任意返回值:
例如:* com.atfwus.service.impl.ATServiceImpl.proxy()
-
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个
*
:
例如:* *.*.*.*.ATServiceImpl.proxy()
-
包名可以使用
..
表示当前包及其子包:
例如:* *..ATServiceImpl.proxy()
-
类名和方法名都可以使用
*
来实现通配:
例如:* *..*.*()
-
参数的写法:
- 基本类型直接写名称,如
int
- 引用类型写包名.类名的方式
java.lang.String
- 以使用通配符表示任意类型,但是必须有参数
- 可以使用
..
表示有无参数均可,有参数可以是任意类型
- 基本类型直接写名称,如
-
全通配切入点表达式:
* *..*.*(..)
-
一般切到业务层实现类下的所有方法,如
* com.atfwus.service.impl.*.*(..)
7.基于注解的Spring AOP
先在xml进行相关的配置:
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.atfwus"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
在通知类上使用@Aspect
注解声明为切面 :
- 使用
@Aspect
注解表示把当前类声明为切面类。
通知类型注解:
@Before
:把当前方法看成是前置通知。属性value
:用于指定切入点表达式,还可以指定切入点表达式的引用。@AfterReturning
:把当前方法看成是后置通知。 属性同上。@AfterThrowing
:把当前方法看成是异常通知。属性同上。@After
:把当前方法看成是最终通知。属性同上。- Spring在以上注解的通知类型,会存在按照顺序执行通知的问题,最好使用环绕通知自己配置。
@Around
:把当前方法看成是环绕通知。属性同上。
切入点表达式注解@Pointcut
:
- 指定切入点表达式。
- 属性
value
:指定表达式的内容。
@Pointcut("execution(* com.atfwus.service.impl.*.*(..))")
private void pt1() {}
完全注解配置:
- 只需在配置类上加上
@EnableAspectJAutoProxy
注解就可以了。
8.Spring AOP的创建代理对象原理(部分源码分析)
在上面我们使用动态代理的时候,使用到了两种模式的动态代理,一种是基于JDK的,需要实现接口,一种是基于Cfglib的。在Spring的最底层,集成了这两种方法。
- 我们找到
org.springframework.aop.framework.DefaultAopProxyFactory
。
查看源码:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
public DefaultAopProxyFactory() {
}
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 如果指定了 optimize为true 或者是proxyTargetClass 为true 或者是 没有实现接口
if (!config.isOptimize() && !config.isProxyTargetClass() &&
!this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {// 目标类找不到 抛出异常
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
} else {
// 目标类是接口 或者是 class是由代理类动态通过getProxyClass方法 或者 newProxyInstance方法生成 使用jdk 动态代理 否则 cglib 代理
return (AopProxy)(!targetClass.isInterface() &&
!Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] ifcs = config.getProxiedInterfaces();
return ifcs.length == 0 || ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]);
}
}
通过上述源码可以发现,Spring生成代理对象的步骤:
- 创建容器对象的时候,根据切入点表达式拦截的类,生成代理对象。
- 如果目标对象有实现接口,使用jdk代理。如果目标对象没有实现接口,则使用Cglib代理。然后从容器获取代理后的对象,在运行期植入"切面"类的方法。
注意:如果目标类没有实现接口,且class为final修饰的,则不能使用Spring AOP。
9.Spring 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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置srping的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.atfwus.service.impl.ATServiceImpl"></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="com.atfwus.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.stfwus.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置环绕通知-->
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
Service层:
package com.atfwus.service.impl;
import com.atfwus.service.IATService;
public class ATServiceImpl implements IATService{
@Override
public void save() {
System.out.println("保存信息");
// int i=1/0;
}
@Override
public void update(int i) {
System.out.println("更新信息"+i);
}
@Override
public int delete() {
System.out.println("删除信息");
return 0;
}
}
日志类:
package com.atfwus.utils;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 记录日志
*/
public class Logger {
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("前置记录日志");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("后置记录日志");
return rtValue;
}catch (Throwable t){
System.out.println("异常日志记录");
throw new RuntimeException(t);
}finally {
System.out.println("最终日志记录");
}
}
}
main方法测试:
import com.atfwus.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 测试AOP的配置
*/
public class AOPTest {
public static void main(String[] args) {
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IATService obj = (IATService)ac.getBean("ATService");
//3.执行方法
obj.save();
}
}
测试截图:
正常:
异常:
10.AOP的优点与用途
优势分析:
- 减少了重复代码。
- 降低了程序间的耦合性。
- 提高开发效率,维护方便 。
- 在程序运行期间,不修改源码对已有方法进行增强。
用途:
- 信息过滤。
- 页面转发。
- 权限判断。
- 日志记录。
- 参数校验。
- Spring声明式事务管理配置。
- 数据库读写分离。
- 敏感关键字替换。
参考书籍:
- 《Spring实战》第四版
- 《SSM项目实战》
感谢耐心的您看到了这,谢谢您的支持!
ATFWUS --Writing By 2020–04-22