-
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效
-
AOP采取横向抽取机制,取代了传统纵向继承体系重复性代码
-
总结:
-
也就是没有用到AOP这种面向切面编程的时候,想要修改一个类,并在类里面针对一个方法,想对这个方法进行事务管理(开启事务,提交事务)的操作。
-
需要对每个父类进行继承,然后重写这个方法,在方法体里, 在super.addUser();前后开启和提交事务,才能实现需要的事务处理功能
-
这样的话重复的代码会比较多,而且耦合度高,子类依赖父类。
-
-
经典应用:事务管理、性能监视、安全检查、缓存 、日志等
-
Spring AOP使用纯Java实现,不需要专门的编译过程和类加载器,在运行期通过代理方式向目标类织入增强代码
-
AspectJ是一个基于Java语言的AOP框架,Spring2.0开始,Spring AOP引入对Aspect的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译时提供横向代码的织入
## AOP原理
为了解决上面的纵向继承的缺陷,用了横向抽取,这种AOP技术
AOP术语:
-
target:目标类,需要被代理的类。例如:UserService
-
Joinpoint(连接点):所谓连接点是指那些可能被拦截到的方法。例如:所有的方
-
PointCut 切入点:已经被增强的连接点。例如:addUser()
-
advice 通知/增强,增强代码。例如:after、before
-
Weaving(织入):是指把增强advice应用到目标对象target来创建新的代理对象proxy的过程.
-
proxy 代理类
-
Aspect(切面): 是切入点pointcut和通知advice的结合
-
一个线是一个特殊的面。 一个切入点和一个通知,组成成一个特殊的面。
AOP原理:
AOP内部实现原理:
-
为了能达到这种切面动态织入技术,需要JDK动态代理,去生成代理类。
-
接口 + 实现类 :spring采用 jdk 的动态代理Proxy。但是JDK动态代理没有接口的话则实现不了!只能通过cglib
-
实现类:spring 采用 cglib字节码增强。(就是在class文件中直接嵌入切面类代码)
在运行时,创建目标类的子类,从而对目标类进行加强
spring-core依赖内部集成了cglib
手动模拟实现AOP
-
通知类(切面类):
/** * 切面类 * 编写通知Advice * 用于增强连接点joinpoint的方法 */ public class MyAspect { public void before(){ System.out.println("开启事务"); } public void after(){ System.out.println("关闭事务"); } }
-
目标类
public interface UserService { void addUser(); void deleteUser(); void updateUser(); } public class UserServiceImpl implements UserService{ public void addUser() { System.out.println("add User"); } public void deleteUser() { System.out.println("delete User"); } public void updateUser() { System.out.println("update User"); } }
-
工厂类
/** * 静态工厂 * 作用:织入切面类与目标类(接口+ 实现类) * 返回代理对象(经过织入后的JDK动态代理对象) */ public class MyBeanFactory { public static UserService createUserService(){ //1. 目标类 final UserService userService = new UserServiceImpl(); //2. 切面类 final MyAspect myAspect = new MyAspect(); return (UserService) Proxy.newProxyInstance(userService.getClass().getClassLoader(), userService.getClass().getInterfaces(), (proxy, method, args)->{ //织入切面类方法 myAspect.before(); //执行这个方法 Object obj = method.invoke(userService, args); //织入切面类方法 myAspect.after(); return obj; }); } }
结果:
内部class文件名称:
JDK动态代理的内部原理
/* 3 代理类:将目标类(切入点)和 切面类(通知) 结合 --> 切面
* Proxy.newProxyInstance
* 参数1:loader ,类加载器,动态代理类 运行时创建,任何类都需要类加载器将其加载到内存。
* 一般情况:当前类.class.getClassLoader();
* 目标类实例.getClass().get...
* 参数2:Class[] interfaces 代理类需要实现的所有接口
* 方式1:目标类实例.getClass().getInterfaces() ;注意:只能获得自己接口,不能获得父元素接口
* 方式2:new Class[]{UserService.class}
* 例如:jdbc 驱动 --> DriverManager 获得接口 Connection
* 参数3:InvocationHandler 处理类,接口,必须进行实现类,一般采用匿名内部
* 提供 invoke 方法,代理类的每一个方法执行时,都将调用一次invoke
* 参数3_1:Object proxy :代理对象
* 参数3_2:Method method : 代理对象当前执行的方法的描述对象(反射)
* 执行方法名:method.getName()
* 执行方法:method.invoke(对象,实际参数)
* 参数3_3:Object[] args :方法实际参数
*
*/
手动模拟实现cglib底层的AOP
工厂类
/** * 静态工厂 * 作用:织入切面类与目标类(接口+ 实现类) * 返回代理对象(经过织入后的JDK动态代理对象) * * tips: 为了测试cglib---要把接口改成只有实现类 */ public class MyBeanFactory { public static UserServiceImpl createUserService(){ //1. 目标类 final UserServiceImpl userService = new UserServiceImpl(); //2. 切面类 final MyAspect myAspect = new MyAspect(); //3. 编写底层为cglib的代理类 /** * cglib 原理: 在运行时,通过创建目标类的子类,来增强目标类 */ //1. cglib 的底层实现的核心类 Enhancer enhancer = new Enhancer(); //2. 设置父类-->目标类 enhancer.setSuperclass(userService.getClass()); //3. 设置回调函数 --> 相当于jdk的 new InvokerHandler()方法的实现,就是回调 --->处理类执行 enhancer.setCallback((MethodInterceptor) (proxy, method, args, methodProxy) -> { //织入切面类的通知 myAspect.before(); //执行方法 Object obj = method.invoke(userService, args); myAspect.after(); return obj; }); //4. 创建代理类 UserServiceImpl proxyUserService = (UserServiceImpl) enhancer.create(); return proxyUserService; } }
特殊注意点:
总结:第二句的写法是:通过第四个参数methodProxy(也就是cglib生成的子类),通过子类去调用父类的方法,invokeSuper(proxy,args), proxy就是一开始动态代理传进来的父类对象。
notes: 两句写法效果是一样的
内部class名称:
Cglib和JDK动态代理的区别
1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。
2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,
是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,
对于final类或方法,是无法继承的。
spring强制使用cglib:
在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
利用Spring的AOP工厂Bean模拟AOP
上面我们是自己创建工厂,然后在工厂里写JDK动态代理,返回代理类。
而我们在之前学过一个叫FactoryBean, 是一个工厂Bean接口,它的实现类ProxyFactoryBean正是AOP的工厂Bean,通过给他注入切面类,目标类的接口,以及目标类的引用(ref)。
就可以通过Spring提供的FactoryBean返回一个代理对象。
切面类
要实现 MethodInterceptor接口,这是Spring通知类型中的环绕通知
/** * 切面类 * 编写环绕通知Advice * 用于增强连接点joinpoint的方法 * * 需要实现MethodInterceptor接口 */ public class MyAspect implements MethodInterceptor { private void before(){ System.out.println("开启事务"); } private void after(){ System.out.println("关闭事务"); } @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { //开启事务 before(); //放行拦截的目标类方法 Object obj = methodInvocation.proceed(); after(); return obj; } }
目标类
public class UserServiceImpl implements UserService { public void addUser() { System.out.println("add User"); } public void deleteUser() { System.out.println("delete User"); } public void updateUser() { System.out.println("update User"); } }
配置文件
<?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"> <!--切面类--> <bean id="myAspect" class="com.jzp.b_facotry_bean.MyAspect"></bean> <!--目标类--> <bean id="userService" class="com.jzp.b_facotry_bean.UserServiceImpl"></bean> <!--spring的aop工厂Bean--> <bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="interfaces" value="com.jzp.b_facotry_bean.UserService"></property> <property name="target" ref="userService"></property> <!--拦截方法,也就是通知Advice,也就是切面类的BeanId, 是个String[]类型的参数,可以用Array标签注入属性--> <property name="interceptorNames" value="myAspect"> </property> </bean> </beans>
Aspect的切入点expression表达式的书写
1.1 切入点表达式【掌握】
1.execution() 用于描述方法 【掌握】
语法:execution(修饰符 返回值 包.类.方法名(参数) throws异常)
修饰符,一般省略
public 公共方法
* 任意
返回值,不能省略
void 返回没有值
String 返回值字符串
* 任意
包,[省略]
com.itheima.crm 固定包
com.itheima.crm.*.service crm包下面子包任意 (例如:com.itheima.crm.staff.service)
com.itheima.crm.. crm包下面的所有子包(含自己)
com.itheima.crm.*.service.. crm包下面任意子包,固定目录service,service目录任意包
类,[省略]
UserServiceImpl 指定类
*Impl 以Impl结尾
User* 以User开头
* 任意
方法名,不能省略
addUser 固定方法
add* 以add开头
*Do 以Do结尾
* 任意
(参数)
() 无参
(int) 一个整型
(int ,int) 两个
(..) 参数任意
throws ,可省略,一般不写。
综合1
execution(* com.itheima.crm..service...*(..))
综合2
<aop:pointcut expression="execution(* com.itheima.WithCommit.(..)) ||
execution(* com.itheima.Service.(..))" id="myPointCut"/>
2.within:匹配包或子包中的方法(了解)
within(com.itheima.aop..*)
3.this:匹配实现接口的代理对象中的方法(了解)
this(com.itheima.aop.user.UserDAO)
4.target:匹配实现接口的目标对象中的方法(了解)
target(com.itheima.aop.user.UserDAO)
5.args:匹配参数格式符合标准的方法(了解)
args(int,int)
6.bean(id) 对指定的bean所有的方法(了解)
bean('userServiceId')
Aspect的几种Advice通知类型的AOP编程
前置,后置通知
切面类
public class MyAspect { public void before(JoinPoint joinPoint){ //joinPoint是连接点,也就是织入时的切入点,可以在运行时获取到这个被拦截的方法的签名Signature //通过签名可以获取这个方法一系列信息,比如名字,参数类型,返回值等等 System.out.println("前置通知"+joinPoint.getSignature().getName()); } //第二个参数ret是目标方法执行后的返回值 public void after(JoinPoint joinPoint,Object ret){ //joinPoint是连接点,也就是织入时的切入点,可以在运行时获取到这个被拦截的方法的签名Signature //通过签名可以获取这个方法一系列信息,比如名字,参数类型,返回值等等 System.out.println("后置通知"+joinPoint.getSignature().getName()+"----->返回值:"+ret); } }
配置文件
<!--切面类--> <bean id="myAspect" class="com.jzp.d_aspect.a_.MyAspect"></bean> <!--目标类--> <bean id="userService" class="com.jzp.d_aspect.a_.UserServiceImpl"></bean> <!--aop编程--> <aop:config> <!--用aspect 切面--> <!--1. 先声明切面类引用--> <aop:aspect ref="myAspect"> <!--声明切入点--> <aop:pointcut id="myPointCut" expression="execution(* com.jzp.d_aspect.a_.*.*(..))"></aop:pointcut> <!--声明为前置通知--> <!--1. 前置通知是有参数的 before(JoinPoint joinPoint) 参数1: joinPoint是连接点,也就是当前切入点方法的对象,可以通过他获取签名(参数,名字等) --> <aop:before method="before" pointcut-ref="myPointCut"></aop:before> <!--声明为后置通知--> <!--1. 后置参数是有参数的 after(JoinPoint joinPoint,Object ret) 参数1: joinPoint是连接点,也就是当前切入点方法的对象,可以通过他获取签名(参数,名字等) 参数2: 配合标签中的returning = "ret" ,ret是目标方法执行后的返回值 --> <aop:after-returning method="after" pointcut-ref="myPointCut" returning="ret"></aop:after-returning> </aop:aspect> </aop:config>
环绕通知,异常抛出通知,最终通知
以前环绕通知是通过AOP联盟alliance包的规范,来实现接口的方式,来编写环绕通知的切面类。
现在我们用Spring的AOP 在XML的配置方式,来实现环绕通知.
顾名思义:在不同阶段执行的相应通知类型
切面类
/** * 切面类 * 编写环绕通知Advice * 用于增强连接点joinpoint的方法 * * 需要实现MethodInterceptor接口 */ public class MyAspect { public void before(JoinPoint joinPoint){ //joinPoint是连接点,也就是织入时的切入点,可以在运行时获取到这个被拦截的方法的签名Signature //通过签名可以获取这个方法一系列信息,比如名字,参数类型,返回值等等 System.out.println("前置通知"+joinPoint.getSignature().getName()); } //第二个参数ret是目标方法执行后的返回值 public void after(JoinPoint joinPoint,Object ret){ //joinPoint是连接点,也就是织入时的切入点,可以在运行时获取到这个被拦截的方法的签名Signature //通过签名可以获取这个方法一系列信息,比如名字,参数类型,返回值等等 System.out.println("后置通知"+joinPoint.getSignature().getName()+"----->返回值:"+ret); } //ProceedingJoinPoint 是 让你手动执行切入点的目标方法的 public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //前置 before(joinPoint); long start = System.currentTimeMillis(); //手动执行切入点目标方法 Object obj = joinPoint.proceed(); //后置 // after(joinPoint,obj.getClass().getName()); System.out.println("环绕后置"); long end = System.currentTimeMillis(); System.out.println(joinPoint.getSignature().getName()+"方法 耗时: "+(end-start)); return obj; } public void afterThrowing(JoinPoint joinPoint,Exception e){ System.out.println("抛出异常通知 "+joinPoint.getSignature().getName()); System.out.println("异常信息"+e.getMessage()); } public void end(JoinPoint joinPoint){ System.out.println("最终通知通知 "+joinPoint.getSignature().getName()); } }
<!--声明为环绕通知--> <aop:around method="around" pointcut-ref="myPointCut"></aop:around> <!--声明为抛出异常通知--> <!-- 参数1: joinPoint是连接点,也就是当前切入点方法的对象,可以通过他获取签名(参数,名字等) 参数2: 获取异常信息的,类型Throwable ,afterThrowing(JoinPoint joinPoint,Exception e) 注意:throwing="e" 要和参数名一致 --> <aop:after-throwing method="afterThrowing" pointcut-ref="myPointCut" throwing="e"></aop:after-throwing> <!--声明最终通知--> <aop:after method="end" pointcut-ref="myPointCut"></aop:after> </aop:aspect> </aop:config>
AOP基于注解
前提:
<!--注解扫描--> <context:component-scan base-package="com.jzp.d_aspect.b_anno"></context:component-scan> <!--让aop注解生效--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
替换Bean
替换切面类
@Component("myAspect") @Aspect 在切面类声明公共切入点(作为其他通知的引用) // 声明公共切入点 @Pointcut(value = "execution(* com.jzp.d_aspect.a_xml.*.*(..))") private void myPointCut(){ }
通知声明的方法,value为公共切入点的引用,其他参数则以逗号分隔,和XML对应。方法名字不用写(绑定在对应方法上)
@Component("myAspect") @Aspect public class MyAspect { @Before("myPointCut()") public void before(JoinPoint joinPoint){ //joinPoint是连接点,也就是织入时的切入点,可以在运行时获取到这个被拦截的方法的签名Signature //通过签名可以获取这个方法一系列信息,比如名字,参数类型,返回值等等 System.out.println("前置通知"+joinPoint.getSignature().getName()); } // 声明公共切入点 @Pointcut(value = "execution(* com.jzp.d_aspect.b_anno.*.*(..))") private void myPointCut(){ } //第二个参数ret是目标方法执行后的返回值 @AfterReturning(value = "myPointCut()",returning = "ret") public void after(JoinPoint joinPoint,Object ret){ //joinPoint是连接点,也就是织入时的切入点,可以在运行时获取到这个被拦截的方法的签名Signature //通过签名可以获取这个方法一系列信息,比如名字,参数类型,返回值等等 System.out.println("后置通知"+joinPoint.getSignature().getName()+"----->返回值:"+ret); } //ProceedingJoinPoint 是 让你手动执行切入点的目标方法的 @Around("myPointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //前置 System.out.println("注解环绕前置"); long start = System.currentTimeMillis(); //手动执行切入点目标方法 Object obj = joinPoint.proceed(); //后置 System.out.println("注解环绕后置"); long end = System.currentTimeMillis(); System.out.println(joinPoint.getSignature().getName()+"方法 耗时: "+(end-start)); return obj; } @AfterThrowing(value = "myPointCut()",throwing = "e") public void afterThrowing(JoinPoint joinPoint,Exception e){ System.out.println("抛出异常通知 "+joinPoint.getSignature().getName()); System.out.println("异常信息"+e.getMessage()); } @After("myPointCut()") public void end(JoinPoint joinPoint){ System.out.println("最终通知通知 "+joinPoint.getSignature().getName()); } }
JdbcTemplate
总结:
// 不可以这样: private JdbcTempate jdbctemplate = this.getJdbcTemplate();
//因为this.getJdbcTemplate()内部是要通过数据源去createJdbcTemplate()模板的
//如果在类构造成员的时候就写,Spring是没有进行数据源注入的,所以为null
//引发空指针异常
UserDao
要继承JdbcTemplate
/** * 继承JdbcDaoSupport * 就只需要借助父类的getJdbcTemplate()即可获取JdbcTemplate,不需要自己创建和注入 */ public class UserDao extends JdbcDaoSupport { // 不可以这样: private JdbcTempate jdbctemplate = this.getJdbcTemplate(); //因为this.getJdbcTemplate()内部是要通过数据源去createJdbcTemplate()模板的 //如果在类构造成员的时候就写,Spring是没有进行数据源注入的,所以为null //引发空指针异常 public void updateUser(User user){ String sql = "update t_user set username = ?,password = ? where id = ?"; Object[] args = {user.getUsername(),user.getPassword(),user.getId()}; this.getJdbcTemplate().update(sql,args); } public List<User> findAll(){ return this.getJdbcTemplate().query("select *from t_user", BeanPropertyRowMapper.newInstance(User.class)); } public User queryById(int id){ return this.getJdbcTemplate().queryForObject("select * from t_user where id = ?",BeanPropertyRowMapper.newInstance(User.class),id); } }
配置文件
<?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"> <!--通过配置文件配合SpEL表达式书写配置源--> <context:property-placeholder location="classpath:db.properties"></context:property-placeholder> <!--配置数据源--> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"></property> <property name="url" value="${jdbc.url}"></property> <property name="username" value="${jdbc.username}"></property> <property name="password" value="${jdbc.password}"></property> </bean> <!--配置JDBC模板--> <!-- UserDao 继承 JdbcDaoSupport,底层自动创建JdbcTemaplte --> <!--<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">--> <!--<!–需要注入数据源–>--> <!--<property name="dataSource" ref="dataSource"></property>--> <!--</bean>--> <!--配置Dao--> <bean id="userDao" class="com.jzp.e_jdbctemplate.c_DAO.UserDao"> <!--因为继承了父类,所以只需要注入数据源即可--> <!--以前是把数据源注入在Jdbc模板里--> <property name="dataSource" ref="dataSource"></property> </bean> </beans>