Sping AOP
Sping AOP
1.什么是AOP
面向切面编程(AOP) 是 面向对象编程的补充(OOP)
传统的业务处理代码中,通常会惊醒事务处理、日志处理等操作。虽然可以使用OOP的组合或继承来实现代码重用,但如果要实现某个功能,同样的代码还是会分散到各个方法中。
如果想要关闭某个功能,或者修改,就必须修改所有相关方法,增加了工作量和出错率。
AOP采用横向抽取机制,将重复代码抽取出来,在程序编译或运行时将代码应用到需要执行的地方。
AOP可以使开发人员编写业务逻辑时专心于核心业务,提高了开发效率,增强了代码的可维护性。
常用框架: Spring AOP 和 AspectJ
转:
- 静态AOP
编译阶段对程序源代码进行修改,生成静态AOP代理类(生成的*.class已经被改掉,需要特定的编译器) 如 AspectJ - 动态AOP
运行阶段动态生成Proxy对象
2.AOP术语
- Aspect(切面):实际应用中,切面通常指封装的用于横向插入系统功能的类,当然也要先通过<bean>元素注册
- Joinpoint(连接点):指方法的调用
- Pointcut(切入点):类或方法名 满足某一条件的方法
- Advice(通知/增强处理):切入点要执行的程序代码,切面的具体实现
- Target Object(目标对象):被增强对象
- Weaving(织入):将切面代码插入到目标对象上,生成代理对象的过程
3.动态代理
①JDK动态代理—基础
JDK动态代理通过 java.lang.reflect.Proxy实现,我们可以使用Proxy.newProxyInstance()来创建代理对象
//切片类 public class MyAspect { public void check_Permissions(){ System.out.println("模拟检查权限"); } public void log(){ System.out.println("模拟记录日志..."); } }
//代理类 public class JdkProxy implements InvocationHandler { //声明目标类接口 private UserDao userDao; //创建代理方法 public Object createProxy(UserDao userDao){ this.userDao = userDao; // 1.类加载器 ClassLoader classLoader = UserDao.class.getClassLoader(); // 2.被代理对象实现的所有接口 Class[] clazz = userDao.getClass().getInterfaces(); // 3.使用代理类,进行增强,返回的是代理后的对象 return Proxy.newProxyInstance(classLoader, clazz,this); } /* 所有动态代理类的方法调用都会交给invoke() 方法来处理 proxy 被代理后的对象 method 将要被执行的方法信息(反射) args 执行方法时需要的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //声明切片 MyAspect myAspect = new MyAspect(); //前增强 myAspect.check_Permissions(); Object obj = method.invoke(userDao,args); myAspect.log(); return obj; } }
//调用类 public class JdkTest { public static void main(String[] args) { JdkProxy jdkProxy = new JdkProxy(); UserDao userDao = new UserDaoImp1(); UserDao userDao1 = (UserDao)jdkProxy.createProxy(userDao); userDao1.addUser(); userDao1.deleteUser(); } }
② CGLIB代理—基础
JDK动态代理要求使用动态代理的对象必须实现一个或多个接口
CGLIB(Code Generation Library)使用底层字节码技术,对指定的目标类生成一个子类,并对子类进行增强
//目标类 public class UserDao { public void addUser() { System.out.println("增加用户..."); } public void deleteUser() { System.out.println("删除用户..."); } } //代理类 public class CglibProxy implements MethodInterceptor { //代理方法 public Object createProxy(Object target){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(this); return enhancer.create(); } /* proxy CGlib生成的代理对象 method 拦截的方法 args 拦截方法的参数数组 methodProxy 方法的代理对象,用于执行父类的方法 */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { MyAspect myAspect = new MyAspect(); myAspect.check_Permissions(); Object obj = methodProxy.invokeSuper(proxy, args); myAspect.log(); return obj; } } //测试类 public class CglibTest { public static void main(String[] args) { CglibProxy cglibProxy = new CglibProxy(); UserDao userDao = new UserDao(); UserDao userDao1 = (UserDao)cglibProxy.createProxy(userDao); userDao1.addUser(); userDao1.deleteUser(); } }
运行结果
模拟检查权限
增加用户...
模拟记录日志...
模拟检查权限
删除用户...
模拟记录日志...
③ProxyFactoryBean类
演示在Spring中,使用在配置文件中创建ProxyFactoryBean,并使用它创建一个AOP环绕通知案例
首先需要两个JAR包
- spring-aop-4.3.6.RELEASE.jar
- aopalliance-1.0.jar (AOP联盟提供的规范包,地址 http://mvnrepository.com/artifact/aopalliance/aopalliance/1.0)
pom.xml
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.3.6.RELEASE</version> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>1.0</version> </dependency>
切面类 (环绕通知需要实现MethodInterceptro接口)
public class MyAspect implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { this.check_Permissions(); Object obj = methodInvocation.proceed(); this.log(); return obj; } public void check_Permissions(){ System.out.println("模拟检查权限..."); } public void log(){ System.out.println("模拟记录日志..."); } }
applicationContext.xml 配置文件
<!-- 1 目标类--> <bean id="userDao" class="com.itheima.jdk.UserDaoImp1"/> <!-- 2 切面类--> <bean id="myAspect" class="com.itheima.factorybean.MyAspect"/> <!-- 3 使用Spring代理工厂定义一个名称为userDaoProxy的代理对象 --> <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!-- 3.1 指定代理实现的接口--> <property name="proxyInterfaces" value="com.itheima.jdk.UserDao"/> <!-- 3.2 指定目标对象--> <property name="target" ref="userDao"/> <!-- 3.3 指定切面,植入环绕通知 --> <property name="interceptorNames" value="myAspect"/> <!-- 3.4 指定代理方式,true:使用cglib,false(默认):使用jdk动态代理 --> <property name="proxyTargetClass" value="true"/> </bean>
//测试类
public class ProxyFactoryBeanTest { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDao = (UserDao) applicationContext.getBean("userDaoProxy"); userDao.addUser(); userDao.deleteUser(); } }
4.AspectJ开发
常用元素配置代码
<!--定义切面 Bean--> <bean id="myAspect" class="com.itheima.aspectj.xml.MyAspect"/> <aop:config> <!-- 1.配置切面--> <aop:aspect ref="myAspect"> <!-- 2.配置切入点--> <aop:pointcut expression="execution(* com.itheima.jdk.*.*(..))" id="myPointCut"/> <!-- 3.配置通知--> <!-- 前置通知--> <aop:before method="myBefore" pointcut-ref="myPointCut"/> <!-- 后置通知--> <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal" /> <!-- 环绕通知--> <aop:around method="myAround" pointcut-ref="myPointCut"/> <!-- 异常通知--> <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/> <!-- 最终通知--> <aop:after method="myAfter" pointcut-ref="myPointCut"/> </aop:aspect> </aop:config>
1.Spring的通知类型
- 环绕通知 在目标执行前后实施增强,可以应用于日志、事务管理等功能。
- 前置通知 目标方法执行前实施增强,可以应用与权限管理等功能
- 后置通知 目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件(相当于最终通知)
- 异常通知 抛出异常后实施增强,应用于处理异常记录日志等功能
2.配置切面
<aop:aspect>元素会将一个已定义好的Spring Bean转换为切面Bean,定义完成后用ref属性即可引用该Bean。
3.配置切入点
通过<aop:pointcut>定义。当作为<aop:config>的子元素定义时,表示该切入点时全局切入点可被多个切面所共享。
当作为<aop:aspect>元素的子元素时,切入点只对当前切面有效
execution(* com.itheima.jdk.*.*(..)) 第一个*表示返回类型,表示所有类型;com.itheima.jdk表示需要拦截的包名,第二个*表示类名;第三个*表示方法名;(..)表示参数 ,..表示任意方法
给出表达式基本格式
excution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern 目标方法访问修饰符 public,private
- ret-type-pattern 返回值类型 void,String
- declaring-type-pattern 类路径
- name-pattern: 目标方法名
- param-pattern: 目标方法的参数
- throws-pattern: 需要被代理的目标方法抛出的异常
- ? 表示不必须配置项
4.配置通知
- pointcut : 切入点表达式
- pointcut-ref:一个已经存在的切入点名称,与pointcut只需要其中一个
- method:指定一个方法名(切面中的一个方法)
- throwing:只对<after-throwing>元素有效,用于指定一个形参名,异常通知方法通过该形参访问目标方法抛出的异常
- returing:只对<after-returning>元素有效,用于指定一个形参名,后置通知方法通过该形参访问目标方法的返回值
5.环境配置
- spring-aspects-4.3.6RELEASE.jar
- aspectjweaver-1.8.10 AspectJ框架提供的规范 http://mvnrepository.com/artifact/org.aspectj/aspectjweaver/1.8.10
注意:aspectjweaver最好使用新版本下面也给出目前的最新版maven,不然使用注解时可能会报 error at ::0 can't find referenced pointcut XXX
pom.xml
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>4.3.6.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.6</version> </dependency>
6.实施代码
/* 切面类此页编写通知 */ public class MyAspect { //前置通知 public void myBefore(JoinPoint joinPoint){ System.out.print("前置通知:模拟执行权限检查...,"); System.out.print("目标类是:"+joinPoint.getTarget()); System.out.println(",被植入增强处理的目标方法为:" +joinPoint.getSignature().getName()); } //后置通知 public void myAfterReturning(JoinPoint joinPoint,Object returnVal){ System.out.print("后置通知:模拟记录日志...,"); System.out.println("被植入增强处理的目标方法为:" +joinPoint.getSignature().getName()); } /*环绕通知 ProceedingJoinPoint 是JoinPoint的子接口,表示可以执行目标方法 1.必须是Object类型的返回值 2.必须接收一个参数,类型为 ProceedingJoinPoint 3.必须throws Throwable */ public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ System.out.println("环绕开始:执行目标方法之前,模拟开启事务..."); Object obj = proceedingJoinPoint.proceed(); System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..."); return obj; } //异常通知 public void myAfterThrowing(JoinPoint joinPoint,Throwable e){ System.out.println("异常通知:"+"出错了"+e.getMessage()); } //最终通知 public void myAfter(){ System.out.println("最终通知:模拟方法结束后释放资源..."); } } //测试类 public class TestXmlAspectj { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao userDao = (UserDao) applicationContext.getBean("userDao"); userDao.addUser(); } }
运行结果
前置通知:模拟执行权限检查...,目标类是:com.itheima.jdk.UserDaoImp1@791f145a,被植入增强处理的目标方法为:addUser
环绕开始:执行目标方法之前,模拟开启事务...
增加用户...
最终通知:模拟方法结束后释放资源...
环绕结束:执行目标方法之后,模拟关闭事务...
后置通知:模拟记录日志...,被植入增强处理的目标方法为:addUser
模拟出错
public void addUser() { int i=1/0; System.out.println("增加用户..."); } //输出 前置通知:模拟执行权限检查...,目标类是:com.itheima.jdk.UserDaoImp1@791f145a,被植入增强处理的目标方法为:addUser 环绕开始:执行目标方法之前,模拟开启事务... 最终通知:模拟方法结束后释放资源... 异常通知:出错了/ by zero
总结:后置通知只有在目标方法执行成功后才会被织入,而最终通知无论如何都会被织入。
5.基于注解的AspectJ
- @Aspect 定义一个切面
- @Pointcut 定义切入点表达式。需要个一返回值是void ,函数体为空的函数辅助
- @Before 用于顶一个前置通知。需要用value/pointcut指定切入点表达式 / 已有切入点
- @AfterReturning 后置通知。 需要指定 value/pointcut 和 returning属性
- @Around 环绕通知。有value/pointcut属性
- @AfterThrowing 处理程序中未处理的异常。 有value/pointcut和throwing属性
- @After 定义final通知 有value/pointcut属性
- @DeclareParents 定义引介通知
演示代码
/* 切面类,在此类中编写通知 */ @Aspect @Component public class MyAspect { //定义切入点表达式 @Pointcut("execution(* com.itheima.jdk.*.*(..))") //使用一个返回值为void、方法体为空的方法来命名切入点 public void myPointCut(){} //前置通知 @Before("myPointCut()") public void myBefore(JoinPoint joinPoint){ System.out.print("前置通知:模拟执行权限检查...,"); System.out.print("目标类是:"+joinPoint.getTarget()); System.out.println(",被植入增强处理的目标方法为:" +joinPoint.getSignature().getName()); } //后置通知 @AfterReturning("myPointCut()") public void myAfterReturning(JoinPoint joinPoint){ System.out.print("后置通知:模拟记录日志...,"); System.out.println("被植入增强处理的目标方法为:" +joinPoint.getSignature().getName()); } //环绕通知 @Around("myPointCut()") public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{ System.out.println("环绕开始:执行目标方法之前,模拟开启事务..."); Object obj = proceedingJoinPoint.proceed(); System.out.println("环绕结束:执行目标方法之后,模拟关闭事务..."); return obj; } //异常通知 @AfterThrowing(value="myPointCut()",throwing="e") public void myAfterThrowing(JoinPoint joinPoint,Throwable e){ System.out.println("异常通知:"+"出错了"+e.getMessage()); } //最终通知 @After("myPointCut()") public void myAfter(){ System.out.println("最终通知:模拟方法结束后释放资源..."); } }
配置文件
<!-- 指定需要扫描的包,使注解生效 --> <context:component-scan base-package="com.itheima"/> <!-- 启动基于注解的声明式AspectJ支持 --> <aop:aspectj-autoproxy />

浙公网安备 33010602011771号