一.关于AOP的理解
面向切面编程是一直流行在学术领域的编程思想,近几年在应用领域流行起来。AOP是对OOP的一个有益的补充,它关注具有横切逻辑的代码。简而言之,横切关注点可以被描述为影响应用多处的功能。例如:事务管理、日志记录、安全等等,应用中的很多方法都会涉及到这些。
在使用面向切面编程时,我们在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。而我把它理解为像“抽屉''一样的插片,哪里需要插到哪里。如此,服务模块只包含核心功能,更加简洁,而次要关注点的代码被转移到切面中了(还可复用)。
因此,AOP是一种思想,而并非Spring独有的功能。Spring只支持方法级别的连接点,因为Spring基于动态代理,通过在代理类中包裹切面,在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。我们以转账业务中的事务控制为例剖析。
二.转账--事务控制
需求:A、B两个账户之间进行转账,基于MVC进行编程。DAO层只负责CRUD,Service层负责业务逻辑处理(避免出现属于DAO层的依赖)。未控制事务时代码如下:
Dao层关键代码
private QueryRunner queryRunner = new QueryRunner(DruidUtil.getDataSource()); //1 查找账户信息 public Account findAccountByAccountName(String accountName) { return queryRunner.query("select * from accounts where accountName = ?", new BeanHandler<Account>(Account.class), accountName); } //2.更新账户信息 public void updateAccount(Account account) { queryRunner.update("update accounts set balance=? where accountName=? ", account.getBalance(),account.getAccountName()); }
private AccountDao accountDao = new AccountDaoImpl(); public void transfer(String sourceAccount, String targetAccount, Float money) { //1.查询到相关账户并进行业务处理 Account sourAccout = accountDao.findAccountByAccountName(sourceAccount); Account targAccount = accountDao.findAccountByAccountName(targetAccount); sourAccout.setBalance(sourAccout.getBalance()-money); targAccount.setBalance(targAccount.getBalance()+money); //2.更新账户信息 accountDao.updateAccount(sourAccout); int i = 1/0; // 若出现异常,则发生事务问题 accountDao.updateAccount(targAccount); }
第一步:编写事务管理器
Tips: 使用TreadLocal进行线程绑定,实现在业务处理中使用同一个连接(即在同一个事务下)
public class TransactionMannager { private static ThreadLocal<Connection> local=new ThreadLocal<Connection>(); //1. 获取连接的方法 public static Connection getConnection() { Connection connection = local.get(); if (connection == null) { connection = DruidUtil.getConnection(); local.set(connection); } return connection; } //2. 给TreadLocal绑定连接,开启事务 public static void startTransaction() { try { Connection connection = getConnection(); connection.setAutoCommit(false); } catch (SQLException e) { throw new RuntimeException("开启事务失败!"); } } //3. 提交 public static void commit() { try { local.get().commit(); } catch (SQLException e) { throw new RuntimeException(e); } } //4. 回滚 ... //5. 关闭连接,移除绑定(避免下次获取到已经关闭的连接) public static void close() { try { local.get().close(); local.remove(); } catch (SQLException e) { throw new RuntimeException(e); } } }
Dao层关键代码
private QueryRunner queryRunner = new QueryRunner(DruidUtil.getDataSource()); //1 查找账户信息 public Account findAccountByAccountName(String accountName) { return queryRunner.query(TransactionMannager.getConnection(), "select * from accounts where accountName = ?", new BeanHandler<Account>(Account.class), accountName); } //2.更新账户信息 public void updateAccount(Account account) { queryRunner.update(TransactionMannager.getConnection(), "update accounts set balance=? where accountName=? ", account.getBalance(),account.getAccountName());
Service层关键代码
public class AccountServiceImpl implements AccountService { private AccountDao accountDao = new AccountDaoImpl(); public void transfer(String sourceAccount, String targetAccount, Float money) { //1.查询到账户进行业务处理 try { TransactionMannager.startTransaction();//开启事务 Account sourAccout = accountDao.findAccountByAccountName(sourceAccount); Account targAccount = accountDao.findAccountByAccountName(targetAccount); sourAccout.setBalance(sourAccout.getBalance()-money); targAccount.setBalance(targAccount.getBalance()+money); //2.更新账户 accountDao.updateAccount(sourAccout); int i = 1/0; accountDao.updateAccount(targAccount); TransactionMannager.commit();//提交事务 } catch (Exception e) { TransactionMannager.rollBack();//回滚事务 }finally { TransactionMannager.close();//关闭连接 } }
第一步:编写静态代理类
public class AccountServiceImplProxy { private AccountService accountService; public AccountServiceImplProxy(AccountService accountService) { this.accountService = accountService; } // 1.方法增强 public void transfer(String sourceAccountName, String targetAccountName,Float money){ try { TransactionMannager.startTransaction();//开启事务 accountService.transfer(sourceAccountName,targetAccountName,money); TransactionMannager.commit();//提交事务 } catch (Exception e) { TransactionMannager.rollBack();//回滚事务 } finally { TransactionMannager.close();//关闭连接 } } }
与使用TransactionManager进行事务管理基本一致,只不过事务控制的代码不写在Service层中,而写在其代理类中,代理类的唯一构造方法需要传入accountService对象。在代理类中对accountService的方法进行增强(添加事务控制)。
3.基于动态代理优化
第一步:编写动态代理
// 使用 Jdk的Proxy 动态代理 public void testTransfer1(){ //创建被代理对象 AccountService accountService = new AccountServiceImpl(); //创建代理 AccountService proxy =(AccountService) Proxy.newProxyInstance(AccountTest.class.getClassLoader(), accountService.getClass().getInterfaces(),new MyInvocationHandler(accountService)); proxy.transfer("a","b",1f); } // Proxy 动态代理类的处理类 class MyInvocationHandler implements InvocationHandler{ private AccountService accountService; //构造方法需要传入被代理对象 public MyInvocationHandler(AccountService accountService){ this.accountService = accountService; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { TransactionMannager.startTransaction(); Object rtValue= method.invoke(accountService,args); TransactionMannager.commit(); System.out.println("proxy动态代理执行了"); return rtValue; }catch (Exception e){ TransactionMannager.rollBack(); throw new RuntimeException(e); }finally { TransactionMannager.close(); } } } //------------------------------------- // 使用Cg-lib 的动态代理 public void testTransfer2(){ AccountServiceImpl accountService = new AccountServiceImpl(); Enhancer proxy = new Enhancer(); proxy.setSuperclass(AccountServiceImpl.class);//指定父类的类型 proxy.setCallback(new MyInvocationHandler1(accountService));//增强策略 AccountServiceImpl pro = (AccountServiceImpl) proxy.create(); pro.transfer("a","b",2f); } // cglib 的动态代理处理类 class MyInvocationHandler1 implements net.sf.cglib.proxy.InvocationHandler{ private AccountServiceImpl accountService;//构造方法需要传入被代理对象 public MyInvocationHandler1(AccountServiceImpl accountService) { this.accountService = accountService; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { TransactionMannager.startTransaction(); Object rtValue = method.invoke(accountService,args); TransactionMannager.commit(); System.out.println("cglib动态代理执行了"); return rtValue; }catch (Exception e){ TransactionMannager.rollBack(); throw new RuntimeException(e); }finally { TransactionMannager.close(); } } }
Proxy代理通过实现和被增强类同样的接口,如果目标没有实现任何的接口,Proxy将无法使用。Cglib是基于子类的动态代理,生成的代理类是被代理类的子类。在spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
-
Proxy 编译时间短,运行效率低。
-
Cglib 编译时间长,运行效率高。
三.Spring AOP及使用配置
1.AOP相关术语
Joinpoint(连接点):
指那些被拦截到的点,在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
Pointcut(切入点):
指我们要对哪些 Joinpoint进行拦截的定义。
Advice(通知/增强):
指拦截到 Joinpoint之后要做的事情就是通知。通知的类型有:前置通知,后置通知,异常通知
最终通知,环绕通知。
Introduction(引介):
引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为类动态地添加
一些方法或Field。
Target(目标对象):
代理的目标对象。
Weaving(织入):
指把增强应用到目标对象来创建新的代理对象的过程。(spring采用动态代理织入,而AspectJ
采用编译期织入和类装载期织入。
Proxy(代理):
一个类被AOP织入增强后,就产生一个结果代理类。
Aspect(切面):
是切入点和通知(引介)的结合。
2.准备必要代码
编写需要加入切面的类
public class UserServiceImpl implements UserService { public void login() { System.out.println("用户的登录方法执行了"); } public void findUser() { System.out.println("用户的查找方法执行了"); } }
//class Logger public void printLogger(){ System.out.println("logger中的printLogger方法执行了"); } public void afterPrintLogger(){ System.out.println("logger中的afgerPrintLogger方法执行了"); }
第一步:导入jar包或Maven坐标
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency>
<!-- 基于xml文件的aop --> <!-- aop 的配置步骤: 第一步:把通知类的创建也交给 spring 来管理 第二步:使用 aop:config 标签开始 aop 的配置 第三步:使用 aop:aspect 标签开始配置切面,写在 aop:config 标签内部 id 属性:给切面提供一个唯一标识 ref 属性:用于引用通知 bean 的 id。 第四步:使用对应的标签在 aop:aspect 标签内部配置通知的类型 使用 aop:befored 标签配置前置通知,写在 aop:aspect 标签内部 method 属性:用于指定通知类中哪个方法是前置通知 pointcut 属性:用于指定切入点表达式。 切入点表达式写法: 关键字:execution(表达式) 表达式内容:全匹配标准写法: 访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表) 例如: public void cn.dintalk.service.impl.AccountServiceImpl.saveAccount() --> <!-- 1.配置bean 及logger通知类 --> <bean id="logger" class="cn.dintalk.Aop.Logger"/> <bean id="userService" class="cn.dintalk.Aop.service.impl.UserServiceImpl"/> <!-- 2.配置Aop --> <aop:config> <!-- 注意顺序,顺序不对会报错 --> <!-- 2.1配置切入点 --> <aop:pointcut id="userServiceM" expression="execution(* cn.dintalk.Aop.service.impl.UserServiceImpl.*(..))"/> <!-- 2.2配置切面(增强类) --> <aop:aspect id="loggerAdvice" ref="logger"> <aop:before method="printLogger" pointcut-ref="userServiceM"/> <aop:after method="afterPrintLogger" pointcut-ref="userServiceM"/> </aop:aspect> </aop:config>
如此,调用userService的方法,在目标方法执行前后会执行相应的通知。
切入点表达式说明:
-
* 可表示任意返回值和任意包(单个包)或任意类及任意方法名
-
.. 可表示当前包及其子包 或参数
通常情况下,我们都是对业务层的代码进行增强,所以切入点表达式都是切到业务层实现类:
execution(* cn.dintalk.service.impl.*.*(..))
-
aop:before
-
aop:after-returning 切入点方法正常执行后执行
-
aop:after-throwing 切入点方法异常后执行
-
aop:after
环绕通知
配置方式
<!-- 配置环绕通知 --> ... <aop:around method="transactionAround" pointcut-ref="pt1"/> </aop:aspect> </aop:config>
当配置完环绕通知之后,没有业务层方法执行(切入点方法执行)。
/** * spring 框架为我们提供了一个接口,该接口可以作为环绕通知的方法参数来使用 * ProceedingJoinPoint。当环绕通知执行时,spring 框架会为我们注入该接口的实现类。 * 它有一个方法 proceed(),就相当于 invoke,明确的业务层方法调用 */ public void aroundPrintLog(ProceedingJoinPoint pjp) { try { System.out.println("前置 Logger 类中的 aroundPrintLog 方法开始记录日志了"); pjp.proceed();//明确的方法调用 System.out.println("后置 Logger 类中的 aroundPrintLog 方法开始记录日志了"); } catch (Throwable e) { System.out.println("异常 Logger 类中的 aroundPrintLog 方法开始记录日志了"); }finally { System.out.println("最终 Logger 类中的 aroundPrintLog 方法开始记录日志了"); } }
第一步:配置applicationContext.xml文件
<!-- 基于注解的AOP:配置注解扫描包 --> <context:component-scan base-package="cn.dintalk.Aop"/> <!-- 1.开启AOP对注解的支持 --> <aop:aspectj-autoproxy/>
// userService实现类 @Service("userService") public class UserServiceImpl implements UserService { //切面类 @Component("logger") @Aspect // 声明为通知类 public class Logger { @Before("execution(* cn.dintalk.Aop..*.*(..))") public void printLogger(){ System.out.println("logger中的printLogger方法执行了"); } // 使用抽取出来的切入点表达式 @AfterReturning("m1()") public void afterPrintLogger(){ System.out.println("logger中的afgerPrintLogger方法执行了"); } @Pointcut("execution(* cn.dintalk.Aop..*.*(..))") public void m1(){ } // 方法无意义,抽取切入点表达式而已
@Configuration @ComponentScan("cn.dintalk.Aop") @EnableAspectJAutoProxy // 开启对注解的支持 public class SpringConfig { } //基于类的注解 获取容器 userService = new AnnotationConfigApplicationContext(SpringConfig.class) .getBean("userService",UserService.class); userService.login();
在程序测试阶段,我们总是需要将加载Spring的配置获取容器,即诸如以下代码:
ApplicationContext applicationContext1 = new ClassPathXmlApplicationContext("applicationContext.xml"); UserServiceImpl userService = applicationContext1.getBean("userService", UserServiceImpl.class);
第一步:导入spring整合Junit的坐标
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> <scope>test</scope> </dependency>
第三步:使用@ContextConfiguration指定spring配置文件的位置
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class UserAopTest {
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class UserAopTest { @Autowired private UserService userService;
使用@RunWith注解替换原有运行器,测试类中的测试方法即使不添加@Test注解也可以运行(有运行按钮)。但是会报如下错误:java.lang.Exception: No runnable methods。此时只要在测试方法上显示的添加@Test注解即可!
关注微信公众号,随时随地学习