目录
一、AOP的相关概念
1.什么是AOP
- AOP:Aspect Oriented Programming,即:面向切面编程。
- 它可以把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们的已有方法进行增强。
2.AOP的作用及优势
- 作用:
在程序运行期间,不修改源码对已有方法进行增强。 - 优势:
1.减少重复代码;
2.提高开发效率;
3.维护方便。
3.AOP的实现方式
使用动态代理技术
4.AOP具体应用
4.1.案例分析
客户的业务层实现类
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public void saveAccount(Account account) throws SQLException {
accountDao.save(account);
}
@Override
public void updateAccount(Account account) throws SQLException{
accountDao.update(account);
}
@Override
public void deleteAccount(Integer accountId) throws SQLException{
accountDao.delete(accountId);
}
@Override
public Account findAccountById(Integer accountId) throws SQLException {
return accountDao.findById(accountId);
}
@Override
public List<Account> findAllAccount() throws SQLException{
return accountDao.findAll();
}
}
- 在上面的业务层实现类的代码中,我们的事务被自动控制了。换言之,就是程序为我们默认使用了
connection
对象的setAutoCommit(true)
。 - 我们使用这种方式控制事务时,每次都执行一条 sql 语句,没有问题,但是如果业务方法一次要执行多条 sql语句,这种方式就无法实现功能了。
4.1.1.在业务层中增加一个方法
- 业务层接口
/**
* 转账
* @param sourceName
* @param targetName
* @param money
*/
void transfer(String sourceName,String targetName,Float money);
- 业务层实现类
@Override
public void transfer(String sourceName, String targetName, Float money) {
//根据名称查询两个账户信息
Account source = accountDao.findByName(sourceName);
Account target = accountDao.findByName(targetName);
//转出账户减钱,转入账户加钱
source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);
//更新转出账户
accountDao.update(source);
int i=1/0; //模拟转账异常
//更新转出账户
accountDao.update(target);
}
当我们执行时,由于执行有异常,转账失败(转出账户减钱了,但是转入账户未增加)。但是因为我们是每次执行持久层方法都是独立事务,导致无法实现事务控制(不符合事务的一致性)
4.1.2.解决其中的问题
- 业务层实现类
/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl implements IAccountService{
private IAccountDao accountDao;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public List<Account> findAllAccount() {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
List<Account> accounts = accountDao.findAllAccount();
//3.提交事务
txManager.commit();
//4.返回结果
return accounts;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}
@Override
public Account findAccountById(Integer accountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
Account account = accountDao.findAccountById(accountId);
//3.提交事务
txManager.commit();
//4.返回结果
return account;
}catch (Exception e){
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
}finally {
//6.释放连接
txManager.release();
}
}
@Override
public void saveAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.saveAccount(account);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}
}
@Override
public void updateAccount(Account account) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.updateAccount(account);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}
}
@Override
public void deleteAccount(Integer acccountId) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
accountDao.deleteAccount(acccountId);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
}finally {
//5.释放连接
txManager.release();
}
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
accountDao.updateAccount(source);
int i=1/0;
//2.6更新转入账户
accountDao.updateAccount(target);
//3.提交事务
txManager.commit();
}catch (Exception e){
//4.回滚操作
txManager.rollback();
e.printStackTrace();
}finally {
//5.释放连接
txManager.release();
}
}
}
TransactionManager
类的代码:
/**
* 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
*/
public class TransactionManager {
//定义一个 DBAssit
private static DBAssit dbAssit = new DBAssit(C3P0Utils.getDataSource(),true);
/**
* 开启事务
*/
public void beginTransaction(){
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
}catch ((SQLException e){
e.printStackTrace();
}
}
/**
* 提交事务
*/
public void commit(){
try {
dbAssit.getCurrentConnection().commit();
}catch ((SQLException e){
e.printStackTrace();
}
}
/**
* 回滚事务
*/
public void rollback(){
try {
dbAssit.getCurrentConnection().rollback();
}catch (SQLException e){
e.printStackTrace();
}
}
/**
* 释放连接
*/
public void release(){
try {
dbAssit.releaseConnection();
}catch (Exception e){
e.printStackTrace();
}
}
}
4.2.动态代理的特点
- 字节码随用随创建,随用随加载。
- 它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。
- 装饰者模式就是静态代理的一种体现。
4.3.动态代理常用的两种方式
4.3.1.方式一:基于接口的动态代理
- 提供者:JDK 官方的 Proxy 类。
- 要求:被代理类最少实现一个接口。
- 创建代理对象:使用Proxy类中的newProxyInstance方法
newProxyInstance
方法的参数:
1.ClassLoader
:类加载器 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。
2.Class[]:字节码数组。它是用于让代理对象和被代理对象有相同方法。
3.InvocationHandler
:用于提供增强的代码它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。此接口的实现类都是谁用谁写。
4.3.1.1.案例
此处我们使用的是一个演员的例子:
在很久以前,演员和剧组都是直接见面联系的。没有中间人环节。
而随着时间的推移,产生了一个新兴职业:经纪人(中间人),这个时候剧组再想找演员就需要通过经纪
人来找了。下面我们就用代码演示出来。
IActor接口
/**
* 一个经纪公司的要求:
* 能做基本的表演和危险的表演
*/
public interface IActor {
/**
* 基本演出
* @param money
*/
public void basicAct(float money);
/**
* 危险演出
* @param money
*/
public void dangerAct(float money);
}
演员类继承Iactor接口
/**
* 一个演员
*/
//实现了接口,就表示具有接口中的方法实现。即:符合经纪公司的要求
public class Actor implements IActor{
public void basicAct(float money){
System.out.println("拿到钱,开始基本的表演:"+money);
}
public void dangerAct(float money){
System.out.println("拿到钱,开始危险的表演:"+money);
}
}
动态代理实现类
public class Client {
public static void main(String[] args) {
//一个剧组找演员:
final Actor actor = new Actor();
IActor proxyActor = (IActor) Proxy.newProxyInstance(
actor.getClass().getClassLoader(),
actor.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)throws Throwable {
String name = method.getName();
Float money = (Float) args[0];
Object rtValue = null;
//每个经纪公司对不同演出收费不一样,此处开始判断
if("basicAct".equals(name)){
//基本演出,没有 2000 不演
if(money > 2000){
//看上去剧组是给了 8000,实际到演员手里只有 4000
//这就是我们没有修改原来 basicAct 方法源码,对方法进行了增强
rtValue = method.invoke(actor, money/2);
}
}
if("dangerAct".equals(name)){
//危险演出,没有 5000 不演
if(money > 5000){
//看上去剧组是给了 50000,实际到演员手里只有 25000
//这就是我们没有修改原来 dangerAct 方法源码,对方法进行了增强
rtValue = method.invoke(actor, money/2);
}
}
return rtValue;
}
});
//没有经纪公司的时候,直接找演员。
// actor.basicAct(1000f);
// actor.dangerAct(5000f);
//剧组无法直接联系演员,而是由经纪公司找的演员
proxyActor.basicAct(8000f);
proxyActor.dangerAct(50000f);
}
}
4.3.2.方式二:基于子类的动态代理
- 要求:被代理对象
不能是最终类
- 用到的类:
Enhancer
- 用到的方法:
create(Class, Callback)
- 方法的参数:
Class
:被代理对象的字节码
Callback
:如何代理 - 需要使用cglib类库
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
4.3.2.1.案例
/**
* 一个演员
*/
public class Actor{//没有实现任何接口
public void basicAct(float money){
System.out.println("拿到钱,开始基本的表演:"+money);
}
}
使用子类的动态代理实现类:
public class Client {
public static void main(String[] args) {
final Actor actor = new Actor();
Actor cglibActor = (Actor) Enhancer.create(actor.getClass(),
new MethodInterceptor() {
/**
* 参数:
* 前三个和基于接口的动态代理是一样的。
* MethodProxy:当前执行方法的代理对象。
* 返回值:
* 当前执行方法的返回值
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
String name = method.getName();
Float money = (Float) args[0];
Object rtValue = null;
if("basicAct".equals(name)){
//基本演出
if(money > 2000){
rtValue = method.invoke(actor, money/2);
}
}
return rtValue;
}
});
cglibActor.basicAct(10000);
}
}
4.3.使用动态代理处理事务
创建一个动态代理工厂,使用动态代理对原有的Service方法进行加强,增加事务。
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;
public void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private TransactionManager txManager;
public final void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public IAccountService getAccountService() {
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
// 添加事务的支持
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1 开启事务
txManager.beginTransaction();
//2 执行操作
rtValue = method.invoke(accountService, args);
//3 提交事务
txManager.commit();
} catch (Exception e) {
//5 回滚操作
txManager.rollback();
} finally {
//6 释放连接
txManager.release();
}
return rtValue;
}
});
}
}
5.Spring中的AOP
5.1.AOP 术语
- Joinpoint(
连接点):
所谓类里面可以被增强的方法,这些方法称为连接点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
- Pointcut
(切入点):
所谓切入点是指我们要对哪些 Joinpoint
进行拦截的定义。
- Advice(
通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
- Introduction
(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。
- Target
(目标对象):
代理的目标对象(要增强的类)。
- Weaving
(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
- Proxy
(代理):
一个类被 AOP 织入增强后,就产生一个结果代理类。
- Aspect
(切面):
是切入点和通知(引介)的结合。
5.2.基于 XML 的 AOP 配置
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新" + i);
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
利用 AOP 实现每次调用 service 的方法之前都调用 printLog 方法
/**
* 用于记录日志的工具类,提供了公共的代码
*/
public class Logger {
//用于向控制台打印日志,在切入点方法执行之前执行
public void printLog(){
System.out.println("Logger类中的printLog方法开始记录日志...");
}
}
spring中基于XML的AOP配置步骤:
- 1.把通知Bean也交给Spring来管理;
- 2.使用aop:config标签表明开始AOP的配置;
- 3.使用aop:aspect标签表明配置切面;
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类bean的Id - 4.在
aop:aspect
标签的内部使用对应标签来配置通知的类型;
aop:before
:表示配置前置通知
method
属性:用于指定Logger
类中哪个方法是前置通知
pointcut
属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置IOC,配置Service对象-->
<bean id="accountService" class="cn.hyz.service.impl.AccountServiceImpl"></bean>
<!--配置Logger类-->
<bean id="logger" class="cn.hyz.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="printLog" pointcut="execution(public void io.github.tjtulong.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
- 切入点表达式
- 四种通知类型的位置
<aop:config>
<!--配置通用切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* cn.hyz.service.impl.*.*(..))"/>
<aop:aspect id="txAdvice" ref="txManager">
<!--配置前置通知,开启事务-->
<aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
<!--配置后置通知,提交事务-->
<aop:after-returning method="comit" pointcut-ref="pt1"></aop:after-returning>
<!--配置异常通知,回滚事务-->
<aop:after-throwing method="rollback" pointcut-ref="pt1"></aop:after-throwing>
<!--配置最终通知,释放连接-->
<aop:after method="release" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
Logger类:
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
环绕通知
<!-- 配置环绕通知 -->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
Logger类:
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置通知");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置通知");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常通知");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终通知");
}
}
- 问题:
当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。 - 分析:
通过对比动态代理中的环绕通知代码,发现动态代理中的环绕通知有明确的切入点方法调用,而我们的代码中没有。 - 解决:
Spring框架为我们提供了一个接口:ProceedingJoinPoint
。该接口有一个方法proceed()
,此方法就相当于明确调用切入点方法。
该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用 - spring中的环绕通知:
它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
5.3.基于注解的 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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="cn.hyz"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
不是用配置文件的方式:
@Configuration
@ComponentScan(basePackages="cn.hyz")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
在切面类上加入注解@Aspect
在方法上加上标注以确定通知类型:
前置通知:@Before()
后置通知:@AfterReturning()
异常通知:@AfterThrowing()
最终通知:@After()
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* io.github.tjtulong.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方法开始记录日志了。。。");
}
}