Spring基础篇:面向切面编程

简介: Spring基础篇:面向切面编程

第一章:代理设计模式

Spring的IOC当中封装了工厂模式,Spring的Aop当中封装了代理模式。静态代理设计模式 + 动态代理设计模式

一:几个前瞻问题

为什么需要代理设计模式

1:哪个层次最重要

在JavaEE分层的开发过程中,哪个层次对我我们来讲是最重要的

Controller,Service,dao层?是Service层,是业务层。

任何一个项目,任何一行代码都是为了满足用户的需求,就是Service层的业务操作,只有Service层才是真正满足用户需求的,dao是满足Service实现业务处理的,主体是Service层。在JavaEE分层开发中最为重要的Service层,

2:Service关键操作

DAO的调用

事务相关操作

额外功能也是Service层当中的业务,不属于核心业务,不具备业务功能,是可有可无的,代码量比较少,根核心功能做对比,这样的代码叫做附加功能的代码。

比方说:Service加事务?客户在组需求分析的时候不会控制事务的这个东西,在查询操作当中事务就是可无的,然而其他的东西,日志也是附加功能。

日志:记录,谁时间,什么事,结果,所谓的日志就是记录(用户,系统,spring)操作的流水账,比方说可以记录启动时spring的都干了点啥,比方说可以记录用户对系统所有的操作,搞一个这样的日志管理系统,结合Elk非常完美。重要操作需要记录这个信息,浏览这个上篇就是没有这么重要了。

性能监控功能,在开发过程中,看一下我们的这个业务操作用了多长时间,监控一下核心业务的性能,记录一个开始时间,记录一个结束时间,做一个差值就完事,这个性能监控也是一个操作。

service层是对我们来讲是很重要的,一个是核心功能,一个是额外功能,核心功能就是:业务运算+dao的调用,额外功能不属于业务,可有可无,代码量很小。

3:额外书写在Service层当中到底好不好?

额外功能写在Service层当中,到底好不好?站在Service层调用者来讲,Controller对于事务来讲肯定需要,保证数据的保证业务的完整性,数据的一致性,软件设计者角度Service当中不需要这些额外的功能,额外功能是可有可无的,不希望在Service层当中加

二:生活中的解决方案

房东不想张贴广告带着看房,执行签合同收钱

租客想看到广告在去看房

解决方法:中介–proxy(代理)这是中介代理的一个角色,广告和看房交由中介来干。

中介的方法名必须和房东的方法名保持一致,这意味着非核心的功能都交由代理来做,这意味着之间的调用关系也发生改变了,不再是租客调用房东,而是租客调用代理,而这个签合同和收钱这个操作租户是没有办法进行操作的,需要房东进行操作,所以代理在调用房东的方法就可以了,代理的方法名需要和房东的方法名完全一样,哪天对中介的这个业务不满意了,我们需要修改该中介的代码不满意了,不需要该中介的代码,只需要创建一个新的中介公司就可以了,核心功能可以稳定下来。好处在于附加功能不满意的时候不需要该,换个新的就可以了。

我们解决问题的方式:引入了一个代理类,调用了我们的目标类,使用了原始类的核心功能

三:代理设计模式

1:什么是代理设计模式

通过代理类为原始类增加额外的功能,我们要想为他增加日志事务这些额外的功能。

就要家代理类, 好处:利于原始类的维护

1):关键名词解释

目标类&&原始类:实际上就是我们的房东,指的也就是我们的业务类,它做的是核心运算和dao的调用

目标方法&&原始方法:目标类,原始类中的方法就是原始类和原始方法。

额外功能&&附加功能:主要以日志、事务、性能为代表,以代理类利于维护。

2):代理开发的核心要素

代理类: 目标类(原始类)、额外功能、和原始类实现相同的接口(中介所提供的方法和房东提供的方法必须一一对应,房东对应的userService,把这个生命成接口,日后这些方法都只完成业务运算和dao的调用)我们日后所开发的代理的中介类必须保证和目标方法已知,也得实现userService这个接口,来保证具有相同的方法,也即是说代理类必须和目标类实现相同的接口,这样的目的是为了二者的方法保持一致

2:静态代理的开发

public class UserServciceProxy implements UserService {
    private UserServiceImpl userService = new UserServiceImpl();
    @Override
    public void register(User user) {
        System.out.println("----log日志---");
        userService.register(user);
    }
    @Override
    public boolean login(String name, String Password) {
        System.out.println("-------log日志----------");
        return userService.login(name,Password);
    }
}

3:静态代理的缺陷

public class OrderServiceProxy implements OrderServcie{
    private OrderServiceImpl orderService = new OrderServiceImpl();
    @Override
    public void showOrder() {
        System.out.println("-------logs---------");
        orderService.showOrder();
    }
}

代理类最后的展现的结构是很相似的,都是在代理类当中实现了额外的功能又调用了原始的方法。

现在编写测试类进行测试,调用的应该调用代理类。

需要注意的是:我们现在应用的这个代理叫做静态代理类。特点就是有一个原始类就必须有一个代理类,都需要手工编写一个代理类。

静态代理存的问题

存在的项文件过多,不利于项目管理。

额外功能维护性差:额外功能吸怪复杂

第二章:Spring动态代理

一:Spring动态代理的概念

概念:通过代理类为原始类,目标类增加额外功能,利于原始类,目标类功能的维护。

二:Spring动态代理开发

1:搭建开发环境

<!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.1.14.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjrt -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.8</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.3</version>
        </dependency>

2:Spring动态代理的开发步骤

以下是Spring安排的动态代理的安排的格式,只有这样Spring才能识别。

1):创建原始类,创建原始类的对象

/**
 * @Auther: DaShu
 * @Date: 2021/6/15 18:57
 * @Description:
 */
public interface UserService {
    public void register(User user);
    public boolean login(String name, String Password);
}
<bean id="userService" class = "com.pactera.spring.proxy.UserServiceImpl"/>

2):额外功能

Spring定义了一组接口MethodBeforeAdvode接口,把额外的功能书写在接口的实现类当中,运行在原始方法执行之前运行额外功能。

<bean id="userService" class = "com.pactera.spring.proxy.UserServiceImpl"/>
    <bean id="before" class="com.pactera.spring.dynamic.Before"/>
/**
 * @Auther: DaShu
 * @Date: 2021/6/15 19:39
 * @Description:
 */
public class Before implements MethodBeforeAdvice {
    //作用:需要把运行在原始方法执行之前的额外功能,书写在before当中
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("---------------method before advice log-----------------");
    }
}

3):定义切入点

切入点:额外功能加入的位置。Spring引入这个切入点的目的,就是由程序员由程序员根据需要,决定额外功能加给哪个原始方法,切入点一旦定义好了之后,额外功能就会加给这些方法。

简单的测试:所有的方法都作为切入点,加入额外的功能。这个是在Spring的配置文件中完完成的。

<aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
    </aop:config>

4):整合

把第二部和第三部进行整合

<aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
        <!--组装,组长的目的就是为了把切入点与额外的功能进行整合-->
        <aop:advisor advice-ref="before" pointcut-ref="pc"/>
    </aop:config>

5):调用

获得Spring创建的动态代理对象并进行调用,前四部就是Spring为我们创建出来了遮掩给的代理对象,这样我们就可以进行调用了,前边的过程都依赖于Spring的工厂。所以,先创建Spring的工厂,那我们如何获取这个类的代理对象?

Spring的工厂通过原始对象的id值获取的代理对象,这是Spring的规矩,原始对象的id是userServcie,通过这个userServcie获取的就是代理对象的值,ctx.getBean(“userServcie”)获取的是代理对象,没有为什么,最终我们需要通过一个引用把这对象存起来,获得代理对象之后耦合与接口,就可以进行存储。

3:动态代理细节分析

动态代理和静态代理的区别

动态代理创建之后,所有的方法都可以复用,静态代理需要匹配一个方法就需要编写一个对应的方法,匹配一个类就需要编写一个对应的静态类,存在大量的冗余代码。

Spring的动态代理类在哪里。

这个代理对象一定是来自于代理类,那么这个代理类从哪里来?在我们的开发过程中这个代理类,这个代理类已知都没有看到,没有看到这个代理类为什么会有这个代理对象呢?

其实肯定是由这个代理类的,这个代理类spring是由spring框架在运行时通过动态字节码技术在虚拟机当中创建的,运行在jvm内部,等程序结束之后,会和jvm一起消失。所以咋编码的过程的过程中是没有办法看到这个代理类的,他会和虚拟机共存,最后随着程序的运行结束而结束。作为spring来讲为什么可以不写源码的情况就可以在虚拟机当中创建这个类呢,这其中是应用了一个动态字节码技术来完成。

什么叫动态字节码技术

java运行一个类就是虚拟机当中来运行这个类的字节码,进而通过这个字节码来创建这个类的对象,来进行响应的功能,编写完java文件之后,对这个java文件进行编译,生成字节码文件,字节码文件中存的就是这个类的字节码,虚拟机进行类加载之后,将字节码加载到虚拟机内存当中,虚拟机可以根据字节码创建响应的对象,基于动态字节码技术,我们不用编写java源文件了,也不用编译成class文件了,这两部都被省略了,此时动态字节码是通过第三方的框架来完成的,比如果ASM、Javassist、Cglib,这是三个比较有名的动态字节码框架,作为这些动态字节码框架,他们可以让我们让spring不写源文件

动态代理编程可以简化代理的开发

这样的话,只要我们加的额外功能是一样的,那么都能复用,这样就简单了。在额外功能不变的前提下,创建其他目标类(原始类)的代理对象时,只需要指定原始对象并进行拼接即可。

动态代理的维护性大大增强。

目标类或者叫原始类都加入了某额外功能,根据开闭原则(打开扩展关闭修改-关闭原则)只需要增加一个新的额外功能然后换掉即可。在spring的配置文件当中重新进行配置即可。这样无需修改原有的java代码这样就比较好。

总结:现在开发过程中已经没有人使用静态代理了,都是spring为我们提供好的动态代理。

动态代理详解

这是一个深化内容的分析,到现在为止呢spring的动态代理开发会分为四个过程目标:

第一个步骤是目标对象,负责核心功能的原始对象或者叫目标对象,额外功能需要实现一个MethodBeforeAdbice接口,切入点需要解决的是哪些方法需要加这些额外功能,哪些切入点不需要加这些额外功能,组装就是把第二部和第三部进行一个整合,

第三章:额外功能详解

额外功能实现了MethodBeforeAdvice接口,实现这个接口就需要实现这个接口当中的before方法,他的作用就是让额外功能运行在原始方法之前,这就是这个before方法的作用,before方法里边有着三个参数,method,Object[],Object,第一个参数method代表的就是要增加的目标方法或者叫原始方法,如何此时要增加的方法是login方法,那么久一Method代表的就是login方法,这个Method是变化的,Object[]代表的是原始方法的参数,Object对应的是原始对象,代表的是原始类或者目标类的实例。我们怎么证明这个事呢?可以通过debug来进行测试

methodbeforeAdvice这个接口核心的作用额外功能运行在原始方法执行之前,进行额外功能操作。

  • MethodBeforeAdvice中before方法中的三个参数如何使用呢?
    作为一个接口所规定的接口我们不一定都得用,根据实际情况进行使用就行,举例来讲:前面我们在学servlet的时候,servlet规定了一个方法,service方法,这个方法有两个参数httpRequest,httpResponse这两个参数,servlet这个接口为我们提供了这样的方法,在我们后续的使用的过程当中,我们没有使用过后边的参数,例如我们只用来接收客户端的请求参数,直接使用request这个参数就好了,如果我们使用servlet是进行页面响应的,那么我们使用response这个参数就好了,使用这个参数获取输出流输出流进行页面响应。通过
    before方法的参数在实战中,会根据需要进行使用。
  • MethodInterceptor 方法拦截器
    这个接口是完成额外功能实现的时候的第二个接口。这个接口和MethodBeforeAdvice有什么区别呢?只能运行在原始方法之前,相对来讲,功能单一,这个的接口的实现类的方法可以运行在原始方法执行之前,也可以运行在执行方法的执行只之后,这个功能更加的强大一点,所以在实际过程中我们更多的使用的事methodinterceptor这个接口,下面的这个:
/**
 * @Auther: DaShu
 * @Date: 2021/6/21 20:12
 * @Description:
 */
public class Around implements MethodInterceptor {
    @Override
    //这个接口的方法是有返回值的这也就解释了spring的注解开发的aop是Around方法里边的返回值是Object类型的
    //这个接口里边有一个invoke方法,我们也就把额外功能写入invoke当中,这个额外功能就能运行在原始方法之前,也能运行在原始方法之后。也可以运行在原始方法之前,之后。
    //原始方法怎么运行呢?这就需要知道这个方法的参数的含义:
    //这个方法的参数:这个就代表的事原始方法,底层相当于是对目标方法对象Method的一个更高级别的封装,可以认为他们两个设计相等的。methodInvocation.proceed()方法就代表目标方法执行了。
    //这个MthodInvocation就代表的事目标方法,而这里的methodinvocation.invoke就代表目标方法的运行。
    //到这:讲清楚了原始方法的运行是怎么回事的,
    //这个方法的返回值:这个返回值的原因在于实现接口的时候,接口按照接口返回值的类型,invoke方法有返回值 ,这里的Object
    //代表的就是原始方法执行之后的返回值,所以这个方法必须有一个Object,如果原始方法的返回值是void,那么我们返回一个null就行了。
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        return null;
    }
}
<?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:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="userService" class = "com.pactera.spring.proxy.UserServiceImpl"/>
    <bean id="orderService" class = "com.pactera.spring.proxy.OrderServiceImpl"/>
<!--    <bean id="before" class="com.pactera.spring.dynamic.Before"/>-->
    <bean id = "around" class = "com.pactera.spring.dynamic.Around"/>
    <aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
        <!--组装,组长的目的就是为了把切入点与额外的功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
</beans>
/*
     * @Target: 用于测试Arround方法
     * @Author: DaShu
     * @Date: 2021/6/21 20:53
     * @Result:
     */
    @Test
    public void test3(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext3.xml");
        com.pactera.spring.proxy.OrderServcie orderService = (com.pactera.spring.proxy.OrderServcie)ctx.getBean("orderService");
        orderService.showOrder();
        //...........................这里是额外功能...............................
        //OrderServiceImpl.showOrder
        //-------------------------------这里还是额外功能----------------------------------------
    }

总结:MehtodInterceptor这个接口的额外功能就是实现这接口的类被spring创建的对象会被视为代理对象,实现并重写的方法invoke方法中书写额外功能,额外功能可以在目标方法前执行,可以在目标方法后执行,这是一种环绕执行的手段,很常用。

总结2:让额外功能运行在原始方法执行之后执行,这就是废话。

总结2:让额外功能运行在原始方法执行之前后执行,这也是废话。

