Spring AOP面向切面编程(三)

简介: Spring AOP面向切面编程

四.基于注解配置Spring AOP


我还是用之前的项目来演示,把applicationContext的bean和AOP都去掉。然后通过注解来配置,然后再xml设置注解扫描。


<?xml version="1.0" encoding="UTF-8"?>
<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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--开启组件注解扫描-->
    <context:component-scan base-package="com.haiexijun"/>
    <!--启用Spring IoC的注解模式-->
    <aop:aspectj-autoproxy/>
</beans>


为dao和service增加注解:

UserDao.java


package com.haiexijun.dao;
import org.springframework.stereotype.Repository;
/**
 *用户表Dao
 */
@Repository
public class UserDao {
    public void insert(){
        System.out.println("新增用户数据");
    }
}


EmployeeDao.java


package com.haiexijun.dao;
import org.springframework.stereotype.Repository;
/**
 * 员工表Dao
 */
@Repository
public class EmployeeDao {
    public void insert(){
        System.out.println("新增员工数据");
    }
}


EmployeeService.java


package com.haiexijun.service;
import com.haiexijun.dao.EmployeeDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
 * 员工服务
 */
@Service
public class EmployeeService {
    @Resource
    private EmployeeDao employeeDao;
    public void entry(){
        System.out.println("执行员工入职业务逻辑");
        employeeDao.insert();
    }
    public EmployeeDao getEmployeeDao() {
        return employeeDao;
    }
    public void setEmployeeDao(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }
}


UserService.java


package com.haiexijun.service;
import com.haiexijun.dao.UserDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
 * 用户服务
 */
@Service
public class UserService {
    @Resource
    private UserDao userDao;
    public void createUser(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行创建用户的业务逻辑");
        userDao.insert();
    }
    public String generateRandomPassword(String type,Integer length){
        System.out.println("按"+type+"方式生成"+length+"位随机密码");
        return "abcdeffdasf";
    }
    public UserDao getUserDao() {
        return userDao;
    }
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}


上面都是IoC的配置,下面来进行AOP的配置

打开切面类,进行如下的配置:


package com.haiexijun.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * 切面类
 */
@Component//标记当前类为一个IoC的组件
@Aspect //说明当前类是一个切面类
public class MethodAspect {
    /**
     * 环绕通知
     * @param pjp ProceedingJoinPoint是一个特殊的连接点。是JoinPoint的升级版,在原有
     *            功能外,还可以控制目标方法是否执行。
     */
    //环绕通知,参数为PointCut切点表达式
    @Around("execution(public * com.haiexijun..*.*(..))")
    public Object printExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
        try {
            //得到起始时间
            Long startTime=new Date().getTime();
            Object obj= pjp.proceed();//执行目标方法
            //得到结束时间
            Long endTime=new Date().getTime();
            //执行时间
            long runTime=endTime-startTime;
            //如果目标方法的运行时间超过了1秒,就输出日志
            if (runTime>1000){
                String className=pjp.getTarget().getClass().getName();
                String methodName=pjp.getSignature().getName();
                SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
                String now =sdf.format(new Date());
                System.out.println("======"+now+":"+className+"."+methodName+" ( "+runTime+" ms) ====== ");
            }
            return obj;
        } catch (Throwable e) {
            //环绕通知,它通常只会去捕获对应异常的产生
            //但如果目标方法产生了异常,作为产生的异常
            //大多数情况下,它会向外抛出去。
            //把异常抛出去是因为在我们当前系统中,未来运行时可能
            //并不只有一个通知,那如果在当前的环绕通知中,对这个异常进行了消化
            //那就意味着其他后序的处理都不会捕捉到这个异常,就可能会产生一些意料之外的问题
            System.out.println("Exception message:"+e.getMessage());
            throw e;
        }
    }
}


当然,还有其他的通知的注解,比如@After、@Before、@AfterReturning等。这里不做演示了。

最后,我们打开程序入口类,运行一下,没有问题:


c96e88dbf4b74b59950dd41738d27021.png


五.AOP中的代理模式的应用


1.Spring AOP的实现原理


在未来,我们去找工作的时候,有一个问题,面试官会经常提起。那就是请你给我讲一下Spring AOP底层的实现原理是什么?

这个问题实在是太常见了,那作为Spring AOP底层的实现原理是什么呢?如下:


Spring是基于代理模式实现功能动态拓展,包含以下两种形式:

第一种是如果目标类实现了接口,则会通过JDK动态代理实现功能拓展。第二种是如果目标类没有实现接口,则Spring AOP底层通过CGLib这个第三方的组件实现功能的扩展。


此时就涉及到一个核心的问题,什么是代理模式?


2.静态代理


代理模式是指通过代理对象对原对象实现功能拓展。


