Spring学习(二)—代理模式/AOP面向切面编程
01 代理模式
在学习AOP之前,我们需要先学习代理模式,因为AOP的底层机制就是基于动态代理实现的。
什么是代理模式?
代理模式
是7种结构设计模式之一,其目的是提供一个代理对象,以控制客户对某个被代理的真实对象(委托类)的直接访问。
通常,
代理类
负责为被代理的真实对象预处理消息,过滤消息并转发消息,以及消息被真实对象执行后的后续处理。但是
核心业务仍然需要转由真实对象处理
。而客户在办理某项业务时,只需要找代理即可,并不需要知道真实对象是如何去处理该业务的。
为了保持
业务的一致性
,代理类和真实对象需要
实现共同的接口
,对于客户来说,这两个类都只是实现类而已。通过代理类,可以有效控制客户对真实对象的直接访问,也可以很好地隐藏和保护真实对象,同时也可以为实施不同的控制策略预留了空间,在设计上获得了更大的灵活性。其实,这样做的最终目的还是为了
解耦
。通过引入第三方代理类,客户与真实对象的关系得以解耦,并且代理类的出现可以帮助管理真实对象。
代理模式可分为静态代理和动态代理
。两者的区别主要体现在
创建时期
,静态代理类由程序员手动创建或是由特定的工具自动生成,在程序运行前,静态代理类的字节码文件就已经存在了。而
动态代理类是在程序运行时运用反射机制动态创建而成的
。
首先,先用一个例子简单说明一下如何实现
静态代理
。
创建UserDao接口,这个接口声明了与用户相关的增删改查的数据库操作方法。
package com.hooi.dao;
public interface UserDao {
//添加用户
public abstract void addUser();
//删除用户
public abstract void delUser();
//修改用户信息
public abstract void modifyUserInfo();
//查询用户信息
public abstract void queryUserInfo();
}
创建真实对象UserDaoImpl,实现UserDao接口中的方法,专注于处理核心业务。
package com.hooi.dao;
import org.springframework.stereotype.Component;
@Component //使用注解注册bean
public class UserDaoImpl implements UserDao {
public void addUser() {
System.out.println("添加用户");
}
public void delUser() {
System.out.println("删除用户");
}
public void modifyUserInfo() {
System.out.println("修改用户信息");
}
public void queryUserInfo() {
System.out.println("查询用户信息");
}
}
创建静态代理类,代理真实对象,为真实对象增加一些控制策略(预处理和后处理),比方说,为真实对象增加一些日志功能。首先,代理类必须和真实对象实现共同的接口,除此之外,还需要持有真实对象的引用,因为代理类并不能处理接口种的核心业务,核心业务需要转由真实对象处理。
package com.hooi.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
//静态代理,为UserDaoImpl添加日志功能
@Component //使用注解注册bean
public class UserDaoImplStaticProxy implements UserDao{
@Autowired//自动注入
@Qualifier("userDaoImpl")
private UserDaoImpl userDaoImpl;
public UserDaoImplStaticProxy() {
}
public UserDaoImplStaticProxy(UserDaoImpl userDaoImpl) {
this.userDaoImpl = userDaoImpl;
}
//代理类持有的预处理方法
public void log(String methodName) {
System.out.println("开始执行:" + methodName);
}
@Override
public void addUser() {
//添加日志(代理为真实对象所做的事情)
log("addUser()");
//代理做不了的事情,转由真实对象操作
userDaoImpl.addUser();
}
@Override
public void delUser() {
}
@Override
public void modifyUserInfo() {
}
@Override
public void queryUserInfo() {
}
}
测试代码:
package com.hooi.dao;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class staticProxyTest {
@Test
public void staticProxyTest() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");//在Spring框架下必须想要使用IoC容器中的对象,需要先获得上下文
UserDaoImplStaticProxy userDaoImplStaticProxy = (UserDaoImplStaticProxy) context.getBean("userDaoImplStaticProxy");
System.out.println(userDaoImplStaticProxy);
userDaoImplStaticProxy.addUser();
}
}
测试结果:
(静态代理)总结:
- 静态代理类和真实对象实现了相同的接口,二者分工明确,静态代理类只负责公共业务的处理,而真实对象只需要专注于自己业务的处理,静态代理类(持有静态对象的引用)通过真实对象实现核心业务的处理,但是这会导致出现大量的重复代码,接口每增加一个方法,对应的所有的静态代理类也需要去实现这些方法,增加了代码维护的复杂度。
- 一个静态代理类只服务于一种类型的对象,如果要服务多个类型的对象,就要对每个对象都进行添加代理的操作,因此,当项目规模较大的时候,仍然采用静态代理显然是一种很鸡肋的做法。比方说,我们为UserDao通过静态代理类添加了可以增加日志打印的公共业务,通过代理类去调用真实对象实现核心业务,避免修改具体的真实对象,这一点满足开闭原则。此时,如果TenantDao也想要增加日志打印的业务,对应的还有更多的类想要增添此项业务,我们就需要为每个接口都创建代理。总结:静态代理只能为特定的某一个接口服务。
那么,怎样才可以只创建一个代理的情况下即能帮助接口实现一类预处理的公共业务,又能同时服务于多个接口呢?在静态代理的例子中可以看到,一个代理只可以服务一个接口,并且在编译期就已经通过持有被代理对象的引用确定被代理的对象。于是,大佬们又提出了一种新的机制—
动态代理
。动态代理是在运行时通过
反射机制
实现的,可以代理各种类型的对象。这样,我们就可以实现一个代理为多个对象实现同一类预处理业务。
到底
如何实现动态代理
呢?
在此之前,我们需要先了解
java.lang.reflect.InvocationHandler接口
和
java.lang.reflect.Proxy类
InvocationHandler
是由代理实例的调用处理程序实现的接口,每个代理实例都具有一个关联的调用处理程序。对代理实例调用方法时,将对方法调用进行编码并将其指派到它的调用处理程序的
invoke
方法。
invoke
方法在代理实例上处理方法调用并返回结果。在与方法关联的代理实例上调用方法时,将在调用处理程序上调用此方法。
Proxy
提供用于创建动态代理类和实例的静态方法。提供的方法如下:
|
返回指定代理实例的调用处理程序。 |
---|---|
|
返回代理类的
对象,并向其提供类加载器和接口数组。 |
|
当且仅当指定的类通过
方法或
方法动态生成为代理类时,返回 true。 |
|
返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。 |
下面开始实现
动态代理
:
首先UserDao接口及UserDaoImpl类与静态代理中的介绍相同,不同的是动态代理类的编写,如下:
package com.hooi.dao;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Component
public class UserDaoImplDynamicProxy implements InvocationHandler {
//目标对象(需要被代理的对象)
private Object target;
//获取代理实例:与目标对象绑定关系即关联接口,绑定具体实现类,获取对应目标对象的代理实例
public Object getProxyInstance(Object target){
//设置目标对象
this.target = target ;
/*
Proxy类提供了创建动态代理实例的静态方法
该方法用于指定类加载器,接口,以及调用处理器生成的动态代理类实例
第一个参数:指定动态代理对象的类加载器,需要与目标对象使用同一个类加载器
第二个参数:指定动态代理对象要实现的接口,需要与目标对象实现的接口一致
第三个参数:表明真实对象被拦截的方法在被拦截时需要执行哪个实现InvocationHandler的类的invoke方法,即this对象
*/
Class<?> targetClass = this.target.getClass();
return Proxy.newProxyInstance(targetClass.getClassLoader(),targetClass.getInterfaces(),this);
}
/*
与本动态代理类关联的被代理类的方法被调用时将被拦截,转为执行本动态代理类的invoke方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
proxy:代理对象
method:目标对象被拦截的目标方法
args:方法参数
*/
Object result = null;
try {
//模拟打印日志信息
System.out.println("开始执行----->"+method.getName());
//调用目标对象的目标方法,传入目标对象和方法参数
result = method.invoke(target, args);
//模拟打印日志信息
System.out.println("执行成功------>");
} catch (Exception e) {
e.printStackTrace();
//模拟打印日志信息
System.out.println("执行失败------>");
}
return result;
}
}
测试代码:
package com.hooi.dao;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class dynamicProxyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDaoImplDynamicProxy dynamicProxy = (UserDaoImplDynamicProxy) context.getBean("userDaoImplDynamicProxy");
UserDao proxyInstance = (UserDao) dynamicProxy.getProxyInstance(new UserDaoImpl());//获取相应目标对象的代理实例
proxyInstance.addUser();//执行方法
}
}
测试结果:
总结:
我们可以通过一个动态代理类代理不同类型的对象,这个动态代理类需要实现
InvocationHandler
的接口,并重写invoke方法。我们在使用动态代理类时,如果想要实现对不同类型对象的代理,需要使用Proxy类的相关静态方法
newProxyInstance()
创建一个动态代理类的实例,在
newProxyInstance()
方法中通过反射绑定相关目标对象的类加载器及其实现的接口以及本动态代理类
this
,绑定
this
是为了声明真实对象被拦截的方法在被拦截时需要执行哪个实现InvocationHandler的类的invoke方法。(可以理解为:如果我们把对外的接口都通过动态代理来实现,那么所有的函数调用最终都会经过invoke函数的转发,因此我们可以在invoke函数中做一些自己想做的事情,比如日志系统,事务,拦截器,权限控制等,这也就是AOP的基本原理)
02 AOP
AOP(Aspect Oriented Programming)面向切面编程:通过
预编译
方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是
OOP
的延续,是软件开发中的一个热点,也是
Spring
框架中的核心原理之一,是
函数式编程
的一种衍生范型。设计模式孜孜不倦追求的时调用者和被调用者之间的解耦,AOP可以说是这种目标的一种实现。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的
耦合度
降低,提高程序的可重用性,同时提高了开发的效率。
AOP在Spring框架中的重要作用:
提供声明式事务:允许用户自定义切面
横切关注点:跨越应用程序多个模块的方法或功能,即那些与我们业务逻辑无关的公共业务,这些业务虽然与我们的核心业务逻辑无关,但却依然需要我们给予关注,这部分公共业务就是横切关注点,如日志,安全,缓存,事务等等…
切面(Aspect):横切关注点被模块化的特殊对象,是一个类,可以理解为一个动态代理类。
通知(Advice):切面必须要完成的工作,即切面中的方法,也可以理解为动态代理类中的方法。
目标(target):被通知的对象,即被代理的对象。
代理(Proxy):向目标对象应用通知后创建的对象,可以理解为动态代理类中注入目标对象后获取的代理实例。
切入点(PointCut):切面通知执行的”地点”的定义
切入点(PointCut):切面通知 执行的 “地点”的定义。
连接点(JointPoint):与切入点匹配的执行点。
Spring中支持5种类型的Advice:
那么,
如何使用Spring框架实现AOP呢?
有两种实现方式,一种基于注解,一种为非注解的实现方式
首先,我们先用
非注解的实现方式
实现AOP。
编写业务类
package com.hooi.service;
public interface UserService {
//添加用户
public abstract void addUser();
//删除用户
public abstract void delUser();
//修改用户信息
public abstract void modifyUserInfo();
//查询用户信息
public abstract void queryUserInfo();
}
编写业务实现类,即真实对象
package com.hooi.service;
import org.springframework.stereotype.Component;
public class UserServiceImpl implements UserService {
public void addUser() {
System.out.println("添加用户");
}
public void delUser() {
System.out.println("删除用户");
}
public void modifyUserInfo() {
System.out.println("修改用户信息");
}
public void queryUserInfo() {
System.out.println("查询用户信息");
}
}
将横切关注点模块化为一个切面Aspect,即一个类,这里将日志作为横切关注点,抽象出了一个Log类,这个类中方法为前置通知,需要实现
MethodBeforeAdvice
接口。
package com.hooi.service;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class Log implements MethodBeforeAdvice {
//这个Log类就是一个切面
//这个before方法就是一个advice
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("开始执行---》"+o.getClass().getName()+"类的"+method.getName()+"方法");
}
}
将事务提交作为第二个横切关注点,同样的模块化为一个切面,即Transaction类。这个类中定义了后置通知,需要实现
AfterReturningAdvice
接口。
package com.hooi.service;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class Transaction implements AfterReturningAdvice {
/*
参数说明:
returnValue : 返回值
method : 被调用的方法
args : 被调用的方法的参数
target : 被调用的目标对象
*/
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName()+"类的"+method.getName()+"的相关事务提交了");
}
}
切面和通知都编写好后,紧接着去spring的配置文件中为每个advice配置aop的切入点。
<?xml version="1.0" encoding="UTF-8"?>
<!--suppress SpringFacetInspection -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<!--业务实现类-->
<bean id="userServiceImpl" class="com.hooi.service.UserServiceImpl"/>
<!--切面1 日志类-->
<bean id="log" class="com.hooi.service.Log"/>
<!--切面2 事务类-->
<bean id="transaction" class="com.hooi.service.Transaction"/>
<!--实现aop
1. 需要导包
2. 需要导入约束
3. 配置aop
3.1 配置切入点
3.2 执行通知
-->
<aop:config>
<!--
配置切入点Pointcut
使用expression表达式表示要切入的位置
语法:execution([类的修饰符] [类的全路径] [方法] [参数])
类的修饰符使用*表示回去全路径下查找
-->
<aop:pointcut id="pointcut" expression="execution(* com.hooi.service.UserServiceImpl.*(..))"/>
<!--
配置通知advice
-->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="transaction" pointcut-ref="pointcut"/>
</aop:config>
</beans>
测试代码:
@Test
public void test1() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userServiceImpl = (UserService) context.getBean("userServiceImpl");
userServiceImpl.addUser();
}
测试结果:
可以看到,我们所提出的横切关注点就这样神不知鬼不觉的织入到了业务核心逻辑中。
接下来,采用注解的方式来实现AOP,这种方式相较于前者,更为方便。
业务接口代码不变,业务实现类采用注解的方式注入IoC容器中
package com.hooi.service;
import org.springframework.stereotype.Component;
@Component
public class UserServiceImpl implements UserService {
public void addUser() {
System.out.println("添加用户");
}
public void delUser() {
System.out.println("删除用户");
}
public void modifyUserInfo() {
System.out.println("修改用户信息");
}
public void queryUserInfo() {
System.out.println("查询用户信息");
}
}
提出横切关注点,使用注解将横切化关注点模块化为切面,编写通知。
package com.hooi.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component //将切面注册进IoC容器中
@Aspect //将此类注册为切面(横切关注点被模块化的对象)
public class AnnoAop {
//前置通知
@Before("execution(* com.hooi.service.UserServiceImpl.*(..))")//配置advice并传入切入点
public void log(){
System.out.println("------开始前置通知:日志记录------>");
}
//后置通知
@After("execution(* com.hooi.service.UserServiceImpl.*(..))")
public void transaction(){
System.out.println("------开始后置通知:事务提交------>");
}
//环绕通知
/*
环绕通知本质:
将目标对象作为参数传递进方法中,
在方法执行的前后,增加一些操作,仅此而已
但是可以通过ProceedingJoinPoint拿到一些属于目标对象的东西;
*/
@Around("execution(* com.hooi.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("------开始环绕通知------->");
System.out.println("------接入点的目标对象为------->"+joinPoint.getTarget().getClass().getName());
System.out.println("------接入点的目标对象的方法参数为------->"+joinPoint.getArgs());
//执行目标方法,获得结果
Object result = joinPoint.proceed();
System.out.println("------结束环绕通知------->");
System.out.println("------方法执行结果:"+result);
}
}
编写spring的配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!--suppress SpringFacetInspection -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--自动扫描包下的component注解-->
<context:component-scan base-package="com.hooi.service"/>
<!--识别aspect注解,自动代理-->
<aop:aspectj-autoproxy/>
</beans>
测试代码:
@Test
public void test2() {
ApplicationContext context = new ClassPathXmlApplicationContext("annoContext.xml");
UserService userServiceImpl = (UserService) context.getBean("userServiceImpl");
userServiceImpl.addUser();
}
测试结果:
总结
Spring实现AOP的步骤:
- 编写业务接口
- 实现业务接口
- 提取横切关注点(一些公共业务)
- 将横切关注点模块化为一个切面,即一个类
- 在切面中编写相应的通知(前置通知,后置通知,环绕通知等)
- 在Spring的配置文件中注册切面,配置切入点以及为切入点绑定相应的通知
关于实现通知的底层源码
- 前置通知需要实现MethodBeforeAdvice接口,这个接口是一个函数式接口(一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口),且这个接口继承了BeforeAdvice类,而BeforeAdvice实现了一个声明式接口Advice
- 同上,后置通知需要实现AfterReturningAdvice接口,这个接口也是一个函数式接口,且这个接口继承了AfterAdvice类,而AfterAdvice也实现了Advice接口。
- 同理可推环绕通知等实现原理…