SpringAOP 就是Spring实现了AOP这种设计模式,AOP的全称为:Aspect Oriented Programming,意为:面向切面编程。为什么会有面向切面编程呢。在java中,java语言是面向对象编程(OOP),但是在面向对象编程中,有时候会有一些问题,比如:当我们要给一些不具有继承关系的对象引入一些公共行为,比如安全日志检查,只能在每个对象中引入公共对象,这样做非常不便于维护,而且可能会有大量重复的代码。这个时候AOP的出现,刚好弥补了面向对象的这一弊端。
下面,为了理清楚SpringAOP的东西,我将从下面几个方面来进行讨论:
一,代理模式简介
在说SpringAOP之前,先说说java的代理模式,代理模式是一种设计模式,那么代理模式到底是什么意思呢。下面,我举一个例子,简单说说代理模式的思想。
假如,你想要去一个地方买一个东西,但是那个地方太远了,你又不想去,于是你就让你男朋友去给你买,这就相当于是一个代理模式的过程。你不需要关心你男朋友是怎么买到东西的,不用关心他在买东西的时候还干了啥事,你只要结果,就是你男朋友吧东西递到你的手上,这大概就是代理模式的思想。在你男朋友出去的过程中,他可能找他的好哥们聚了聚,可能见了见他的前女友,但是这都不是他到那个地方去的目的,他的目的始终是给你买东西,只是他在买东西的过程中还可能做一些别的事,然后回来之后把你想要的东西给你。过程可以用下面的图表示:
在整个过程中,小明主要要干的事是给他女朋友买吃的,小明就相当于是一个代理对象,代替他女朋友出去买吃的。只是在过程中,他还干了一些其他的次要操作,比如陪前女友逛街,和小伙伴打球等,但是他的主要业务还是去买零食,就算他在这个过程中没有配前女友逛街,没有去打球,也不会影响他买零食这件事。而对于他女朋友来说,她不会关心小明出去干了什么,只关心结果,就算小明带回来的零食。这就是代理模式的主要思想。
二、静态代理实现及分析
2.1 实现静态代理的步骤:
- 写一个接口:描述真正要干的事(比如:买零食)
- 两个类,都继承这个接口
- 一个表示执行真实业务(比如:买零食)
- 另一个就是代替第一个类,去真正的实现买零食的过程(比如:见前女友---> 买零食--->打球)
- 测试结果(比如:给女朋友上交零食)
2.2 实现案例:
案例1:上面说的小明买零食的例子
首先写一个接口: BuySnacks.java
然后一个类,实现真实业务方法 : BuySnacksImpl.java
另一个类,也实现同样的接口,具体实现真实业务的全过程:XiaoMing.java
// 小明去买零食
public class XiaoMing implements BuySnacks {
// 给小明一个真实业务(要干的事)的对象,调用具体要干的事
private BuySnacks buy;
public XiaoMing(BuySnacks buy) {
this.buy = buy;
}
// 实现他的主要业务,干要干的事
@Override
public void buySnacks() {
// 买零食之前,和前女友逛街
hangOutWithPreGirFrie();
// 调用真实业务,买零食
buy.buySnacks();
// 买零食之后,陪小伙伴打球
playBall();
}
// 除了买零食,小明还见了前女友,陪前女友逛街
public void hangOutWithPreGirFrie(){
System.out.println("--------------和前女友逛街-----------------");
}
// 买完零食,和小伙伴打球
public void playBall(){
System.out.println("------------和小伙伴打球-------------------");
}
}
最后,测试结果:Test.java
运行结果:
案例2:代理模式模拟增删查改
一个表示真实业务的接口: IUserManager.java
实现真实业务的接口的类:UserManagerImpl.java
它实现了接口中的方法,也就是告诉我们这些真实业务具体是干什么的,但是它不会自己调用这些方法,而是交给代理类的对象去调用
代理类也要实现相同的接口:
/**
* 代理类 用来代理真实类执行真实业务
*/
public class MyProxy implements IUserManager{
// 先写一个真实类的对象
private IUserManager userManager ;
public MyProxy(IUserManager userManager) {
this.userManager = userManager;
}
// 在执行真实操作之前,先要进行一些辅助操作,比如安全性检测
private void MySecurityCheck()
{
System.out.println("check security first...");
}
// 覆写真实业务方法
public void addUser() {
MySecurityCheck(); // 在执行添加用户的方法之前先执行一个安全检查
userManager.addUser();
}
public void delUser() {
MySecurityCheck();// 在执行删除用户的方法之前先执行一个安全检查
userManager.delUser();
}
public void modUser() {
MySecurityCheck(); // 在执行修改用户的方法之前先执行一个安全检查
userManager.modUser();
}
public void search() {
MySecurityCheck(); // 在执行查询用户的方法之前先执行一个安全检查
userManager.search();
}
}
最后,测试 Test.java
以上两个案例就是我们自己实现的代理模式,又叫静态代理
2.3 静态代理总结:
静态代理的优点:
可能有人会觉得上面这种操作是多此一举,既然在增删查改的时候需要进行安全检查,那干嘛不直接在一个类里面实现了就行了,还要再多写一个类。但是实际上,这样做的好处有:可以将主要业务和辅助方法分离开来,当我们需要加一些其他的辅助操作的时候直接在代理类中操作就可以了,不需要再去改动主要业务中的代码;同时还可以使代码结构看起来更清晰,试想一下如果将主要业务和辅助操作都放在一个类里面,看起来就会特别复杂。
静态代理的缺点:
由于代理类和目标类需要实现相同的接口,因此两个类中要实现相同的方法,所以一旦接口中的方法有改动的时候,两个子类都需要手动维护,那么这个情况怎么解决呢,答案是:JDK动态代理。
三、JDK动态代理的实现及分析
JKD动态代理是代理模式的一种实现方式,JDK动态代理只能代理接口,在JDK动态代理实现需要以下条件:
- 需要实现一个接口 InvocationHandler(实现接口中的invoke方法,用来调用真实的业务方法)
- 需要使用到一个类 Proxy ( Proxy里面的一个静态方法 newProxyInstance 用来返回一个代理对象,供客户端调用)
- 和上面的静态代理一样,还需要一个辅助真实业务执行的方法(比如上面的安全性检查)
下面在代码中再具体说这个类和接口怎么用:
1、首先,依然要写一个接口,里面是真实业务的抽象方法:
2、然后,有一个真实业务的类实现该接口
3、自己写一个代理类,使用上面说的接口和类:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 基于JDK的动态代理
* 需要实现一个接口:InvocationHandler 实现里面的方法invoke,用来调用真实的业务方法(通过反射)
* 需要使用一个类:Proxy里面的一个静态方法newProxyInstance去返回一个代理对象,供客户端调用方法
* 需要一个辅助真实业务的方法(比如安全性检查)
*/
public class MyProxy implements InvocationHandler{
// 这个目标对象就是上面这个实现了真实业务接口的类的对象
private Object targetObject ;
public MyProxy(Object targetObject) {
this.targetObject = targetObject;
}
// 生成一个代理对象 最后我们在客户端(测试类)调用的就是这个方法
public Object returnProxy()
{
/**
* 方法原型:
* =======================================================================
* public static Object newProxyInstance(ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h)
* throws IllegalArgumentException
*=======================================================================
*
* Proxy.newProxyInstance 里面的三个参数:
* -----------------------------------------------------------------------------------------
* ClassLoader loader 目标类(UserManager)的ClassLoader
* Class<?>[] interfaces 目标类(UserManager)类实现的接口[有真实业务的接口]
* InvocationHandler h 实现InvocationHandler的类对象(在这里就是当前类实现的这个接口)
* return 返回值Object就是系统帮我们生成的代理对象
*-----------------------------------------------------------------------------------------
*/
return Proxy.newProxyInstance(
this.targetObject.getClass().getClassLoader(),
this.targetObject.getClass().getInterfaces(),
this);
}
/**
* 这个是实现InvocationHandler接口中的 invoke() 方法 用来执行接口中的真实业务方法
* 这个方法是系统调用,不用我们自己调用
* @param proxy 代理类对象
* @param method 代理的真实方法
* @param args 方法里面的参数
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object obj = null;
try {
MySecurityCheck(); //安全监测
obj = method.invoke(this.targetObject, args); // 通过反射调用真实方法
return obj;
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
// 在执行真实操作之前,先要进行一些辅助操作,比如安全性检测
private void MySecurityCheck()
{
System.out.println("check security first...");
}
}
最后,测试运行结果:
JDK动态代理总结:
动态代理和静态代理相比,JDK动态代理大大降低了我们的开发任务,同时减少了对业务接口的依赖,大大降低了代码的耦合度,底层原理就是使用反射机制来调用的业务方法。但是JDK动态态代理任然有一个缺陷,就是实现类必须使用接口定义业务方法,如果没有实现接口,就无法使用JDK动态代理。
那么对于那些没有实现接口的类,又是如何实现动态代理的呢???
下面,就要用到CGLIB动态代理。
四、CGLIB动态代理实现及分析
cglib的动态代理底层采用的是字节码技术,通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,从而达到动态代理的效果。下面用代码来实现一下
4.1 要使用cglib的动态代理,首先需要导入cglib所要依赖的jar包,这里我创建的是maven项目,所以直接在pom文件中导入jar包:
<!--cglib jar包-->
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
4.2 这里依然需要一个类,用来说明主要业务有哪些(不需要实现接口)
4.3 需要一个代理类,进行一些辅助操作
/**
* cglib实现的动态代理 要实现MethodInterceptor接口 , 底层原理是采用字节码技术为一个类创建子类
* 并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势植入横切逻辑,因为使用的是继承,所以
* 不能对final修饰的方法进行代理
*/
public class MyProxy implements MethodInterceptor {
// 维护一个目标对象
private Object target;
public Object getProxyInstance(final Object target) {
this.target = target;
//Enhancer 是cglib中的一个字节码增强器,他可以方便的对你想要处理的类进行扩展
Enhancer enhancer = new Enhancer();
// 将被代理的对象设置为父类
enhancer.setSuperclass(this.target.getClass());
// 回调方法,设置拦截器
enhancer.setCallback(this);
// 动态创建一个代理类
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 执行业务方法之前先进行安全检查
securityCheck();
// 执行主要业务方法
Object obj = methodProxy.invokeSuper(o, objects);
return obj;
}
//进行安全检查的辅助方法
private void securityCheck()
{
System.out.println("------securityCheck()-------");
}
}
4.4 最后,进行方法的测试:
通过上面的分析,接下来在看看SpringAOP
五、SpringAOP
5.1 AOP简介
在文章最开始的时候,已经大概介绍了一下AOP,说过的部分这里就不重复了。
在AOP中,通常把软件系统分为两部分:核心关注点 和 横切关注点
- 业务处理的主要流程是核心关注点
- 与之关系不大的部分是横切关注点。横切关注点有一个特点:经常发生在核心关注点的多处,而各处都基本相似(比如:权限认证,安全监测,事务处理等)
核心关注点和横切关注点的关系如下图:
AOP的主要作用也就是将系统中的核心关注点和横切关注点分离开来。
5.2 AOP术语(白话)描述
AOP术语的专业解释网上到处都是,但是对于初学者而言,个人感觉那个解释不是太友好,比较难理解,所以在这里我就不贴那些专业的解释了,就自己对AOP专业术语的理解,用白话的方式进行了总结,具体如下图:
5.2.1 Aspect(切面):
在我们的主要业务中会有许多横切关注点,这些横切关注点通俗的来说就是一些方法,用来辅助主要业务,我们通常会把这些方法放到一个专门的类中,这个类就叫做切面类。
注:这张图以及下面这几张图是根据个人理解所画,如果有不正确的地方,希望大佬们可以指出来
5.2.2 Advice(通知)
切面里面的方法又叫做通知,通知就是说明了这个方法是干什么的(比如是安全监测的方法),通知还决定了这个方法什么时候执行。
Spring切面中的五类通知(这里先做了解,下面代码中在具体看怎么用):
- 前置通知(Before):在目标方法被调用之前调用通知
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出结果是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常之后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行之定义的行为
5.2.3 PointCut(切点):
切点:和上面的通知相似,通知定义的是什么时候调用方法,而切点定义的是在什么地方(哪一个业务方法上)调用辅助方法(比如要在增加用户的方法上调用安全检查的辅助方法);
5.2.4 Join Point (连接点)和 Weave (织入)
连接点:是一个抽象的概念,在我们要将一个切面 ‘放入’ 我们的主要业务(增删查该等)中的时候,在我们眼中,我们的业务那部分代码是一个整体,我们不能把业务代码一下分成两部分,然后把切面放在中间,而是在主要业务中的某个位置(通知定义的什么时候,切点定义的什么位置)有一个 点 ,就在这个点的位置调用我们的辅助方法(横切关注点)。
织入:上面说的吧切面 ‘放入’ 主要业务,这个 '放入' 的过程就叫做 织入, 最后所有术语关系如下图所示。
以上就是AOP术语的大概意思,如果是一个初学者,看来可能还是不太明白,接下来再结合下面的SpringAOP的实现代码看看应该就差不多理解了。下面提供两种方式实现SpringAOP,分别是配置文件的方式和注解的方式
5.2 SpringAOP的实现
5.2.1 通过注解的方式实现
1.首先,因为这是一个Spring的项目,所以需要导入Spring相关的jar包,要使用AOP,也好导入AOP的jar包,pom文件如下,有单元测试的话,导入junit的jar包:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.sonatype.sisu</groupId>
<artifactId>sisu-inject-bean</artifactId>
<version>1.4.2</version>
</dependency>
<!--这个是AOP的jar包-->
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.3</version>
</dependency>
2、同样的是一个接口和一个实现类
3、一个代理类:MyProxy.java
4、由于这是一个Spring的项目,所以需要用Spring配置文件:contextApplication-anno.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"
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 ">
<!--这个表示Spring只是AOP-->
<aop:aspectj-autoproxy/>
<!--将类交给Spring管理-->
<bean id="myProxy" class="per.fei.proxy.SpringAOP.annotation.MyProxy"/>
<bean id="realImpl" class="per.fei.proxy.SpringAOP.annotation.RealImpl"/>
</beans>
5、最后,写出测试代码
5.2.2 通过配置文件的方式实现
这里同样需要上面的jar包和一个接口一个类,这里就不重复截屏了,不同的只是代理类 MyProxy.java 和 Spring的配置文件
代理类:MyProxy.java 此时的MyProxy.java中只需要放一些辅助方法,别的都不需要放了,其他的都在配置文件中定义
配置文件:contextApplication-configFile.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"
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 ">
<aop:aspectj-autoproxy/>
<bean id="realImpl" class="per.fei.proxy.SpringAOP.config_file.RealImpl"/>
<bean id="myProxy" class="per.fei.proxy.SpringAOP.config_file.MyProxy"/>
<aop:config>
<!--定义切面-->
<aop:aspect id="myAspect" ref="myProxy">
<!--定义切点--> <!--给add开头的方法上定义切点-->
<aop:pointcut id="myPointCut" expression="execution(* add*(..))"/>
<!--定义通知--> <!-- Before表示在调用业务方法之前调用横切关注点 -->
<aop:before method="securityCheck" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
</beans>
测试方法:
5.3 SpringAOP源码简单分析
其实SpringAOP使用的是动态代理模式,上面说到两种动态代理,那么它到底是用的哪种代理模式呢,下面这段Spring的源码+注释会给我们答案
看到这差不多就可以总结SpringAOP了:如果使用了接口,就默认使用JDK动态代理,如果没有使用接口,就使用CGLIB的动态代理
以上是我学完SpringAOP后结合书籍+理解+网上看相关资料后的总结,希望可以帮助到需要的人,同时,如果有总结的不对的地方,希望大家可以指出来.