总结3:什么样的额外功能在方法的运行之前和运行之后都要添加呢?

(日志,事务,性能,)-中的事务。如图:

还有一个额外功能不拘于之前之后,是运行在原始方法抛出异常的时候。

额外功能执行在原始方法抛出异常的时候,这样的话,我们就在原始方法抛出异常的时候,在这个invoke当中的proceed方法周围进行trycatch,将异常进行捕获,捕获之后,将额外功能写到catch的代码快中就行了。

package com.pactera.spring.dynamic;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
/**
 * @Auther: DaShu
 * @Date: 2021/6/21 20:12
 * @Description:
 */
public class Around implements MethodInterceptor {
    @Override
    //这个接口的方法是有返回值的这也就解释了spring的注解开发的aop是Around方法里边的返回值是Object类型的
    //这个接口里边有一个invoke方法,我们也就把额外功能写入invoke当中,这个额外功能就能运行在原始方法之前,也能运行在原始方法之后。也可以运行在原始方法之前,之后。
    //原始方法怎么运行呢?这就需要知道这个方法的参数的含义:
    //这个方法的参数:这个就代表的事原始方法,底层相当于是对目标方法对象Method的一个更高级别的封装,可以认为他们两个设计相等的。methodInvocation.proceed()方法就代表目标方法执行了。
    //这个MthodInvocation就代表的事目标方法,而这里的methodinvocation.invoke就代表目标方法的运行。
    //到这:讲清楚了原始方法的运行是怎么回事的,
    //这个方法的返回值:这个返回值的原因在于实现接口的时候,接口按照接口返回值的类型,invoke方法有返回值 ,这里的Object
    //代表的就是原始方法执行之后的返回值,所以这个方法必须有一个Object,如果原始方法的返回值是void,那么我们返回一个null就行了。
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("...........................这里是额外功能...............................");
        Object proceed = null;
        try {
            proceed = methodInvocation.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("-------------------------------这里还是核心功能------------------------");
        }
        System.out.println("-------------------------------这里还是额外功能----------------------------------------");
        return proceed;
    }
}

MethodInterceptor中的可以以影响原始方法中的返回值,我们在添加功能的时候我们可以获取到原始方法的返回值,然后我们可以把原始方法的返回值进行修改,这样就达到了修改的目的。

其实你看spring的的这个aop确实挺强大的,但是呢,前提是你得按照他的aop的4个方法和步骤进行实现才行,最基本的附加功能类和目标类必须得是spring进行创建的把,切入点也得定义把,然后也得组装把,只有孩子这样才能搞出来这个aop。

切入点的详解:

所谓的切入点:所有的切入点决定了额外功能的加入的位置,

<aop:pointcut id=“pc” expression = execution(* (…))
expression = excution(
*(…))–>>匹配了所有的方法但是这样处理就太粗暴了,真心的蛋疼。

excution()这个东西叫做切入点函数

  • *(…)切入点表达式—真正起作用的是这个部分

切入点表达式

方法切入点表达式
* *(..) -->这样匹配的是所有的表达式
定义一份方法的时候有五要素,修饰符、返回值、方法名、参数表、方法体
认为划分的话就是实际上就是三个部分,修饰符+返回值    方法名   参数表
分别对应的事*  * (..),这里是由严格的对应关系的,*的含义是通配符,*表示的对什么没有要求,*表示的事对所有的事的匹配。
* --->修饰符  返回值没有要求
* --->方法名没有要求
() --->参数表
.. --->对于参数没有要求(参数有没有都行,参数有几个都行,参数是什么类型都行)

定义login方法作为切入点
* login(..)
<aop:pointcut id="pc" expression  = execution(* login(..))
定义register方法作为切入点
* register(..)
定义对于某些参数类型的方法作为切入点,一般都是指定具体的方法名
定义login方法且login方法中有两个字符串的参数作为切入点
这里的参数的个数和类型仿照底下写就可以了,他看中的是个数和参数的类型。
* login(String,String)  这里只关注参数的类型,
/*
     * @Target: 用于测试* login(String,String)这个切入点表达式
     * @Author: DaShu
     * @Date: 2021/6/21 20:53
     * @Result: 这个切入点定义到了login方法,并且具有两个参数,并且两个参数都是String类型;
     */
    @Test
    public void test5(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext3.xml");
        UserService userService = (UserService)ctx.getBean("userService");
        userService.login("zhangjie","zhangjie");
        //...........................这里是额外功能...............................
        //UserServiceImpl.login
        //-------------------------------这里还是额外功能----------------------------------------
    }

如果并没有定义的方法切入点的话,那么aop是不会抛出异常的仅仅不执行aop功能而已。如果参数中的参数不是java.lang包下的,需要写全限定类名来定义切入点方法的的参数类型,例如:

* login(com.tk.medule.Bean,com.tk.module.bean1);
* 不是java.lang包下的类写要写全限定类名。 
*  login(String,..) //我们longin方法第一个参数是String,后边的参数有没有,有几个都无所谓。login(String)login(String,String)login(String,com.shit.ShitClass);
..可以和具体的参数类型连用。
运行结果和上面一致。

我们目前所讲解的方法的切入点表达式相对比较简单,而且不精准

  • 不精准的原因在这:
  • login(…)这样写专注于login方法,但是在不同的包下往往有着相同的文件名的类,他们中间有着相同的实现方法,

    这样会有多个满足要求的方法会经过匹配。这样写不精准,此时我们想选择的是某个一个类当中的方法的话,需要加上类的信息,这样也是不行的,还需要加上包的信息,这样包+类+方法,甚至:包+类+方法+参数 就精准了。这样应该这样写,这样写就相对来讲比较完整了。

  • 精准的方法切入点的返回值的限定。

<?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:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="userService" class = "com.pactera.spring.proxy.UserServiceImpl"/>
    <bean id="orderService" class = "com.pactera.spring.proxy.OrderServiceImpl"/>
<!--    <bean id="before" class="com.pactera.spring.dynamic.Before"/>-->
    <bean id = "around" class = "com.pactera.spring.dynamic.Around"/>
    <aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
        <aop:pointcut id="pc" expression="execution(* com.pactera.spring.proxy.UserServiceImpl.login(String,..))"/>
        <!--组装,组长的目的就是为了把切入点与额外的功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
</beans>
类切入点

假设我需要把一个类当中的所有的方法定义为切入点,这样我我们可以使用这个类切入点:

类切入点:指定类切入点,只不过是这样的一个称呼,所有的额外功能还是加在了这个类的全部的方法上,这样的形式是包+类:

详细的切入点配置如下,测试结果不贴出来了,和其他的一样没啥区别。

<?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:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="userService" class = "com.pactera.spring.proxy.UserServiceImpl"/>
    <bean id="orderService" class = "com.pactera.spring.proxy.OrderServiceImpl"/>
<!--    <bean id="before" class="com.pactera.spring.dynamic.Before"/>-->
    <bean id = "around" class = "com.pactera.spring.dynamic.Around"/>
    <aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
        <aop:pointcut id="pc" expression="execution(* com.pactera.spring.proxy.UserServiceImpl.*(String,..))"/>
        <!--组装,组长的目的就是为了把切入点与额外的功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
</beans>
* com.com.com.impl.*(..)   --->类中的所有方法加入了额外功能。这是第一种语法形式。

指定特定类作为切入点,额外功能加入的我这,自然这个类当中的所有的方法都会加上对应的额外功能的。

不同包下的同名的类作为类切入点,
* *.UserServiceImpl.*(..) -->这样写是不可以的,一个*只能匹配一级包,一般我们的类不会写在com包下的,这样写是有问题的,如果是多级包应该是*..表达的就是一级包乃至多级包。
* *..UserServiceImpl.*(..)-->这样写就能匹配多级目录下的这个类了。
任何包下的这个类都作为切入点。

包切入点表达式

我想为所有的某个包下的所有的方法添加切入点:

* com.baizhuedu.a.*.*(..) -->匹配这个包下的所有类的所有的方法。
注意上边这样写只能在a的包下,不能在a的子包下,子包下是不能被匹配到的。
* com.baizhuedu..a.*.*(..) -->匹配a包,包括a子包下的所有的类。

将来我们的这个切入店,包的切入点的使用价值更高。

切入点函数

切入点函数的作用就是用执行切入点表达式的函数,

excution:最为重要的一个切入点函数,也是功能最全的

执行方法切入点表达式,也可以执行类切入点表达式,也可以执行包切入点表达式

弊端:excution执行切入点表达式的时候,书写麻烦,比如:execution(* com.baizhiedu.proxy….(…);这样写起来很麻烦,spring为我们提供了其他的切入点函数,其他的切入点函数只是起到了简化execution切入点函数的作用,功能还是一样的的,本质上是没有任何区别的,简化的事execution书写的复杂度,但是功能上是完全一致的。

args
作用:主要用于函数(方法的)参数的匹配,
需求1:方法的切入点其他我不关系,参数必须是两个String的方法作为切入点
execution(* *(String,String)) -->使用excution这样写
args(String,String) -->使用args这样写。

代码和测试结果如下:

<aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
<!--        <aop:pointcut id="pc" expression="execution(* com.pactera.spring.proxy.UserServiceImpl.*(String,..))"/>    -->
        <aop:pointcut id="pc" expression="args(String,String))"/>
        <!--组装,组长的目的就是为了把切入点与额外的功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
/*
     * @Target: 用于测试* login(String,String)这个切入点表达式_测试多个切入点表达形式
     * @Author: DaShu
     * @Date: 2021/6/21 20:53
     * @Result: 这个切入点定义到了login方法,并且具有两个参数,并且两个参数都是String类型;
     */
    @Test
    public void test6(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext3.xml");
        UserService userService = (UserService)ctx.getBean("userService");
        userService.login("zhangjie","zhangjie");
        //...........................这里是额外功能...............................
        //UserServiceImpl.login
        //-------------------------------这里还是额外功能----------------------------------------
    }
whthin这个函数
主要用于类、包切入点的表达式的匹配。
需求:切入点想选择UserServiceImpl这一个类,不关心包
execution(* *..UserService.*(..)) -->使用execution方式这样写。
within(*..UserServiceImpl) -->这个不关系包只关心类的书写方法。
execution(* com.baizhiedu.proxy..*.*(..)) --使用execution这样写。
within(com.baizhiedu.proxy..*)

其实这种简化的写法就是在execution函数选择了一些东西,省略了一些东西。

@Annotation

作用:为具有特定注解的方法加入额外功能,这个是最核心的功能

分析一下:

<aop:config>
        <!--这个标签就是用来定义切入点的,expression这个是切入点表达式,代表所有的方法都要加上这个额外功能-->
        <!--所有的方法,都作为切入点作为额外功能。-->
<!--        <aop:pointcut id="pc" expression="execution(* com.pactera.spring.proxy.UserServiceImpl.*(String,..))"/>    -->
        <aop:pointcut id="pc" expression="@annotation(com.pactera.spring.annotation.Log)"/>
        <!--组装,组长的目的就是为了把切入点与额外的功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
/**
 * @Auther: DaShu
 * @Date: 2021/6/15 18:58
 * @Description:
 */
public class UserServiceImpl implements UserService{
    @Override
    @Log
    public void register(User user) {
        //这代表的事业务运算+dao的调用,额外功能不写在这里边。
        System.out.println("UserServiceImpl.register");
    }
    @Override
    public boolean login(String name,String password) {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}

切入点函数的逻辑运算

切入点函数的逻辑运算值得是整合多个切入点函数一起配合工作,进而完成更为复杂的需求

逻辑运算指的就是整合多个切入点函数的,让他们一起起作用,进而满足可能会遇到的更为复杂的需求,基本操作

and 与操作

案例1:login得叫login,参数是两个String类型即可。
execution(* login(String,String))  -->使用原生execution的书写方式
execution(* login(..))  and  args(String,String)  --使用切入点函数的逻辑运算。
两个表达式必须同时满足,两者取的是交集
注意:与操作不能用于同种类型的切入点函数,

or 或操作

两个切入点满足之一即可进行。

!非操作

<aop:config>
<!--        <aop:pointcut id="pc" expression="@annotation(com.spring.annotation.Log) "/>-->
        <aop:pointcut id="pc" expression="@annotation(com.spring.annotation.Log) and !execution(* com.spring.proxy.UserServiceImpl.re*(..))"/>
        <aop:pointcut id="pc01" expression="@annotation(com.spring.annotation.Log) and !execution(* com.spring.proxy.UserServiceImpl.register(..))"/>
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
@Test
    public void test1() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.register(new User());
        userService.register01(new User());
        //UserServiceImpl.register
        //UserServiceImpl.register01
    }
    @Test
    public void test2() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.login("sun", "123456");
        //...........................这里是额外功能1...............................
        //UserServiceImpl.login
        //-------------------------------这里还是外功能2----------------------------------------
    }

这个非操作使用的是!这个符号。

Spring动态代理开发总结

  • 为什么使用spring动态代理开发?
    动态代理开发就是代理开发,作用就是通过代理类对目标类或者叫做原始类增加额外的功能,好处就利于原始类的维护,这里代理类的好处,具体到spring的动态代理的来讲呢,一共是四步,spring创建目标对象,spring创建额外功能对象,定义切入点,整合切入点和额外功能。

第四章:AOP编程

Aop编程 Aspect Oriented Programming面向切面编程,什么叫做面向切

面编程,oop面向对象编程,pop面向过程编程

  • 面向对象编程含义

以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用来达到开发目的。

  • 面向过程编程

以过程为基本单位的层程序开发,以过程之间的相互协同,相互调用来达到开发的目的,这里的古城值得就是方法或者说函数

  • 面向切面编程
    以切面为基本单位的层程序开发,以切面之间的相互协同,相互调用来达到开发的目的。如果我们能找到切面,并且以切面进行开发,就是以切面进行开发

切面:切入点+额外功能。他们两个被组装后形成切面。这是spring动态代理的开发当中是面向切面编程的基础。spring的动态代理开发其实就是面向切面编程,这两个是完全等价的,Aop编程的背后的技术就是动态代理技术。

  • 什么是Aop编程
    Aop编程的本质就是Spring动态代理的开发,通过代理为原始类增加额外的功能,好处就是利于原始类的维护,有了Aop编程之后就可以取代oop这个说法是不正确的,她是从oop上发展来的,不存在 取代的关系,他仅仅是oop编程的一种有益的补充。

Aop的开发步骤

准备原始对象

准备原始功能

切入点

组装成切面(额外功能+切入点)

这里的开发步骤和spring的动态代理技术是没有任何区别的。

  • 名词解释-切面
    切面:切入点+额外功能
  • 为什么spring的切入点+额外功能叫做切面呢?
    不用管,没啥用。

    形成了一个切面,所以叫切面。

第五章:AOP底层实现原理

