3、Spring的AOP详解和案例
AOP(Aspect Oriented Programming),即面向切面编程。
1、OOP回顾
在介绍AOP之前先来回顾一下大家都比较熟悉的OOP(Object Oriented Programming)。OOP主要是为了实现编程的重用性、灵活性和扩展性。它的几个特征分别是继承、封装、多态和抽象。OOP重点体现在编程架构,强调的是类之间的层次关系。
2、OOP缺陷
为了更好的说明OOP的概念,我们接下来讲一个OOP的实例,重点分析OOP存在哪些缺陷,以便更好的理解AOP的相关内容。
先看如下的一张图:
上面这张图有三个类:Dog,Cat和Duck,他们都有一个方法run。按照OOP的设计理念,我们很容易就会想到抽象出一个Animal父类,同时让这三个子类继承Animal父类。这样的设计可以用如下的图示表示:
在OOP思想中,我们会使用大量的类似上图的编程方式,对类进行抽象、继承、封装和多态来实现编程的重用性、灵活性和扩展性。但是这样的编程仍然有一定的局限性,有时候,OOP并不能很好解决我们再实际开发中遇到的问题。为了说明这个问题,看下面的图示:
看到上面的图,我们暂时还不能发现有什么问题。为了大家便于理解,接下来我来给大家讲解一下上面类图的实现过程。描述如下:马戏团有一条表演的小狗,这条小狗可以跑和跳,但是它完成跑和跳两个动作之前必须是在接到驯兽师发出的命令后,同时完成跑和跳的动作之后,驯兽师会给与响应的奖励,比如一块肉。
了解了实现过程之后,我们在来看一下具体的代码。
public class Dog { public void run() { System.out.println("驯兽师发出命令!") System.out.println("小狗开始跑!"); System.out.pringln("驯兽师给与奖励"); } public void jump() { System.out.println("驯兽师发出命令!") System.out.println("小狗开始跳!"); System.out.pringln("驯兽师给与奖励"); } }
仔细看上面的代码,我们可以看出在run方法和jump方法中,存在一些相同的内容(驯兽师发出命令和给与奖励),这些内容并不能完全进行抽象,即不能按照OOP编程思想进行处理。类似这样的情况同样会出现在我们编程中的很多地方,例如:日志记录、性能统计、安全控制、事务处理、异常处理等等。但是这样的情况该如何解决呢?这就引入了AOP编程思想。
3、AOP简介
AOP为Aspect Oriented Programming的缩写,即面向切面编程(也叫面向方面),是一种可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。
4、AOP实现实例
为了大家更好的理解AOP如何实现,接下来我们优化一下上述代码。
首先是Dog类
public interface Animal { public void run(); public void jump(); } public class Dog implements Animal{ public void run(){ System.out.println("小狗开始跑!"); } public void jump(){ System.out.println("小狗开始跳!"); } }
对比之前的代码我们可以明显看出,我们将关于驯兽师的相关内容从run和jump中进行了抽取,接下来,我们如何在程序运行中将关于驯兽师的动作加入到程序中呢?这就是我们这次用到的AOP实现的核心技术动态代理(Dynamic Proxy)。具体代码如下:
public class MyProxy implements InvocationHandler{ private Object targetObject; public Object createProxyInstance(Object targetObject) { this.targetObject = targetObject; return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { command(); Object ret = method.invoke(targetObject, args); award(); return ret; } private void command() { System.out.println("驯兽师发出命令!"); } private void award(){ System.out.println("驯兽师给与奖励!"); } }
上述代码实现完成之后,我们改如何调用呢?参考代码如下:
public class Client { public static void main(String[] args) { MyProxy hander = new MyProxy(); Animal dog = (Animal)hander.createProxyInstance(new Dog()); dog.run(); dog.jump(); } }
执行结果如下:
关于AOP编程的实例演示就完成了,接下来重新回顾一下AOP与OOP的相关概念。
5、AOP与OOP的关系
OOP针对业务处理过程的实体(Dog、Cat、Duck)及其属性和行为(run)进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中(run或jump)的切面(command和award)进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。
一、AOP案例如下:
1、创建如下项目结构
2、在com.entity包下创建User.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.entity; 2 3 public class User { 4 private Integer id; // 用户ID 5 private String username; // 用户名 6 private String password; // 密码 7 private String email; // 电子邮件 8 9 // getter & setter 10 public Integer getId() { 11 return id; 12 } 13 14 public void setId(Integer id) { 15 this.id = id; 16 } 17 18 public String getUsername() { 19 return username; 20 } 21 22 public void setUsername(String username) { 23 this.username = username; 24 } 25 26 public String getPassword() { 27 return password; 28 } 29 30 public void setPassword(String password) { 31 this.password = password; 32 } 33 34 public String getEmail() { 35 return email; 36 } 37 38 public void setEmail(String email) { 39 this.email = email; 40 } 41 42 @Override 43 public String toString() { 44 return "User [email=" + email + ", id=" + id + ", password=" + password 45 + ", username=" + username + "]"; 46 } 47 48 49 50 }
3、在com.dao包下创建IUserDao.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.dao; 2 3 import com.entity.User; 4 5 public interface IUserDao { 6 public void save(User user); 7 }
4、在com.dao.impl包下创建UserDaoImpl.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.dao.impl; 2 3 import com.dao.IUserDao; 4 import com.entity.User; 5 /** 6 * 用户DAO类,实现IDao接口,负责User类的持久化操作 7 */ 8 public class UserDaoImpl implements IUserDao{ 9 /** 10 * 保存 11 */ 12 public void save(User user) { 13 // 这里并未实现完整的数据库操作,仅为说明问题 14 System.out.println("保存用户信息到数据库"); 15 } 16 }
5、在com.biz包下创建IUserBiz.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.biz; 2 3 import com.entity.User; 4 5 public interface IUserBiz { 6 public void addNewUser(User user); 7 }
6、在com.biz.impl包下创建UserBizImpl.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.biz.impl; 2 3 import com.biz.IUserBiz; 4 import com.dao.impl.UserDaoImpl; 5 import com.entity.User; 6 7 8 /** 9 * 用户业务类,实现对User功能的业务管理 10 */ 11 public class UserBizImpl implements IUserBiz { 12 13 // 声明接口类型的引用,和具体实现类解耦合 14 private UserDaoImpl dao; 15 16 17 public void addNewUser(User user) { 18 //调用用户dao的方法保存用户信息 19 dao.save(user); 20 21 } 22 23 public UserDaoImpl getDao() { 24 return dao; 25 } 26 27 public void setDao(UserDaoImpl dao) { 28 this.dao = dao; 29 } 30 31 32 public UserBizImpl() { 33 } 34 35 public UserBizImpl(UserDaoImpl dao) { 36 this.dao = dao; 37 } 38 39 40 41 42 }
7、在com.aop包下创建LoggerBefore.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.aop; 2 3 import java.lang.reflect.Method; 4 import java.util.Arrays; 5 6 import org.apache.log4j.Logger; 7 import org.springframework.aop.MethodBeforeAdvice; 8 9 /** 10 * 通过MethodBeforeAdvice实现前置增强 11 */ 12 public class LoggerBefore implements MethodBeforeAdvice { 13 private static final Logger log = Logger.getLogger(LoggerBefore.class); 14 15 public void before(Method method, Object[] arguments, Object target) 16 throws Throwable { 17 // Arrays.toString()数组内容转换为字符串 18 log.info("调用 " + target + "的" + method.getName() + "方法。方法传入参数:" 19 + Arrays.toString(arguments)); 20 } 21 22 }
8、在com.aop包下创建LoggerAfterReturning.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.aop; 2 3 import java.lang.reflect.Method; 4 import org.apache.log4j.Logger; 5 import org.springframework.aop.AfterReturningAdvice; 6 /** 7 * 通过AfterReturningAdvice实现后置增强 8 */ 9 public class LoggerAfterReturning implements AfterReturningAdvice { 10 private static final Logger log = Logger.getLogger(LoggerAfterReturning.class); 11 12 public void afterReturning(Object returnValue, Method method, 13 Object[] arguments, Object target) throws Throwable { 14 log.info("调用 " + target + "的" + method.getName() + " 方法方法返回值为" 15 + returnValue); 16 } 17 18 }
9、在src下创建applicationContext.xml
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 7 http://www.springframework.org/schema/aop 8 http://www.springframework.org/schema/aop/spring-aop-3.1.xsd"> 9 <!-- 实例化接口对象 --> 10 <bean id="userDao" class="com.dao.impl.UserDaoImpl"></bean> 11 12 <!-- 实例化业务对象 --> 13 <bean id="biz" class="com.biz.impl.UserBizImpl"> 14 <!-- 注入方式1:设置注入接口对象 --> 15 <!-- 16 <property name="dao" ref="userDao"/> 17 --> 18 <!-- 注入方式2:构造注入对象 --> 19 <!-- type表示参数的类型,index表示参数的位置索引 --> 20 <constructor-arg index="0" ref="userDao" /> 21 </bean> 22 23 <!-- 由于默认走jdk代理,有时可能找不到代理,所以此处指定走cglib代理 --> 24 <aop:aspectj-autoproxy proxy-target-class="true"/> 25 26 27 28 <!-- 实例化日志前置增强对象 --> 29 <bean id="loggerBefore" class="com.aop.LoggerBefore"></bean> 30 31 <!-- 实例化日志后置增强对象 --> 32 <bean id="loggerAfterReturning" class="com.aop.LoggerAfterReturning"></bean> 33 34 <!-- 35 面向切面编程的配置即将某个功能动态的配置到某个流程中,而不改变后台代码 36 实现业务代码和日志代码是完全分离的,经过配置后,不做任何代码的修改,就在addNewUser方法前后实现了日志输出 37 --> 38 <aop:config> 39 <!-- 切入点的配置 --> 40 <aop:pointcut id="pointcut" 41 expression="execution(public void addNewUser(com.entity.User))" /> 42 <!-- 将增强处理和切入点结合, --> 43 <aop:advisor pointcut-ref="pointcut" advice-ref="loggerBefore" /> 44 <aop:advisor pointcut-ref="pointcut" advice-ref="loggerAfterReturning" /> 45 </aop:config> 46 </beans> 47 48 applicationContext.xml
10、在src下创建log4j.properties
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 # rootLogger是所有日志的根日志,修改该日志属性将对所有日志起作用 2 # 下面的属性配置中,所有日志的输出级别是info,输出源是console 3 log4j.rootLogger=info,console 4 # 定义输出源的输入位置是控制台 5 log4j.appender.console=org.apache.log4j.ConsoleAppender 6 # 定义输出日志的布局采用的类 7 log4j.appender.console.layout=org.apache.log4j.PatternLayout 8 # 定义日志输出布局 9 log4j.appender.console.layout.ConversionPattern=%d %p [%c]%n - %m%n
11、在com.test包下创建Test.java
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 package com.test; 2 3 import org.springframework.context.ApplicationContext; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 6 import com.biz.IUserBiz; 7 import com.entity.User; 8 9 public class Test { 10 11 public static void main(String[] args) { 12 //读取和加载xml配置文件 13 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); 14 //获取配置文件的bean的实例 15 IUserBiz biz = (IUserBiz) ctx.getBean("biz"); 16 17 //实例化User对象 18 User user = new User(); 19 user.setId(1); 20 user.setUsername("test"); 21 user.setPassword("123456"); 22 user.setEmail("test@pbdevj.com"); 23 24 //添加信息 25 biz.addNewUser(user); 26 } 27 28 }
12、运行结果如下: