Spring中的面向切面编程。
事务控制
1 2 3 4 5 6 7 8
| public void transfer(String sourceName, String targetName, Float money) { Account source = accountDao.findAccountByName(sourceName); Account target=accountDao.findAccountByName(targetName); source.setMoney(source.getMoney()-money); target.setMoney(target.getMoney()+money); accountDao.updateAccount(source); accountDao.updateAccount(target); }
|
多次连接如果中间出现一个异常,前面执行后面没有执行,且无法回滚。需要使用ThreadLocal
对象把Connection合当前线程绑定,从而使一个线程中只有一个能控制事务的对象。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class AccountDaoImpl_OLD implements IAccountDao { private QueryRunner runner; public void setRunner(QueryRunner runner) { this.runner = runner; } public List<Account> findAllAccount() { try { return runner.query("select * from account",new BeanListHandler<Account>(Account.class)); } catch (SQLException e) { throw new RuntimeException(e); } } }
|
1 2 3 4 5 6
| public class AccountServiceImpl implements IAccountService { private IAccountDao accountDao; public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; } }
|
事务控制应该在业务层,转移。
整合示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class AccountDaoImpl implements IAccountDao { private QueryRunner runner; private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; }
public void setRunner(QueryRunner runner) { this.runner = runner; }
public List<Account> findAllAccount() { try { return runner.query(connectionUtils.getThreadConnection(),"select * from account",new BeanListHandler<Account>(Account.class)); } catch (SQLException e) { throw new RuntimeException(e); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class ConnectionUtils { private ThreadLocal<Connection> tl =new ThreadLocal<Connection>(); private DataSource dataSource;
public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; }
public Connection getThreadConnection(){ Connection conn=tl.get(); try { if (conn == null) { conn = dataSource.getConnection(); tl.set(conn); } return conn; }catch (Exception e){ throw new RuntimeException(e); } } public void removeConnection(){ tl.remove(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class TransactionManager { private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; }
public void beginTransaction(){ try{ connectionUtils.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } public void commit(){ try{ connectionUtils.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } public void rollback(){ try{ connectionUtils.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } public void release(){ try{ connectionUtils.getThreadConnection().close(); }catch (Exception e){ e.printStackTrace(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class AccountServiceImpl_OLD implements IAccountService { private IAccountDao accountDao; private TransactionManager transactionManager;
public void setTransactionManager(TransactionManager transactionManager) { this.transactionManager = transactionManager; }
public void setAccountDao(IAccountDao accountDao) { this.accountDao = accountDao; }
public List<Account> findAllAccount() { try{ transactionManager.beginTransaction(); List<Account> accounts = accountDao.findAllAccount(); transactionManager.commit(); return accounts; }catch (Exception e){ transactionManager.rollback(); throw new RuntimeException(e); }finally { transactionManager.release(); } } }
|
1 2 3 4 5 6 7 8 9 10 11
| <bean id="accountService" class="com.ztygalaxy.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> <property name="transactionManager" ref="transactionMananger"></property> </bean> <bean id="accountDao" class="com.ztygalaxy.dao.impl.AccountDaoImpl"> <property name="runner" ref="runner"></property> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> </bean>
|
动态代理
字节码随用随创建,随用随加载。在不修改源码的基础上对方法增强。
如何创建代理对象
基于接口的动态代理
使用Proxy
类中的newProxyInstance
方法,参数:
ClassLoader
:类加载器,用于加载代理对象字节码,和被代理对象使用相同类加载器。固定写法。
Class[]
:字节码数组,用于让代理对象和被代理对象有相同方法。固定写法。
InvocationHandler
:用于提供增强的代码。让我们写如何代理。一般都是写一个该接口的实现类,通常情况下是匿名内部类,但不是必须的。
- 被代理类最少实现一个接口,如果没有则不能使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| final Producer producer=null; IProducer proxyProducer= (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object retuernValue=null; Float money = (Float) args[0]; if ("saleProduct".equals(method.getName())){ retuernValue= method.invoke(producer,money*0.8f); } return retuernValue; } }); proxyProducer.saveProduct(1000f);
|
基于子类的动态代理
使用cglib
提供的Enhancer
类中的create
方法,参数:
Class
:字节码,用于指定被代理对象的字节码。
Callback
:用于提供增强的代码。一般写的都是该接口的子接口的实现类MethodIntercepter
。
- 要求被代理类不能是最终类,即不能是被
final
修饰的类。
-
配置pom.xml
坐标
1 2 3 4 5
| <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.1_3</version> </dependency>
|
-
代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| final Producer producer =new Producer(); Producer cglibProducer= (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() { public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object retuernValue=null; Float money = (Float) args[0]; if ("saleProduct".equals(method.getName())){ retuernValue= method.invoke(producer,money*0.8f); } return retuernValue; } }); cglibProducer.saleProduct(1000f);
|
事务动态代理
使用动态代理将事务管理引入到增强代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public class BeanFactory { private IAccountService accountService; private TransactionManager transactionManager;
public final void setTransactionManager(TransactionManager transactionManager) { this.transactionManager = transactionManager; }
public void setAccountService(IAccountService accountService) { this.accountService = accountService; }
public IAccountService getAccountService() { return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object rtValue=null; try{ transactionManager.beginTransaction(); rtValue=method.invoke(accountService, args); transactionManager.commit(); return rtValue ; }catch (Exception e){ transactionManager.rollback(); throw new RuntimeException(e); }finally { transactionManager.release(); } } }); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = "classpath:bean.xml") public class AccountService { @Autowired @Qualifier("proxyAccountService") private IAccountService as; @Test public void transfer(){ as.transfer("bbb","ccc",234f); } }
|
1 2 3 4 5 6 7 8 9
| <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<bean id="beanFactory" class="com.ztygalaxy.factory.BeanFactory"> <property name="accountService" ref="accountService"></property> <property name="transactionManager" ref="transactionMananger"></property> </bean>
|
Spring中的AOP
Aspect Oriented Programming,面向切面编程。在不修改源码的基础上对方法增强。减少重复代码,提高开发效率,维护方便。
相关术语
Jointpoint(连接点)
:指那些被拦截的点。在Spring中,这些点指的是方法,因为Spring只支持方法类型的连接点。即业务层所有方法。
Pointcut(切入点)
:指我们要对哪些Jointpoint
进行拦截定义。被增强的方法。
Advice(通知/增强)
:指拦截到Jointpoint
之后所要做的事情就是通知。类型:前置通知(invoke之前),后置通知(invoke之后),异常通知(catch里),最终通知(finally里),环绕通知(整个invoke方法在执行就是环绕通知)。
Introduction(引介)
:一种特殊的通知,在不修改类的前提下,可以在运行期为类动态地添加一些方法或Field。
Target(对象)
:代理地目标对象。
Weaving(织入)
:把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
Proxy(代理)
:一个类被AOP
织入增强后,就产生一个结果代理类。
Aspcet(切面)
:是切入点和通知(引介)的结合。
分工
基于XML的AOP
-
pom.xml的配置
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <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.7</version> </dependency> </dependencies>
|
-
类设计
目标:在每个方法执行前,执行日志的打印函数。
1 2 3 4 5 6 7 8 9 10 11 12
| public class AccountService implements IAccountService { public void saveAccount() { System.out.println("执行了保存"); } public void updateAccount(int i) { System.out.println("执行了更新"); } public int deleteAccount() { System.out.println("执行了删除"); return 0; } }
|
1 2 3 4 5 6
| public class Logger { public void printLog(){ System.out.println("Logger中的print开始记录日志"); } }
|
-
配置bean.xml
1 2 3 4 5 6 7 8 9
| <?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 https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> </beans>
|
- 配置Spring的IOC,把service对象配置进去。
1
| <bean id="accountService" class="com.ztygalaxy.service.impl.AccountService"></bean>
|
-
Spring中基于XML的AOP配置步骤
-
把通知Bean也交给Spring来管理
1
| <bean id="logger" class="com.ztygalaxy.utils.Logger"></bean>
|
-
使用aop:config
标签表明开始AOP的配置
-
使用aop:aspect
标签表明开始配置切面
id
:给切面提供一个唯一标识
ref
:指定通知类bean的id
-
在aop:aspect
标签的内部使用对应标签来配置通知类型
切入点表达式的写法:
关键字:execution(表达式)
表达式:访问修饰符 返回值 包名.包名.类名.方法名(参数列表)
标准的表达式写法:
1
| execution(public void com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())
|
访问修饰符可以省略:
1
| execution(void com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())
|
返回值可以使用通配符,表示任意返回值:
1
| execution(* com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())
|
包名可以使用通配符,表示任意包,但是有几级包就用几个*.
:
1
| execution(* *.*.*.*.AccountServiceImpl.saveAccount())
|
包名可以用..
表示当前包及其子包:
1
| execution(* *..AccountServiceImpl.saveAccount())
|
类名和方法名都可以使用*
来进行通配:
参数列表可以直接写数据类型,基本类型直接写名称,引用类型写包名.类名的方式。也可以使用通配符但必须有参数。可以使用..
表示有无参数都可,有参数为任意类型。
1 2
| execution(* *..*.*(int))
|
全通配写法:
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
1
| * com.ztygalaxy.service.impl.*.*(..)
|
1 2 3 4 5 6
| <aop:config> <aop:aspect id="logAdvice" ref="logger"> <aop:before method="printLog" pointcut="execution(public void com.ztygalaxy.service.impl.AccountServiceImpl.saveAccount())"></aop:before> </aop:aspect> </aop:config>
|
1 2 3 4
| ApplicationContext ac= new ClassPathXmlApplicationContext("bean.xml"); IAccountService as= (IAccountService) ac.getBean("accountService"); as.saveAccount();
|
四种常用通知类型
1 2 3 4 5 6 7 8
| <aop:config> <aop:aspect id="logAdvice" ref="logger"> <aop:before method="beforePrintLog" pointcut="execution(* *..*.*(..))"></aop:before> <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* *..*.*(..))"></aop:after-returning> <aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* *..*.*(..))"></aop:after-throwing> <aop:after method="afterPrintLog" pointcut="execution(* *..*.*(..))"></aop:after> </aop:aspect> </aop:config>
|
前置通知
:在切入点方法执行之前执行
后置通知
:在切入点方法正常执行之后执行
异常通知
:在切入点方法执行产生异常后执行
最终通知
:无论切入点方法是否正常执行,他都会在其后面执行
后置通知和异常通知只能执行一个,即四个方法中一次最多三个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <aop:aspect id="logAdvice" ref="logger"> <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before> <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning> <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing> <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after> <aop:pointcut id="pt1" expression="execution(* *..*.*(..))"></aop:pointcut> </aop:aspect>
<aop:config> <aop:pointcut id="pt1" expression="execution(* *..*.*(..))"></aop:pointcut> <aop:aspect id="logAdvice" ref="logger"> <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before> <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning> <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing> <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after> </aop:aspect> </aop:config>
|
环绕通知
1
| <aop:around method="aroundPrinting" pointcut-ref="pt1"></aop:around>
|
1 2 3 4
| public void aroundPrinting(ProceedingJoinPoint pjp){ System.out.println("aroundPrinting"); }
|
- 问题:配置好了环绕通知后,切入点方法并没有执行,而环绕通知的方法执行了。
- 分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明显的切入点方法调用,而我们没有。
- 解决:Spring框架为我们提供了一个接口,ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,Spring会为我们提供该接口的实现类供我们使用。
它是Spring框架为我们提供的一种可以在代码中控制增强代码何时执行的通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public Object aroundPrinting(ProceedingJoinPoint pjp){ Object rtValue=null; try{ Object[] args=pjp.getArgs(); System.out.println("aroundPrinting--前置通知"); rtValue=pjp.proceed(args); System.out.println("aroundPrinting--后置通知"); return rtValue; }catch (Throwable t){ System.out.println("aroundPrinting--异常通知"); throw new RuntimeException(t); }finally { System.out.println("aroundPrinting--最终通知"); } }
|
1 2 3 4 5 6 7 8
| as.saveAccount();
|
基于注解的AOP
-
pom.xml
的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?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" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.ztygalaxy"></context:component-scan> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
|
-
业务层配置
1 2
| @Service("accountService") public class AccountServiceImpl implements IAccountService {
|
-
切面层配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| @Component("logger") @Aspect public class Logger { @Pointcut("execution(* com.ztygalaxy.service.impl.*.*(..))") private void pt1(){ } @Before("pt1()") public void beforePrintLog(){ System.out.println("前置通知Logger中的beforeprintLog开始记录日志"); } @AfterReturning("pt1()") public void afterReturningPrintLog(){ System.out.println("后置通知Logger中的afterReturningPrintLog开始记录日志"); } @AfterThrowing("pt1()") public void afterThrowingPrintLog(){ System.out.println("异常通知Logger中的afterThrowingPrintLog开始记录日志"); } @After("pt1()") public void afterPrintLog(){ System.out.println("最终通知Logger中的afterPrintLog开始记录日志"); } @Around("pt1()") public void aroundPrinting(ProceedingJoinPoint pjp){ System.out.println("aroundPrinting"); } }
|
一些问题
使用xml可以让通知执行顺序可靠,配置文件的最终通知和后置通知在事务控制上可能会错乱,因此可以使用环绕通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Around("pt1()") public Object aroundAdvice(ProceedingJoinPoint pjp){ Object rtValue=null; try{ Object[] args = pjp.getArgs(); this.beginTransaction(); rtValue=pjp.proceed(args); this.commit(); return rtValue; }catch (Throwable e){ this.rollback(); throw new RuntimeException(e); }finally { this.release(); } }
|