2.2 AOP介绍
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
2.2.1 为什么使用AOP
在上一节中讲到定义了约定好的方法代理执行的流程(方法可前后和异常处理),那么我们编写的其他方法也可以按照这种方式织入实现约定好的流程中,避免了写大量重复的代码,也提高了代码的可维护性。而SpringBoot中是使用注解的方式将方法织入约定的流程中。
下面以数据库事务处理的情景说明其概念。传统的JDBC实现插入用户的代码。
public class UserService{
public int insertUser(){
UserDao userDao = new UserDao();
user user = new User();
user.setUsername("lei");
Connection cnnn = null;
int result = 0;
try{
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","1234");
//非自动提交事务
conn.setAutoCommit(false);
result = userDao.insertUser(conn, user);
//提交事务
conn.commit();
}catch (Exception e){
try{
//回滚事务
conn.rollback();
}catch (SQLException e){
e.printStackTrace();
}
e.printStackTrace();
}finally{
if(conn != null){
try{
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
return result;
}
}
public class UserDao{
public int insertUser(Connect conn, User user) throws SQLException{
PreparedStatement ps = null;
try{
ps = conn.prepareStatement("insert into user(username) values(?)");
ps.setString(1,user.getUsername());
return ps.excuteUpdate();
}finally{
ps.close();
}
}
}
从代码中可以看到从获取数据库连接、事务操控和关闭数据库连接的过程,都需要使用大量的try…catch…finally语句,这显然是大量重复的工作。这个处理流程是固定的,我们是否可以把执行SQL的方法织入这个流程中执行呢?这样就可以省略大量的重复代码。下面先看一下这个约定的流程。
我们需要的是只编写执行SQL的语句,可以改造成如下
使用这种将方法织入流程的方式后,在Spring中,我们的代码可以变成非常简洁
@AutoWired
private UserDao = null;
@Transactional
public int inserUser(User user){
return userDao.insertUser(user);
}
仅仅一个@Transactional就表明该方法需要事务运行,没有任何数据库打开和关闭,也没有事务回滚和提交的代码,却实现了数据库资源的打开、关闭、事务回滚和提交。这看起来和很棒。
2.2.2 AOP基本要素
在AOP中把上面说到的一些概念进行整合,并分别以相应的名称命名。
-
连接点( join point)
对应的是具体被拦截的对象,因为 Spring只能支持方法,所以被拦截的对象往往就是指特定的方法,例如,我们前面提到的 HelloServicelmpl的 sayHello方法就是一个连接点,AOP将通过动态代理技术把它织入对应的流程中 -
切点( point cut)
有时候,我们的切面不单单应用于单个方法,也可能是多个类的不同方法,这时,可以通过正则式和指示器的规则去定义,从而适配连接点。切点就是提供这样个功能的概念。 -
通知( advice)
就是按照约定的流程下的方法,分为前置通知( before advice)、后置通知(after advice)、环绕通知( around advice)、事后返回通知( after Returning advice)和异常通知( after Throwing advice),它会根据约定织入流程中,需要弄明白它们在流程中的顺序和运行的条件。目标对象( target):即被代理对象,例如,约定编程中的 HelloServicelmpl实例就是一个目标对象,它被代理了。
-
引入( introduction)
是指引入新的类和其方法,增强现有Bean的功能 -
织入( weaving)
它是一个通过动态代理技术,为原有服务对象生成代理对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程 -
切面( aspect)
是一个可以定义切点、各类通知和引入的内容, Spring AOP将通过它的信息来增强Bean的功能或者将对应的方法织入流程。
2.2.2 Spring AOP简单例子
上面说到了AOP基本概念及其作用,我们可以知道AOP可以很大的提高开发效率。下面以一个简单的例子说明Spring中的AOP使用方法。
确定链接点,也就是需要织入约定流程的方法。
public interface UserService {
void printUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void printUser(User user){
if(user == null){
throw new NullPointerException("用户为空");
}
System.out.println("用户名 = "+user.getName());
}
}
如之前说的MyInterceptor拦截器就像是一个切面,这里定义一个切面(约定的流程),同时定义切点(确定需要织入流程的方法)
@Aspect
public class MyAspect {
//定义切点,用于确定哪些方法需要使用织入切面中
@Pointcut("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..))")
public void pointCut(){
}
@Before("pointCut()")
public void before(){
System.out.println("before");
}
@After("pointCut()")
public void after(){
System.out.println("after");
}
@AfterReturning("pointCut()")
public void afterReturning(){
System.out.println("afterReturning");
}
@AfterThrowing("pointCut()")
public void afterThrowing(){
System.out.println("afterThrowing");
}
}
使用Controller来测试AOP是否成功
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService = null;
@RequestMapping("/print")
@ResponseBody
public User printUser(String name) {
User user = new User();
user.setName(name);
userService.printUser(name == null ? null : user);
return user;
}
}
配置Springboot启动类和注入切面到容器中。
@SpringBootApplication
public class AopApplication {
@Bean
public MyAspect myAspect(){
return new MyAspect();
}
public static void main(String[] args) {
SpringApplication.run(AopApplication.class, args);
}
}
项目启动后,浏览器输入
http://localhost:8080/user/print?name=lei
测试结果,方法printUser织入切面MyAspect中。
before
用户名 = lei
after
afterReturning
浏览器输入
http://localhost:8080/user/print
测试结果,方法printUser织入切面MyAspect中,异常正确处理
before
after
afterThrowing
2.2.3 AOP环绕通知
环绕通知在AOP中是最强大的通知,但是也难以控制,如果使用不当很容易出现问题。环绕通知可以取代原有目标对象的方法。也就说使用了环绕通知,调用的方法就不是原来的方法了,原来的方法将不会再执行了。当然它也能再环绕通知里面调用原来的对象的方法。
在上面的MyAspect切面中加入环绕通知
@Around("pointCut()")
public void arround(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("arround before");
//回调原有目标对象的方法
joinPoint.proceed();
System.out.println("arround after");
}
执行后可以看到打印结果
before
arround before
用户名 = lei
arround after
after
afterReturning
2.2.4 引入
在上面的程序中,当user为空时会抛出异常,从而进入切面的afterThrowing流程。但是当user不为空,但是用户名为空时确实正常执行没有异常的。这不是我们需要,假设需要的是用户名为空时不执行打印,也不进去切面流程。而在开发中也会遇到其他的接口我们需要增强其功能(不能修改原接口)的情况。这时我们可以引入新的接口来增强原来的接口。
public interface UserValidator {
boolean validate(User user);
}
public class UserValidatorImpl implements UserValidator {
@Override
public boolean validate(User user) {
System.out.println("引入新的接口:"+UserValidator.class.getSimpleName());
return !user.getName().isEmpty();
}
}
在MyAspect切面中引入新的类来增强服务
/**
* 引入新的类来增强服务
* value指要增强的目标对象
* defaultImpl指引入增强功能的类
* */
@DeclareParents(value = "xyz.mxlei.aop.UserServiceImpl+", defaultImpl = UserValidatorImpl.class)
public UserValidator userValidator;
测试上面引入的增强功能
@RequestMapping("/printValidate")
@ResponseBody
public User printUserValidate(String name) {
User user = new User();
user.setName(name);
//强制转换
UserValidator userValidator = (UserValidator) userService;
//验证用户名是否为空
if(userValidator.validate(user)){
userService.printUser(user);
}
return user;
}
执行发现,当name为空时,不会进入切面流程;当name不为空时,切面流程正常执行。
在上面的代码中,发现UserService对象怎么能够强制类型转换为UserValidator对象呢?。那么它是根据什么原理来增强原有对象功能的呢?,在切面生成代理对象的代码为:
Object proxy Proxy. newProxyInstance(
target getclass().getclassLoader(),
target getclass().getInterfaces(),
proxyBean);
这里的 newProxylnstance的第二个参数为一个对象数组,也就是说这里生产代理对象时, Spring
会把 UserService和 UserValidator两个接口传递进去,让代理对象下挂到这两个接口下,这样这个代理对象就能够相互转换并且使用它们的方法了。所以可以看到代码清单4-20中强制转换的代码,为了验证这点,我们可以加入断点进行测试。
2.2.5 参数传递到切面通知
在切面中的before,after等方法中,除了arround环绕通知默认带参数ProceedingJoinPoint可以获取原方法的参数,其他的通知也可以获取原参数,可以在切点处声明args(参数名)的方式传递参数过来。如下
//定义切点
@Pointcut("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..)) && args(user)")
public void pointCut(User user){
}
@Before("pointCut(user)")
public void before(User user){
System.out.println("before");
}
另外,也可以用JoinPoint传递参数。
2.2.6 织入
上面的例子中,printUser()方法其实就是织入了约定的流程(Aspect切面)中。在JDK中默认被织入的对象必须有接口才能被动态代理所处理运行,而在CGLIB中则不需要被织入的对象带有接口形式。Spring会自动判断被织入的对象是否带有接口,从而选择使用JDK或者CGLIB的实现。
2.2.7 多切面的执行顺序
Spring中可以多个切面同时作用于一个方法,这是各切面的通知执行顺序是乱的。这个时候有两种方式确定切面的执行顺序。
- @Order注解切面类
- 切面类实现Ordered接口
推荐使用@Order注解更为方便。
@Aspect
@Order(1)
public class MyAspect1 {
@Before("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..))")
public void before(User user) {
System.out.println("before");
}
}
@Aspect
@Order(2)
public class MyAspect2 {
@Before("execution(* xyz.mxlei.aop.UserServiceImpl.printUser(..))")
public void before(User user) {
System.out.println("before");
}
}
执行顺序为前置通知before根据order值从小到大,后置通知从大到小,这是典型的责任链模式的顺序。
附上源代码
https://github.com/mxxlei/study-spring-aop