那代理对象又是什么呢?其实在我们日常生活中随处可见,比如你去到一个全新的城市,希望租一套房子。这时你会怎么做,难道是我到处从电线杆上看这些求租的信息吗?肯定不会,大多数人的第一选择是找到中介公司,通过中介系统的数据库,去查询附近有哪些符合我要求的房子,看价钱是否合适。如果觉得还OK,那就由中介带着我去实地考察,看一下我是否满意,如果满意,那就成交,不满意的话,就继续找。而反过来,房东也依赖于中介。因为房东往往也有自己的工作,不可能天天拿着钥匙给租房的人开门去吧?这时房东就可以委托中介,房东把钥匙给中介,让中介的人带租房的人去看房子。可以看到中介的办事人员就是一个典型的代理人。这个案例放到我们程序中就称之为代理模式。


5dfaad549fdb439388b9a7d1a45b2cbf.png


所谓代理模式,其核心的理念是我们要去创建一个代理类,在代理类中,持有最原始的委托类。作为代理类和委托类,他们要共同实现相同的接口。而客户则是则是通过代理类,完成客户所需要的功能。按照刚才的例子,那个客户类呢,就是租房的人。代理类就是中介的办事人员,而委托类是房东。作为中介和房东,他们的目的是一致的。都是要把房子给租出去。正是因为有着相同的目的,所以他们就实现了共同的接口,这个接口中提供了一个租房的方法,代理类和委托类都去实现租房的这个逻辑。作为代理类,它内部持有了委托类的对象,所以在代理类被实例化以后,也就是代理对象执行的过程中,可以对原始的逻辑产生额外的行为。比如说这个中介代理类,在为客户看完房子以后,除了交付给原始的房东原始的租金以外,它还要向客户收取代理费,这就是额外的扩展逻辑了。放在程序中也是一样的。


那作为代理模式,我们如何去实现呢?下面通过代码来演示一下。


下面来创建一个新的Maven工程:

增加一个service包,在service包中增加一个接口。刚才强调过,无论是代理类和委托类他们都要实现相同的接口,这个接口名为UserService。我们模拟一下现实的环境,在这个用户服务接口中,提供一个createUser()方法,所有实现类都要去实现这个方法。那与此同时,在service包下再创建一个新的实现类,名为UserServiceImpl,它实现UserService接口和里面的方法。我们示意性书写一下。


UserService.java


package com.haiexijun.service;
public interface UserService {
    public void createUser();
}


UserServiceImpl.java


package com.haiexijun.service;
public class UserServiceImpl implements UserService {
    @Override
    public void createUser() {
        System.out.println("执行创建用户的业务逻辑");
    }
}


作为这段代码,我们调用其实非常简单,新增加一个Application类,在类中增加Main方法,然后编写代码:


package com.haiexijun.service;
public class Application {
    public static void main(String[] args) {
        UserService userService=new UserServiceImpl();
        userService.createUser();
    }
}


然后运行,当然没问题。


ed4b272648d9429ca1099bca27ef29de.png


但是我提出新的要求,希望将这个方法执行的时间打印出来,该这么办呢?这个需求我们之前在学习Spring AOP的时候已经遇到过了。但是是通过开发切面类来完成的。但是,如果放在我们代理模式中如何做呢?如果需要实现这个功能的扩展,就必须基于UserService接口创建对应的代理类。同时在代理类中去持有与之对应的具体实现。


下面来看一下具体做法:

在service包下面创建一个全新的类,名为UserServiceProxy 。代理的英文单词就是proxy。作为当前的代理类,其核心特点就是持有委托类的对象。定义一个私有的UserService类型的属性。接下来,关键的地方来了,这里定义一个带参的构造方法,参数为UserService。这个参数是在我们代理类实例化的时候,从外侧传入进来的,同时对内部的UserService来进行赋值。这样是不是就相当于在我们创建代理对象的时候,通过外侧传入的某个UserService的实现类,为内部的这个类的UserService赋值,相当于持有委托类的对象了。于此同时,不要忘记作为代理类和委托类都要实现相同的接口,也就是UserService,然后实现createUser()方法。在当前的代理类方法中,因为之前已经持有了委托类的对象,我们可以在createUser方法中发起委托类具体的职责,比如所createUser。同时,在这个方法执行前,我们还可以扩展其他的代码,比如对当前时间运行时的时间。这是不是就是功能的拓展啊?


UserServiceProxy.java


package com.haiexijun.service;
import java.text.SimpleDateFormat;
import java.util.Date;
public class UserServiceProxy implements UserService {
    //持有委托类的对象
    private UserService userService;
    public UserServiceProxy(UserService userService){
        this.userService=userService;
    }
    @Override
    public void createUser() {
        System.out.println("======"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date()));
        userService.createUser();
    }
}


