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代理



相关文章
|
20天前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
41 1
|
11天前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
2天前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
23 9
|
29天前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
【9月更文挑战第9天】AOP(面向切面编程)通过分离横切关注点提高模块化程度,如日志记录、事务管理等。Micronaut AOP基于动态代理机制,在应用启动时为带有特定注解的类生成代理对象,实现在运行时拦截方法调用并执行额外逻辑。通过简单示例展示了如何在不修改 `CalculatorService` 类的情况下记录 `add` 方法的参数和结果,仅需添加 `@Loggable` 注解即可。这不仅提高了代码的可维护性和可扩展性,还降低了引入新错误的风险。
38 13
|
25天前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
2月前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
24 0
Spring高手之路22——AOP切面类的封装与解析
|
2月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
40 0
|
2月前
|
Java Spring 容器
彻底改变你的编程人生!揭秘 Spring 框架依赖注入的神奇魔力,让你的代码瞬间焕然一新!
【8月更文挑战第31天】本文介绍 Spring 框架中的依赖注入(DI),一种降低代码耦合度的设计模式。通过 Spring 的 DI 容器,开发者可专注业务逻辑而非依赖管理。文中详细解释了 DI 的基本概念及其实现方式,如构造器注入、字段注入与 setter 方法注入,并提供示例说明如何在实际项目中应用这些技术。通过 Spring 的 @Configuration 和 @Bean 注解,可轻松定义与管理应用中的组件及其依赖关系,实现更简洁、易维护的代码结构。
37 0
|
2月前
|
Java Spring 供应链
Spring 框架事件发布与监听机制,如魔法风暴席卷软件世界,开启奇幻编程之旅!
【8月更文挑战第31天】《Spring框架中的事件发布与监听机制》介绍了Spring中如何利用事件发布与监听机制实现组件间的高效协作。这一机制像城市中的广播系统,事件发布者发送消息,监听器接收并响应。通过简单的示例代码,文章详细讲解了如何定义事件类、创建事件发布者与监听器,并确保组件间松散耦合,提升系统的可维护性和扩展性。掌握这一机制,如同拥有一把开启高效软件开发大门的钥匙。
39 0
|
2月前
|
缓存 安全 Java
Spring AOP 中两种代理类型的限制
【8月更文挑战第22天】
16 0
下一篇
无影云桌面