提到Spring的AOP就一定少不了代理模式,那么什么是代理模式呢,代理模式为了解决哪些问题呢,这里我们先不讲SpringAOP,先带大家理解一下什么是代理模式。
1. 代理模式
什么是代理模式?
代理模式的定义:为其他对象提供一种代理
以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
上面的介绍出自百度,比较晦涩难懂,我们以租房举例:
- 房东:出租房屋
- 中介:代理房东出租房屋
- 租户:租房
定义解释:
-
为其他对象提供一种代理以控制对这个对象的访问
中介给房东提供代理,帮助房东出租房子
-
在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用
有的时候,房东没那么多的时间和精力去贴广告找租客,租客要看房的时候房东也不一定有时间,而这个时候就体现中介的作用了,中介代理房东来出租房子,房东就不用操心怎么找租客,需要随时带租客看房子的问题了。
1.1 静态代理
将以上租房的逻辑进一步抽象,经过分析后得到以下四种角色
角色分析
- 抽象角色:我们可以把上面例子中还可能涉及到的功能都抽象出来,比如说:看房、收钱、签合同等功能都抽象成一个角色,我们将这个角色称之为抽象角色。一般会以接口或者抽象类的形式来实现
- 真实角色:需要被代理的角色,就是例子中的房东
- 代理角色:代理真实角色,就是例子中的中介代理房东,一般中介除了替房东租房,还会有附加的操作,比如说跟租客收取中介费。
- 客户端:例子中的租客
接下来我们用代码来实现一下这个静态代理的模式,上代码~
抽象角色:有收房租、看房、签合同的功能
AbsFangDong.java
public interface AbsFangDong {
void shouFangZu();
void kanFang();
void qianHeTong();
}
真实角色:即房东,拥有抽象角色的所有功能
FangDong.java
@Data
public class FangDong implements AbsFangDong{
private String name;
@Override
public void shouFangZu() {
System.out.println(this.name+"shou fang zu le ...");
}
@Override
public void kanFang() {
System.out.println(this.name+"dai ni kan fang ...");
}
@Override
public void qianHeTong() {
System.out.println(this.name+"dai ni qian he tong qu ...");
}
}
代理角色:即中介,拥有房东的所有功能,同时也可扩展自己的功能,比如说,是个服务都得收费
ZhongJie.java
public class ZhongJie implements AbsFangDong{
private FangDong fangdong;
public ZhongJie(FangDong fangDong){
this.fangdong = fangDong;
}
@Override
public void shouFangZu() {
this.shouFee();
this.fangdong.shouFangZu();
}
@Override
public void kanFang() {
this.shouFee();
this.fangdong.shouFangZu();
}
@Override
public void qianHeTong() {
this.shouFee();
this. fangdong.qianHeTong();
}
public void shouFee(){
System.out.println("zhong jie shou fei hou,dai ti ");
}
}
客户:即租客,访问代理角色
public class Zuke {
@Test
public void test(){
FangDong fangdong = new FangDong();
fangdong.setName("zhag san ");
ZhongJie zhongjie = new ZhongJie(fangdong);
zhongjie.kanFang();
zhongjie.qianHeTong();
zhongjie.shouFangZu();
}
}
测试:此时我们看到黑中介开始乱收费了
zhong jie shou fei hou,dai ti
zhag san shou fang zu le ...
zhong jie shou fei hou,dai ti
zhag san dai ni qian he tong qu ...
zhong jie shou fei hou,dai ti
zhag san shou fang zu le ...
进程已结束,退出代码为 0
至此 一个简单的静态代理模式我们就已经实现了~
使用代理模式的优缺点:
优点:
- 真实角色可以无需关注代理角色的业务,职责清晰
- 公共业务(抽象角色)交给代理角色,实现业务分工
- 公共业务需要拓展的时候,方便管理
- 当代理角色需要对业务做扩展时,不会影响真实角色
缺点:
- 每新增一个真实角色就需要产生一个代理角色,代码量较大,效率较低
静态代理的特点
1、目标角色固定
2、在应用程序执行前就得到目标角色
3、代理对象会增强目标对象的行为
4、有可能存在多个代理 引起"类爆炸"(缺点)
1.2 动态代理
在讲动态代理之前,我们先来比较Java的class和interface的区别:
- 可以实例化
class(非abstract); - 不能实例化
interface。
所有interface类型的变量总是通过某个实例向上转型并赋值给接口类型变量的:
AbsFangDong absFangDong = new AbsFangDong() {
@Override
public void shouFangZu() {}
@Override
public void kanFang() {}
@Override
public void qianHeTong() {}
};
要么是这样:
public class FangDong implements AbsFangDong{
@Override
public void shouFangZu() {}
@Override
public void kanFang() {}
@Override
public void qianHeTong() {}
}
有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?
这是可能的,因为Java标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。
什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。静态代理参考上面讲到的静态代理,下面我们来看一下如何实现动态代理。
动态代理的角色还是和静态代理的角色一样,还是静态代理的四种角色。
上代码:
AbsFangDong.java
抽象角色:和静态代理一样,没有任何变化
public interface AbsFangDong {
void shouFangZu();
void kanFang();
void qianHeTong();
}
FangDong.java
真实角色:和静态代理一样,没有任何变化
需要实现抽象角色
@Data
public class FangDong implements AbsFangDong {
private String name;
@Override
public void shouFangZu() {
System.out.println(this.name+"shou fang zu le ...");
}
@Override
public void kanFang() {
System.out.println(this.name+"dai ni kan fang ...");
}
@Override
public void qianHeTong() {
System.out.println(this.name+"dai ni qian he tong qu ...");
}
}
代理角色:
这里代理角色,不在是ZhongJie.java了,代理角色的生成交由Java反射机制里的Proxy类来实现,Proxy生成代理示例需要几个重要的参数,这里讲解一下:
public class ProxyInvocationHandler implements InvocationHandler {
private Object object;
public ProxyInvocationHandler(Object object){
this.object = object;
}
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(), object.getClass().getInterfaces(),this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+" starting...");
Object res = method.invoke(this.object, args);
System.out.println(method.getName()+" stoped...");
return res;
}
}
用户:即租客,这里租客只需要想办法联系到中介,无需关注房东
Client.java
public class Client {
public static void main(String[] args) {
FangDong fangDong = new FangDong();
fangDong.setName("zhang san");
//创建房东的代理调用处理(中介处理器)
ProxyInvocationHandler handler = new ProxyInvocationHandler(fangDong);
//生成动态代理对象
AbsFangDong proxy = (AbsFangDong)handler.getProxy();
//动态代理房东执行操作
proxy.kanFang();
proxy.qianHeTong();
proxy.shouFangZu();
}
}
测试:
kanFang starting...
zhang sandai ni kan fang ...
kanFang stoped...
qianHeTong starting...
zhang sandai ni qian he tong qu ...
qianHeTong stoped...
shouFangZu starting...
zhang sanshou fang zu le ...
shouFangZu stoped...
通过以上方式实现的动态代理 即可代理任何真实角色,并动态生成代理角色
相比于静态代理,动态代理在创建代理对象上更加的灵活,动态代理类的字节码在程序运行时,由Java反射机制动态产生。它会根据需要,通过反射机制在程序运行期,动态的为目标对象创建代理对象,无需程序员手动编写它的源代码。
动态代理的特点
- 目标对象不固定
- 在应用程序执行时动态创建目标对象
- 代理对象会增强目标对象的行为
动态代理一般分为两种:
- 基于接口实现动态代理
上面演示的例子就是基于JDK实现的动态代理,它就是基于接口的动态代理 - 基于类实现的动态代理
还有一种实现方式是通过cglib来实现基于类的动态代理
2. AOP
2.1 什么是AOP
AOP(Aspect-OrientedProgramming,面向切面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
Spring为实现AOP提供了两种方式,可以选择AspectJ、Spring AOP 或两者兼而有之。还可以自由选择使用@AspectJ注解或者SpringXML配置文件的方式来实现。
Spring AOP 默认为 AOP 代理使用标准 JDK 动态代理,这使得任何接口(或一组接口)都可以被代理。其底层还是基于我们上面实现动态代理的方式。
Spring AOP 也可以使用 CGLIB 代理。这是代理类而不是接口所必需的。默认情况下,如果业务对象未实现接口,则使用 CGLIB。
接下来我们通过三种方式来实现AOP
2.2 使用SpringAOP
在使用Spring AOP之前 我们先来了解一下官方给提供的几个相关术语:
- 横切关注点:跨越应用程序多个模块的方法或功能。即与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点,如日志、权限、缓存、事务等等
- 切面(Aspect):横切关注点被模块化的特殊对象。官方解释比较晦涩难懂,实际上就是个类。
- 通知(Advice):切面必须要完成的工作。实际上就是类中的一个方法。
- 目标(Target):被通知对象。即需要被切入的接口或对象。
- 代理(Proxy):向目标对象应用通知之后创建的对象。即代理类。
- 切入点(PointCut):切面通知执行的”位置“的定义。
- 连接点(JoinPoint):与切入点匹配的执行点。
2.2.1 AOP 实现方式一(通过Spring AOP API)接口实现
这里假设有一个userService接口,并对其所有方法,在执行前后打印输出日志
上代码:
pom.xml
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
使用Spring AOP的接口创建俩通知(Advice)
新建cc.leeleo.aop包,并新建以下两个类
AfterFunction.java
public class AfterFunction implements AfterReturningAdvice {
/**
* 在切点之后执行的方法
*
* @param o 返回值
* @param method 要执行的目标对象的方法
* @param objects 参数
* @param o1 目标对象
* @throws Throwable throwable
*/
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("在执行了"+o1.getClass().getName()+"的"+method.getName()+"方法之后,打印这个方法的返回值:"+o);
}
}
BeforeFunction.java
public class BeforeFunction implements MethodBeforeAdvice {
/**
* 在切点之前执行的方法
*
* @param method 要执行的目标对象的方法
* @param objects 参数
* @param o 目标对象
* @throws Throwable throwable
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("在执行"+o.getClass().getName()+"的"+method.getName()+"方法之前,打印了这句话");
}
}
新建cc.leeleo.service包,并创建一个接口和实现类
UserService.java
public interface UserService {
void add();
boolean del();
void update();
void select();
}
UserServiceImpl.java
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("add...");
}
@Override
public boolean del() {
System.out.println("del...");
return true;
}
@Override
public void update() {
System.out.println("update...");
}
@Override
public void select() {
System.out.println("select...");
}
}
beans.xml
这里注意需要引入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
">
<bean id="userService" class="cc.leeleo.service.UserServiceImpl"/>
<bean id="after" class="cc.leeleo.aop.AfterFunction"/>
<bean id="before" class="cc.leeleo.aop.BeforeFunction"/>
<aop:config>
<!--切入点,定义了对哪(些||个)类的哪(些||个)方法进行植入,当前指定了对UserServiceImpl下的所有方法进行切入。expression->详情百度:Spring execution表达式-->
<aop:pointcut id="point" expression="execution(* cc.leeleo.service.UserServiceImpl.*(..))"/>
<!--
翻译过来是顾问的意思,实际上就是指定 切点对应的哪些方法,
以下两条配置是将after和before两个类植入到point切点上
-->
<aop:advisor advice-ref="after" pointcut-ref="point"/>
<aop:advisor advice-ref="before" pointcut-ref="point"/>
</aop:config>
</beans>
接下来我们测试获取这个service这个接口,并对其实现类进行接口回调,看能否对其所有方法执行前后进行打印输出日志。
这里需要注意,以下是定义了UserService 接口,并通过获取UserServiceImpl对象将其实例。
service.del();调用这个方法的过程称为对象功能的接口回调。
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService service = context.getBean("userService", UserService.class);
service.del();
}
测试结果:
在执行cc.leeleo.service.UserServiceImpl的add方法之前,打印了这句话
add...
在执行了cc.leeleo.service.UserServiceImpl的add方法之后,打印这个方法的返回值:null
在执行cc.leeleo.service.UserServiceImpl的del方法之前,打印了这句话
del...
在执行了cc.leeleo.service.UserServiceImpl的del方法之后,打印这个方法的返回值:true
2.2.2 AOP 实现方式二(通过切面实现)
这里我们就不需要上面的AfterFunction.java和BeforeFunction.java这两个类了,我们通过一个类来实现一下,这里cc.leeleo.service包里的东西不变,在cc.leeleo.aop的包里新建一个类
Aspect.java 很简单 就两个方法
public class Aspect {
public void after(){
System.out.println("--------------------end--------------------");
}
public void before(){
System.out.println("--------------------start--------------------");
}
}
再修改一下beans.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
">
<bean id="userService" class="cc.leeleo.service.UserServiceImpl"/>
<!--注册切面bean-->
<bean id="aspect" class="cc.leeleo.aop.Aspect"/>
<aop:config>
<!--这里直接定一个切面,不同于上面的pointcut,定义的是一个切点-->
<aop:aspect ref="aspect">
<!--和上面的切点一样,这里指定cc.leeleo.service下的所有类和子类的所有方法都是切入点-->
<aop:pointcut id="p" expression="execution(* cc.leeleo.service..*(..))"/>
<!--指定在切入点之前执行的方法,可以称之为通知(Advice)-->
<aop:before method="before" pointcut-ref="p"/>
<!--指定在切入点之后执行的方法,可以称之为通知(Advice)-->
<aop:after method="after" pointcut-ref="p"/>
</aop:aspect>
</aop:config>
</beans>
还是上面的测试方法执行:
输出如下
--------------------start--------------------
add...
--------------------end--------------------
--------------------start--------------------
del...
--------------------end--------------------
2.2.3 注解实现AOP
通过注解的方式实现切入点的植入就很简单了,我们只需定义一个类,使用@ASpect注解即可
在cc.leeleo.aop中新建一个类
AnnotationAspect.java
@Aspect//标注这是一个切面
public class AnnotationAspect {
@Before("execution(* cc.leeleo.service..*(..))")
public void before(){
System.out.println("之前执行");
}
@After("execution(* cc.leeleo.service..*(..))")
public void after(){
System.out.println("之后执行");
}
}
beans.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
">
<bean id="userService" class="cc.leeleo.service.UserServiceImpl"/>
<!--注册切面-->
<bean class="cc.leeleo.aop.AnnotationAspect"/>
<!--开启切面注解支持-->
<aop:aspectj-autoproxy/>
</beans>
还是上面的测试方法:
测试结果
之前执行
add...
之后执行
之前执行
del...
之后执行
这里我们在做点拓展
除了上面的after、before、还有一种植入方式around环绕,
细心的小伙伴可能发现了,使用注解方式和切面的方式实现的切点,都不能获取到当前目标对象、代理对象、方法、返回值等,但是通过Spring AOP的API:AfterReturningAdvice、MethodBeforeAdvice 这两种接口等实现的切点却能获取的到,其实通过around的方式也可以获取到当前的目标对象和代理对象、返回值等信息。
@Around("execution(* cc.leeleo.service..*(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前");
//参数
Object[] args = joinPoint.getArgs();
//代理对象
Object proxy = joinPoint.getThis();
//目标对象
UserServiceImpl target = (UserServiceImpl) joinPoint.getTarget();
//返回值
Object res = joinPoint.proceed();
System.out.println("执行结果:"+res);
System.out.println("环绕后");
}
这里我们需要注意下before、around、after的执行顺序,加上上面的代码以后我们看下执行结果
环绕前
之前执行
del...
之后执行
执行结果:true
环绕后