SpringAop
-
AOP(Aspect Oriented Programming):面向切面编程
OOP(Object Oriented Programming):面向对象编程
- 面向切面编程,是基于OOP基础之上新的编程思想,指在程序运行期间,将某段代码动态地切入到指定地方法的指定位置进行运行的这种编程方式,即为面向切面编程。
实例:计算器运行计算方法的时候进行日志记录
加日志记录:
- 直接编写在方法内部;不推荐,修改维护比较麻烦,而我们更倾向于专精于业务逻辑。日志部分只起到了辅助作用。
- 可以使用动态代理来将日志代码动态的在目标方法执行前后
public interface Calculator {
public int add(int i,int j);
public int sub(int i,int j);
public int mul(int i,int j);
public int div(int i,int j);
}
public class MyMathCalculator implements Calculator {
@Override
public int add(int i, int j) {
// TODO Auto-generated method stub
return i+j;
}
@Override
public int sub(int i, int j) {
// TODO Auto-generated method stub
return i-j;
}
@Override
public int mul(int i, int j) {
// TODO Auto-generated method stub
return i*j;
}
@Override
public int div(int i, int j) {
// TODO Auto-generated method stub
return i/j;
}
}
/**
* 帮Calculator.java生成代理对象的类
* @author 22007
*public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
*/
public class CalculatorProxy {
/**
* 为传入的参数对象创建一个动态代理对象
* @param calculator
* @return
*
* Calculator calculator:被代理对象
*/
public static Calculator getProxy(final Calculator calculator) {
//获取被代理对象的类加载器
ClassLoader loader=calculator.getClass().getClassLoader();
//获取被代理对象的所有接口
Class<?>[] interfaces=calculator.getClass().getInterfaces();
//方法执行器,帮我们目标对象执行目标方法
InvocationHandler h=new InvocationHandler() {
/**
* Object proxy,代理对象,给jdk使用,任何时候都不要动这个对象
* Method method,当前将要执行的目标对象的方法
* Object[] args 这个方法调用时外界传入的参数值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//利用反射执行目标方法
// 目标方法执行后的返回值
Object result=null;
try {
LogUtils.logStart(method, args);
result=method.invoke(calculator, args);
LogUtils.logReturn(method,args);
} catch (Exception e) {
LogUtils.logException(method,e);
}finally{
LogUtils.logEnd(method);
}
//返回值必须返回出去外界才能拿到真正执行后的返回值
return result;
}
};
//Proxy为目标对象代理对象
Object proxy=Proxy.newProxyInstance(loader, interfaces, h);
return (Calculator) proxy;
}
}
public class AOPTest {
/**
* 有了动态代理,日志记录可以更加强大
* 动态代理的不足:写起来难,jdk默认的动态代理,如果目标对象没有实现任何接口,是无法为他创建代理对象的
* Spring实现了AOP功能,底层就是动态代理
* 1.可以利用Spring一句代码不写去创建动态代理
* 2.实现简单,并且没有强制要求目标对象必须实现接口
*/
@Test
public void test() {
Calculator calculator=new MyMathCalculator();
//如果是拿到了这个对象的代理对象,代理对象执行加减乘除;
//代理对象与被代理对象唯一能产生的管理就是实现了相同的接口Calculator
Calculator proxy=CalculatorProxy.getProxy(calculator);
proxy.add(2, 1);
}
}
- 指在程序运行期间,将某段代码(运行日志)动态地切入(不把日志代码写死在业务逻辑方法中)到指定的方法(加减乘除)的指定位置(方法的开始、结束、异常)进行运行的这种编程方式,即为面向切面编程。(Spring简化了面向切面编程)
- AOP术语
- AOP的使用步骤
导包
commons-logging-1.1.3.jar spring-aop-4.0.0.RELEASE.jar spring-beans-4.0.0.RELEASE.jar spring-context-4.0.0.RELEASE.jar spring-core-4.0.0.RELEASE.jar spring-expression-4.0.0.RELEASE.jar <!--面向切面编程的基础包--> spring-aspects-4.0.0.RELEASE.jar <!--加强版面向切面编程(即使目标对象没有实现任何接口也能创建动态代理)--> com.springsource.net.sf.cglib-2.2.0.jar com.springsource.org.aopalliance-1.0.0.jar com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
写配置
将目标类和切面类(封装了通知方法,即在目标方法执行前后执行的方法)加入到容器中。
还应该告诉Spring哪个为切面类,加注解@Aspect
告诉Spring,切面类里面的每一个方法,都是何时何地运行的
/** * 如何将这个切面类中的这些方法(通知方法)动态的在目标方法运行的哥哥位置切入 * @author 22007 * 五个通知注解: * @Before:在目标方法之前运行 前置通知 * @After:在目标方法结束之后 后置通知 * @AfterReturning:在目标方法正常返回之后 返回通知 * @AfterThrowing:在目标方法抛出异常之后运行 异常通知 * @Around:环绕 环绕通知 */ @Aspect @Component public class LogUtils { //方法开始 //想在执行目标方法之前运行,写切入点表达式 //execution(访问权限符 返回值类型 方法签名) //*代表该类下的所有方法 @Before("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public static void logStart(){ System.out.println("【xxx】方法开始执行,用的参数列表【xxx】"); } //方法结束 //想在目标方法正常执行完成之后执行 @AfterReturning("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public static void logReturn() { System.out.println("【xxx】方法执行完成,计算结果是:【xxx】"); } //方法异常报错 //想在目标方法抛出错误的时候 @AfterThrowing("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public static void logException() { System.out.println("【xxx】出现异常,异常信息是【xxx】"); } //方法结束 //想在目标方法结束的时候执行 @After("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public static void logEnd() { System.out.println("【xxx】方法最终结束了"); } }
开启基于注解的AOP模式
<context:component-scan base-package="com.atguigu"></context:component-scan> <!-- 开启基于注解的AOP功能,aop名称空间 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml"); @Test public void test() { //1.从ioc容器中拿到的目标对象,注意如果想要用类型,一定要用他的接口类型,不要用本类 Calculator cal=ioc.getBean(Calculator.class); cal.add(1, 2); }
- AOP细节
细节一:IOC中保存的组件时代理对象,cglib也可以为没有实现接口的对象创建代理对象
@Test public void test() { //1.从ioc容器中拿到的目标对象,注意如果想要用类型,一定要用他的接口类型,不要用本类 Calculator cal=ioc.getBean(Calculator.class); cal.add(1, 2); //细节一:IOC中保存的组件时代理对象 //com.atguigu.Impl.MyMathCalculator@6cf0e0ba //class com.sun.proxy.$Proxy12 System.out.println(cal); System.out.println(cal.getClass()); }
细节二:切入点表达式 的写法(通配符)
/** * 固定格式:execution(访问权限符 返回值类型 方法全类名) * 通配符 * *:1.匹配一个或多个字符 * execution(public int com.atguigu.Impl.MyMath*.*(int, int) * 2.匹配任意一个参数 * execution(public int com.atguigu.Impl.MyMath*.*(int, *) * 3.只能匹配一层路径 * execution(public int com.atguigu.*.MyMathCalculator.*(int, int) * 4.权限的位置不能写*,public是可选的 * ..:1.匹配任意多个参数 * execution(public int com.atguigu.Impl.MyMathCalculator.*(..) * 2.匹配任意多层路径 * execution(public int com.atguigu..MyMathCalculator.*(int, int) * 这两种通配符,一种时精确的,一种是模糊的 * 最模糊的:execution(* *.*(..)),相当于匹配任意包的任意方法,不推荐写 * 最精确的:execution(public int com.atguigu.Impl.MyMathCalculator.add(int, int)) * * 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。 * &&:切入的位置要满足这两个表达式 * execution(public int com.atguigu.Impl.MyMathCalculator.*(..)&& * execution(public int com.atguigu.Impl.MyMath*.*(int, int) * ||:满足任意一个表达式即可 * execution(public int com.atguigu.Impl.MyMathCalculator.*(..)|| * execution(public int com.atguigu.Impl.MyMath*.*(int, int) * !:不是这个表达式就行 * !execution(public int com.atguigu.Impl.MyMathCalculator.*(..) */ //方法结束 //想在目标方法正常执行完成之后执行 @AfterReturning("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public static void logReturn() { System.out.println("【xxx】方法执行完成,计算结果是:【xxx】"); }
//细节二:切入点表达式 的写法(通配符) @Test public void test02(){ Calculator bean=ioc.getBean(Calculator.class); bean.add(1, 2); }
细节三:通知方法的执行顺序
//细节三:通知方法的执行顺序 /** * 正常执行顺序:@Before(前置通知)===@After(后置通知)===@AfterReturning(正常返回) * 异常执行顺序:@Before(前置通知)===@After(后置通知)===@AfterThrowing(方法异常) */ @Test public void test03(){ Calculator bean=ioc.getBean(Calculator.class); bean.div(1, 0); }
细节四、我们可以在通知方法运行的时候拿到目标方法的详细信息
/** * 1.只需要为通知方法的参数列表上写一个参数 * JoinPoint joinPoint封装了当前目标方法的详细信息 */ @Before("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public static void logStart(JoinPoint joinPoint){ //获取到目标方法运行时使用的参数 Object[] args=joinPoint.getArgs(); //获取到方法签名 Signature signature=joinPoint.getSignature(); System.out.println("【"+signature.getName()+"】方法开始执行,用的参数列表【"+Arrays.asList(args)+"】"); }
细节五、returning、throwing用来指定哪个值接受结果与异常
/** * 告诉Spring参数result用来接收返回值 * returning="result" * 异常情况用throwing="exception" * @param joinPoint * @param result */ @AfterReturning(value="execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))",returning="result") public static void logReturn(JoinPoint joinPoint,Object result) { //获取到目标方法运行时使用的参数 Object[] args=joinPoint.getArgs(); //获取到方法签名 Signature signature=joinPoint.getSignature(); System.out.println("【"+signature.getName()+"】方法执行完成,计算结果是:【"+result+"】"); } //方法异常报错 //想在目标方法抛出错误的时候 @AfterThrowing(value="execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))",throwing="exception") public static void logException(JoinPoint joinPoint,Exception exception) { //获取到目标方法运行时使用的参数 Object[] args=joinPoint.getArgs(); //获取到方法签名 Signature signature=joinPoint.getSignature(); System.out.println("【"+signature.getName()+"】出现异常,异常信息是【"+exception+"】"); }
//细节五、returning、throwing用来指定哪个值接受结果与异常 @Test public void test05(){ Calculator bean=ioc.getBean(Calculator.class); bean.div(1, 0); }
- 细节六、Spring对通知方法的约束(参数一定正确)
- Spring对通知方法的要求不严格,但唯一要求的就是方法的参数列表一定不能乱写,通知方法时Spring利用反射调用的,每次方法调用得确定这个方法的参数表的值,参数表上的每一个参数Spring都得知道是什么,如JointPoint对于Spring就是已知的,如果是未知的,就得告诉Spring这是什么参数,如细节五所讲的内容。
- Exception exception:指定通知方法可以接收什么异常,如果设置异常范围太小,则可能会获取不到其他异常。
细节七、抽取可重用的切入点表达式
/** * 细节七、抽取可重用的切入点表达式 * 1.随便生命一个没有实现的返回void的空方法 * 2.给方法上标注注解 * */ @Pointcut("execution(public int com.atguigu.Impl.MyMathCalculator.*(int, int))") public void MyPoint(){}; @Before("MyPoint()") public static void logStart(JoinPoint joinPoint){ //获取到目标方法运行时使用的参数 Object[] args=joinPoint.getArgs(); //获取到方法签名 Signature signature=joinPoint.getSignature(); System.out.println("【"+signature.getName()+"】方法开始执行,用的参数列表【"+Arrays.asList(args)+"】"); }
细节八、环绕通知
/** * 细节八、环绕通知 * @Around:是Spring最强大的通知 * @Around:环绕 动态代理 * try{ * //前置通知 * method.invoke(obj,args); * //返回通知 * }catch(e){ * //异常通知 * }finally{ * //后置通知 * } * 四合一通知就是环绕通知 */ @Around("MyPoint()") public void myAround(ProceedingJoinPoint joinPoint){ String methodName=joinPoint.getSignature().getName(); Object proceed=null; Object[] object=joinPoint.getArgs(); try { System.out.println("【环绕前置通知】【"+methodName+"方法开始】"); //利用反射原理实现方法 proceed=joinPoint.proceed(object); System.out.println("【环绕返回通知】【"+methodName+"方法,返回值】"+proceed); } catch (Throwable e) { System.out.println("【环绕异常通知】【"+methodName+"方法,异常信息】"+e); }finally{ System.out.println("【环绕后置通知】【"+methodName+"方法开始】"); } }
细节九、环绕通知的执行顺序
/* 环绕通知,是优先于普通通知执行*/ 【普通前置】 { try{ 【环绕前置】 环绕执行、目标方法执行 【环绕返回】 }catch(e){ 【环绕异常】 }finally{ 【环境后置】 } * 【普通后置】 * 【普通方法返回/方法异常】 /* * 新的顺序:前面两个顺序无所谓 * (环绕前置---》普通前置)---》目标方法执行---》环绕正常返回/出现异常---》环绕后置---》普通后置---》普通返回或=者异常 * */
细节十、多切面的运行顺序
多个切面只有普通通知方法时
默认根据切面类的名字排序来确定谁先执行方法,执行顺序像栈
可以在切面类前面添加注解@Order(value)来确定切面类的优先级,value值越小,优先级越高
[LogUtils-前置]【add】方法开始执行,用的参数列表【[1, 1]】
[VaApsect-前置]【add】方法开始执行,用的参数列表【[1, 1]】
方法内部执行
[VaApsect-后置]【add】方法最终结束了
[VaApsect-返回]【add】方法正常执行完成,计算结果是:2
[LogUtils-后置]【add】方法最终结束了
[LogUtils-返回]【add】方法正常执行完成,计算结果是:2
*
多个切面中其中包含中环绕的情况,只影响带环绕的那个切面类,不影响另外一个切面类,执行顺序为
Log加环绕(环绕只是影响当前切面)
环绕前置
log前置
va前置
目标方法
va后置
va返回
环绕返回
环绕后置
log后置
log返回
- AOP使用场景:
- AOP 加日志保存到数据库
- AOP做权限验证
- AOP做安全检查
- AOP做事务控制
- 基于注解的AOP步骤
- 将目标类和切面类都加入到ioc容器中,@Component
- 告诉Spring哪个是切面类,@Aspect
- 在切面类中使用五个通知注解来配置切面中的这些通知方法都何时何地运行
- 开启基于注解的AOP功能
- 基于配置的AOP
其他的基本不变,将Spring注解删掉即可
<bean id="myMathCalculator" class="com.atguigu.Impl.MyMathCalculator"></bean> <bean id="BValidateAspect" class="com.atguigu.utils.BValidateAspect"></bean> <bean id="logUtils" class="com.atguigu.utils.LogUtils"></bean> <aop:config> <aop:pointcut expression="execution(* com.atguigu.impl.*.*(..))" id="mypoint"/> <aop:aspect ref="logUtils" order="3"> <aop:around method="myAround" pointcut-ref="mypoint"/> <aop:before method="logStart" pointcut-ref="mypoint"/> <aop:after-returning method="logReturn" returning="result" pointcut-ref="mypoint"/> <aop:after-throwing method="logException" throwing="exception" pointcut-ref="mypoint"/> <aop:after method="logEnd" pointcut-ref="mypoint"/> </aop:aspect> <aop:aspect ref="BValidateAspect" order="1"> <aop:before method="logStart" pointcut-ref="mypoint"/> <aop:after-returning method="logReturn" returning="result" pointcut-ref="mypoint"/> <aop:after-throwing method="logException" throwing="exception" pointcut-ref="mypoint"/> <aop:after method="logEnd" pointcut-ref="mypoint"/> </aop:aspect> </aop:config>
- 注解与配置
- 注解快速方便
- 配置功能完善
- 重要的用配置,不重要的用注解
- 声明式事务:需要操作数据库,spring提供了JDBCTemplate
环境搭建
导入sql文件、导入jar
写几个类和方法模拟结账操作;
package com.atguigu.dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class BookDao { @Autowired JdbcTemplate jdbcTemplate; /** * 1、减余额 * * 减去某个用户的余额 */ public void updateBalance(String userName,int price){ String sql = "UPDATE account SET balance=balance-? WHERE username=?"; jdbcTemplate.update(sql, price,userName); } /** * 2、按照图书的ISBN获取某本图书的价格 * @return */ public int getPrice(String isbn){ String sql = "SELECT price FROM book WHERE isbn=?"; return jdbcTemplate.queryForObject(sql, Integer.class, isbn); } /** * 3、减库存;减去某本书的库存;为了简单期间每次减一 */ public void updateStock(String isbn){ String sql = "UPDATE book_stock SET stock=stock-1 WHERE isbn=?"; jdbcTemplate.update(sql, isbn); } }
package com.atguigu.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.atguigu.dao.BookDao; @Service public class BookService { @Autowired BookDao bookDao; /** * 结账;传入哪个用户买了哪本书 * @param username * @param isbn */ public void checkout(String username,String isbn){ //1、减库存 bookDao.updateStock(isbn); int price = bookDao.getPrice(isbn); //2、减余额 bookDao.updateBalance(username, price); }
声明式事务和编程式事务的区别
声明式事务: 以前通过复杂的编程来编写一个事务,替换为只需要告诉Spring哪个方法是事务方法即可; Spring自动进行事务控制;
编程式事务:
TransactionFilter{ try{ //获取连接 //设置非自动提交 chain.doFilter(); //提交 }catch(Exception e){ //回滚 }finllay{ //关闭连接释放资源 } }
AOP:环绕通知可以去做;
//获取连接 //设置非自动提交 目标代码执行 //正常提交 //异常回滚 //最终关闭 最终效果: BookService{ @this is a tx-method(Transactional) public void checkout(){ //xxxxx } }
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架****实现声明式事务管理。自己要写这个切面还是很麻烦;这个切面已经有了;(事务切面===事务管理器);
- 这个事务管理器就可以在目标方法运行前后进行事务控制(事务切面);我们目前都使用DataSourceTransactionManager;即可;
- 基于配置的事务
快速的为某个方法添加事务:
- 配置出这个事务管理器让他工作;
- 开启基于注解的事务
- 给事务方法加@Transactional注解
<!-- 配置数据源--> <bean id="pooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.password}"></property> <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="driverClass" value="${jdbc.driverClass}"></property> </bean> <!-- 配置JdbcTemplate --> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="pooledDataSource"></property> </bean> <!-- 事务控制 --> <!--1:配置事务管理器(切面)让其进行事务控制;一定导入面向切面编程的几个包 spring-aspects-4.0.0.RELEASE.jar com.springsource.net.sf.cglib-2.2.0.jar com.springsource.org.aopalliance-1.0.0.jar com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar --> <bean id="tm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 控制住数据源 --> <property name="dataSource" ref="pooledDataSource"></property> </bean> <!--2:开启基于注解的事务控制模式;依赖tx名称空间 --> <tx:annotation-driven transaction-manager="tm"/> <!--3:给事务方法加注解@Transactional -->
@Transactional public void checkout(String username,String isbn){ //1、减库存 bookDao.updateStock(isbn); int price = bookDao.getPrice(isbn); //2、减余额 bookDao.updateBalance(username, price); }
- 事务细节
isolation:事务隔离级别
*
*
*
noRollbackFor:哪些异常事务可以不回滚
noRollbackForClassName:String的全类名
propagation:事务的传播行为
- Propagation.REQUIRED:在大事务中跟其他事务一条线,受其他事务影响;如果是REQUIRED,事务的属性都是继承大事务的,自己是调整不了的,如超时设置,而REQUIRES_NEW是可以设置的。
- Propagation.REQUIRES_NEW:在大事务中另外开一条线,不受其他事务影响,任何已经完成REQUIRES_NEW的事务不会崩;
readOnly:设置事务为只读事务,可以进行事务优化,readOnly=true,加快查询速度,不用管事务那一堆操作
rollbackFor:哪些异常事务需要回滚(可以让原来默认回滚的异常给他不回滚)
rollbackForClassName:
timeout(秒为单位):超时,事务超出事务指定执行时长后自动终止并回滚
异常分类:
- 运行时异常(非检查异常):可以不用处理,默认都回滚;
- 编译时异常(检查异常):要么try-catch,要么在方法上声明throws默认
注意:有事务的业务逻辑,容器中保存的是这个业务逻辑的代理对象
- 基于配置的事务配置
<aop:config> <aop:pointcut expression="execution(* com.atguigu.ser*.*.*(..))" id="txPoint"/> <!-- 事务建议;事务增强 advice-ref:指向事务管理器的配置 --> <aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"/> </aop:config> <!-- 配置事务管理器; 事务建议;事务增强;事务属性; transaction-manager="transactionManager":指定是配置哪个事务管理器; --> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <!--事务属性 --> <tx:attributes> <!-- 指明哪些方法是事务方法;切入点表达式只是说,事务管理器要切入这些方法,哪些方法加事务使用tx:method指定的 --> <tx:method name="*"/> <tx:method name="checkout" propagation="REQUIRED" timeout="-1"/> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice> <!-- 都用;重要的用配置,不重要的用注解 -->
<!-- 事务建议;事务增强 advice-ref:指向事务管理器的配置 --> <aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"/> </aop:config> <!-- 配置事务管理器; 事务建议;事务增强;事务属性; transaction-manager="transactionManager":指定是配置哪个事务管理器; --> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <!--事务属性 --> <tx:attributes> <!-- 指明哪些方法是事务方法;切入点表达式只是说,事务管理器要切入这些方法,哪些方法加事务使用tx:method指定的 --> <tx:method name="*"/> <tx:method name="checkout" propagation="REQUIRED" timeout="-1"/> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice> <!-- 都用;重要的用配置,不重要的用注解 -->