Spring入门第六讲——Spring AOP的XML开发
发布日期:2021-06-30 18:00:23 浏览次数:3 分类:技术文章

本文共 15158 字,大约阅读时间需要 50 分钟。

什么是AOP?

Spring是用来解决实际开发中的一些问题的,AOP解决了OOP中遇到的一些问题,是OOP的延续和扩展。我们可从以下三个方面来理解AOP。

  • 第一个方面:扩展功能不是通过修改源代码实现的,这可通过Struts2框架中的拦截器来理解;

  • 第二个方面:AOP采用横向抽取机制实现。要理解横向抽取机制,就必须先认识纵向抽取机制。例如有如下的一个类:

    public class UserDao {
    public void save() {
    添加的逻辑... }}

    现在我们想在save()方法中要扩展一个功能,即日志添加功能,添加完该功能之后,就可记录在什么时候添加了哪个用户,我们想到的最原始的方法就是直接修改源代码。

    public class UserDao {
    public void save() {
    添加的逻辑... 直接写添加日志记录的代码以实现... }}

    很显然这是一种愚蠢的做法,并且这儿还有一个原则:修改功能一般不是直接修改源代码来实现的。顺其自然地,咱现在就要来讲纵向抽取机制了,这时我们可编写一个BaseDao类。

    public class BaseDao {
    public void wirtelog() {
    记录日志的逻辑... }}

    接下来让UserDao类继承BaseDao类,如下:

    public class UserDao extends BaseDao {
    public void save() {
    添加的逻辑... // 记录日志 super.wirtelog(); }}

    这样是不是就万事大吉了呢?你懂的!因为当父类的方法名称变化时,子类调用的方法也必然要进行修改。最后,终于要讲横向抽取机制了,横向抽取机制分为两种情况,下面分别加以简单阐述。

    • 有接口情况的横向抽取机制

      例如有一个如下接口:

      public interface UserDao {
      public void add();}

      接口的一个实现类如下:

      public class UserDaoImpl implements UserDao {
      public void add() {
      ... }}

    我们现在就可以使用动态代理技术来增强类里面的方法,即创建接口的实现类代理对象,并增强UserDaoImpl类里面的add()方法。

    • 无接口情况的横向抽取机制

      例如有一个如下User类:

      public class User {
      public void add() {
      ... }}

    我们现在也可以使用动态代理技术来增强类里面的方法,即创建被增强方法所在类的子类代理对象,并增强User类里面的add()方法。

  • 第三个方面:AOP底层使用动态代理技术实现。它同样也要分为两种情况:

    • 有接口情况:创建接口实现类代理对象;
    • 没有接口情况:创建增强方法所在类的子类代理对象。

为什么学习AOP?

因为在不修改源代码的情况下,即可对程序进行增强,所以使用AOP咱就可以进行权限校验、日志记录、性能监控以及事务控制了。

Spring AOP的由来

AOP最早是由AOP联盟的组织提出的,他们制定了一套规范,Spring将AOP思想引入到了它的框架中,所以它也必须遵守AOP联盟的规范。

Spring AOP的底层实现原理

Spring AOP的底层用到了两种代理机制,它们分别是:

  • JDK的动态代理:针对实现了接口的类产生代理,所以这种代理机制只能对实现了接口的类产生代理;
  • Cglib的动态代理:针对没有实现接口的类产生代理,采用的是底层的字节码增强的技术,生成当前类的子类对象(对没有实现接口的类产生代理对象,其实就是生成那个类的子类对象)。其实它有点类似于javassist第三方代理技术。

JDK的动态代理

首先创建一个动态web项目,例如spring_demo02_aop,然后导入Spring框架相关依赖jar包,要导入哪些jar包呢?老子不想说了,自己想去。然后,在src目录下创建一个com.meimeixia.spring.demo01包,并在该包下创建一个名为UserDao的接口。

package com.meimeixia.spring.demo01;public interface UserDao {
public void save(); public void update(); public void find(); public void delete();}

接着,在com.meimeixia.spring.demo01包下创建以上接口的一个实现类——UserDaoImpl.java。

package com.meimeixia.spring.demo01;public class UserDaoImpl implements UserDao {
@Override public void save() {
System.out.println("保存用户......"); } @Override public void update() {
System.out.println("修改用户......"); } @Override public void find() {
System.out.println("查询用户......"); } @Override public void delete() {
System.out.println("删除用户......"); }}

现在来了这样一个需求:我们想在UserDaoImpl实现类的save方法执行之前,进行一个权限校验。那该咋怎?可以使用JDK的动态代理来增强实现类里面的save方法,即创建该实现类的代理对象,并增强实现类里面的save方法。说干就干,在com.meimeixia.spring.demo01包下编写一个名为JDKProxy的类,在该类中使用JDK的动态代理机制来对UserDaoImpl实现类产生代理对象。

package com.meimeixia.spring.demo01;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;/** * 使用JDK的动态代理机制来对UserDao产生代理 * @author liayun * */public class JDKProxy implements InvocationHandler {
//得将被增强的对象传递到我们的代理当中 private UserDao userDao;//被增强的对象 public JDKProxy(UserDao userDao) {
this.userDao = userDao; } /* * 产生UserDao代理的方法 */ public UserDao createProxy() {
UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(), userDao.getClass().getInterfaces(), this); return userDaoProxy; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判断方法名是不是save? if ("save".equals(method.getName())) {
//增强save方法,进行一个权限校验 System.out.println("权限校验~~~~~~~~~~~~~~~"); return method.invoke(userDao, args); } return method.invoke(userDao, args); } }

紧接着,在com.meimeixia.spring.demo01包下创建一个SpringDemo01的单元测试类,其内容如下:

package com.meimeixia.spring.demo01;import org.junit.Test;public class SpringDemo01 {
/* * 假设:我想让你在save方法执行之前,进行一个权限校验 * * 如何自己手写代码:JDK的动态代理。 */ @Test public void demo01() {
/* UserDao userDao = new UserDaoImpl(); userDao.save(); userDao.update(); userDao.find(); userDao.delete(); */ UserDao userDao = new UserDaoImpl(); //创建代理 UserDao proxy = new JDKProxy(userDao).createProxy(); proxy.save(); proxy.update(); proxy.find(); proxy.delete(); } }

最后,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

在这里插入图片描述

Cglib的动态代理

首先,在src目录下创建一个com.meimeixia.spring.demo02包,并在该包下创建一个名为CustomerDao的类,我们将其作为目标类。

package com.meimeixia.spring.demo02;/** * 目标类 * @author liayun * */public class CustomerDao {
public void save() {
System.out.println("保存客户......"); } public void update() {
System.out.println("修改客户......"); } public void find() {
System.out.println("查询客户......"); } public void delete() {
System.out.println("删除客户......"); }}

现在来了这样一个需求:我们想在CustomerDao类中的save方法执行之前,进行一个权限校验。那该咋怎?这时就只能使用Cglib的动态代理来增强类里面的save方法了,即创建被增强方法所在类的子类代理对象,并增强类里面的save方法。说干就干,在com.meimeixia.spring.demo02包下编写一个名为CglibProxy的类,在该类中使用Cglib的动态代理机制来对CustomerDao类产生代理对象。

package com.meimeixia.spring.demo02;import java.lang.reflect.Method;import org.springframework.cglib.proxy.Enhancer;import org.springframework.cglib.proxy.MethodInterceptor;import org.springframework.cglib.proxy.MethodProxy;/** * Cglib动态代理 * @author liayun * *///一旦CglibProxy这个类实现了MethodInterceptor接口,那么这个类就相当于InvocationHandlerpublic class CglibProxy implements MethodInterceptor {
private CustomerDao customerDao; public CglibProxy(CustomerDao customerDao) {
this.customerDao = customerDao; } /* * 使用Cglib产生代理的方法 */ public CustomerDao createProxy() {
//1.创建Cglib的核心类的对象 Enhancer enhancer = new Enhancer(); //2.给它设置父类 enhancer.setSuperclass(customerDao.getClass()); //3.设置回调(回调类似于InvocationHandler对象) enhancer.setCallback(this); //4.创建代理对象 CustomerDao proxy = (CustomerDao) enhancer.create(); return proxy; } /* * proxy:即CustomerDao的代理对象,也即它的子类。 */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//判断方法名是不是save? if ("save".equals(method.getName())) {
//增强save方法,进行一个权限校验 System.out.println("权限校验~~~~~~~~~~~~~~~~"); return methodProxy.invokeSuper(proxy, args); } //如果不是save方法,那么执行子类的父类里面的方法(父类里面的方法是没有增强的) return methodProxy.invokeSuper(proxy, args); } }

然后,在com.meimeixia.spring.demo02包下创建一个SpringDemo02的单元测试类,其内容如下:

package com.meimeixia.spring.demo02;import org.junit.Test;public class SpringDemo02 {
/* * Cglib的测试 */ @Test public void demo01() {
CustomerDao customerDao = new CustomerDao(); //产生一个代理对象 CustomerDao proxy = new CglibProxy(customerDao).createProxy(); proxy.save(); proxy.update(); proxy.find(); proxy.delete(); }}

最后,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

在这里插入图片描述

Spring的基于AspectJ的AOP开发

AOP开发中的相关术语

咱们要进行Spring AOP的XML开发,那么得知道AOP开发中一些比较专业的术语。大家初次见到这些东西,可能不太懂,但代码写的多了,自然而然就会理解了。

在这里插入图片描述
为了让大家更好地理解这些AOP开发中比较专业的术语,下面我会通过一个代码示例来说明它们。
在这里插入图片描述

AspectJ的简介

AOP思想最早是由AOP联盟组织提出的,而Spring是使用这种思想最好的一个框架。Spring的AOP有自己实现的方式,但这种方式非常繁琐。正好,AspectJ是一个AOP的框架,所以Spring2.X引入了AspectJ来作为自身AOP的开发。这样看来,Spring其实有两套AOP的开发方式,它们分别是:

  • Spring传统方式,不过已经被弃用了;
  • Spring基于AspectJ的AOP的开发,这种方式使用的非常广泛,这儿讲的就是它。

说了这么多,那么啥是AspectJ呢?AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,也就是说AspectJ是一个基于Java语言的AOP框架。Spring2.0以后新增了对AspectJ切点表达式的支持。@AspectJ是AspectJ1.5新增的功能,通过JDK5注解技术,允许直接在Bean类中定义切面。新版本Spring框架,都建议使用AspectJ方式来开发AOP,使用AspectJ需要导入Spring AOP和AspectJ相关的jar包。

从上面的阐述中,我们应认识到AspectJ并不是Spring框架的一部分,而是一个单独的面向切面的框架,只不过它经常和Spring框架一起使用进行AOP的操作而已。使用AspectJ方式来开发AOP共有下面两种方式。

  • 第一种方式:基于AspectJ的XML配置文件的方式
  • 第二种方式:基于AspectJ的注解的方式

只不过本文讲解的是基于AspectJ的XML配置文件的方式,下一讲再讲第二种方式。

使用基于AspectJ的XML配置文件的方式进行AOP开发

引入相应的jar包

上面我说过,Spring其实有两套AOP的开发方式,它们分别是:

  • Spring传统的AOP开发:不过它已经被弃用了,如果真想使用这种方式,那么得导入下面两个jar包。
    在这里插入图片描述
  • Spring基于AspectJ的AOP的开发:使用这种方式,除了要导入最基本的jar包外,还需要导入Spring AOP和AspectJ相关的jar包。
    在这里插入图片描述

编写目标类

首先,在src目录下创建一个com.meimeixia.spring.demo03包,并在该包下创建一个名为ProductDao的接口。

package com.meimeixia.spring.demo03;public interface ProductDao {
public void save(); public void update(); public void find(); public void delete(); }

接着,在com.meimeixia.spring.demo03包下创建以上接口的一个实现类——ProductDaoImpl.java。

package com.meimeixia.spring.demo03;public class ProductDaoImpl implements ProductDao {
@Override public void save() {
System.out.println("保存商品......"); } @Override public void update() {
System.out.println("修改商品......"); } @Override public void find() {
System.out.println("查询商品......"); } @Override public void delete() {
System.out.println("删除商品......"); }}

创建增强的类以及增强的方法

现在来了这样一个需求:我们想在ProductDaoImpl实现类的save方法执行之前,进行一个权限校验,那该咋怎?这时我们可以编写一个切面类,并在切面类中编写一个进行权限校验的方法。待会,我们就在Spring配置文件中对其进行配置,让切面类中的权限校验方法在save方法执行之前执行。

package com.meimeixia.spring.demo03;/** * 切面类 * @author liayun * */public class MyAspectXML {
/* * 前置通知 * * 权限校验的方法 */ public void checkPri() {
System.out.println("权限校验~~~~~~~~~~~~~~"); } }

在Spring配置文件中进行配置

首先,在Spring配置文件中引入aop约束,那么问题来了,这个约束又该怎么写呢?可参考docs\spring-framework-reference\html目录下的xsd-configuration.html文件,在其内容中找到如下内容。

在这里插入图片描述
然后,在Spring配置文件配置好目标对象(即被增强的对象)和切面类。

接着,通过AOP的配置来完成对目标对象去产生代理。

在这里插入图片描述

编写一个单元测试类并进行测试

首先,在com.meimeixia.spring.demo03包下创建一个SpringDemo03的单元测试类,其内容如下:

package com.meimeixia.spring.demo03;import org.junit.Test;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;/** * AOP的入门 * @author liayun * */public class SpringDemo03 {
@Test public void demo01() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); ProductDao productDao = (ProductDao) context.getBean("productDao"); productDao.save(); productDao.update(); productDao.find(); productDao.delete(); } }

然后,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。

在这里插入图片描述

Spring整合JUnit单元测试

其实,Spring也可以整合JUnit单元测试,Spring对JUnit4进行了支持,可以通过注解方便的测试Spring程序,所以就不必写那么麻烦的单元测试类了。如果Spring真要整合JUnit单元测试,那么首先得导入如下jar包:

在这里插入图片描述
然后,将SpringDemo03的单元测试类修改成下面这个样子。

package com.meimeixia.spring.demo03;import javax.annotation.Resource;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/** * AOP的入门 * @author liayun * *///下面是固定写法:@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration("classpath:applicationContext.xml")//加载类路径下的配置文件public class SpringDemo03 {
//想用谁,就注入谁(但是注意这个类得交给Spring管) @Resource(name="productDao") private ProductDao productDao; @Test public void demo01() {
productDao.save();//作断点调试,可以看到使用到的是JDK的动态代理:JdkDynamicAopProxy productDao.update(); productDao.find(); productDao.delete(); } }

这里,大家可能会有一个疑惑,为什么在以上单元测试类中能直接使用@Resource注解完成属性的注入呢?这是因为咱引入了spring-test-4.2.4.RELEASE.jar这个jar包。引入该包之后,只能在单元测试类中去使用@Resource注解完成属性的注入,否则的话,你想在一些普通的类里面完成属性注入,那你肯定得配置组件扫描了。

此时,运行以上demo01单元测试方法,Eclipse控制台就会打印出如下内容。
在这里插入图片描述

演示其他通知类型

前置通知

前置通知是指在目标方法执行之前进行操作。上面我演示的就是前置通知,只不过,还有一点我还没有说到,那就是前置通知还可以获得切入点的信息。为了验证这一点,我们可以将以上MyAspectXML切面类修改成下面这个样子。

package com.meimeixia.spring.demo03;import org.aspectj.lang.JoinPoint;/** * 切面类 * @author liayun * */public class MyAspectXML {
/* * 前置通知 * * 权限校验的方法 */ public void checkPri(JoinPoint joinPoint) {
System.out.println("权限校验~~~~~~~~~~~~~~" + joinPoint); } }

此时,Spring的配置文件不用做修改,试着运行一下SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。

在这里插入图片描述

后置通知

后置通知是指在目标方法执行之后进行操作。它除了可以获得切入点的信息以外,还可以获得方法的返回值。为了验证这一点,首先,将ProductDao接口修改成下面这个样子(主要是修改了一下delete方法的声明,让其返回一个字符串)。

package com.meimeixia.spring.demo03;public interface ProductDao {
public void save(); public void update(); public void find();// public void delete(); public String delete(); }

然后,将以上接口的实现类(ProductDaoImpl.java)修改成下面这个样子。

package com.meimeixia.spring.demo03;public class ProductDaoImpl implements ProductDao {
@Override public void save() {
System.out.println("保存商品......"); } @Override public void update() {
System.out.println("修改商品......"); } @Override public void find() {
System.out.println("查询商品......"); } @Override public String delete() {
System.out.println("删除商品......"); return "删除商品成功"; }}

接着,在MyAspectXML切面类中添加一个日志记录的方法,一定要注意方法中参数的写法哟!

在这里插入图片描述
紧接着,咱还需要配置切入点和切面。
在这里插入图片描述
最后,运行一下SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。
在这里插入图片描述

环绕通知

环绕通知是功能最强的一个通知,它是指在目标方法执行之前和之后进行操作。很重要的一点就是它可以阻止目标方法的执行。为了验证这一点,我们可以在以上MyAspectXML切面类中添加一个性能监控的方法。

package com.meimeixia.spring.demo03;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;/** * 切面类 * @author liayun * */public class MyAspectXML {
/* * 前置通知 * * 权限校验的方法 */ public void checkPri(JoinPoint joinPoint) {
System.out.println("权限校验~~~~~~~~~~~~~~" + joinPoint); } /* * 后置通知 * * 日志记录的方法 */ public void writeLog(Object result) {
//你配置里面这个地方写的是啥,这个地方传参数就得传啥。 System.out.println("日志记录~~~~~~~~~~~~~~" + result); } /* * 性能的监控 */ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前增强~~~~~~~~~~~~~~"); Object obj = joinPoint.proceed();//相当于执行目标程序,有可能会有返回值 System.out.println("环绕后增强~~~~~~~~~~~~~~"); return obj; } }

接着,配置切入点和切面。这里,咱主要是在ProductDaoImpl实现类中的update方法(目标方法)执行之前和之后做一些事情。

在这里插入图片描述
最后,运行SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。
在这里插入图片描述

异常抛出通知

异常抛出通知是指在程序出现异常的时候而进行的操作,而且它还可以获得异常的信息。异常抛出通知能想到的一个应用场景就是在事务管理的时候会用到。为了验证这一点,首先,修改一下ProductDaoImpl实现类中的find方法,使其抛出一个除零异常。

package com.meimeixia.spring.demo03;public class ProductDaoImpl implements ProductDao {
@Override public void save() {
System.out.println("保存商品......"); } @Override public void update() {
System.out.println("修改商品......"); } @Override public void find() {
System.out.println("查询商品......"); int i = 10 / 0; }// @Override// public void delete() {
// System.out.println("删除商品......");// } @Override public String delete() {
System.out.println("删除商品......"); return "删除商品成功"; }}

然后,在MyAspectXML切面类中添加一个异常抛出的方法,一定要注意方法中参数的写法哟!

在这里插入图片描述
紧接着,咱还需要配置切入点和切面。
在这里插入图片描述
最后,运行SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。
在这里插入图片描述

最终通知

无论目标方法是否出现异常,最终通知都会执行。此时,我们已经知道了ProductDaoImpl实现类中的find方法(也即目标方法)抛出了一个除零异常。现在,咱就是要看看find方法(也即目标方法)出现了异常,最终通知会不会执行。为了验证这一点,我们在以上MyAspectXML切面类中添加如下的一个after方法。

package com.meimeixia.spring.demo03;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;/** * 切面类 * @author liayun * */public class MyAspectXML {
/* * 前置通知 * * 权限校验的方法 */ public void checkPri(JoinPoint joinPoint) {
System.out.println("权限校验~~~~~~~~~~~~~~" + joinPoint); } /* * 后置通知 * * 日志记录的方法 */ public void writeLog(Object result) {
//你配置里面这个地方写的是啥,这个地方传参数就得传啥。 System.out.println("日志记录~~~~~~~~~~~~~~" + result); } /* * 性能的监控 */ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕前增强~~~~~~~~~~~~~~"); Object obj = joinPoint.proceed();//相当于执行目标程序,有可能会有返回值 System.out.println("环绕后增强~~~~~~~~~~~~~~"); return obj; } /* * 异常抛出 */ //得到异常的信息 public void afterThrowing(Throwable ex) {
System.out.println("异常抛出通知~~~~~~~~~~~~~~" + ex.getMessage()); } /* * 最终通知:相当于finally代码块中的内容 */ public void after() {
System.out.println("最终通知~~~~~~~~~~~~~~"); } }

接着,配置切入点和切面。

在这里插入图片描述
最后,运行SpringDemo03单元测试类中的demo01方法,你就会看到Eclipse控制台打印出了如下内容。
在这里插入图片描述
从上面输出的结果中,我们就证明了即使目标方法出现了异常,最终通知也会执行。

Spring中切入点表达式的写法

Spring通过execution函数,可以定义切入点切入的方法。其语法为:[访问修饰符] 方法的返回值 包名.类名.方法名(参数)。下面,我就举几个例子演示一下Spring中切入点表达式的写法。

  • 匹配所有类的public方法
    在这里插入图片描述
  • 匹配指定包(但不包含其子包)下所有类的所有方法
    在这里插入图片描述
  • 匹配指定包及其子孙包下所有类的所有方法
    在这里插入图片描述
  • 匹配指定类的所有方法
    在这里插入图片描述
  • 匹配实现特定接口的所有类的方法
    在这里插入图片描述
  • 匹配所有以save开头的方法
    在这里插入图片描述
  • 匹配所有类里面的所有方法,这个太彪悍了!
    在这里插入图片描述

了解完上面的知识点以后,你可以试着解决一下这个需求:在ProductDaoImpl实现类中的所有方法执行之前,应用前置增强。此时,你的Spring配置文件可能要改成下面这个样子。

在这里插入图片描述

转载地址:https://liayun.blog.csdn.net/article/details/69663779 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Spring入门第八讲——Spring的JDBC模板
下一篇:Spring入门第一讲——Spring框架的快速入门

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2024年04月24日 06时48分39秒