与此同时,作为我们的客户,也就是使用者来说,不在直接面向UserServiceImpl,取而代之的是UserServiceProxy ,去面向这个代理类来调用。那么在代理类调用时,需要传入一个具体的UserServiceImpl。


application.java


package com.haiexijun.service;
public class Application {
    public static void main(String[] args) {
        UserService userService=new UserServiceProxy(new UserServiceImpl());
        userService.createUser();
    }
}

运行结果,这就是通过代理类实现功能的额外扩展


a4c89e51bc284580a85d0c19d7c8f3b0.png


代理模式可以嵌套使用的。

再打个比方,你手不是见过这样的情况,某一个租户,他把房子整栋租下来,然后再把房间租给很多其他的租客,这种形式在北京上海深圳这种房价高的地方,是很常见的。我们也称这种租客叫二房东。


那在代理模式中也是支持的。因为委托类和代理类都实现了相同的接口。同时,在创建对象的时候,又允许传入对应接口的实现类。因此,我们可以再创建一个全新的代理UserServiceProxy1,具体的做法和之前是一样的,实现UserService接口和方法。然后我们可以为create内扩展一些其他的业务代码,在UserService的createUser方法后拓展执行一个输出语句。这时,系统中就出现了两个代理类,一个是在createUser方法前执行,一个是在createUser方法后执行拓展


package com.haiexijun.service;
public class UserServiceProxy1  implements UserService{
    private UserService userService;
    public UserServiceProxy1(UserService userService){
        this.userService=userService;
    }
    @Override
    public void createUser() {
        userService.createUser();
        System.out.println("======后置扩展功能=====");
    }
}


那租客肯定是要面向二房东来执行的,下面就是new UserServiceProxy1了,里面传一房东UserServiceProxy。感觉有一点套娃。


package com.haiexijun.service;
public class Application {
    public static void main(String[] args) {
        UserService userService=new UserServiceProxy1(new UserServiceProxy(new UserServiceImpl()));
        userService.createUser();
    }
}


运行后,发现前置和后置的额外拓展功能都打印出来了。


679d3c31d9184bffb66dbe41fa810e2a.png


这就是代理模式的精妙之处。可以实现对功能的无限层次的拓展。但在这里,我们每进行一次功能的拓展,都要自己来创建一个代理类啊,这样有一个缺点,随着我们的功能的不断地扩大,每一个具体的实现类,他都要至少拥有一个代理类。而这个代理类是要按照这个规则来自己进行书写的。这样呢,假如我们系统中,有成百上千个具体的业务实现类,那就意味着,也有成百上千个具体的代理类来为具体实现类实现扩展职责。这会让我们系统变得无比的臃肿。对于这种必须要手动创建代理类的使用方式,我们称之为静态代理。 静态代理是最简单的一种代理模式的使用方式,但是也是最麻烦的一种使用方式。


那说起手动创建,就有与之对应的自动创建。在JDK1.2以后,由于反射机制的引入,为我们自动创建代理类提供了可能。那下面就来学习与静态代理对应的动态代理。


3.AOP底层原理—JDK动态代理


下面新创建一个Maven工程,因为JDK动态代理的功能不用到其他第三方组件,所以我们不用在Maven中添加任何依赖。然后,把上一个案例的UserService接口和UserServiceImpl实现类拿过来。


要想实现基于JDK动态代理来实现UserServiceImpl的功能扩展,首先要在service包下再额外的创建一个类ProxyInvocationHandler ,这个类要实现一个至关重要的接口InvocationHandler 。我们要实现它的invoke方法。这个invoke是不是在那里遇见过呢?没错,是在之前学习反射中遇到过,通过invoke来调用目标方法。这里的invoke也是一样的道理。


ProxyInvocationHandler实现InvocationHandler,它的职能非常明确。InvocationHandler是JDK提供的反射类,用于在JDK动态代理中对目标方法进行增强。 InvocationHandler实现类与Spring AOP的切面类的环绕通知类似。我们在invoke方法里面对目标方法进行增强。其中,invoke方法包含了3个参数。第一个参数Object代表了代理类对象,作为这个代理类对象,通常是由我们JDK动态代理自动生成的。第二个参数Method是目标方法对象,说明了目标方法的信息,包括方法名等。而第三个参数是一个Object数组,表示目标方法的实参。该方法返回一个Object,代表目标方法运行后的返回值。而最后抛出Throwable表示抛出目标方法异常。


package haiexijun.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ProxyInvocationHandler implements InvocationHandler {
    private Object target;//目标对象
    public ProxyInvocationHandler(Object target) {
        this.target = target;
    }
    /**
     * 在invoke方法对目标方法进行增强
     * @param proxy 代理类对象
     * @param method 目标方法
     * @param args 目标方法的实参
     * @return 目标方法运行后的返回值
     * @throws Throwable 目标方法抛出的异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("======"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(new Date())+" ========");
        Object ret =method.invoke(target,args);//调用目标方法
        return ret;
    }
}


然后在Application类中编写代码使用:


package haiexijun.service;
import java.lang.reflect.Proxy;
public class Application {
    public static void main(String[] args) {
        //UserService为目标对象
        UserService userService=new UserServiceImpl();
        ProxyInvocationHandler invocationHandler= new ProxyInvocationHandler(userService);
        //invoke方法要从传入代理类Proxy
        //创建动态代理类,
        // 通过newProxyInstance方法来创建,传入类加载器,类要实现的接口,还有对目标方法进行扩展的InvocationHandler
        UserService userServiceProxy  = (UserService)Proxy.newProxyInstance(userService.getClass().getClassLoader(),userService.getClass().getInterfaces(),invocationHandler);
        userServiceProxy.createUser();
    }
}


但是,动态代理必须要实现一个实现类的接口才能够运行,如果没有实现接口,反射过程必然会报错。可是在我们实际情况下,有着大量的类都没有实现接口,该怎么做呢?这时候,Spring又为我们提供了另外一种解决方案。依赖于spring的第三方组件CGLib实现对类的增强。


4.CGLib实现代理类


CGLib是运行时字节码增强技术。全名为Code Generation Library

当我们某一个类它没有实现接口的时候, Spring AOP 会在运行时生成目标继承类字节码的方式进行扩展。


下面来具体逻辑一下所谓生成目标继承类字节码:


f2b2e7f25d0a4fa886230e964b2d6c2c.png

上面有一个Service类,里面有一个findById的按id号查询的方法。里面写入具体的业务代码。可以看到里面的Service是没有实现任何接口的,那显然Jdk动态代理无法对其进行扩展。Spring看到这个类没有实现接口,则自动会使用CGLib通过继承的方式来对类进行扩展。这个继承类是在JVM运行过程中自动生成的,他的生成规则是,前面是类的原始名字,后面增加两个$$符号,然后加EnhancerByCGLIB(spring5以后是EnhancerBySpringCGLIB)。然后继承自Service父类。可以对findById方法进行重写,方法里面通过super指向父类的业务代码,并可以添加拓展的前置代码和后置代码等。客户端在调用的时候,面向的是这个增强的子类。

总结:

要增强的目标类实现了接口时,AOP底层调用的是JDK动态代理,没有实现接口时,AOP底层调用的是CGLib代理



相关文章
|
28天前
|
XML Java 数据安全/隐私保护
Spring Aop该如何使用
本文介绍了AOP(面向切面编程)的基本概念和术语,并通过具体业务场景演示了如何在Spring框架中使用Spring AOP。文章详细解释了切面、连接点、通知、切点等关键术语,并提供了完整的示例代码,帮助读者轻松理解和应用Spring AOP。
Spring Aop该如何使用
|
1月前
|
安全 Java 编译器
什么是AOP面向切面编程?怎么简单理解?
本文介绍了面向切面编程(AOP)的基本概念和原理,解释了如何通过分离横切关注点(如日志、事务管理等)来增强代码的模块化和可维护性。AOP的核心概念包括切面、连接点、切入点、通知和织入。文章还提供了一个使用Spring AOP的简单示例,展示了如何定义和应用切面。
90 1
什么是AOP面向切面编程?怎么简单理解?
|
2月前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
43 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
1月前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
30 1
|
1月前
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
61 5
|
1月前
|
安全 Java 测试技术
Java开发必读,谈谈对Spring IOC与AOP的理解
Spring的IOC和AOP机制通过依赖注入和横切关注点的分离,大大提高了代码的模块化和可维护性。IOC使得对象的创建和管理变得灵活可控,降低了对象之间的耦合度;AOP则通过动态代理机制实现了横切关注点的集中管理,减少了重复代码。理解和掌握这两个核心概念,是高效使用Spring框架的关键。希望本文对你深入理解Spring的IOC和AOP有所帮助。
35 0
|
2月前
|
Java 编译器 Spring
Spring AOP 和 AspectJ 的区别
Spring AOP和AspectJ AOP都是面向切面编程(AOP)的实现,但它们在实现方式、灵活性、依赖性、性能和使用场景等方面存在显著区别。‌
94 2
|
2月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
137 9
|
2月前
|
Java 数据库连接 Spring
【2021Spring编程实战笔记】Spring开发分享~(下)
【2021Spring编程实战笔记】Spring开发分享~(下)
31 1
|
2月前
|
Java 容器
AOP面向切面编程
AOP面向切面编程
43 0