学习视频:8001 Spring AOP概述_哔哩哔哩_bilibili8006 基于注解的AOP实现_哔哩哔哩_bilibili
目录
在Spring的配置文件中,使用元素配置了5种常用通知,分别为前置通知、后置通知、环绕通知、返回通知和异常通知。
1.AOP概述
AOP的全称是Aspect Oriented Programming,即面向切面编程。和OOP不同,AOP主张将程序中相同的业务逻辑进行横向隔离,并将重复的业务逻辑抽取到一个独立的模块中,以达到提高程序可重用性和开发效率的目的。
在传统的业务处理代码中,通常都会进行事务处理、日志记录等操作。虽然使用OOP可以通过组合或者继承的方式来达到代码的重用,但如果要实现某个功能(如日志记录),同样的代码仍然会分散到各个方法中。
未使用AOP的面向切面编程案例
例如,订单系统中有添加订单信息、更新订单信息和删除订单信息3个方法,这3个方法中都包含事务管理业务代码,订单系统的逻辑如图所示。
Spring AOP术语
AOP并不是一个新的概念,AOP中涉及很多术语,如切面、连接点、切入点、通知/增强处理、目标对象、织入、代理和引介等,下面针对AOP的常用术语进行简单介绍。
连接点(Joinpoint)
连接点是程序执行过程中某个特定的节点,例如,某方法调用时或处理异常时。在Spring AOP中,一个连接点通常是一个方法的执行。
所以UserService中的四个方法都能叫做连接点
切入点(Pointcut)
当某个连接点满足预先指定的条件时,AOP就能够定位到这个连接点,在连接点处插入切面,该连接点也就变成了切入点。
要对save和delete方法做增强,所以切入点是这两个方法
通知/增强处理(Advice)
通知/增强处理就是插入的切面程序代码。可以将通知/增强处理理解为切面中的方法,它是切面的具体实现。
要做什么样的增强,这里是做日志输出的增强,所以Advice是printLog()方法
切面(Aspect)
切面是指关注点形成的类(关注点是指类中重复的代码),通常是指封装的、用于横向插入系统的功能类(如事务管理、日志记录等)。在实际开发中,该类被Spring容器识别为切面,需要在配置文件中通过<bean>元素指定。
这里比如输出日志到底是在save之前输出还是之后输出,配置这个关系的文件就是切面
目标对象(Target)
目标对象是指被插入切面的方法,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象。
比如UserService对象就是目标对象,它的save是要增强的方法
织入(Weaving)
将切面代码插入到目标对象上,从而生成代理对象的过程。织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。
比如这些关系给到Spring底层如何做的这个就叫织入
代理(Proxy)
将通知应用到目标对象之后,程序动态创建的通知对象,就称为代理。代理类既可能是和原类具有相同接口的类,也可能是原类的子类,可以采用调用原类相同的方式调用代理类。
比如给这个save方法增强后生成的对象
引介(Introduction)
引介是一种特殊的通知,它可为目标对象添加一些属性和方法。这样,即使一个业务类原本没有实现某一个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
2.Spring AOP的实现机制
Spring AOP的默认代理方式
默认情况下,Spring AOP使用JDK动态代理,JDK动态代理是通过java.lang.reflect.Proxy 类实现的,可以调用Proxy类的newProxyInstance()方法创建代理对象。JDK动态代理可以实现无侵入式的代码扩展,并且可以在不修改源代码的情况下,增强某些方法。
JDK动态代理
代码实现
public interface UserDao {
public void saveUser();
public void deleteUser();
}
public class UserDaoImpl implements UserDao{
@Override
public void saveUser() {
System.out.println("保存用户信息");
}
@Override
public void deleteUser() {
System.out.println("删除用户");
}
}
public class MyAspect {
public void check_permission()
{
System.out.println("模拟检查权限...");
}
public void log()
{
System.out.println("模拟日志输入出...");
}
}
public class MyProxy implements InvocationHandler {
private UserDao userDao;
public Object createProxy(UserDao userDao) {
this.userDao = userDao;
//创建jdk的动态代理对象需要3个参数 ,类加载器,被代理对象实现的接口,处理器(增强功能的具体实现)
ClassLoader classLoader = MyProxy.class.getClassLoader();
Class<?>[] interfaces = userDao.getClass().getInterfaces();
//创建动态代理对象
Object proxy= Proxy.newProxyInstance(classLoader, interfaces, this);
return proxy;
}
/**
*
* @param proxy 代理对象本身
*
* @param method 将要增强的方法
* @param args 方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
//创建增强类对象
MyAspect myAspect=new MyAspect();
myAspect.check_permission();
//保证原有功能的执行
Object object=method.invoke(userDao,args);
//日志增强
myAspect.log();
return object;
}
}
public class JdkProxyTest {
public static void main(String[] args) {
MyProxy myProxy=new MyProxy();
UserDao userDao=new UserDaoImpl();
UserDao proxyUserDao=(UserDao)myProxy.createProxy(userDao);
proxyUserDao.saveUser();
proxyUserDao.deleteUser();
}
}
CGlib代理
JDK与CGLib动态代理的比较
JDK动态代理存在缺陷,它只能为接口创建代理对象,当需要为类创建代理对象时,就需要使用CGLib(Code Generation Library)动态代理,CGLib动态代理不要求目标类实现接口,它采用底层的字节码技术,通过继承的方式动态创建代理对象。Spring的核心包已经集成了CGLib所需要的包,所以开发中不需要另外导入JAR包。
public class UserDao2 {
public void saveUser()
{
System.out.println("userDao2中的保存用户");
}
public void deleteUser()
{
System.out.println("userDao2中的删除用户");
}
}
import com.it.demo01.MyAspect;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyCglibProxy implements MethodInterceptor {
public Object createProxy(Object target) {
//被代理对象的类
Enhancer enhancer = new Enhancer();
//被代理对象的类
enhancer.setSuperclass(target.getClass());
//增强的具体代码实现
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
MyAspect myAspect = new MyAspect();
//增强的功能
myAspect.check_permission();
//保证原有功能的执行
methodProxy.invokeSuper(o, objects);
//增强的功能
myAspect.log();
return null;
}
}
public class CglibTest {
public static void main(String[] args) {
MyCglibProxy cglibProxy=new MyCglibProxy();
//被代理对象
UserDao2 userDao2=new UserDao2();
UserDao2 userDao21=(UserDao2)cglibProxy.createProxy(userDao2);
userDao21.saveUser();
userDao21.deleteUser();
}
}
3.基于XML的AOP实现
使用AOP代理对象的好处
因为Spring AOP中的代理对象由IoC容器自动生成,所以开发者无须过多关注代理对象生成的过程,只需选择连接点、创建切面、定义切点并在XML文件中添加配置信息即可。 Spring提供了一系列配置Spring AOP的XML元素。
配置Spring AOP的XML元素
配置切面
在Spring的配置文件中,配置切面使用的是<aop:aspect>元素,该元素会将一个已定义好的Spring Bean转换成切面Bean,因此,在使用<aop:aspect>元素之前,要在配置文件中先定义一个普通的Spring Bean。Spring Bean定义完成后,通过<aop:aspect>元素的ref属性即可引用该Bean。配置<aop:aspect>元素时,通常会指定id和ref两个属性。
<aop:aspect>元素的id属性和ref属性的描述
配置切入点
在Spring的配置文件中,切入点是通过<aop:pointcut>元素来定义的。当<aop:pointcut>元素作为<aop:config>元素的子元素定义时,表示该切入点是全局的,它可被多个切面共享;当<aop:pointcut>元素作为<aop:aspect>元素的子元素时,表示该切入点只对当前切面有效。定义<aop:pointcut>元素时,通常会指定id、expression属性。
<aop:pointcut>元素的id属性和expression属性描述
Spring AOP切入点表达式的基本格式
execution(modifiers-pattern?ret-type-pattern
declaring-type-pattern?
name-pattern(param-pattern) throws-pattern?)
execution表达式各部分参数说明
- modifiers-pattern:表示定义的目标方法的访问修饰符,如public、private等。
- ret-type-pattern:表示定义的目标方法的返回值类型,如void、String等。
- declaring-type-pattern:表示定义的目标方法的类路径,如com.itheima.jdk.UserDaoImpl。
- name-pattern:表示具体需要被代理的目标方法,如add()方法。
- param-pattern:表示需要被代理的目标方法包含的参数,本章示例中目标方法参数都为空。
- throws-pattern:表示需要被代理的目标方法抛出的异常类型。
配置通知
在Spring的配置文件中,使用<aop:aspect>元素配置了5种常用通知,分别为前置通知、后置通知、环绕通知、返回通知和异常通知。
<aop:aspect>元素的常用属性
代码实现
public interface UserDao {
public void save();
public void delete();
public void findById();
public void update();
}
public class UserDaoImpl implements UserDao{
@Override
public void save() {
System.out.println("保存用户信息");
}
@Override
public void delete() {
System.out.println("删除用户信息");
}
@Override
public void findById() {
System.out.println("根据id查询用户信息");
}
@Override
public void update() {
System.out.println("更新用户信息");
}
}
public class XmlAdvice {
/**
*
* @param joinPoint 内部封装了切入点的相关信息
*/
public void before(JoinPoint joinPoint)
{
System.out.println("前置通知");
System.out.println("目标类:"+joinPoint.getTarget());
System.out.println("切入点的方法名:"+joinPoint.getSignature().getName());
}
}
<context:component-scan base-package="com.it"></context:component-scan>
<bean id="userDao" class="com.it.demo03.UserDaoImpl"></bean>
<!--增强类所在的对象,切面类对象-->
<bean id="xmlAdvice" class="com.it.demo03.XmlAdvice"></bean>
<!-- aop配置根标签,声明开始aop配置-->
<!--在切面中配置增强和切入点的关系-->
<aop:config >
<!-- 全局的切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.it.demo03.UserDaoImpl.*(..))"></aop:pointcut>
<!--配置切面-->
<aop:aspect ref="xmlAdvice">
<!-- 配置关系-->
<aop:before method="before" pointcut-ref="pointcut"></aop:before>
</aop:aspect>
</aop:config>
public class XmlAopTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao=(UserDao)context.getBean("userDao");
userDao.delete();
userDao.findById();
userDao.save();
userDao.update();
}
}
public void after(JoinPoint joinPoint)
{
System.out.println("后置通知");
}
<aop:after method="after" pointcut-ref="pointcut"></aop:after>
这个时候在save()方法里搞个int i=1/0 测试的时候save方法会抛异常但是还是会执行,但是save后面的update方法没法执行了
<aop:after-returning method="after_returning" pointcut-ref="pointcut"></aop:after-returning>
这个是返回通知,返回值结束才会执行(如果出行异常,不会执行)
这个的话就是异常通知——出现异常才会执行
环绕通知
public Object around(ProceedingJoinPoint proceedingJoinPoint)
{
System.out.println("自定义环绕通知1111,在切入点之前执行");
//执行原有功能
Object res= null;
try {
res = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("自定义环绕通知2222,在切入点之后执行");
//返回原有方法的返回值
return res;
}
<aop:around method="around" pointcut-ref="pointcut"></aop:around>
4.基于注解的AOP实现
Spring AOP的注解
public interface UserDao {
public void save();
public void delete();
public void findById();
public void update();
}
public class UserDaoImpl implements UserDao {
@Override
public void save() {
System.out.println("保存用户信息");
}
@Override
public void delete() {
System.out.println("删除用户信息");
}
@Override
public void findById() {
System.out.println("根据id查询用户信息");
}
@Override
public void update() {
System.out.println("更新用户信息");
}
}
@Aspect
public class AnnoAdvice {
@Pointcut("execution(* com.it.demo04.UserDaoImpl.*(..))")
public void pointcut() {
}
/**
*
* @param joinPoint 内部封装了切入点的相关信息
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint)
{
System.out.println("前置通知2");
System.out.println("目标类:"+joinPoint.getTarget());
System.out.println("切入点的方法名:"+joinPoint.getSignature().getName());
}
@After("pointcut()")
public void after(JoinPoint joinPoint)
{
System.out.println("后置通知2");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint)
{
System.out.println("自定义环绕通知1111,在切入点之前执行2");
//执行原有功能
Object res= null;
try {
res = proceedingJoinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("自定义环绕通知2222,在切入点之后执行2");
//返回原有方法的返回值
return res;
}
@AfterReturning("pointcut()")
public void after_returning()
{
System.out.println("返回通知,返回值结束才会执行(如果出现异常,不会执行2)");
}
@AfterThrowing("pointcut()")
public void after_throwing()
{
System.out.println("异常通知,(出现异常才会执行2)");
}
}
public class AnnotationAopTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao=(UserDao)context.getBean("userDao2");
userDao.delete();
userDao.findById();
userDao.save();
userDao.update();
}
}