在上一篇Spring框架基础(二)IOC容器和DI 中我们介绍了IOC和DI,本篇介绍Spring另一个重要的概念--面向切面编程AOP。在介绍AOP之前,我们先介绍一下代理(Proxy)。代理是一种设计模式,提供了对目标对象的另一种访问方式,也就是说,我们可以通过代理对象访问到目标对象。这种做法的好处:可以在不改变目标对象的基础上,增加新的功能。常用的场景有:事务、日志、权限等等。
一、三种代理方式
1.1、静态代理
特点:代理对象要实现与目标对象一样的接口
举例:目标对象UserDao类,实现了IUserDao接口。那么代理对象UserDaoProxy 如下
public class UserDaoProxy implements IUserDao{
// 接收保存目标对象
private IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开始事务..."); // 额外的方法
target.save(); // 执行目标对象的方法
System.out.println("提交事务..."); //额外的方法
}
}
public class App {
public static void main(String[] args) {
// 目标对象
IUserDao target = new UserDao();
// 代理
IUserDao proxy = new UserDaoProxy(target);
proxy.save(); // 执行的是,代理对象的save方法
}
}
1.2、总结
1)可以在不修改目标对象功能前提下,扩展目标对象的功能。
2)缺点:
因为代理对象需要实现与目标对象一样的接口,所以会存在很多代理类。同时接口一旦增加新的方法,目标对象与代理对象都要维护。
为了解决这个问题,我们引入动态代理
2.1、动态代理(也叫JDK代理、接口代理)
特点:目标对象需要实现接口,而代理对象不需要实现接口,是利用JDK的API动态的在内存中构建代理对象。
JDK中生成代理对象的API:
|-- Proxy
static Object newProxyInstance(
ClassLoader loader, 指定当前目标对象使用类加载器
Class<?>[] interfaces, 目标对象实现的接口的类型
InvocationHandler h 事件处理器
)
举例:目标对象UserDao类,实现了IUserDao接口。那么代理对象UserDaoProxy 如下
public class ProxyFactory {
// 维护一个目标对象
private Object target;
public ProxyFactory(Object target){
this.target = target;
}
// 给目标对象,生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("开启事务");
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务");
return returnValue;
}
});
}
}
public class App {
public static void main(String[] args) {
// 目标对象
IUserDao target = new UserDao();
// 给目标对象,创建代理对象
IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
// 执行方法 【代理对象】
proxy.save();
}
}
2.2、总结
1)代理对象不需要实现接口,但是目标对象一定要实现接口,否则不能使用动态代理
2)缺点:目标对象必须实现接口
如果目标对象没有实现接口呢?这时就需要第三种代理,以子类的方式实现代理(也叫做cglib代理)
3.1、cglib代理(也叫子类代理)
特点:目标对象不需要实现接口,通过在内存中构建一个子类对象实现对目标对象功能的扩展。
举例:目标对象UserDao类,注意没有实现接口,代理对象UserDaoProxy 如下
public class ProxyFactory implements MethodInterceptor{
// 维护目标对象
private Object target;
public ProxyFactory(Object target){
this.target = target;
}
// 给目标对象创建代理对象
public Object getProxyInstance(){
//1. 工具类
Enhancer en = new Enhancer();
//2. 设置父类
en.setSuperclass(target.getClass());
//3. 设置回调函数
en.setCallback(this);
//4. 创建子类(代理对象)
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("开始事务.....");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务.....");
return returnValue;
}
}
public class App {
public static void main(String[] args) {
// 目标对象
UserDao target = new UserDao();
// 代理对象
UserDao proxy = (UserDao) new ProxyFactory(target).getProxyInstance();
// 执行代理对象的方法
proxy.save();
}
}
3.2、 总结
1)代理对象通过实现MethodInterceptor接口,构建子类对象。
2)缺点:由于这里是构建子类对象,这就要求我们的目标对象不能为final,否则会报错。如果我们目标对象的方法为final/static, 那么就不会被拦截,即不会执行目标对象额外的业务方法。
为什么我们要介绍代理模式呢?因为在Spring的AOP编程中,底层就是通过代理来实现面向切面编程的。
如果加入容器的目标对象有实现接口,用JDK代理;如果目标对象没有实现接口,用Cglib代理;
二、面向切面编程AOP
2.1、 常用概念
面向切面编程(aspect object programming,AOP)
让关注点代码(重复执行的代码)与业务代码分离
关注点
重复执行的代码就叫做关注点
切面
关注点形成的类,就叫切面(类)
切入点
执行目标对象方法,动态植入切面代码。
可以通过切入点表达式,指定拦截哪些类的哪些方法, 给指定的类在运行的时候植入切面类代码。
2.2、 实例
我们以保存学生信息为例,我们都知道,在进行数据库操作时,需要添加事务。通常我们操作如下
public class StrudentDao implements IStrudentDao{
@Override
public void save() {
System.out.println("开始事务/异常");
System.out.println("执行保存");
System.out.println("提交事务/关闭");
}
}
这里只是一个增加方法,对于修改,删除方法都需要添加事务
也就是说,System.out.println("开始事务/异常"); 和System.out.println("提交事务/关闭");就是我们上面提到的关注点
真正的业务代码只有System.out.println("执行保存");这一行
如何通过面向切面编程的思想对我们的代码进行优化呢?
接下来我们介绍两种方式:XML配置方式和注解方式来实现面向切面编程
1)XML配置方式
首先,我们提取出关注点代码,形成一个切面类Aop.class,如下
// 切面类
public class Aop {
//前置通知
public void begin(){
System.out.println("开始事务/异常");
}
//后置通知
public void after(){
System.out.println("提交事务/关闭");
}
//返回后通知
public void afterReturning() {
System.out.println("afterReturning()");
}
//异常通知
public void afterThrowing(){
System.out.println("afterThrowing()");
}
//环绕通知
public void around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("环绕前....");
pjp.proceed(); // 执行目标方法
System.out.println("环绕后....");
}
}
然后在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:p="http://www.springframework.org/schema/p"
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">
<!-- dao 实例 -->
<bean id="strudentDao" class="cn.zc.f_aop_xml.StrudentDao"></bean>
<!-- 切面类 -->
<bean id="aop" class="cn.zc.f_aop_xml.Aop"></bean>
<!-- Aop配置 -->
<aop:config>
<!-- 定义一个切入点表达式: 拦截哪些方法 -->
<aop:pointcut expression="execution(* cn.zc.f_aop_xml.*.*(..))" id="pt"/>
<!-- 切面 -->
<aop:aspect ref="aop">
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="pt"/>
<!-- 前置通知: 在目标方法调用前执行 -->
<aop:before method="begin" pointcut-ref="pt"/>
<!-- 后置通知: -->
<aop:after method="after" pointcut-ref="pt"/>
<!-- 返回后通知 -->
<aop:after-returning method="afterReturning" pointcut-ref="pt"/>
<!-- 异常通知 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="pt"/>
</aop:aspect>
</aop:config>
</beans>
至此,优化完毕
2)注解方式
首先将dao类和Aop切面类加入容器,Aop切面类中的需要用到以下几个注解,如下
@Component
@Aspect // 指定当前类为切面类
public class Aop {
// 指定切入点表单式: 拦截哪些方法; 即为哪些类生成代理对象
@Pointcut("execution(* cn.zc.e_aop_anno.*.*(..))")
public void pointCut_(){
}
// 前置通知 : 在执行目标方法之前执行
@Before("pointCut_()")
public void begin(){
System.out.println("开始事务/异常");
}
// 后置/最终通知:在执行目标方法之后执行 【无论是否出现异常最终都会执行】
@After("pointCut_()")
public void after(){
System.out.println("提交事务/关闭");
}
// 返回后通知: 在调用目标方法结束后执行 【出现异常不执行】
@AfterReturning("pointCut_()")
public void afterReturning() {
System.out.println("afterReturning()");
}
// 异常通知: 当目标方法执行异常时候执行此关注点代码
@AfterThrowing("pointCut_()")
public void afterThrowing(){
System.out.println("afterThrowing()");
}
// 环绕通知:环绕目标方式执行
@Around("pointCut_()")
public void around(ProceedingJoinPoint p) throws Throwable{
System.out.println("环绕前通知....");
p.proceed(); // 执行目标方法
System.out.println("环绕后通知....");
}
}
然后在Spring的核心配置文件中开启注解扫描以及AOP注解方式
<!-- 开启注解扫描 -->
<context:component-scan base-package="cn.zc.e_aop_anno"></context:component-scan>
<!-- 开启aop注解方式 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
总结
本文首先介绍了何为代理,以及代理的三种方式
然后引入面向切面编程概念,以及通过XML和注解的方式进行实现