一、转账业务存在的问题
首先新建项目,选择 webapps 骨架,在 pom.xml 中导入 jar 包依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jia</groupId>
<artifactId>AOPDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>AOPDemo Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<finalName>AOPDemo</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
接下来把账户 Account 相关的代码拷贝过来:
在 resources 包中新建 bean.xml:
<?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: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/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--使用注解,需要告知Spring 在创建容器时需要扫描的包-->
<context:component-scan base-package="com.jia">
</context:component-scan>
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--注入数据源-->
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="user" value="root"></property>
<property name="password" value="jdpy1229jiajia"></property>
</bean>
</beans>
在业务类接口中新增转账方法:
/**
* 转账
* @param sourceName 转出账户
* @param targetName 转入账户
* @param money 转账金额
*/
void transfer(String sourceName,String targetName,float money);
转账需要 6 个步骤:
1.根据名称查询转入账户
2.根据名称查询转出账户
3.转出账户减钱
4.转入账户加钱
5.更新转出账户
6.更新转入账户
因此需要在 IAccountDao 中新建一个 根据名称查询用户的方法:
/**
* 根据名称查询账户
* @param accountName
* @return 如果有唯一结果就返回,如果没有结果就返回 null
* 如果结果集个数超过 1 ,就抛异常
*/
Account findAccountByNames(String accountName);
实现:
@Override
public Account findAccountByNames(String accountName) {
try{
List<Account> accounts=runner.query("select * from account where name = ? ",new BeanListHandler<Account>(Account.class),accountName);
if(accounts==null||accounts.size()==0)
return null;
if(accounts.size()>1)
throw new RuntimeException("结果集不唯一,数据有问题");
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
完善转账方法:
@Override
public void transfer(String sourceName,String targetName,float money)
{
//1.根据名称查询转入账户
Account source=accountDao.findAccountByNames(sourceName);
//2.根据名称查询转出账户
Account target=accountDao.findAccountByNames(targetName);
//3.转出账户减钱
source.setMoney(source.getMoney()-money);
//4.转入账户加钱
target.setMoney(target.getMoney()+money);
//5.更新转出账户
accountDao.updateAccount(source);
//6.更新转入账户
accountDao.updateAccount(target);
}
测试转账方法:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccoutServiceTest {
@Autowired
private IAccountService as;
@Test
public void testTransfer()
{
as.transfer("aaa","bbb",100);
}
}
转账成功:
但是如果在更新转入与转出之间加上 1/0:
//5.更新转出账户
accountDao.updateAccount(source);
int i=1/0;
//6.更新转入账户
accountDao.updateAccount(target);
执行测试会报异常,而数据 不满足一致性原则:
这是因为转账的步骤中1.2.5.6 各获取了一次连接,这样相当于有 4 次 Connection,各自是一次事务,这样在 5 和 6 之间出现了异常,会导致 5 提交了 而 6 未提交。解决方法——使用 ThreadLocal 对象把 Connectrion 和当前线程绑定,从而使一个线程中只有一个能控制事务的对象,这样多次操作就使用一个事务,可以保证一致性、原子性。
在 utils 包下新建一个 ConnectionUtils ,用于获取连接,并实现和线程的绑定:
package com.jia.utils;
//连接的工具类,它用于从数据源中获取一个连接,
//并且实现和线程的绑定
public class ConnectionUtils
{
public ConnectionUtils()
{}
private ThreadLocal<Connection> tl=new ThreadLocal<>();
//由 Spring 容器注入,需要提供 set 方法
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程的连接
* @return
*/
public Connection getThreadConnection() {
//1.先从 ThreadLocal 上获取
Connection connection=tl.get();
//2.判断当前线程上是否有连接
if(connection==null)
{
//3.从数据源中获取连接,并且绑定到线程上,即存在 ThreadLocal 中
try {
connection=dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
tl.set(connection);
}
//返回当前线程上的连接
return connection;
}
接下来写管理事务的工具类:
package com.jia.utils;
//和事务管理相关的工具类
//包含 开启事务、提交事务、回滚事务、释放连接
public class TranscationManager {
//由 Spring 容器注入,需要提供 set 方法
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
//开启事务
public void beginTranscation()
{
try {
//关闭自动提交
connectionUtils.getThreadConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交事务
public void commit()
{
try{
connectionUtils.getThreadConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
//回滚事务
public void rollback()
{
try {
connectionUtils.getThreadConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
//释放连接
public void release()
{
try {
//不是真正关闭连接,只是把连接放回连接池中(类似于线程池)
connectionUtils.getThreadConnection().close();
//和线程解绑
connectionUtils.removeConnection();
/*
需要在 ConnectionUtils 类中提供 removeConnection 方法:
public void removeConnection()
{
tl.remove();
}
*/
} catch (SQLException e) {
e.printStackTrace();
}
}
}
接下来修改 AccountServiceImpl 类中的代码,如:
@Override
public List<Account> findAllAccount() {
try {
//1.开启事务
transcationManager.beginTranscation();
//2.执行操作
List<Account> accounts=accountDao.findAllAccount();
//3.提交事务
transcationManager.commit();
//4.返回结果
return accounts;
} catch (Exception e)
{
//5.回滚操作
transcationManager.rollback();
}
finally {
//6.释放连接
transcationManager.release();
}
return null;
}
转账的方法修改为:
@Override
public void transfer(String sourceName,String targetName,float money)
{
try {
//1.开启事务
transcationManager.beginTranscation();
//2.执行操作
//2.1 根据名称查询转入账户
Account source = accountDao.findAccountByNames(sourceName);
//2.2 根据名称查询转出账户
Account target = accountDao.findAccountByNames(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.提交事务
transcationManager.commit();
}
catch (Exception e)
{
//4.回滚事务
transcationManager.rollback();
e.printStackTrace();
}
finally {
//5.释放连接
transcationManager.release();
}
}
现在不希望从 QueryRunner 中获取连接了,所以需要把 “注入数据源” 去掉:
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<!--需要去掉:注入数据源>
<property name="dataSource" ref="dataSource"> </property-->
</bean>
然后把
//由 Spring 容器注入,需要提供 set 方法
private ConnectionUtils connectionUtils;
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
每个方法都需要传入 线程绑定的连接作为参数,如:
runner.query(connectionUtils.getThreadConnection(),
"select * from accounts",new BeanListHandler<Account>(Account.class));
在 bean.xml 中注入相应内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--配置Service-->
<bean id="accountService" class="com.jia.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao">
</property>
<!--注入事务管理器-->
<property name="transcationManager" ref="transcationManager">
</property>
<!--配置Dao对象-->
<bean id="accountDao" class="com.jia.dao.impl.AccountDaoImpl">
<!--注入QueryRunner-->
<property name="runner" ref="runner"></property>
<!--注入 ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置 Connection 工具类-->
<bean id="connectionUtils" class="com.jia.utils.ConnectionUtils">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"> </property>
</bean>
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="user" value="root"></property>
<property name="password" value="jdpy1229jiajia"></property>
</bean>
<!--配置事务管理器-->
<bean id="transcationManager" class="com.jia.utils.TranscationManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>
进行测试:
@Autowired
private IAccountService as;
@Test
public void testTransfer()
{ as.transfer("aaa","bbb",200); }
测试通过,而且 5 和 6 之间有运算异常,而执行结果是符合预期的,进行了事务控制:
二、动态代理
这样写的话,方法之间的依赖性很强,而且代码重复度很高,这是我们不希望看到的,应当改变,复习一下动态代理:
-
动态代理的特点:
字节码随用随创建,随用随加载 -
动态代理的作用:
不修改源码的基础上,增强方法
新建工程 DynamicProxy ,不选择骨架。
提供生产者接口:
package com.jia.proxy;
public interface IProducer {
//出售产品
public void saleProduct(float money);
//提供售后服务
public void afterService(float money);
}
实现接口:
package com.jia.proxy;
//生产者
public class Producer implements IProducer {
//出售产品
public void saleProduct(float money)
{
System.out.println("销售产品,并拿到钱:"+money);
}
//提供售后服务
public void afterService(float money)
{
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
消费者:
- 动态代理的分类:
1.基于接口的
涉及的类:
Proxy
提供者:
JDK 官方
创建代理对象:
使用 Proxy 类中的 newProxyInstance 方法,
要求:被代理类至少实现一个接口,否则不能使用
newProxyInstance 的参数:
①ClassLoader 类加载器
用于加载代理对象字节码,和被代理对象使用相同的类加载器
②Class[ ] 字节码数组
用于让代理对象和被代理对象有相同方法
③InvocationHandler 用于提供增强的代码
让写如何代理的,一般写该接口的实现类,通常情况下都是匿名内部类
package com.jia.proxy;
//模拟一个消费者
public class Client {
public static void main(String[] args) {
final Producer producer=new Producer();
//消费者付10000万,就可以购得电脑
producer.saleProduct(10000);
IProducer proxyProducer=(IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
//执行被代理对象的任何接口方法都会经过该方法,即该方法有拦截的功能
/**
*
* @param proxy 代理对象的引用,一般不用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//提供增强的代码
Object returnValue=null;
//1.获取方法执行的参数
Float money=(Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName()))
//经销商拿到两成利润,对于生产者来说,只能拿到八成的钱
returnValue=method.invoke(producer,money*(float)0.8);
return returnValue;
}
});
proxyProducer.saleProduct(10000);
}
}
运行结果:
那如果类没有实现接口的话,就应当考虑基于子类的动态代理,需要第三方库支持,导入坐标:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
2.基于子类的
涉及的类:
Enhoncer
提供者:
第三方 cglib 库
创建代理对象:
使用 Enhoncer 类的 create 方法
要求:被代理类不能是 final 类
Enhoncer 的 create 方法,有 2 个参数:
①class 类型:
指定被代理对象的指定字节码
②callback 类型:
用于实现方法的增强,一般是该接口的子接口实现类:
MethodInterceptor
package com.jia.cglib;
//模拟一个消费者
public class Client {
public static void main(String[] args) {
final Producer producer=new Producer();
Producer cglibproducer=(Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param o 代理对象的引用
* @param method 当前执行的方法
* @param objects 当前执行方法所需的参数
* @param methodProxy 当前执行方法的代理对象
* @return 和被代理对象有相同的返回值
* @throws Throwable
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object returnValue=null;
//1.获取方法执行的参数
Float money=(Float)objects[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName()))
//对于生产者来说,只能拿到八成的钱
returnValue=method.invoke(producer,money*(float)0.8);
return returnValue;
}
});
cglibproducer.saleProduct(12000);
}
}
运行结果:
之前说 Connection 的 close 方法不能真正关闭连接,只是把连接还回池中,那就可以使用动态代理的方式,对 该方法进行增强,把它添加回池里。
回到转账业务中,需要先提供一个 BeanFactory——即 用于创建 Service 的代理对象的工厂:
package com.jia.factory;
public class BeanFactory {
//由 Spring 容器注入,需要提供 set 方法
private TranscationManager transcationManager;
public final void setTranscationManager(TranscationManager transcationManager) {
this.transcationManager = transcationManager;
}
//由 Spring 容器注入,需要提供 set 方法
private IAccountService accountService;
public void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
/**
* 获取 Service 代理对象
* @return
*/
public IAccountService getAccountService()
{
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 添加事务支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
Object rtValue=null;
try {
//1.开启事务
transcationManager.beginTranscation();
//2.执行操作
method.invoke(accountService,args);
//3.提交事务
transcationManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e)
{
//5.回滚操作
transcationManager.rollback();
} finally {
//6.释放连接
transcationManager.release();
}
return null;}
});
}
}
那么 AccountServiceImpl 类中也就不再需要 TranscationManager 类型的引用 ,将它及 set 方法删除即可。因为已经都由代理对象控制了,这样事务控制和业务层的方法就进行了真正的分离。
接下来修改 bean.xml 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--使用注解,需要告知Spring 在创建容器时需要扫描的包-->
<!--context:component-scan base-package="com.jia">
</context:component-scan-->
<!--配置代理的Service,使用实例工厂的方式进行配置-->
<bean id="proxyAccountService" factory-bean="beanFactory"
factory-method="getAccountService">
</bean>
<!--配置 BeanFactory-->
<bean id="beanFactory" class="com.jia.factory.BeanFactory">
<!--注入Service-->
<property name="accountService" ref="accountService">
</property>
<!--注入事务管理器-->
<property name="transcationManager" ref="transcationManager">
</property>
</bean>
<!--配置Service-->
<bean id="accountService" class="com.jia.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao">
</property>
</bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.jia.dao.impl.AccountDaoImpl">
<!--注入QueryRunner-->
<property name="runner" ref="runner"></property>
<!--注入 ConnectionUtils-->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置 Connection 工具类-->
<bean id="connectionUtils" class="com.jia.utils.ConnectionUtils">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"> </property>
</bean>
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
</bean>
<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="user" value="root"></property>
<property name="password" value="jdpy1229jiajia"></property>
</bean>
<!--配置事务管理器-->
<bean id="transcationManager" class="com.jia.utils.TranscationManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>
编写测试方法,因为这时 bean.xml 中配置了两个类型相同的 IAccountService 的实现类,所以需要使用 @Qualifier 注解指定名称 :
@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;
@Test
public void testTransfer()
{
as.transfer("aaa","bbb",400);
}
测试通过。
三、AOP
AOP 的作用和优势:
- 作用:
在程序的运行期间,不修改源码对已有方法进行增强。 - 优势:
减少重复代码
提高开发效率
维护方便
Spring 的 AOP 是通过配置的方式实现功能。
AOP 相关概念
-
连接点:
那些被拦截到的点,在 Spring 中,这些点指的是方法,因为 Spring 只支持方法类型的连接点。比如转帐项目中,业务层的所有方法都是连接点:
-
切入点:
那些被增强的方法,所有的切入点都是连接点,但是连接点不一定是切入点。 -
通知(/增强):
拦截到连接点后所要做的事情。
通知的类型:
前置通知,后置通知,异常通知,最终通知,环绕通知。
比如 该项目中,动态代理的 invoke 方法中会拦截被代理对象的所有方法,拦截到方法后 需要 开启事务、执行操作、提交事务 等事务支持。
在 invoke 方法中,在 method.invoke 之前执行的就是前置通知,在 method.invoke 之后执行的就是 后置通知, catch( )中的是是 异常通知,finally 中的是 最终通知。
环绕通知:是指整个invoke 方法在执行。环绕通知中会有明确的切入点方法调用。
-
引介:
引介是特殊的通知,在不修改类的代码的前提下,可以在运行期为类动态地添加一些方法或属性。 -
target 目标对象:
被代理的对象 。 -
织入:
把 增强 应用到 目标对象 来创建 新的代理对象的过程。
Spring 采用动态代理织入,而 AspectJ 采用编译期织入 和 类装载期织入。
切面:
切入点和通知的结合。
学习 Spring 中的 AOP 要明确的方法:
1. 开发阶段
编写核心业务代码(开发主线)
把公用代码抽取出来,制作成通知,(开发阶段最后再做)
在配置文件中,声明切入点 和 通知之间的关系,即切面。
2. 运行阶段(Spring 框架完成的)
Spring 框架监控切入点方的执行。一旦监控到切入点方法被执行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
新建一个模板 SpringAOP:
先在 pom.xml 中导入 jar 包依赖:
<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>
提供一个业务层接口,分别写 无返回值无参、无返回值有参、有返回值无参 的 三个方法:
package Service;
/*
* 账户的业务层接口
*/
public interface IAccountService1 {
//模拟保存账户
void saveAccount();
//模拟更新账户
void updateAccount(int i);
//删除账户
int deleteAccount();
}
简单实现这个接口:
package Service;
//账户的业务层实现类
public class AccountServiceImpl1 implements IAccountService1 {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新");
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
在 utils 包中新建 Logger 类,用于记录日志的工具类,提供公共的代码:
package Utils;
/*
用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
//用于打印日志,计划让其在切入点方法执行之前执行
//(切入点 即 业务方法)
public void printLog()
{
System.out.println("Logger 类中的 prinLOg 方法开始记录日志");
}
}
写 bean.xml:
<?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">
<!--spring 中基于XML的AOP配置步骤-->
<!--1、把通知Bean 也交给 Spring来管理-->
<!-- 配置 Srping 的 IOC ,把 service 对象配置进来-->
<bean id="accountService" class="Service.impl.AccountServiceImpl1"></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="Utils.Logger"></bean>
<!-- 2、使用aop:config标签表明开始AOP的配置-->
<!--配置AOP-->
<aop:config>
<!--3、使用aop:aspect标签表明配置切面
id属性:是给切面提供一个唯一标识,这里是日志的通知
ref属性:是指定通知类bean的Id。-->
<aop:aspect id="logAdvice" ref="logger">
<!--- 4、在aop:aspect标签的内部使用对应标签来配置通知的类型
我们现在示例是让 printLog 方法在切入点方法执行之前之前,所以是前置通知
前置通知 使用 aop:before:表示配置前置通知
method属性:用于指定 Logger 类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强-->
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution( public void Service.impl.AccountServiceImpl1.saveAccount( )))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
- 切入点表达式的写法:
关键字:execution (表达式)
表达式是由以下部分组成:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
如:标准的表达式写法:
public void Service.impl.AccountServiceImpl.saveAccount( )
这时编写测试类,看效果:
package com.jia.test;
public class aopTest {
//1.获取容器
ApplicationContext ac=new ClassPathXmlApplicationContext("bean1.xml");
//2.获取对象
IAccountService1 as=(IAccountService1) ac.getBean("accountService");
@Test
public void test()
{
//3.执行方法,记录日志
as.saveAccount();
}
}
运行结果:
如果在测试方法中新增:
as.deleteAccount();
as.updateAccount(1);
因为在切点表达式子中只有
pointcut="execution( public void Service.impl.AccountServiceImpl1.saveAccount( )))"
,
所以只有 saveAccount 方法会被增强,运行结果如下:
那如果想对所有方法进行增强,就要考虑 切入点表达式除了标准写法之外,其他写法:
- 全通配写法
- *..*.*(..)
这样的话所有方法之前都进行了增强。
- 访问修饰符可以省略
如
void Service.impl.AccountServiceImpl1.saveAccount( )
- 返回值可以使用通配符,表示任意返回值
如:
- Service.impl.AccountServiceImpl1.saveAccount( )
- 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个
*
*.*.*.AccountServiceImpl1.saveAccount())
- 包名可以使用,
..
表示当前包及其子包
- *..AccountServiceImpl1.saveAccount()
- 类名和方法名都可以使用
*
来实现通配
* *..*.*( )
<!--类名和方法名都可以使用通配符,这时无参数的方法会被增强-->
<aop:before method="printLog" pointcut="execution(* *..*.*( ))"></aop:before>
- 参数列表:
(1)可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
(2)可以使用通配符表示任意类型,但是必须有参数
(3)可以使用…表示有无参数均可,有参数可以是任意类型
✨ 实际开发中切入点表达式的通常写法:✨
切到业务层实现类下的所有方法
* Service.impl.*.*(..)
四种常用的通知类型:
新建一个工程 AOPAdviceType,不使用 骨架,并把之前的 Service 包 和 Utils 包下的内容拷贝过来,先在 pom.xml 中导入依赖:
<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>
编写 Logger 类:
package Utils;
/*
用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
//前置通知
public void BeforePrintLog()
{
System.out.println("Logger 类中的 BeforePrintLog()");
}
//后置通知
public void AfterReturningPintLog()
{
System.out.println("Logger 类中的 AfterPintLog()");
}
//异常通知
public void AfterThrowingPrintLog()
{
System.out.println("Logger 类中的 AfterThrowingPrintLog()");
}
//最终通知
public void finalPrintLog()
{
System.out.println("Logger 类中的finalPrintLog()");
}
}
编写测试方法:
package com.jia.test;
public class AOPTest {
//1.获取容器
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService1 as=(IAccountService1) ac.getBean("accountService");
@Test
public void test()
{
//3.执行方法,记录日志
as.saveAccount();
}
}
在 bean.xml 进行配置:
<?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">
<!-- 配置 Srping 的 IOC ,把 service 对象配置进来-->
<bean id="accountService" class="Service.impl.AccountServiceImpl1"></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="Utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知-->
<aop:before method="BeforePrintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:before>
<!--配置后置通知-->
<aop:after-returning method="AfterReturningPintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:after-returning>
<!--配置异常通知-->
<aop:after-throwing method="AfterThrowingPrintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:after-throwing>
<!--配置最终通知-->
<aop:after method="finalPrintLog" pointcut="execution(* Service.impl.*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
</beans>
运行结果:
在 saveAccount() 方法的实现中新增 int i=1/0;
,看看异常通知的执行情况:
后置通知和异常通知只能执行一个。
环绕通知:
在 bean.xml 中配置切入点表达式,这样之前的 pointcut="execution(* Service.impl.*.*(..))"
就都可以替换成 pointcut-ref="pt1"
:
<!--配置切入点表达式
id 属性用于指定表达式的唯一标准
expression 属性用于指定表达式内容
此标签写在 aop:aspect 标签之中,只能在当前切面使用
它还可以写在 aop:aspect 标签之外,就是所有标签可用
-->
<aop:pointcut id="pt1"
expression="execution(* Service.impl.*.*(..))"></aop:pointcut>
如果把它写在标签之外,需要注意顺序:
<!--配置AOP-->
<aop:config>
<!--顺序要求在此,不能再 aop:config 外部-->
<aop:pointcut id="pt1" expression="execution(* Service.impl.*.*(..))"></aop:pointcut>
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知-->
<aop:before method="BeforePrintLog" pointcut-ref="pt1"></aop:before>
... ...
在 bean.xml 中配置 环绕通知:
<!--配置环绕通知-->
<aop:around method="arroundPrintLog" pointcut-ref="pt1">
在 Logger 类中新增:
//环绕通知
public void arroundPrintLog()
{
System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
}
执行后只看到输出:
通过与 动态代理中的环绕通知代码 对比,发现:动态代理的环绕通知有明确的切入点。解决方法:Spring 框架提供了一个接口:ProceedingJoinPoint ,该接口有 proceed( )方法,此方法就相当于明确调用切入点方法,可以作为环绕通知的方法参数在程序执行时,Spring 框架会为我们提供该接口的实现类供我们使用。✨环绕通知是 Spring 框架为我们提供的 可以在代码中手动控制增强方法何时执行的一种方式✨:
//环绕通知
public Object arroundPrintLog(ProceedingJoinPoint pjp)
{
Object rtValue=null;
//明确调用业务层方法,即 切入点方法
try {
//得到方法执行所需的参数
Object[] args=pjp.getArgs();
/*
*写在 proceed 方法前就表示它是前置通知
System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
*/
rtValue=pjp.proceed(args);
/*
*写在 proceed 方法后就表示它是后置通知
System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
*/
return rtValue;
} catch (Throwable throwable) {
/*
*写在 catch 中就表示它是异常通知
System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
*/
throwable.printStackTrace();
}
finally {
/*
*写在 finally 中就表示它是最终通知
System.out.println("环绕通知:Logger 类中的 arroundPrintLog() ");
*/
}
return rtValue;
}
Spring 基于注解的 AOP 配置:
新建 项目,在 pom.xml 中导入 spring-context 和 aspectjweaver 依赖,并把之前 src 包中的内容都拷贝过来。
需要在 bean.xml 头中添加如下,并配置 Spring 创建容器时需要扫描的包:
xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--配置 Spring 创建容器时要扫描的包-->
<context:component-scan base-package="com.jia">
</context:component-scan>
<!--配置 Spring 开启注解 AOP 的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
不需要在 bean.xml 中配置,而是直接使用注解的方式:
//账户的业务层实现类
@Service("accountService")
public class AccountServiceImpl1 implements IAccountService1
package com.jia.Utils;
/*
用于记录日志的工具类,它里面提供了公共的代码
*/
@Component("logger")
//表示当前类是个切面类
@Aspect
public class Logger {
@Pointcut("execution(* com.jia.Service.impl.*.*(..))")
private void pt1()
{}
//前置通知
@Before("pt1()")
public void BeforePrintLog()
{
System.out.println("前置通知:Logger 类中的 BeforePrintLog()");
}
@AfterReturning("pt1()")
//后置通知
public void AfterReturningPintLog()
{
System.out.println("后置通知:Logger 类中的 AfterPintLog()");
}
@AfterThrowing("pt1()")
//异常通知
public void AfterThrowingPrintLog()
{
System.out.println("异常通知:Logger 类中的AfterThrowingPrintLog()");
}
@After("pt1()")
//最终通知
public void finalPrintLog()
{
System.out.println("最终通知:Logger 类中的 finalPrintLog()");
}
}
运行结果:
如果放开环绕通知的注释:
@Around("pt1()")
//环绕通知
public Object arroundPrintLog(ProceedingJoinPoint pjp)
执行结果: