Spring基础(1):两个概念

最近看了点Spring的源码,于是来稍微扯一扯,希望能帮一部分培训班出身的朋友撕开一道口子,透透气。

广义上的Spring指的是Spring整个项目,包含SpringBoot、SpringCloud、SpringFramework、SpringData等等,

本系列文章只讨论狭义上的Spring,也就是SpringFrameWork。

主要内容:

  • 盲点
  • Spring说,万物皆可定义
  • 默默付出的后置处理器
  • 利用后置处理器返回代理对象

盲点

如果你恰好非科班转行且从未独立看过源码,那么你很可能至今都不曾注意某两个概念。

你以为我会说IOC和AOP?NO。

看到这里,一部分读者心里一惊:卧槽,说的啥玩意,Spring不就IOC和AOP吗?!这两个都不说,你这篇文章为啥能写这么长?

不错,我就是这么长。其实我要讲的是:

  • BeanDefinition
  • BeanPostProcessor

大部分人一听到“请你谈谈对Spring的理解”,就会下意识地搬出IOC和AOP两座大山,赶紧糊弄过去。大概是这样的:

IOC

所谓的控制反转。通俗地讲,就是把原本需要程序员自己创建和维护的一大堆bean统统交由Spring管理。

也就是说,Spring将我们从盘根错节的依赖关系中解放了。当前对象如果需要依赖另一个对象,只要打一个@Autowired注解,Spring就会自动帮你安装上。

AOP

所谓的面向切面编程。通俗地讲,它一般被用来解决一些系统交叉业务的织入,比如日志啦、事务啥的。打个比方,UserService的method1可能要打印日志,BrandService的method2可能也需要。亦即:一个交叉业务就是要切入系统的一个方面。具体用代码展示就是:

AOP图一:这个切面,可以是日志,也可以是事务

交叉业务的编程问题即为面向切面编程。AOP的目标就是使交叉业务模块化。做法是将切面代码移动到原始方法的周围:

AOP图二

原先不用AOP时(图一),交叉业务的代码直接硬编码在方法内部的前后,而AOP则是把交叉业务写在方法调用前后。那么,为什么AOP不把代码也写在方法内部的前后呢?两点原因:

  • 首先,这与AOP的底层实现方式有关:动态代理其实就是代理对象调用目标对象的同名方法,并在调用前后加增强代码。

InvocationHandler介于代理对象和目标对象中间,作用有两个:1.衔接调用链 2.存放增强代码

  • 其次,这两种最终运行效果是一样的,所以没什么好纠结的。

而所谓的模块化,我个人的理解是将切面代码做成一个可管理的状态。比如日志打印,不再是直接硬编码在方法中的零散语句,而是做成一个切面类,通过通知方法去执行切面代码。

我相信大部分培训班出来的朋友也就言尽于此,讲完上面内容就准备打卡下班了。

怎么说呢,IOC按上面的解释,虽然很浅,但也马马虎虎吧。然而AOP,很多人对它的认识是非常片面的...

这样吧,我问你一个问题,现在我自己写了一个UserController,以及UserServiceImpl implements UserService,并且在UserController中注入Service层对象:

@Autowired
private UserService userService;

那么,这个userService一定是我们写的UserServiceImpl的实例吗?

如果你听不懂我要问什么,说明你对Spring的AOP理解还是太少了。

实际上,Spring依赖注入的对象并不一定是我们自己写的类的实例,也可能是userServiceImpl的代理对象。下面分别演示这两种情况:

  • 注入userServiceImpl对象

注入的是UserServiceImpl类型

  • 注入userServiceImpl的代理对象(CGLib动态代理)

注入的是CGLib动态代理生成的userServiceImpl的代理对象

为什么两次注入的对象不同?

因为第二次我给UserServiceImpl加了@Transactional 注解。

此时Spring读取到这个注解,便知道我们要使用事务。而我们编写的UserService类中并没有包含任何事务相关的代码。如果给你,你会怎么做?

动态代理嘛!

上面说了,InvocationHandler作用有两个:1.衔接调用链, 2.存放增强代码。

用动态代理在InvocationHandler的invoke()中开启关闭事务即可完成事务控制。

看到这里,我仿佛听到有一部分兄弟默默说了句:卧槽,原来是这样...

