Spring-AOP切面编程(1)
一、概要
面向对象的特点是封装继承、多态。而封装的核心就是将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类的复用性增加。但是新的问题又来了,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程,我们把切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
参考资料
二、面向切面编程
1、什么切面编程
面向切面编程(也叫面向方面):Aspect Oriented Programming(AOP)。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP是OOP的补充和完善
2、主要解决的问题
- 解决代码复用的问题
- 解决关注点分离
- 水平分离: 例如 控制层—服务层—数据库
- 垂直分离: 例如 用户模块—商品模块
- 关注点分离(功能): 业务和非业务
三、实例讲解
1、原始代码
- 需求
比如我们要写一个UserService,在调用保存用户和删除用户的时候需要去打开数据库和开启事务 -
示例代码
- 说明
通过上面的代码我们发现打开数据,开启事务,提交事务,关闭数据库是重复的代码,有重复的代码我们怎么办?
通过类封装成方法提取重复的代码啊!(抽取成类的方式我们称之为:纵向抽取)
2、提取类封装成方法
- 说明
这个时候我们新建一个DBManager类 定义四个方法 - 示例代码
public class DBManager { public void open() { System.out.println("打开数据库..."); } public void colse() { System.out.println("关闭数据库..."); } public void begin() { System.out.println("开启事务"); } public void commit() { System.out.println("提交事务"); } }
public class UserService { private DBManager manager = new DBManager(); public void save() { manager.open(); manager.begin(); System.out.println("保存用户信息"); manager.commit(); manager.colse(); } public void delete() { manager.open(); manager.begin(); System.out.println("删除用户"); manager.commit(); manager.colse(); } }
- 说明
通过上面的案例解决了代码重复性的问题,但同时也会带来另外两个问题:- 耦合度:会造成封装类和业务类的耦合,
- 侵入性强:被我们提取的逻辑代码还是融合到业务逻辑中
但是,这种做法相比最原始的代码写法,已经有了一些的改进,那么有没有一种方案能解决耦合度,侵入性强的问题,
答案就是代理模式
四、代理模式
1、代理模式概要
代理模式是一种非常好理解的一种设计模式,举几个简单的代理的例子:
- 比如玩游戏升级太麻烦了,这个是时候我们可以去请代练帮我们升级,那这个代练其实就是一个代理
- 比如我们回家过年买不到火车票,通常加价请第三方的一些黄牛帮我们去买
- 歌星或者明星都有一个自己的经纪人,这个经纪人就是他们的代理人,当我们需要找明星表演时,不能直接找到该明星,只能是找明星的经纪人
让代练帮我们升级,我们可能就想下副本,让黄牛帮我们买票,我们就的目的就是想回家,明星让经纪人接拍电影,明星只是需要去拍电影就行了。无论是游戏代练,黄牛,还是经纪人其它他们其实都是在帮我们在做事(做一些我们不想做,或者做不来的事情),但并不能把所有的事情都帮我们做了,比如黄牛帮我们买点票了,回家你还的自己回吧,
2、分类
- 静态
由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。 - 动态
在程序运行时运用反射机制动态创建而成。
3、作用
代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可,而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但必须,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法
五、静态代理
1、说明
静态代理就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了
步骤
1:将业务抽象为接口
2:代理对象和被代理对象都实现该接口
3:代理对象持有被代理对象的引用,在执行具体核心业务时交由被代理对象完成。
2、示例代码
- 业务代码
/** * 简单业务层接口 */ public interface UserService{ public void saveUser(); } /** * 业务层实现类,实现save方法 */ public class UserServiceImpl implements UserService{ @Override public void saveUser() { System.out.println("2:保存用户信息"); } }
- 代理类
/** * 代理类 */ public class UserServiceProxy implements UserService{ private UserService userService; public UserServiceProxy(UserService userService) { super(); this.userService = userService; } public void open(){ System.out.println("1:打开数据库连接"); } public void close(){ System.out.println("3:关闭数据库连接"); } @Override public void saveUser() { this.open(); userService.saveUser(); this.close(); } }
- 测试代码
/** * 测试类 */ public class TestProxy { public static void main(String[] args) { UserService userService =new UserServiceProxy(new UserServiceImpl()); userService.saveUser(); } }
3、分析
3.1、优点
- 代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合)
3.2、缺点
- 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度
- 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了
由于每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类
所以我们就会想能不能通过一个代理类完成全部的代理功能?,那么我们就需要用动态代理
六、JDK动态代理
1、概念
JDK动态代理所用到的代理类在程序调用到代理类对象时才由JVM真正创建,JVM根据传进来的业务实现类对象 以及方法名 ,动态地创建了一个代理类并执行,然后通过该代理类对象进行方法调用。我们需要做的,只需指定代理类的预处理、调用后操作即可
目前Java开发包中包含了对动态代理的支持,但是其实现只支持对接口的的实现。 其实现主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。
- Proxy类主要用来获取动态代理对象,
- InvocationHandler接口用来约束调用者实现
2、重要类介绍
2.1、Proxy
- 说明
这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象 - 核心方法
返回值 方法 说明 static InvocationHandler getInvocationHandler(Object proxy) 方法返回指定接口的代理类的实例,这些接口将调用方法调用到指定的调用处理程序。 static Class getProxyClass(ClassLoader loader, Class[] interfaces) 用于获取关联于指定类装载器和一组接口的动态代理类的类对象 static boolean isProxyClass(Class cls) 该方法用于判断指定类对象是否是一个动态代理类 staticObject newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
2.2、InvocationHandler
- 说明
这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问 - 语法
// 该方法负责集中处理动态代理类上的所有方法调用。,第二个参数是被调用的方法对象 // 第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行 Object invoke(Object proxy, Method method, Object[] args)
- 参数说明
- proxy
第一个参数既是代理类实例 - method
被调用的方法对象 - args
调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行
- proxy
3、示例代码
3.1、声明代理类
- 说明
动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类 - 示例代码
// 第一步 定义代理类,实现InvocationHandler接口 public class DynamicProxy implements InvocationHandler { //代理目标对象 private Object target; public Object newProxyInstance(Object object) { this.target = object; return Proxy.newProxyInstance(object.getClass().getClassLoader() , object.getClass().getInterfaces(), this); } /** * 关联的这个实现类的方法被调用时将被执行 * @param proxy 定义代理类的类的实例 * @param method 代理类要实现的接口列表 * @param args 指派方法调用的调用处理程序 * @return 返回执行的目标对象 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("====start===="); /** * 打印方法的参数 */ if (args != null && 0 != args.length) { for (Object arg : args) { System.out.println(arg); } } System.out.println("====方法被执行前===="); Object invoke = null; try { String name = method.getName(); if (name.equals("add") || name.equals("delete")) { // 核心方法被执行 invoke = method.invoke(target, args); System.out.println("====方法被执行后===="); } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { System.out.println("====方法出错误了===="); } System.out.println("====end===="); return invoke; } }
3.2 声明代理接口与代理类
- 说明
jdk动态代理只能代理接口,所以第一步先声明接口,然后在定义个实现类 - 示例代码
//代理接口类 public interface UserDao { void add(String name); void delete(String uid); } public interface ShopDao { boolean addShop(); }
//代理接口的实现类 public class UserDaoImpl implements UserDao { @Override public void add(String name) { System.out.println("add:===" + name); } @Override public void delete(String uid) { System.out.println("delete" + uid); } public class ShopDaoImpl implements ShopDao { @Override public boolean addShop() { System.out.println("核心方法====添加商品信息"); return false; } }
- 测试代码
public class TestProxy { public static void main(String[] args) { DynamicProxy proxy = new DynamicProxy(); UserDao userDao = (UserDao) proxy.newProxyInstance(new UserDaoImpl()); userDao.add(); userDao.delete(); ShopDao shopDao = (ShopDao) proxy.newProxyInstance(new ShopDaoImpl()); shopDao.addShop(); } }
3.3、总结
可以看到,我们可以通过DynamicProxy代理不同类型的对象,如果我们把对外的接口都通过动态代理来实现,那么所有的函数调用最终都会经过invoke函数的转发,因此我们就可以在这里做一些自己想做的操作
七、CGLib动态代理
1、说明
JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。
因为是第三方的所有需要导入第三方的jar包