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最早是由AOP联盟的组织提出的,他们制定了一套规范,Spring将AOP思想引入到了它的框架中,所以它也必须遵守AOP联盟的规范。
Spring AOP的底层用到了两种代理机制,它们分别是:
首先创建一个动态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控制台就会打印出如下内容。
首先,在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接口,那么这个类就相当于InvocationHandler
public 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 AOP的XML开发,那么得知道AOP开发中一些比较专业的术语。大家初次见到这些东西,可能不太懂,但代码写的多了,自然而然就会理解了。
为了让大家更好地理解这些AOP开发中比较专业的术语,下面我会通过一个代码示例来说明它们。
AOP思想最早是由AOP联盟组织提出的,而Spring是使用这种思想最好的一个框架。Spring的AOP有自己实现的方式,但这种方式非常繁琐。正好,AspectJ是一个AOP的框架,所以Spring2.X引入了AspectJ来作为自身AOP的开发。这样看来,Spring其实有两套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配置文件的方式,下一讲再讲第二种方式。
上面我说过,Spring其实有两套AOP的开发方式,它们分别是:
首先,在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配置文件中引入aop约束,那么问题来了,这个约束又该怎么写呢?可参考docs\spring-framework-reference\html目录下的xsd-configuration.html文件,在其内容中找到如下内容。
然后,在Spring配置文件配置好目标对象(即被增强的对象)和切面类。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="productDao" class="com.meimeixia.spring.demo03.ProductDaoImpl" />
<bean id="myAspect" class="com.meimeixia.spring.demo03.MyAspectXML" />
beans>
首先,在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对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通过execution函数,可以定义切入点切入的方法。其语法为:[访问修饰符] 方法的返回值 包名.类名.方法名(参数)
。下面,我就举几个例子演示一下Spring中切入点表达式的写法。
了解完上面的知识点以后,你可以试着解决一下这个需求:在ProductDaoImpl实现类中的所有方法执行之前,应用前置增强。此时,你的Spring配置文件可能要改成下面这个样子。