但是,上面对IOC和AOP的理解,也仅仅是应用级别,是一个面。仅仅了解到这个程度,对Spring的了解还是非常扁平的,不够立体。


Spring说,万物皆可定义

上帝说,要有光。于是特斯拉搞出了交流电。

Java说,万物皆对象。但是Spring另外搞了BeanDefinition...

什么BeanDefinition呢?其实它是bean定义的一个顶级接口:

正如BeanDefinition接口的注释所言:一个BeanDefinition是用来描述一个bean实例的

哎呀卧槽,啥玩意啊。描述一个bean实例?我咋想起了Class类呢。

其实,两者并没有矛盾。

BeanDefinition的实现类很多,这里仅以AbstractBeanDefinition为例,它实现了BeanDefinition

Class只是描述了一个类有哪些字段、方法,但是无法描述如何实例化这个bean!如果说,Class类描述了一块猪肉,那么BeanDefinition就是描述如何做红烧肉:

  • 单例吗?
  • 是否需要延迟加载?
  • 需要调用哪个初始化方法/销毁方法?
在容器内部,这些bean定义被表示为BeanDefinition对象,包含以下元数据:

1.包限定的类名:通常,定义bean的实际实现类。
2.Bean行为配置:它声明Bean在容器中的行为(范围、生命周期回调,等等)。
3.Bean依赖:对其他Bean的引用。
4.对当前Bean的一些设置:例如,池的大小限制或在管理连接池的bean中使用的连接数。
——Spring官方文档

大部分初学者以为Spring解析<bean/>或者@Bean后,就直接搞了一个bean存到一个大Map中,其实并不是。

  • Spring首先会扫描解析指定位置的所有的类得到Resources(可以理解为读取.Class文件)
  • 然后依照TypeFilter和@Conditional注解决定是否将这个类解析为BeanDefinition
  • 稍后再把一个个BeanDefinition取出实例化成Bean

就好比什么呢?你从海里吊了一条鱼,但是你还没想好清蒸还是红烧,那就干脆先晒成鱼干吧。一条咸鱼,其实蕴藏着无限可能,因为它可能会翻身!


默默付出的后置处理器

接下来,我们讨论一下咸鱼如何翻身。

最典型的例子就是AOP。上面AOP的例子中我说过了,如果不加@Transactional,那么Controller层注入的就是普通的userServiceImpl,而加了注解以后返回的实际是代理对象。

为什么Spring要返回代理对象?因为我们压根就没在UserServiceImpl中写任何commit或者rollback等事务相关的代码,但是此时此刻代理对象却能完成事务操作。毫无疑问,这个代理对象已经被Spring加了佐料(事务增强代码)。

那么Spring是何时何地加佐料的呢?说来话长,我们先绕个弯子。

大部分人把Spring比作容器,其实潜意识里是将Spring完全等同于一个Map了。其实,真正存单例对象的Map,只是Spring中很小很小的一部分,仅仅是BeanFactory子类的一个字段,我更习惯称它为“单例池”。

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

这里的ApplicationContext和BeanFactory是接口,实际上都有各自的子类。比如注解驱动开发时,Spring中最关键的就是AnnotationConfigApplicationContext和DefaultListableBeanFactory。

所以,很多人把Spring理解成一个大Map,还是太肤浅了。就拿ApplicationContext来讲,它也实现了BeanFactory接口,说明它其实也是一个容器。但是同为容器,与BeanFactory不同的是,ApplicationContext主要用来包含各种各样的组件,而不是存bean:

ApplicationContext的部分组件示意图(包括Bean工厂)

那么,Spring是如何给咸鱼加佐料(事务代码的织入)的呢?关键就在于后置处理器。

后置处理器其实可以分好多种,属于Spring的扩展点之一:

前三个BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor、BeanPostProcessor都算是后置处理器,这里篇幅有限,暂且先只介绍一下BeanPostProcessor。

BeanFactoryPostProcessor是处理BeanFactory的,所以存在ApplicationContext中。而BeanPostProcessor是处理Bean的,所以存在BeanFactory中,请务必注意!

BeanFactoryPostProcessor是用来干预BeanFactory创建的,而BeanPostProcessor是用来干预Bean的实例化。不知道大家有没有试过在普通Bean中注入ApplicationContext实例?你第一时间想到的是:

@Autowired
ApplicationContext annotationConfigApplicationContext;

除了利用Spring本身的IOC容器自动注入以外,你还有别的办法吗?

我们可以让Bean实现ApplicationContextAware接口:

实现ApplicationContextAware接口,并实现setApplicationContext()方法,用成员变量去接收形参applicationContext

后期,Spring会调用setApplicationContext()方法传入ApplicationContext实例。

Spring官方文档:
一般来说,您应该避免使用它,因为它将代码耦合到Spring中,并且不遵循控制反转样式。

这是我认为Spring最牛逼的地方:代码具有高度的可扩展性,甚至你自己都懵逼,为什么实现了一个接口,这个方法就被莫名其妙调用,还传进了一个对象...

这其实就是后置处理器的工作!

什么意思呢?

也就是说,虽然表面上在我们只要让Bean实现一个接口就能完成ApplicationContext组件的注入,看起来很简单,但是背地里Spring做了很多事情。Spring会在框架的某一处搞个for循环,遍历当前容器中所有的BeanPostProcessor,其中就包括一个叫ApplicationContextAwareProcessor的后置处理器,它的作用是:处理实现了ApplicationContextAware接口的Bean。

上面这句话有点绕,大家停下来多想几遍。

Spring Bean的生命周期,创建过程必然经过BeanPostProcessor

要扩展的类(Bean)是不确定的,但是处理扩展类的流程(循环BeanPostProcessor)是写死的。因为一个程序,再怎么高度可扩展,总有一个要定下来吧。也就是说,在这个Bean实例化的某一紧要处,必然要经过很多BeanPostProcessor。但是,BeanPostProcessor也不是谁都处理,有时也会做判断。比如:

if (bean instanceof Aware) {if (bean instanceof EnvironmentAware) {((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());}if (bean instanceof EmbeddedValueResolverAware) {((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);}if (bean instanceof ResourceLoaderAware) {((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);}if (bean instanceof ApplicationEventPublisherAware) {((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);}if (bean instanceof MessageSourceAware) {((MessageSourceAware) bean).setMessageSource(this.applicationContext);}if (bean instanceof ApplicationContextAware) {((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);}
}

所以,此时此刻一个类实现ApplicationContextAware接口,有两层含义:

  • 作为后置处理器的判断依据,只有你实现了该接口我才处理你
  • 提供被后置处理器调用的方法


利用后置处理器返回代理对象

大致了解Spring Bean的创建流程后,接下来我们尝试着用BeanPostProcessor返回当前Bean的代理对象。

pom.xml

<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>4.3.12.RELEASE</version></dependency>
</dependencies>

AppConfig

@Configuration //JavaConfig方式,即当前配置类相当于一个applicationContext.xml文件
@ComponentScan //不写路径,则默认扫描当前配置类(AppConfig)所在包及其子包
public class AppConfig {}

Calculator

public interface Calculator {public void add(int a, int b);
}

CalCulatorImpl

@Component
public class CalculatorImpl implements Calculator {public void add(int a, int b) {System.out.println(a+b);}
}

后置处理器MyAspectJAutoProxyCreator

使用步骤:

  1. 实现BeanPostProcessor
  2. @Component加入Spring容器
@Component
public class MyAspectJAutoProxyCreator implements BeanPostProcessor {public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {final Object obj = bean;//如果当前经过BeanPostProcessors的Bean是Calculator类型,我们就返回它的代理对象if (bean instanceof Calculator) {Object proxyObj = Proxy.newProxyInstance(this.getClass().getClassLoader(),bean.getClass().getInterfaces(),new InvocationHandler() {public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("开始计算....");Object result = method.invoke(obj, args);System.out.println("结束计算...");return result;}});return proxyObj;}//否则返回本身return obj;}
}

测试类

public class TestPostProcessor {public static void main(String[] args) {System.out.println("容器启动成功!");AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();//打印当前容器所有BeanDefinitionfor (String beanDefinitionName : beanDefinitionNames) {System.out.println(beanDefinitionName);}System.out.println("============");//取出Calculator类型的实例,调用add方法Calculator calculator = (Calculator) applicationContext.getBean(Calculator.class);calculator.add(1, 2);
}

先把MyAspectJAutoProxyCreator的@Component注释掉,此时Spring中没有我们自定义的后置处理器,那么返回的就是CalculatorImpl:

把@Component加上,此时MyAspectJAutoProxyCreator加入到Spring的BeanPostProcessors中,会拦截到CalculatorImpl,并返回代理对象:

代理对象的add()方法被增强:前后打印日志


本文是Spring源码系列的第一篇,仅仅是介绍了两个重要概念:BeanDefinition和BeanPostProcessor。更详细的内容, 比如Bean的生命周期流程及其它后置处理器的介绍,以后有机会再慢慢更新。通过本文,大家只要有朦胧的Spring Bean生命周期的概念,以及知道BeanDefinition和BeanPostProcessor即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/186032.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

sql学习

因为之前sql学的太烂了&#xff0c;想整理一下 一.什么是 SQL&#xff1f; SQL 是用于访问和处理数据库的标准的计算机语言。 SQL 指结构化查询语言SQL 使我们有能力访问数据库SQL 是一种 标准计算机语言 二.SQL 能做什么&#xff1f; SQL 面向数据库执行查询SQL 可从数据库…

17. 机器学习 - 随机森林

Hi&#xff0c;你好。我是茶桁。 我们之前那一节课讲了决策树&#xff0c;说了决策树的优点&#xff0c;也说了其缺点。 决策树实现起来比较简单&#xff0c;解释解释性也比较强。但是它唯一的问题就是不能拟合比较复杂的关系。 后来人们为了解决这个问题&#xff0c;让其能…

华为荣耀软开秋招面经问题整理

一、八股 1.linux常用命令 Linux常用命令&#xff08;面试题&#xff09;_linux常用命令面试题-CSDN博客 常用命令、系统命令、打包命令、vim、开关机命令 2.socket通信调用api过程 TCP UDP 3.进程和线程的区别 进程是系统进行资源分配和调度的基本单元&#xff0c;线程…

初阶JavaEE(15)(Cookie 和 Session、理解会话机制 (Session)、实现用户登录网页、上传文件网页、常用的代码片段)

接上次博客&#xff1a;初阶JavaEE&#xff08;14&#xff09;表白墙程序-CSDN博客 Cookie 和 Session 你还记得我们之前提到的Cookie吗&#xff1f; Cookie是HTTP请求header中的一个属性&#xff0c;是一种用于在浏览器和服务器之间持久存储数据的机制&#xff0c;允许网站…

【Linux】初识进程地址空间

❤️前言 大家好&#xff01;这里是好久没有营业的大懒虫lion&#xff0c;今天要和大家聊的内容是我最近新学习的关于进程地址空间的相关知识。 正文 当我们使用C/C语言进行内存管理时&#xff0c;经常会接触到这样的一张图片&#xff1a; 它常常被我们称作程序地址空间&#…

Netty 是如何利用EventLoop实现千万级并发的

经过前面几篇文章的介绍&#xff0c;我们掌握了 Netty 的 5 个核心组件&#xff0c;但是有了这 5 个核心组件 Netty 这个工厂还是无法很好的运转&#xff0c;因为缺少了一个最核心的组件&#xff1a;EventLoop&#xff0c;它 是 Netty 中最最核心的组件&#xff0c;也是 Netty …

使用C++的QT框架实现五子棋

最近有点无聊正好想玩五子棋&#xff0c;那就实现一下这个游戏吧&#xff0c;网上的五子棋逻辑又长又复杂&#xff0c;我这个逻辑还是蛮简单的&#xff0c;展示如下&#xff08;检测函数在最后&#xff09; 这是一个简单的五子棋&#xff0c;今天就了解一下这个游戏的思路&…

机器学习——回归

目录 一、线性回归 1、回归的概念&#xff08;Regression、Prediction&#xff09; 2、符号约定 3、算法流程 4、最小二乘法&#xff08;LSM&#xff09; 二、梯度下降 梯度下降的三种形式 1、批量梯度下降&#xff08;Batch Gradient Descent,BGD&#xff09;&#xff…

【2023.11.6】OpenAI发布会——近期chatgpt被攻击,不能使用

OpenAI发布会 写在最前面发布会内容GPT-4 Turbo 具有 128K 上下文函数调用更新改进了指令遵循和 JSON 模式可重现的输出和对数概率更新了 GPT-3.5 Turbo 助手 API、检索和代码解释器API 中的新模式GPT-4 Turbo 带视觉DALLE 3文字转语音 &#xff08;TTS&#xff09;收听语音样本…

[unity]切换天空盒

序 unity是自带天空盒的&#xff1a; 但有的时候不想用自带的。怎么自定义&#xff1f;如何设置&#xff1f; 官方文档 Unity - Manual: The Lighting window (unity3d.com) 相关窗口的打开方法 天空盒对应的选项 实际操作 从标准材质球到天空盒材质球 新建一个材质球&…

Powerpoint不小心被覆盖?PPT误删文件如何恢复?

PowerPoint不小心删除了&#xff0c;这可能是众多学生和工作人员最头痛的事情了。PPT被覆盖或误删可能意味着几个小时的努力付之东流。那么PPT覆盖的文档要如何救回来呢&#xff1f;小编将会在本篇文章中为大家分享几个解决方案&#xff0c;使PPT文档覆盖还原操作成为可能&…

为什么有的孩子玩着玩着就成了学霸?

毫不夸张地说&#xff0c;几乎所有的父母都想养出聪明宝宝&#xff0c;孩子上学之后能成为学霸就更省心了。 可“聪明”毕竟不能量化&#xff0c;不是说让孩子上几天课就能提升的。很多家长都在促进孩子大脑发育上使足了劲&#xff0c;可到头来却发现是在做“无用功”。 事实…

Linux-命令行命令

注&#xff1a;[]的内容说明是可选的 1.ls ls [-a -l -h] [Linux路径] >如果没有参数&#xff0c;就展示当前工作目录的内容 > -a&#xff1a;all的意思&#xff0c;即列出所有文件&#xff08;包含隐藏文件/文件夹&#xff09; > -l&#xff1a;以列表形式展示内容&…

3、Dockerfile 深入与其他细节

Dockerfile 在 Docker 中创建镜像最常用的方式&#xff0c;就是使用 Dockerfile。Dockerfile 是一个 Docker 镜像 的描述文件&#xff0c;我们可以理解成火箭发射的 A、B、C、D…的步骤。Dockerfile 其内部包含了一 条条的指令&#xff0c;每一条指令构建一层&#xff0c;因此每…

代码随想录算法训练营第16天|104. 二叉树的最大深度111.二叉树的最小深度222.完全二叉树的节点个数

JAVA代码编写 104. 二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3示例 2&#xff1a; …

鸡尾酒学习——原谅(自制)

1、材料&#xff1a;冰块、君度、蓝橙力娇酒、雪碧、橘子。 2、口感&#xff1a;甜味为主带着一丝丝酸味&#xff0c;喝起来比较清爽&#xff0c;没有一丝酒味的小甜酒。&#xff08;喜欢喝酒的可以多加酒&#xff0c;不喜欢喝酒的可以适量减少酒&#xff09; 3、视觉效果&…

cookie 里面都包含什么属性?

结论先行&#xff1a; Cookie 中除了名称和值外&#xff0c;还有几个比较常见的&#xff0c;例如&#xff1a; Domain 域&#xff1a;指定了 cookie 可以发送到哪些域&#xff0c;只有发送到指定域或其子域的请求才会携带该cookie&#xff1b; Path 路径&#xff1a;指定哪些…

MySQL:锁机制

目录 概述三种层级的锁锁相关的 SQLMyISAM引擎下的锁InnoDB引擎下的锁InnoDB下的表锁和行锁InnoDB下的共享锁和排他锁InnoDB下的意向锁InnoDB下的记录锁&#xff0c;间隙锁&#xff0c;临键锁记录锁&#xff08;Record Locks&#xff09;间隙锁&#xff08;Gap Locks&#xff0…

彻底删除Ubuntu双系统(联想小新2022)

彻底卸载Ubuntu双系统 以里联想小新pro16 i9-12900h为例子 把开机启动项设为默认Windows启动 以联想电脑为例子&#xff0c;关机后一直点击Fn F2进入Bios把windows启动项移到最上面&#xff0c;这样可以开机默认启动windows了删除ubuntu系统分区 使用磁盘管理软件 DiskGeniu…

【手把手教你】将python程序打包成exe可执行文件

1. 安装环境 pip install pyinstaller6.0.02. 打包文件 pyinstaller -D “要启动的文件名“.py比如我的命令就是&#xff1a;pyinstaller -D eval.py 执行完后&#xff0c;会生两个文件夹dist和bulib两个文件和一个xxx.spec文件 3. 删除生成的文件 删除生成的bulid和dist文…