也就是动态代理的底层实现原理

  • 核心问题:

1、 Aop如何帮我们创建这个动态代理类,这个动态代理类是依赖于一门动态字节码技术,动态字节码技术到底是如何通过我们的编码将jvm中的动态代理类创建出来的呢?
2、Spring在做动态代理或者aop编程的时候,有一个非常有意思的事,他是怎么把原始对象的id值,拿到的事动态代理对象的id值的呢?
以上的问题就等效与:spring是如何创建和加工动态代理对象的。

如何创建动态代理类

spring创建动态代理类有两种方式,第一种方式是jdk动态代理,第二种是Cglib动态代理。

  • JDK动态代理
    这里研究的事动态代理对象的创建,而并不是动态代理类的创建,真正的动态代理类的创建是Proxy.newInstatnce(…)这个方法执行的时候,在jvm内存中创建的动态代理类,上边这个方法获取的是动态代理对象,

spring的动态代理或者说spring的aop的代理对象的创建和使用,他的本质也是使用的事jdk动态代理或者cglib动态代理,他的写法已经在是spring当中固定好了,所以,原始对象必须得是交由spring进行创建,另外本质的spring向外暴露的动态代理的实现方式还是对jdk底层动态代理实现或者cglib动态代理实现的封装。

创建动态代理的三要素:原始对象、额外功能、动态代理和原始类实现相同的接口。

这个动态代理在这一组参数当中,invocationHandler这是一个接口,这个接口对应了我们的额外功能,在这个接口当中有一个invoke方法,实现了这个接口之后呢,这个接口当中有三个方法,方法中有三个参数,如下,执行任何一个方法(原始方法)都需要三个要素,对象、方法、参数列表,不论是普通调用,或者使用反射,都是这么个情况,所以第二个参数是方法对象,第三个参数是方法的参数数组,还需要第一步创建的原始对象,这样就可以执行原始方法了,这个invoke方法的返回值代表的是原始方法的返回值,这里的这个接口当中的invoke方法是原生jdk提供的动态代理的实现方式,spring对我们暴露的实现方式呢有那个MethodInceptor这个接口,这个接口当中的invoke方法中有一个参数对象,可以执行proceed()方法其实这个就是对原生jdk的一个封装,原生的肯定复杂一点,封装之后的肯定更好用。

代理对象要和原始对象实现相同的接口,为什么我们动态代理对象要和原始对象实现相同的接口呢,因为都耦合与一个接口才能迷惑调用者呀,

我们创建的是jdk的动态代理,也需要实现和目标类相同的接口,这个条件不是我们来实现,而是proxy.newProxyInstance这个方法类帮我们实现,这里的传入的第二个参数就是干这个用的。提供这个参数是使用,是采用UserService.class对象,就可以使用userService.getClass().getInterfaces();这个形式获取就可以了。

//创建原始对象
        UserService userService = new UserServiceImpl();
        //创建动态代理基于jdk,创建动态代理基于jdk的方式,需要调用jdk为我们提供的一个工具proxy
        //这个方法的返回值就是这个类的动态代理对象。
        //这个方法需要三个参数,第一个参数

到这里我们实现动态代理的的三个要素就准备好了,原始对象,我们自己new的,

额外功能呢由于我们采用的事jdk形式的动态代理,所以我们使用的接口是,将额外功能书写在这接口实现类中的invoke方法当中,进行响应的处理,实现同样的接口,就是获取原始对象实现的接口传递给这个方法,然而在这个Proxy.newProxyInstatnce()这个方法当中还有一个来加载器对象这样的一个参数,这样的一个参数究竟作用是什么呢,这就是

类加载器的作用:(单纯谈类加载器的作用)

1.通过类加载器把对应类的字节码文件加载到虚拟机当中,

2.通过类加载器创建类的class对象,进而创建这个类的实例。

Class对象记录着这个类最完整的信息。

我们书写的java文件是java的源码文件,java文件经过编译之后的文件称为字节码文件,也就是.class文件,程序的运行是class文件加载到jvm内存当中之后被执行的结果,那么将class字节码文件加载到虚拟机当中是谁干的呢? 是类加载器,字节码进入到虚拟机当中之后,类加载器帮我们加载class文件到jvm内存之后,还会帮助我们创建这个类的Class对象,这里边有这个类最全的信息,进而可以创建这个类的实例。这个类加载器在我们的创建对象的过程当中是尤为重要的,类加载器这么重要,如何获取类加载器呢?

虚拟机会为每一个class分配对应的类加载器,有了这个类加载器之后呢,我们就可以创建这个类的字节码对象了,在动态代理的创建中为什么需要类加载器呢?动态代理也是在虚拟机当中获得动态代理类,进而帮我们创建动态代理对象,这个动态代理类没有源文件,也买有字节码文件,动态代理类他是怎么获取这个类的字节码来创建对象的呢?动态代理是通过动态字节码技术来创建字节码,直接就将字节码写到了虚拟机当中,没有一个加载字节码的过程,这个动态字节码技术在我们的虚拟机当中是怎么实现的?我们是怎么对他进行编程的呢?他暴露给我们程序员的就是Proxy.newProxyInstance()这个方法,这个方法中就会使用这个动态字节技术,作为这个动态字节码技术,而且我们在这个方法中已经传入了所有的创建动态代理类所需要的原材料,那么使用这个动态字节码技术呢,直接将字节码写到了虚拟机当中,所以呢类加载的第一个作用就没有用武之地了,这里的类加载器起的作用个呢是床架你这个动态代理类的Class对象,也就是发挥他的第二个技术,省略了一个加载的过程,有了这个字节码对象我们就能创建这个类的实例了(所有的创建对象都是如此),到了这里呢,就不太好搞了,之前的普通类加载的时候呢是虚拟机根据字节码文件(.class文件)进行分配,现在呢,动态代理类是动态字节码,并没有这个Class文件,没有这个class文件,虚拟机就不会为他分配类加载器,也就没有办法生成Class对象,此时在动态代理创建的过程当中,需要类加载器创建代理类的Class对象,可是动态代理类没哟对应的Class问文件,虚拟机也就不会为他分配类加载器,但是我们又需要,怎么办?借一个,借用的是源文件的类加载对象,来过来为动态代理类创建Class对象的,进而创建这个类的实例,这也是这里为什么需要一个类加载器参数的原因。借用谁的?比方说UserService,或者UserServiceImpl

package com.spring;
import com.spring.proxy.User;
import com.spring.proxy.UserService;
import com.spring.proxy.UserServiceImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
 * @Auther: DaShu
 * @Date: 2021/6/23 19:23
 * @Description:
 */
public class JdkProxyTest {
    public static void main(String[] args) {
        //创建原始对象
        UserService userService = new UserServiceImpl();
        //创建动态代理基于jdk,创建动态代理基于jdk的方式,需要调用jdk为我们提供的一个工具proxy
        //这个方法的返回值就是这个类的动态代理对象。
        //这个方法需要三个参数,第一个参数
        //为了简单采用内部类的方式来实现invocationhANDLER
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("-----------这里是代理对象中的额外功能-------------");
                //目标方法执行
                Object invoke = method.invoke(userService, args);
                System.out.println("-----------这里是代理对象中的额外功能-------------");
                return invoke;
            }
        };
        UserService proxyUserService = (UserService) Proxy.newProxyInstance(
                JdkProxyTest.class.getClassLoader(),
                userService.getClass().getInterfaces(),
                invocationHandler);
        proxyUserService.login("崔磊磊", "12345");
        proxyUserService.register(new User());
        //-----------这里是代理对象中的额外功能-------------
        //UserServiceImpl.login
        //-----------这里是代理对象中的额外功能-------------
        //-----------这里是代理对象中的额外功能-------------
        //UserServiceImpl.register
        //-----------这里是代理对象中的额外功能-------------
    }
}

我们说这个了类的类加载器是借的,我们借的谁的都行。

注意:jdk8.0之前呢 内部类访问外部类类的变量需要外部类的成员边量是final修饰的,jdk8.0之后呢会自动加上这个final,

  • CGLIB动态代理创建方式
    cglib动态代理也是spring底层默认支持的一种动态代理创建方式,他跟jdk动态代理的区别在哪里呢?
    jdk动态代理中:原始对象需要实现一个接口,比方说UserServiceImpl 实现 UserService接口,代理对象需要和原始兑现实现相同的接口,为什么会有一个这样的要求呢?第一保证代理类和原始类方法一致,第二点保证代理类当中提供对应方法的新的实现,就可以加入额外功能了,也可以调用原始方法,所以说我们的代理类,这样代理的诉求就实现了。这就是jdk的动态代理。
    作为cglib来讲创建动态代理类有什么特点呢?原始类没有实现任何接口,没有实现任何接口的这样的代理类创建代理对象的时候,反正我们的初衷就是我们的代理类和原始类中必须有相同的方法,新的实现里边包括这个额外功能和原始功能的调用,新的代理类可以使用什么来开发呢?cglib要求他所创建的这个代理类要去继承这个原始类,这样来保证你有的这个方法我也有,变相也保证了代理类和原始类有这个共同方法的要求,然后就有机会添加新的实现,调用原始方法super.就可以了,这样的实现的方式不同,但是最终的结果是一致的。cglib采用的事这个父子继承的关系,不管原始类有没有实现接口。

cgilib实现动态代理的的步骤,1,准备原始对象UserService原始对象的userService不需要实现任何的接口,里边准备两个原始方法,第二部,创建代理对象,创建代理对象需要cgilib对应的jar包,这个jar包spring已经默认帮我们引入进来了,这样我们就可以直接使用,cgilib的jar包中最为关键的类是一个Enhancer的接口,作为这个类来讲,我们调用create方法进行创建代理对象,这里边需要几个参数,他所需要的参数和jdk的参数是一模一样的,只不过这里边的接口类型指定的值父类的class对象,还有一个父类的原始功能,Enhancer.setClassLoader()Enhance.setSupperClass,Enhance.setCallBack(MethodInterceptor)这个接口是cgilib中的接口跟当时的spring不是一个接口,

创建这个cglib的Methodinterceptor的接口的时候,采用一个内部类的形式,创建他的子类的实例对象,子类对象需要重写他的intereceptor方法,

package com.spring.cglib;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
 * @Auther: DaShu
 * @Date: 2021/6/24 19:41
 * @Description:
 */
public class TestCglib {
    public static void main(String[] args) {
        UserService userService = new UserService();
        //通过cgilib方式进行创建,
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(TestCglib.class.getClassLoader());
        enhancer.setSuperclass(userService.getClass());
        MethodInterceptor interceptor = new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                System.out.println("----------cgilib日志额外功能。----------");
                //等同于invocationHandler中的invoke方法,
                Object invoke = method.invoke(userService, args);
                return invoke;
            }
        };
        enhancer.setCallback(interceptor);
        UserService proxyUserService = (UserService)enhancer.create();
        proxyUserService.login();
        proxyUserService.register();
        //----------cgilib日志额外功能。----------
        //UserService.login
        //----------cgilib日志额外功能。----------
        //UserService.register
    }
}
///**
// * @Auther: DaShu
// * @Date: 2021/6/24 19:39
// * @Description: 这个代表原始类。
// */
//public class UserService {
//    public void login(){
//        System.out.println("UserService.login");
//    }
//    public void register(){
//        System.out.println("UserService.register");
//    }
//}
  • 总结
    jdk动态代理,Proxy.newProxyInstance();,通过接口原始对象的代理类,cgilib动态代理依赖于Eahancer这个类,通过继承原始类创建原始类的代理类,在spring的后续过程中,如果在面试过程中可以把这些方法中的对应的参数说清楚,这是一个大大的加分项。

BeanPostProcessor分析

后置处理Bean,这个东西的全称叫做BeanPostProcessor,最主要的作用是对spring创建的对象进行再加工,这个是spring工厂中的一个非常有用的高级特性,在spring底层很多高级的封装的时候都有这个技术的影子,讲了aop底层的实现的时候,会发现这个技术的价值是非常高的,spring的aop底层实现很大的一部分是这个在进行技术进行技术支撑,

BeanPostProcessor是对spring的bean工厂进行在加工,spring工厂创建对象的过程是:spring获取spring的核心配置文件之后对spring的核心配置文件进行解析,获取到响应的bean标签会后,通过反射技术调用该类的构造方法,创建该类的实例,第二件事是spring对这个类中的成员变量按照配置进行注入,注入完成之后,调用这个类的初始化方法,对这个类进行一个初始化,这个初始化方法是我们自己定义的,经过这样的一个过程,这个对象就创建并初始化好了,现在就可以通过getBean通过id进行获取这个对象,而这个后置处理bean,是在工厂创建完成这个对象之后,对这个对象进行再次加工,这就是这门技术的意义,那么她是怎么实现对这个工厂创建出来的对象进行再次加工的呢?BeanPostProcessor是一个接口,我们需要把这个对象加工的内容写到接口实现类对应的方法中即可。

在这个接口当中呢有两两个方法,第一个方法是postProcessBeforeInitialization,另外的方法叫after,这样的接口,当Bean实现了这个接口之后,spring在创建这个对象的过程中,在spring完成调用构造方法创建实例,并依据配置进行注入之后,spring就会调用这个类的postProcessBeforeIntitialzation方法进行一个初始化操作,是通过这个方法中的第一个Object类型bean参数进行传递的,另外一个参数,是这个对象在spring中的id值。在这个方法中通过这两个参数就获取到了这个参数,就可以在方法中对这个实例进行加工了。还要把这个加工好的对象交还给spring,通过参数把这个对象进行处理,交还给spring之后,执行我们配置好的初始化方法,然后将这个对象交给after这个方法,通过这个方法进行一次在加工,加工完毕之后,将这个参数传递个返回给spring。最后在客户处可以通过getBean方法进行获取。

这就是bean引入了Beanpostprocessor这个技术之后的全部的过程分析。后置处理bean的运行原理分析。

对于使用者,我们的作用就是让我们的bean实现这样的接口和接口中的方法,返回的值是Object

创建实例—注入—before方法—初始化方法—After方法,

有了这两个方法之后呢,实际的过程中,我们我们极少做初始化操作,所以,我们区分这个之前之后,这两个操作就合二为一的,

实际上spring的初始操作使用的很少,所以,这个所谓的前后,也就没有什么前后之分了,两个挨着呢,实现其中的一个方法即可,也就是将代码写入到一个方法中就可以了。选谁,选After就可以了。但是啥也不干你也得有一个return bean得到操作。需要注意的事before这个操作,我们必须return bean对象,这张图使我们必须要理解的内容。

开发步骤

定义一个类实现这个接口

package com.spring;
import com.spring.postBeanProcessor.Catagory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
 * @Auther: DaShu
 * @Date: 2021/6/24 21:17
 * @Description:
 */
