文章目录
一、事务问题分析(引入AOP原理)
下面先准备一个基本xml配置的SpringIOC工程。
1.1、创建数据表和数据
create table account(
id int primary key auto_increment,
name varchar(40),
money float
)character set utf8 collate utf8_general_ci;
insert into account(name,money) values('aaa',1000);
insert into account(name,money) values('bbb',1000);
1.2、项目准备工作
1.新建Maven项目:
2.环境搭建pom.xml:
<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.7</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>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
3.创建domain.Account实体类:
package com.it.domain;
import java.io.Serializable;
/**
* 账户的实体类
*/
public class Account implements Serializable {
private Integer id;
private String name;
private Float money;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Float getMoney() {
return money;
}
public void setMoney(Float money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
4.创建service.IAccountService.java:
package com.it.service;
import com.it.domain.Account;
import java.util.List;
/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
* 查询所有
* @return
*/
List<Account> findAllAccount();
/**
* 查询一个
* @return
*/
Account findAccountById(Integer accountId);
/**
* 保存
* @param account
*/
void saveAccount(Account account);
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 删除
* @param acccountId
*/
void deleteAccount(Integer acccountId);
/**
* 转账
* @param sourceName 转出账户名称
* @param targetName 转入账户名称
* @param money 转账金额
*/
void transfer(String sourceName,String targetName,Float money);
//void test();//它只是连接点,但不是切入点,因为没有被增强
}
5.创建dao.IAccountDao.java:
package com.it.dao;
import com.it.domain.Account;
import java.util.List;
/**
* 账户的持久层接口
*/
public interface IAccountDao {
/**
* 查询所有
* @return
*/
List<Account> findAllAccount();
/**
* 查询一个
* @return
*/
Account findAccountById(Integer accountId);
/**
* 保存
* @param account
*/
void saveAccount(Account account);
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 删除
* @param acccountId
*/
void deleteAccount(Integer acccountId);
/**
* 根据名称查询账户
* @param accountName
* @return 如果有唯一的一个结果就返回,如果没有结果就返回null
* 如果结果集超过一个就抛异常
*/
Account findAccountByName(String accountName);
}
6.创建service.AccountServiceImpl.java:
package com.it.service.impl;
import com.it.dao.IAccountDao;
import com.it.domain.Account;
import com.it.service.IAccountService;
import java.util.List;
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService{
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
@Override
public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}
@Override
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}
@Override
public void saveAccount(Account account) {
accountDao.saveAccount(account);
}
@Override
public void updateAccount(Account account) {
accountDao.updateAccount(account);
}
@Override
public void deleteAccount(Integer acccountId) {
accountDao.deleteAccount(acccountId);
}
@Override
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//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);
}
}
7.创建service.AccountDaoImpl.java:
package com.it.dao.impl;
import com.it.dao.IAccountDao;
import com.it.domain.Account;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {
private QueryRunner runner;
public void setRunner(QueryRunner runner) {
this.runner = runner;
}
@Override
public List<Account> findAllAccount() {
try{
return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findAccountById(Integer accountId) {
try{
return runner.query("select * from account where id = ? ",new BeanHandler<Account>(Account.class),accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void saveAccount(Account account) {
try{
runner.update("insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void updateAccount(Account account) {
try{
runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void deleteAccount(Integer accountId) {
try{
runner.update("delete from account where id=?",accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public Account findAccountByName(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);
}
}
}
8.创建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">
<!-- 配置Service -->
<bean id="accountService" class="com.it.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.it.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"></property>
</bean>
<!--配置QueryRunner-->
<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"> <!--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/test"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
</beans>
9.创建AccountServiceTest测试类:
package com.it.test;
import com.it.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 使用Junit单元测试:测试我们的配置
*/
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
@Autowired
//@Qualifier("proxyAccountService")
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
9、测试testTransfer()
方法能正常运行。
1.3、事务问题分析
问题:上述代码虽然能够成功执行,但是存在事务问题。比如在执转账业务的时候,A向B转账。如果A成功扣款之后,将要向B账户转账的时候,这时突然断电了,或者服务器崩溃了,那么此时A的账户已扣款,B的账户却没增加款?
下面我们详细分析下这个问题:
那么如何解决上述问题呢?
解:需要使用ThreadLocal对象把Connection和当前线程绑定,从而使一个线程中只有一个能控制事务对象
1、新建一个包个类:
代码:
package com.it.utils;
import javax.sql.DataSource;
import java.sql.Connection;
/**
* 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
//因为dataSource需要使用注入的方式获取对象,所以需要set方法
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* 获取当前线程上的连接
* @return
*/
public Connection getThreadConnection() {
try{
//1.先从ThreadLocal上获取
Connection conn = tl.get();
//2.判断当前线程上是否有连接
if (conn == null) {
//3.从数据源中获取一个连接,并且存入ThreadLocal中
conn = dataSource.getConnection();
tl.set(conn);
}
//4.返回当前线程上的连接
return conn;
}catch (Exception e){
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
*/
public void removeConnection(){
tl.remove();
}
}
2、再建一个事务管理类:
package com.it.utils;
/**
* 和事务管理相关的工具类,它包含了,开启事务,提交事务,回滚事务和释放连接
*/
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();//还回连接池中
connectionUtils.removeConnection();//实现线程与连接解绑
}catch (Exception e){
e.printStackTrace();
}
}
}
3、使用工具类去完善逻辑转账。(其实里面所有的逻辑都可以需要使用这个逻辑事务控制,这里我们就以转账放方法为例子):
4、之前我们在使用QueryRunner的时候,是使用在QueryRunner下注入连接的方式来注入值的。如图:
那么现在我们不想使用构造函数的方法想要修改成set注入,所以应该修改accountDaoImpl:
然后给每个方法前面都增加一个参数,例如:
5、同时,修改bean.xml文件。注入用到的bean对象:
<?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">
<!-- 配置Service -->
<bean id="accountService" class="com.it.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
<!-- 注入事务管理器 -->
<property name="txManager" ref="txManager"></property>
</bean>
<!--配置Dao对象-->
<bean id="accountDao" class="com.it.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="runner"></property>
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置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/test"></property>
<property name="user" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置Connection的工具类 ConnectionUtils -->
<bean id="connectionUtils" class="com.it.utils.ConnectionUtils">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务管理器-->
<bean id="txManager" class="com.it.utils.TransactionManager">
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
</beans>
6、测试运行,此时发现,在出现异常的时候,账户上的金额不会再发生改变。
1.4、动态代理
由于该章节内容比较多,且比较重要,所以我重新发表了文章类写:
动态代理(SpringAOP核心原理)
下面我简单的写一个使用匿名内部类实现的动态代理的例子:
1、抽象对象:
/**
* 对生产厂家要求的接口
*/
public interface IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}
2、真实对象:
/**
* 一个生产者
*/
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
3、代理对象:
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
//创建一个真实对象(内代理对象)
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。固定写法。
* InvocationHandler:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.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 returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
1.5、动态代理实现事务控制
动态代理实现业务逻辑与事务控制分离:
1、新增一个包和类:
package com.it.factory;
import com.it.service.IAccountService;
import com.it.utils.TransactionManager;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public final 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 {
if("test".equals(method.getName())){
return method.invoke(accountService,args);
}
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e) {
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
txManager.release();
}
}
});
}
}
2、修改部分文件代码:
去除真实类中的事务管理的set方法
修改transfer()方法为:
修改bean.xml文件:
3、分别对转账中途异常情况和正常情况进行测试:
总结:
(1)使用动态代理使重复的代码消失了
(2)本质解决了方法与方法之间的依赖,将事务控制代码和业务代码分离开
(3)同时提高了开发效率
二、SpringAOP
2.1、AOP概念
AOP(Aspect Oriented Programming)面向切面编程。
通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性。
与OOP对比,AOP是处理一些横切行问题。这些横切性问题不会影响到主逻辑的实现,但是会散落到代码的各个部分,难以维护。AOP就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。
简单说就是 AOP 可以在不修改现有代码的情况下对现有代码增加一些功能,那么这就是 AOP 最强大的功能。
作用:在程序运行期间,不修改源码,对已有方法进行增强。
优点:
减少重复代码
提高开发效率
方便开发与维护
程序解耦
目前最受欢迎的 AOP 库有两个,一个是 AspectJ, 另外一个就是我们今天讲的 Spring AOP。
传统OOP是自上而下的逻辑开发;
AOP是一种面向切面的编程思想。这些横切性问题,把它们抽象为一个切面,关注点在切面的编程,这就是所谓的AOP。
AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的面,也就是所谓的“切面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
AOP常用于:日志记录,权限验证,效率检查,事务管理…
2.1.1、AOP概念
AOP底层的核心原理就是使用了动态代理模式,其中有两种情况,(1)有接口的情况,使用JDK动态代理。
(2)没有接口的情况,使用CGLIB动态代理。
2.2、SpringAOP中概念
我们将学习如何使用 Spring AOP 在我们的代码中
AOP的基本概念:
Aspect(切面):
通常是一个类,里面可以定义切入点和通知(增强方法),且可以定义切入点和通知方法先后执行顺序,执行逻辑等,以此构成了一个切面。把通知应用到切入点的过程就是一个切面
Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
JointPoint(连接点):
程序执行过程中被拦截到的点,一般是方法的调用。例如service接口中的方法就是连接点,连接业务逻辑代码与增强代码。
Advice(通知/增强):
AOP在特定的切入点上执行的增强处理,在增加处理过程中会调用到增强方法,这个过程就叫增强处理或通知。
其中增强方法的调用分为以下几种:
通知方法:
前置通知:在我们执行目标方法之前运行(@Before
)
后置通知:在我们目标方法运行结束之后 ,不管有没有异常(@After
)
返回通知:在我们的目标方法正常返回值后运行(@AfterReturning
)
异常通知:在我们的目标方法出现异常后运行(@AfterThrowing
)
环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知(@Around
)
Pointcut(切入点)
:就是带有通知的连接点,在程序中主要体现为书写切入点表达式。简单的说就是:一个方法作为连接点被拦截了,但是因为没有对该方法进行增强,那么该方法只是连接点,不是切入点。
所有的切入点都是连接点,所有的连接点并不一定是切入点
引介(Introduction)
: 引介是一种特殊的通知(增强),它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
目标对象(Target)
:即被代理的对象。增强逻辑的织入目标类。如果没有AOP,目标业务类需要自己实现所有逻辑,而在AOP的帮助下,目标业务类只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑则可以使用AOP动态织入到特定的连接点上。
目标对象可以是接口,实现类,或子类
织入(Weaving)
: 织入是将增强添加对目标类具体连接点上的过程。
比如:根据不同的目标对象创建代理对象,然后对代理对象中的方法增强
最后再返回所需要的代理对象,成创建到返回代理对象的过程就叫织入。
AOP像一台织布机,将目标类、增强或引介通过AOP这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP有三种织入的方式:
a、编译期织入,这要求使用特殊的Java编译器。
b、类装载期织入,这要求使用特殊的类装载器。
c、动态代理织入,在运行期为目标类添加增强生成子类的方式。
Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。
代理(Proxy)
:一个类被AOP织入增强后,就产生一个结果代理类。该类也称为代理类或代理对象。
AOP代理
:AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
所以,以上例子和专业术语组成的原理图如图所示:
结论:
1、每个切入点肯定有一个连接点,每个连接点不一定有切入点(连接点的配置主要看bean.xml)。
2、一个切面是由一个或多个切点组成的。
3、一个切点是由连接点和通知组成的。
4、通知就是将连接点和通知织入切点的过程。
2.3、SpringAOP实战开发
知识:
1、什么是AspectJ?
Spring框架一般都是基于AspectJ实现AOP操作其中AspectJ是单独的一个框架,不是Spring的组成成分,独立AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作。
2、基于AspectJ实现AOP操作
(1)基于xml配置文件实现
(2)基于注解方式实现(常用)
3、在项目工程里面引入AOP相关依赖
如果加上之前所讲使用的jar,共这几个:
在开发之前,我们得明确什么事情我们来做,什么事情Spring来做:
开发阶段(我们来做)
(1)编写核心业务代码。
(2)把公共的代码抽取出来,制成通知。
(3)配置切面:在配置文件中,声明切入点与通知之间的关系
运行阶段(Spring框架完成)
Spring框架监控切入点方法的执行,一旦切入点方法被执行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码执行。
2.3.1、基于XML方式的AOP(AspectJ)
准备数据:
1、新建一个Maven项目:
导入AOP相关依赖 jar包:
<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>
2、创建接口:
IAccountService:
package com.it.service;
/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
*
*/
void saveAccount();
/**
* 模拟更新账户
* @param i
*/
void updateAccount(int i);
/**
* 删除账户
* @return
*/
int deleteAccount();
}
AccountServiceImple:
package com.it.service.impl;
import com.it.service.IAccountService;
/**
* 账户的业务层实现类
*/
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;
}
}
Logger:
package com.it.utils;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {
/**
* 用于打印日志:计划让其在切入点方法执行之前执行(
* 切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的pringLog方法开始记录日志了。。。");
}
}
2.3.1.1、基于XML配置AOP常用标签
3、新建一个bean.xml文件,开始配置AOP:
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">
</bean>
xml中配置AOP知识:
spring中基于XML的AOP配置步骤
1、把通知Bean
也交给spring来管理
2、使用<aop:config>
标签表明开始AOP的配置
3、使用<aop:aspect>
标签表明配置切面
id属性:是给切面提供一个唯一标识
ref属性:是指定通知类<bean>
的Id。
4、在<aop:aspect>
标签的内部使用对应标签来配置通知的类型
我们现在示例是让printLog方法在切入点方法执行之前之前:所以是前置通知:
<aop:before>
:表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
4、测试:
2.3.1.2、切入点表达式
切入点表达式的作用:告诉Spring对哪个类里面的哪个方法进行增强
知识:
切入点表达式的写法:
关键字:execution(表达式)
语法结构:execution([权限修饰符][返回值类型][类全路径][方法名称][参数列表])
表达式:
访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
标准的表达式写法:
public void com.it.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.it.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.it.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..
表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
例如:* *..*.*(int)
引用类型写包名.
类名的方式 java.lang.String
例如:* *..*.*(com.it.User)
可以使用通配符表示任意类型,但是必须有参数
可以使用..
表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)
2.3.1.3、四种常用通知类型
Advice(通知/增强):
AOP在特定的切入点上执行的增强处理,在增加处理过程中会调用到增强方法,这个过程就叫增强处理或通知。
其中增强方法的调用分为以下几种:
通知方法:
前置通知:在我们执行目标方法之前运行(@Before
)
后置通知:在我们目标方法运行结束之后 ,不管有没有异常(@After
)
返回通知:在我们的目标方法正常返回值后运行(@AfterReturning
)
异常通知:在我们的目标方法出现异常后运行(@AfterThrowing
)
环绕通知:动态代理, 需要手动执行joinPoint.procced()(其实就是执行我们的目标方法执行之前相当于前置通知, 执行之后就相当于我们后置通知(@Around
)
通知类Logger.java:
package com.it.test.utils;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
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方法开始记录日志了。。。");
}
}
配置bean.xml:
测试:
知识补充:
<aop:pointcut>
标签用于配置全局切面表达式
id
:id属性用于指定表达式的唯一标识。
expression
:expression属性用于指定表达式内容
注意:
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
用法如下:
用于切面内:
用于所有切面:
环绕通知:
问题:当我们按照前面四种通知方式配置了环绕通知之后,切入点方法没有执行,而环绕通知方法执行了。
分析:通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
解决:
Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
spring中的环绕通知
:它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
除了配置bean.xml外,还需要在logger.java中增加环绕通知方法:
测试:
2.3.2、基于注解的AOP(AspectJ)
由上一个Demo来进行基于注解的AOP配置:
1、修改bean.xml文件:
因为注解的方式需要对类上的注解进行扫描,然后注入到Spring容器,所以除了aop命名空间外需要增加context命名空间来开启注解扫描:xmlns:context="http://www.springframework.org/schema/context"
2、修改Logger通知类为注解:
3、修改bean.xml文件:
4、测试:
结论:
基于注解的的通知方法除了环绕通知,其他四种通知的调用顺序都有问题,调用顺序不稳定。基本注解的环绕方式之所以稳定,是因为我们代码里面的先后顺序就是写死的。所以在开发过程中请慎重考虑使用。
2.3.3、作业:基于注解的AOP
写一个User类,通过AOP实现方法增强
1、新建一个User类,和一个UserProxy代理类
2、配置bean.xml:
(1)引入aop命名空间
(2)引入context命名空间
(3)开启注解扫描
3、除了以上的jar包以外,还需要导入Aspect额外的jar包:
总共:
4、导入ApsjectJ以后,在增强类上面添加@Aspect注解,让其可以生成代理对象
5、在bean.xml文件中开启生成代理对象的配置
6、配置不同类型的通知
(1)在增强类里面,在作为通知的方法上面添加通知类型注解,并且使用切入点表达式来配置内容。
(2)测试:
7、往增强类中增加其他3中通知方式
然后进行测试:
此时发现异常通知没执行,原因是在执行add方法的时候没有出现异常,现在我们来手动搞一个异常出来:
然后再次执行测试:
8、使用@Pointcut注解对切入点进行抽取
9、有多个增强类对同一个方法进行增强,设置增强的优先级
(1)创建一个增强类,同时对add方法也进行增强
(2)在增强类上面添加@Order注解
,数字类型值越小,优先级越高。
10、将bean.xml也换成注解的方式: