改造案例
- 改造操作account的代码,因为没有加入事务控制,执行sql时如果代码报错,可能造成数据库数据异常,因此需要编写事务控制,使得异常出现时事务可以回滚。
/*
* 连接的工具类,用于从数据源中获取一个连接,并且实现和线程的绑定
*/
public class ConnectionUtils {
private ThreadLocal<Connection> tl = new ThreadLocal<Connection>();
private DataSource dataSource;
//需要spring注入
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 (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 把连接和线程解绑
* 归还连接后,判断当前线程连接不为空,但是连接已关闭,无法使用
* 所以需要解绑
*/
public void removeConnection(){
tl.remove();
}
}
/**
* 事务管理的工具类。包括:
* 开启事务
* 提交事务
* 回滚事务
* 释放连接
*/
public class TransactionManager {
private ConnectionUtils connectionUtils;
//注入
public void setConnectionUtils(ConnectionUtils connectionUtils) {
this.connectionUtils = connectionUtils;
}
/**
* 开启事务
*/
public void beginTransaction(){
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(); //解绑
} catch (SQLException e) {
e.printStackTrace();
}
}
}
- 在bean.xml中配置工具类的注入。
- 测试:
缺点:每种操作都需要写一遍事务控制,代码冗杂。
/**
* 账户的业务层实现类
*
* 事务控制应该在业务层
*/
public class AccountServiceImpl_OLD implements IAccountService {
private IAccountDao accountDao;
private TransactionManager txManager;
//提供注入
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public List<Account> finaAllAccount() {
try {
//1 开启事务
txManager.beginTransaction();
//2 执行操作
List<Account> accounts = accountDao.finaAllAccount();
//3 提交事务
txManager.commit();
//4 返回结果
return accounts;
} catch (Exception e) {
//5 回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6 释放连接
txManager.release();
}
}
...
}
动态代理
- 动态代理:
特点:字节码随用随创建,随用随加载
作用:不修改源码的基础上,对方法增强
分类:
基于接口的动态代理
基于子类的动态代理
- 基于接口的动态代理
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创造代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理对象至少实现一个接口,如果没有则不能使用
*
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的,和被代理对象使用相同的类加载器。固定写法
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。固定写法
* InvocationHandler:提供增强的代码
* 它是让我们写如何代理,我们一般都是写一个该接口的实现类,通常情况下都行匿名内部类,当不是必须的
* 此接口的实现类都行谁用谁写
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
//基于接口,使用接口接受
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]; //只有一个参数
//判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer,money*0.8f); //生产厂家拿8000
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f); //消费者给10000
}
}
- 基于子类的动态代理
* 1 基于子类的动态代理
* 涉及的类:Enhancer
* 提供者:第三方cglib
* 如何创造代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
*
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码
* Callback:用于提供增强的代码
* 我们一般写的都想该接口的子接口实现类:MethodInterceptor
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* ------以上三个参数和基于接口的动态代理中invoke方法的参数是一样的------------
* @param methodProxy 当前执行方法的代理对象
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1 获取方法执行的参数
Float money = (Float)args[0]; //只有一个参数
//判断当前方法是不是销售
if("saleProduct".equals(method.getName())){
returnValue = method.invoke(producer,money*0.8f); //生产厂家拿8000
}
return returnValue;
}
});
cglibProducer.saleProduct(10000);
}
}
- 使用动态代理改造案例
bean.xml
<!--配置代理的service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>
<!--配置beanFactory-->
<bean id="beanFactory" class="com.itheima.factory.beanFactory">
<!--注入service-->
<property name="accountService" ref="accountService"></property>
<!--注入事务管理器-->
<property name="txManager" ref="txManager"></property>
</bean>
/**
* 用于创建Service代理对象的工厂
*/
public class beanFactory {
private IAccountService accountService;
//注入
public void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
/**
* 获取service的代理对象
* @return
*/
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 {
//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();
}
}
});
}
}
这样,在service实现类中我们只要调用方法,不需要再写事务控制的代码,因为调用方法经过代理service都会执行事务控制。
AOP
- 概念:
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。
- 作用:
在程序运行期间,不修改源码对已有方法进行增强。 - 优势:
减少重复代码
提高开发效率
维护方便 - AOP 的实现方式
使用动态代理技术
Spring中的aop
- aop相关术语:
- Joinpoint(连接点):
所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。 - Pointcut(切入点): 切入点都行连接点,但连接点不一定是切入点
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。(被增强的) - Advice(通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
- Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。 - Target(目标对象): 被代理的目标对象。
- Weaving(织入): 加入增强代码的过程
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。 - Proxy(代理):代理对象
一个类被 AOP 织入增强后,就产生一个结果代理类。 - Aspect(切面):是切入点和通知(引介)的结合。
- Joinpoint(连接点):
spring基于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:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
* 切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名...包名.类名.方法名(参数列表)
标准的表达式写法:public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
* 访问修饰符可以省略:void com.itheima.service.impl.AccountServiceImpl.saveAccount()
-返回值可以使用通配符,表示任意返回值:* com.itheima.service.impl.AccountServiceImpl.saveAccount()
-包名可以使用通配符,表示任意包,但是有几级包,就需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount()
-包名可以使用..表示当前包及其子包:* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配:* *..*.*() -> 没有参数的方法会增强
-参数列表:
可以直接写数据类型:* *..*.*(int)
基本类型直接写名称
引用类型写包名.类名的方式:java.lang.String
可以使用通配符表示任意类型,但是必须有参数:* *..*.*(*)
可以使用..表示有无参数均可,有参数可以是任意类型
全通配写法(少用)
* *..*.*(..)
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法:
* com.itheima.service.impl.*.*(..)
<?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的ioc,把service对象配置进来,这个类中的方法需要增强-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点方法的关联
printLog会对saveAccount进行增强-->
<aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
</aop:aspect>
</aop:config>
</beans>
配置之后,在执行saveAccount的时候,printLog会对它进行增强,即在它之前执行(前置通知)
- 四种常用通知类型
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点方法的关联
printLog会对saveAccount进行增强-->
<!--前置通知,在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut="execution(* *..*.*(..))"></aop:before>
<!--后置通知和异常通知永远只能执行一个-->
<!--后置通知,在切入点方法正常执行后执行-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* *..*.*(..))"></aop:after-returning>
<!--异常通知,在切入点方法执行产生异常之后执行-->
<aop:after-throwing method="afterThrowingLog" pointcut="execution(* *..*.*(..))"></aop:after-throwing>
<!--最终通知,无论切入点方法是否正常执行,它都会在后面执行-->
<aop:after method="agterPrintLog" pointcut="execution(* *..*.*(..))"></aop:after>
</aop:aspect>
</aop:config>
- 可以简化切入点表达式
将原先配置的切入点表达式使用pointcut-ref代替
切面表达式配置的位置可以决定使用范围。
<aop:config>
<!--配置切入点表达式 id用于指定表达式唯一标识,expression指定表达式内容
此标签写在aop:aspect标签内部,只能当前切面使用。
它还可以写在aop:aspect标签外面,此时变成了所有切面可用
-->
<aop:pointcut id="pt1" expression="execution(* *..*.*(..))"></aop:pointcut>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知类型,并且建立通知方法和切入点方法的关联
printLog会对saveAccount进行增强-->
<!--前置通知,在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
...
<!--当前切面可用
<aop:pointcut id="pt1" expression="execution(* *..*.*(..))"></aop:pointcut>-->
</aop:aspect>
</aop:config>
- 环绕通知
* 环绕通知
* 问题:
* 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
* 分析:
* 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有
* 解决:
* Spring框架为我们提供了一个接口,ProceedingJoinPoint,该接口有一个方法proceed(),该方法就相当于明确调用切入点方法
* 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用
* spring中的环绕通知:
* 它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtVlaue = null;
try {
Object[] args = pjp.getArgs();
System.out.println("前置通知...");
rtVlaue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("后置通知...");
return rtVlaue;
} catch (Throwable throwable) {
System.out.println("异常通知...");
throw new RuntimeException(throwable);
}finally {
System.out.println("最终通知...");
}
}
spring基于注解的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"
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="com.itheima"></context:component-scan>
<!--配置spring开启aop注解的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
- 使用注解配置aop时,调用方法的顺序可能会有问题,如果采用手写的环绕通知则不会有问题。
@Component("logger")
@Aspect //表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.itheima.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 afterThrowingLog(){
System.out.println("异常通知:Logger类中的afterThrowingLog方法开始记录日志了...");
}
//最终通知
// @After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知:Logger类中的agterPrintLog方法开始记录日志了...");
}
//方法调用顺序正常
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtVlaue = null;
try {
Object[] args = pjp.getArgs();
System.out.println("前置通知...");
rtVlaue = pjp.proceed(args); //明确调用业务层方法(切入点方法)
System.out.println("后置通知...");
return rtVlaue;
} catch (Throwable throwable) {
System.out.println("异常通知...");
throw new RuntimeException(throwable);
}finally {
System.out.println("最终通知...");
}
}
}
- 当然,也可以使用纯注解配置aop(了解)
@EnableAspectJAutoProxy
改造account案例,使用aop实现事务控制
1 基于xml的aop实现事务控制
不使用代理工程创建代理对象,直接使用我们自己的service对象
<!--配置事务管理器-->
<bean name="txManager" class="com.itheima.utils.TransactionManager">
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置aop-->
<aop:config>
<!--配置切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<aop:aspect id="txAdvice" ref="txManager">
<!--配置前置通知,开启事务-->
<aop:before method="beginTransaction" pointcut-ref="pt1"></aop:before>
<!--配置后置通知,提交事务-->
<aop:after-returning method="commit" 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>
2 基于注解的aop实现事务控制
需要使用的类使用注解@Service / @Repository,需要注入的地方使用自动注入@Autowired
<?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创建容器时要扫描的包 ioc -->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!--配置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?characterEncoding=utf8"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!--开启spring对注解aop的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
主要是事务管理的类需要配置aop
@Component("txManager")
@Aspect //表明当前类是切面类
public class TransactionManager {
@Autowired
private ConnectionUtils connectionUtils;
//配置切入点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
public void beginTransaction(){...}
public void commit(){...}
public void rollback(){...}
public void release(){...}
/*
由于使用注解分别配置通知方法,方法的执行顺序有问题,会导致程序出错
我们可以使用环绕通知,手动指定方法执行顺序
*/
@Around("pt1()")
public Object aroundAdvice(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
//1 获取参数
Object[] args = pjp.getArgs();
//2 开启事务
this.beginTransaction();
//3 执行方法
rtValue = pjp.proceed(args);
//4 提交事务
this.commit();
//返回结果
return rtValue;
}catch (Throwable e){
//5 回滚事务
this.rollback();
throw new RuntimeException(e);
}finally {
//6 释放资源
this.release();
}
}
}