public class TestPostBeanProcessor {
    @Test
    public void test1(){
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext1.xml");
        //-----------------------before-----------------------
        //PostBeanProcessorTest{id=10, name='xioajianren'}
        //-----------------------before-----------------------
        //------------------------init-----------------------
        //PostBeanProcessorTest{id=11, name='xiaojianren1'}
        //--------------------------init---------------------
        //---------------------------after-------------------------------
        //PostBeanProcessorTest{id=12, name='xiaojianren2'}
        //---------------------------after-------------------------------
    }
}
package com.spring.postBeanProcessor;
/**
 * @Auther: DaShu
 * @Date: 2021/6/24 21:38
 * @Description:
 */
public class Catagory {
    private int id;
    private String name;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "PostBeanProcessorTest{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
    public void init(){
        System.out.println("------------------------init-----------------------");
        System.out.println(this.toString());
        System.out.println("--------------------------init---------------------");
    }
}
package com.spring.postBeanProcessor;
        import org.springframework.beans.BeansException;
        import org.springframework.beans.factory.config.BeanPostProcessor;
/**
 * @Auther: DaShu
 * @Date: 2021/6/24 21:04
 * @Description:
 */
public class PostBeanProcessorTest implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if(bean instanceof com.spring.postBeanProcessor.Catagory){
            System.out.println("-----------------------before-----------------------");
            System.out.println(bean.toString());
            ((com.spring.postBeanProcessor.Catagory)bean).setId(11);
            ((com.spring.postBeanProcessor.Catagory)bean).setName("xiaojianren1");
            System.out.println("-----------------------before-----------------------");
        }
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("---------------------------after-------------------------------");
        ((com.spring.postBeanProcessor.Catagory)bean).setId(12);
        ((com.spring.postBeanProcessor.Catagory)bean).setName("xiaojianren2");
        System.out.println(bean.toString());
        System.out.println("---------------------------after-------------------------------");
        return bean;
    }
}
<?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:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id = "catagory" class = "com.spring.postBeanProcessor.Catagory" init-method="init">
        <property name="id" value="10"/>
        <property name="name" value="xioajianren"/>
    </bean>
    <bean id = "postBeanProcessorTest" class = "com.spring.postBeanProcessor.PostBeanProcessorTest" />
</beans>

再看这个postbeanprocessor在Aop中的使用,

  • Spring工厂如何加工创建代理对象
    为什么通过原始对象id获取的是代理对象的id值呢?这是aop的核心原理的第二个问题,spring的工厂是如何加工和创建这个对象呢?
    在spring创建一个对象的时候,spring的工厂可以通过BeanPostProcessor这个工厂来进一步加工这个对象,我们的当时写的案例非常的简单,创建好对象之后,spring工厂调用,在后置BeanPostProcessor当中对这个bean进行赋值,然后将处理后的bean进行返回,当动态代理结合上beanpostprocessor之后,就可以实现这个通过原始的id返回对应的代理对象,其原理呢就是将这个加工代理对象的方法写入到这个后置处理bean中BeanPostProcessor当中的PostProcessorAfterInization这个方法中,然后将代理对象的地址进行返回即可。
    我们知道我们很少做这个初始化操作,所以,这个前置处理和后置处理的位置是挨着的,他们的作用是完全一样的,这样的before我们就不用了,直接将bean对象进行返回,交还给spring即可,然后呢,这个所有加工的代理都写在了后置处理的这个方法中,调用的事Proxy.newProxyInstance()这个方法,这里的实现前边都讲过了,然后将创建好的代理对象交还给spring工厂,这样spring工厂就可以通过id获取的事代理对象,这就是通过原始对象的id值获取的事代理对象的加工。

Spring工厂对于代理的加工

是通过通过spring工厂创建并获取代理对象的,这就解释了spring如何是将通过id的值获取的事代理的对象。

package com.spring.factory;
import com.spring.proxy.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
 * @Auther: DaShu
 * @Date: 2021/6/28 21:34
 * @Description:
 */
public class Test {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext2.xml");
        UserService userService = (UserService)ctx.getBean("userService");
        userService.login("suns","liming");
        userService.register(new User());
        //------------new Log()----------------
        //UserServiceImpl.login
        //------------new Log()----------------
        //UserServiceImpl.register
    }
}
package com.spring.factory;
import com.spring.annotation.Log;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
 * @Auther: DaShu
 * @Date: 2021/6/28 21:24
 * @Description:
 */
public class ProxyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("------------new Log()----------------");
                Object invoke = method.invoke(bean, args);
                return invoke;
            }
        };
        //这样返回的已经不再是原始对象而是,加工之后产生的代理对象。
        return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(),bean.getClass().getInterfaces(),handler);
    }
}
<?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:util="http://www.springframework.org/schema/util"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="userService" class = "com.spring.factory.UserServiceImpl"/>
    <bean id = "proxyBeanPostProcessor" class = "com.spring.factory.ProxyBeanPostProcessor"/>
</beans>

基于注解的AOP编程

从jdk5.0之后就java就引入了注解的开发,作为spring框架来讲,支持基于注解进行aop开发,但其核心仍然是aop的开发,所以,仍然包括那四步;

原始对象

原始对象这一块,仍然需要在配置文件中配置这个对象。
下边的三个步骤其实就是为了创建组装切面。切面是由切入点和额外功能组装而成,要想构建切面现有额外功能,再有切入点,在进行组装就好了,所以,后三个过程就是为了创建切面。在基于注解的开发过程中它所定义的切面就是一个切面类。我们创建一个MyAspect这样的切面类。想要表达这个类是一个切面类的话我们需要一个这样的注解,@Aspect这样的注解,一旦我们的类加上这个注解之后,这个类就成了切面类,切面类中包含额外功能和切入点。

额外功能

需要在切面类上增加一个@Aspect注解,通过这个注解,就可以定义这是一个切面类,但凡是一个切面,都必须要有额外功能和切入点,这样才能组成切面,如何才查看?
在切面类当中如何定义一个切面方法,这个额外功能的方法可以随意命名,但是上边必须要有一个@Around注解,这里的注解就替换了之前实现接口的问题,

切入点

组装切面

通过切面定义了额外功能,@Around通过切面类也定义了切入点@Around(excution( login(…))),@Aspect定义了切面类。这样所有的内容就和xml开发模式匹配了。我们需要将切面类交由Spring进行创建对象还需要告诉Spring我们现在是基于注解开发Spring的AOP*

  • 注解开发的基本示例
package com.spring.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
 * @Auther: DaShu
 * @Date: 2021/6/28 21:54
 * @Description:
 */
@Aspect
public class MyAspect {
    /**
     * ProceedingJoinPoint这个等效于MethodInvocation
     * 这里写的是额外功能的部分,现在的额外功能不需要实现接口了,只需要加入注解。
     * */
    @Around("execution(* login(..))")
    public Object arround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("--------------aspect log-----------");
        Object proceed = joinPoint.proceed();
        return proceed;
    }
}
<!--这里是原始对象-->
    <bean id="userService" class = "com.spring.aspect.UserServiceImpl"/>
    <!--这里边既体现了额外功能,又体现了切入点-->
    <bean id ="around" class = "com.spring.aspect.MyAspect"/>
    <!--告知Spring我们现在是基于注解进行Spring编程了-->
    <aop:aspectj-autoproxy/>```
  • 如何做多个额外功能 & 切入点复用

在以上基础写法的基础上进行修改,修改MyAspect这个类,其他保持一致。所谓的切入点复用,就是在切面类中定义一个函数,这个函数上边加上@Point注解,后续更加附加功能有利于切入点的复用。

package com.spring.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
 * @Auther: DaShu
 * @Date: 2021/6/28 21:54
 * @Description:
 */
@Aspect
public class MyAspect {
    //定义一个公共切入点表达式
    @Pointcut("execution(* login(..))")
    public void myPointcut(){}
    /**
     * ProceedingJoinPoint这个等效于MethodInvocation
     * 这里写的是额外功能的部分,现在的额外功能不需要实现接口了,只需要加入注解。
     * */
    @Around(value="myPointcut()")//在这里调用切入点表达式,就完成了整合。
    public Object arround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("--------------aspect log-----------");
        Object proceed = joinPoint.proceed();
        return proceed;
    }
    @Around(value="myPointcut()")
    public Object arround1(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("--------------aspect tx-----------");
        Object proceed = joinPoint.proceed();
        return proceed;
    }
}

Spring对于JDK和Cglib动态代理的切换

Aop底层是实现过程中一共有两种实现方式,一种是jdk的,通过实现接口做新的实现类的方式 创建代理对象,Cglib他是通过继承父类的方式来做新的子类来创建最终的代理对象,这种基于注解的AOP代理方式,他最底层是应用那种方式呢?它里边的代理类使用debugger来查看的话是proxy这个代理jdk来做的,默认情况下Aopbiancheng 底层用用的事JDK的动态代理方式,如果切换CGLIB应该怎么办?引入proxy-target-class="true"这个属性就好了,就可以实现切换了,当然这种增加属性的这种方式只适用于针对注解的开发方式

<!--告知Spring我们现在是基于注解进行Spring编程了-->
  <!--proxy-target-class="true"属性默认不写是false,使用JDK代理,指定为TRUE,则使用CGLIB动态代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

如果按照传统的AOP开发应该怎么修改呢?

我們要知道无论是基于注解的还是传统的Aop开发都是默认是JDK动态代理,传统的方式想要切换也是在配置文件中进行配置,配置方式是:

第六章:AOP开发中的坑

编程人员在常规过程中不常遇到的问题,但是一旦遇到这个问题,就一定会出错

一:代码实例

public class TestASpectProxy {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext3.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.register(new User());
        //--------------aspect log-----------
        //--------------aspect tx-----------
        //UserServiceImpl.register
        //UserServiceImpl.login
    }
}
@Aspect
public class MyAspect {
    //这是一个类切入点,给所有的都加上额外功能
    @Pointcut("execution(* *..UserServiceImpl.*(..))")
    public void myPointcut(){}
    /**
     * ProceedingJoinPoint这个等效于MethodInvocation
     * 这里写的是额外功能的部分,现在的额外功能不需要实现接口了,只需要加入注解。
     * */
    @Around(value="myPointcut()")//在这里调用切入点表达式,就完成了整合。
    public Object arround(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("--------------aspect log-----------");
        Object proceed = joinPoint.proceed();
        return proceed;
    }
    @Around(value="myPointcut()")
    public Object arround1(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("--------------aspect tx-----------");
        Object proceed = joinPoint.proceed();
        return proceed;
    }
}
public class UserServiceImpl implements UserService {
    @Override
    public void login(String username, String password) {
        System.out.println("UserServiceImpl.login");
    }
    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");
        this.login("suns","123456");
    }
}

测试结果并不符合预期,login方法调用的时候额外功能并没有展示,这个就是那个坑,为什么login方法没有执行额外功能呢?当我们的调用者调用register方法的时候,我们调用的是Proxy代理对象的Register方法,加入了日志加入了事务,最终执行了这两个额外功能,没有问题,但是调用login方法的时候,是在原有功能当中调用的login方法,调用的并不是代理对象中的login方法而是调用的login原生的login方法所以没有额外功能的加入。

这个告诉我们的道理就是:方法中调用类中原始对象的方法的的时候,那么久不会有额外功能的引入,要想使调用方法的额外功能也展示需要调用代理对象的对应的方法,而不是原始对象的对应的方法。也就是说我们需要在我们的register方法中获取到Spring的工厂对象,进而拿到我们的代理对象,进而调用我们的代理对象的login方法,然而初步的想法是在register方法中创建Spring的工厂对象,然而这是不可取的,这是重量级资源,占用大量内存,可以采取的途径是让我们的UserServiceImpl 实现ApplicationContextAware接口,Aware的含义是知道的意思,这个接口中有一个方法是setApplicationContext,通过这个方法入参是工厂对象,Spring容器启动时会回调这个方法,将引用赋值给这个这个方法,所以我们实现方法之后,存一份就好了。**

public class UserServiceImpl implements UserService, ApplicationContextAware {
    private ApplicationContext ctx;
    @Override
    public void login(String username, String password) {
        System.out.println("UserServiceImpl.login");
    }
    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl.register");
        UserService userService = (UserService) ctx.getBean("userService");//userService是代理对象
        userService.login("suns","123456");
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }
}

二:测试结果

public class TestASpectProxy {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext3.xml");
        UserService userService = (UserService) ctx.getBean("userService");
        userService.register(new User());
        //--------------aspect log-----------
        //--------------aspect tx-----------
        //UserServiceImpl.register
        //    --------------aspect log-----------
        //--------------aspect tx-----------
        //UserServiceImpl.login
    }
}
//实现了被调用的时候也有这个额外功能的实现。

总结

在同一个业务类中,进行业务方法间的相互调用的时候,只有最外层的方法,加入了额外功能了。

内部的方法通过普通的方式进行调用,并没有额外功能,都是原始方法,那么如果想让内层的方法也调用代理对象的方法,就需要通过ApplicationContextAware获得工厂,进而获得代理对象

相关文章
|
2月前
|
Java Spring
【编程笔记】在 Spring 项目中使用 RestTemplate 发送网络请求
【编程笔记】在 Spring 项目中使用 RestTemplate 发送网络请求
94 0
|
3月前
|
Java 数据库连接 应用服务中间件
Spring5源码(39)-Aop事物管理简介及编程式事物实现
Spring5源码(39)-Aop事物管理简介及编程式事物实现
24 0
|
4月前
|
Java 程序员 Maven
Spring AOP入门指南:轻松掌握面向切面编程的基础知识
Spring AOP入门指南:轻松掌握面向切面编程的基础知识
|
4月前
|
XML 设计模式 SQL
Spring6 面向切面(AOP)
Spring6 面向切面(AOP)
|
5月前
|
XML Java 程序员
Spring基础篇:注解编程
Spring基础篇:注解编程
|
7天前
|
安全 Java Maven
[AIGC] Spring Boot中的切面编程和实例演示
[AIGC] Spring Boot中的切面编程和实例演示
|
19天前
|
Java Spring
切面编程的锋芒:Spring切入点的玩法与技巧
切面编程的锋芒:Spring切入点的玩法与技巧
16 0
切面编程的锋芒:Spring切入点的玩法与技巧
|
19天前
|
Java 数据库 Spring
切面编程的艺术:Spring动态代理解析与实战
切面编程的艺术:Spring动态代理解析与实战
27 0
切面编程的艺术:Spring动态代理解析与实战
|
22天前
|
XML 安全 Java
spring面向切面编程AOP
spring面向切面编程AOP
|
4月前
|
XML 存储 Java
JAVAEE框架整合技术之Spring02-AOP面向切面编程技术
JAVAEE框架整合技术之Spring02-AOP面向切面编程技术
55 0
JAVAEE框架整合技术之Spring02-AOP面向切面编程技术