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



相关文章
|
1月前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
301 0
|
2月前
|
Java API 开发者
Spring 控制反转与依赖注入:从玄学编程到科学管理
在传统开发中,手动`new`对象导致紧耦合、难以维护和测试。控制反转(IoC)将对象创建交给框架,实现解耦。Spring通过IOC容器自动管理对象生命周期,开发者只需声明依赖,无需关心创建细节。依赖注入(DI)是IoC的具体实现方式,支持构造器、Setter和字段注入。构造器注入推荐使用,保证依赖不可变且易于测试。对于多个同类型Bean,可用`@Qualifier`或`@Primary`解决冲突。此外,Spring还支持依赖查找(DL),开发者主动从容器获取Bean,适用于动态场景,但侵入性强。掌握IoC与DI,有助于构建灵活、可维护的Spring应用。
|
5月前
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
|
2月前
|
人工智能 监控 安全
Spring AOP切面编程颠覆传统!3大核心注解+5种通知类型,让业务代码纯净如初
本文介绍了AOP(面向切面编程)的基本概念、优势及其在Spring Boot中的使用。AOP作为OOP的补充,通过将横切关注点(如日志、安全、事务等)与业务逻辑分离,实现代码解耦,提升模块化程度、可维护性和灵活性。文章详细讲解了Spring AOP的核心概念,包括切面、切点、通知等,并提供了在Spring Boot中实现AOP的具体步骤和代码示例。此外,还列举了AOP在日志记录、性能监控、事务管理和安全控制等场景中的实际应用。通过本文,开发者可以快速掌握AOP编程思想及其实践技巧。
|
2月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
2月前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
6月前
|
人工智能 监控 Java
面向切面编程(AOP)介绍--这是我见过最易理解的文章
这是我见过的最容易理解的文章,由浅入深介绍AOP面向切面编程,用科普版和专家版分别解说,有概念,有代码,有总结。
|
7月前
|
Java API 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——Spring Boot 中的 AOP 处理
本文详细讲解了Spring Boot中的AOP(面向切面编程)处理方法。首先介绍如何引入AOP依赖,通过添加`spring-boot-starter-aop`实现。接着阐述了如何定义和实现AOP切面,包括常用注解如`@Aspect`、`@Pointcut`、`@Before`、`@After`、`@AfterReturning`和`@AfterThrowing`的使用场景与示例代码。通过这些注解,可以分别在方法执行前、后、返回时或抛出异常时插入自定义逻辑,从而实现功能增强或日志记录等操作。最后总结了AOP在实际项目中的重要作用,并提供了课程源码下载链接供进一步学习。
780 0
|
7月前
|
Java 开发者 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——什么是AOP
本文介绍了Spring Boot中的切面AOP处理。AOP(Aspect Oriented Programming)即面向切面编程,其核心思想是分离关注点。通过AOP,程序可以将与业务逻辑无关的代码(如日志记录、事务管理等)从主要逻辑中抽离,交由专门的“仆人”处理,从而让开发者专注于核心任务。这种机制实现了模块间的灵活组合,使程序结构更加可配置、可扩展。文中以生活化比喻生动阐释了AOP的工作原理及其优势。
392 0