- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
大家好,我是三友~~ 。 今天来跟大家聊一聊Spring的9大核心基础功能. 其实最近有小伙伴私信问我怎么不写文章了,催更来了 。 其实我不是不写,而是一直在写这篇文章,只不过令我没想到的是,从前期的选题、准备、翻源码、动手到写完,前后跨度接近一个月的时间,花了好几个周末,写了三万字,最终才算完成. 所以如果本篇文章对你有所帮助,还请多多点赞、转发、在看,非常感谢!! 。 话不多说,先上目录 。 友情提示,本文过长,建议收藏,嘿嘿嘿! 。 资源管理是Spring的一个核心的基础功能,不过在说Spring的资源管理之前,先来简单说一下Java中的资源管理. Java中的资源管理主要是通过 java.net.URL 来实现的,通过URL的 openConnection 方法可以对资源打开一个连接,通过这个连接读取资源的内容. 资源不仅仅指的是网络资源,还可以是本地文件、一个jar包等等. 举个例子,比如你想到访问 www.baidu.com 这个百度首页网络资源,那么此时就可以这么写 。 解释一下上面代码的意思: 运行结果 。 成功读取到百度首页的数据. 当然,也可以通过URL访问本地文件资源,在创建URL的时候只需要指定协议类型为 file:// 和文件的路径就行了 。 这种方式这里我就不演示了. 其实这种方式实际上最终也是通过 FileInputStream 来读取文件数据的,不信你可以自己debug试试. 每种协议的URL资源都需要一个对应的一个URLStreamHandler来处理. 比如说, http:// 协议有对应的URLStreamHandler的实现, file:// 协议的有对应的URLStreamHandler的实现. Java除了支持 http:// 和 file:// 协议之外,还支持其它的协议,如下图所示: 对于的URLStreamHandler如下图所示 。 当在构建URL的时候,会去解析资源的访问协议,根据访问协议找到对应的URLStreamHandler的实现. 当然,除了Java本身支持的协议之外,我们还可以自己去扩展这个协议,大致只需要两步即可: 不过需要注意的是,URLStreamHandler的实现需要放在 sun.net.www.protocol.协议名称 包下,类名必须是 Handler ,这也是为什么截图中的实现类名都叫 Handler 的原因. 当然如果不放在指定的包下也可以,但是需要实现 java.net.URLStreamHandlerFactory 接口. 对于扩展我就不演示了,如果你感兴趣可以自行谷歌一下. 虽然Java提供了标准的资源管理方式,但是Spring并没有用,而是自己搞了一套资源管理方式. 在Spring中,资源大致被抽象为两个接口 。 Resource接口继承了InputStreamSource接口,而InputStreamSource接口可以获取定义了获取输入流的方法 。 WritableResource 继承了 Resource 接口,可以获取到资源的输出流,因为有的资源不仅可读,还可写,就比如一些本地文件的资源,往往都是可读可写的 。 Resource 的实现很多,这里我举几个常见的: 比如,想要通过Spring的资源管理方式来访问前面提到百度首页网络资源,就可以这么写 。 如果是一个本地文件资源,那么除了可以使用UrlResource,也可以使用FileSystemResource,都是可以的. 虽然 Resource 有很多实现,但是在实际使用中,可能无法判断使用具体的哪个实现,所以Spring提供了 ResourceLoader 资源加载器来根据资源的类型来加载资源. 通过 getResource 方法,传入一个路径就可以加载到对应的资源,而这个路径不一定是本地文件,可以是任何可加载的路径. ResourceLoader 有个唯一的实现 DefaultResourceLoader 。 比如对于上面的例子,就可以通过 ResourceLoader 来加载资源,而不用直接new具体的实现了 。 除了 ResourceLoader 之外,还有一个 ResourcePatternResolver 可以加载资源 。 ResourcePatternResolver 继承了 ResourceLoader 。 通过 ResourcePatternResolver 提供的方法可以看出,他可以加载多个资源,支持使用通配符的方式,比如 classpath*: ,就可以加载所有classpath的资源. ResourcePatternResolver 只有一个实现 PathMatchingResourcePatternResolver 。 到这就讲完了Spring的资源管理,这里总结一下本节大致的内容 。 Java的标准资源管理: Spring的资源管理: Spring的资源管理在Spring中用的很多,比如在SpringBoot中, application.yml 的文件就是通过ResourceLoader加载成Resource,之后再读取文件的内容的. 上一节末尾举的例子中提到,SpringBoot配置文件是通过ResourceLoader来加载配置文件,读取文件的配置内容 。 那么当配置文件都加载完成之后,这个配置应该存到哪里,怎么能够读到呢? 这就引出了Spring框架中的一个关键概念,环境,它其实就是用于管理应用程序配置的. Environment就是环境抽象出来的接口 。 Environment继承PropertyResolver 。 如上是PropertyResolver提供的部分方法,这里简单说一下上面方法的作用 。 所以Environment主要有一下几种功能: 来个demo 。 先在 application.yml 的配置文件中加入配置 。 测试代码如下 。 启动应用,获取到ConfigurableEnvironment对象,再获取到值 。 ConfigurableEnvironment是Environment子接口,通过命名也可以知道,他可以对Environment进行一些功能的配置. 运行结果: PropertySource是真正存配置的地方,属于配置的来源,它提供了一个统一的访问接口,使得应用程序可以以统一的方式获取配置获取到属性. 来个简单demo 。 简单说一下上面代码的意思 。 最后成获取到属性 。 除了MapPropertySource之外,还有非常多的实现 。 比如CommandLinePropertySource,它其实就封装了通过命令启动时的传递的配置参数 。 既然PropertySource才是真正存储配置的地方,那么Environment获取到的配置真正也就是从PropertySource获取的,并且他们其实是一对多的关系 。 其实很好理解一对多的关系,因为一个应用程序的配置可能来源很多地方,比如在SpringBoot环境底下,除了我们自定义的配置外,还有比如系统环境配置等等,这些都可以通过Environment获取到 。 当从Environment中获取配置的时候,会去遍历所有的PropertySource,一旦找到配置key对应的值,就会返回 。 所以,如果有多个PropertySource都含有同一个配置项的话,也就是配置key相同,那么获取到的配置是从排在前面的PropertySource的获取的 。 这就是为什么,当你在配置文件配置username属性时获取到的却是系统变量username对应的值,因为系统的PropertySource排在配置文件对应的PropertySource之前 。 SpringBoot是通过PropertySourceLoader来解析配置文件的 。 load方法的第二个参数就是我们前面提到的资源接口Resource 。 通过Resource就可以获取到配置文件的输入流,之后就可以读取到配置文件的内容,再把配置文件解析成多个PropertySource,之后把PropertySource放入到Environment中,这样我们就可以通过Environment获取到配置文件的内容了. PropertySourceLoader默认有两个实现,分别用来解析 properties 和 yml 格式的配置文件 。 此时,上面的图就可以优化成这样 。 在上一节介绍Environment时提到了它的 getProperty(String key, Class<T> targetType) 可以将配置的字符串转换成对应的类型,那么他是如何转换的呢? 这就跟本文要讲的Spring类型转换机制有关了 。 Spring类型转换主要涉及到以下几个api: 接下来我会来详细介绍这几个api的原理和他们之间的关系. PropertyEditor并不是Spring提供的api,而是JDK提供的api,他的主要作用其实就是将String类型的字符串转换成Java对象属性值. 就拿项目中常用的 @Value 来举例子,当我们通过 @Value 注解的方式将配置注入到字段时,大致步骤如下图所示: 在转换的过程中,Spring会先调用PropertyEditor的setAsText方法将字符串传入,然后再调用getValue方法获取转换后的值. Spring提供了很多PropertyEditor的实现,可以实现字符串到多种类型的转换 。 在这么多实现中,有一个跟我们前面提到的Resource有关的实现 ResourceEditor ,它是将字符串转换成Resource对象 。 也就是说,可以直接通过@Value的方式直接注入一个Resource对象,就像下面这样 。 其实归根到底,底层也是通过ResourceLoader来加载的,这个结论是不变的. 所以,如果你想知道@Value到底支持注入哪些字段类型的时候,看看PropertyEditor的实现就可以了,当然如果Spring自带的都不满足你的要求,你可以自己实现PropertyEditor,比如把String转成Date类型,Spring就不支持. 由于PropertyEditor局限于字符串的转换,所以Spring在后续的版本中提供了叫Converter的接口,他也用于类型转换的,相比于PropertyEditor更加灵活、通用 。 Converter是个接口,泛型S是被转换的对象类型,泛型T是需要被转成的类型. 同样地,Spring也提供了很多Converter的实现 。 这些主要包括日期类型的转换和String类型转换成其它的类型 。 GenericConverter也是类型转换的接口 。 这个接口的主要作用是可以处理带有泛型类型的转换,主要的就是面向集合数组转换操作,从Spring默认提供的实现就可以看出 。 那Converter跟GenericConverter有什么关系呢? 这里我举个例子,假设现在需要将将源集合 Collection<String> 转换成目标集合 Collection<Date> 。 假设现在有个String转换成Date类型的Converter,咱就叫StringToDateConverter,那么整个转换过程如下: 所以通过这就可以看出Converter和GenericConverter其实是依赖关系 。 对于我们使用者来说,不论是Converter还是GenericConverter,其实都是类型转换的,并且类型转换的实现也很多,所以Spring为了方便我们使用Converter还是GenericConverter,提供了一个门面接口ConversionService 。 我们可以直接通过ConversionService来进行类型转换,而不需要面向具体的Converter或者是GenericConverter 。 ConversionService有一个基本的实现GenericConversionService 。 同时GenericConversionService还实现了ConverterRegistry的接口 。 ConverterRegistry提供了对Converter和GenericConverter进行增删改查的方法. 这样就可以往ConversionService中添加Converter或者是GenericConverter了,因为最终还是通过Converter和GenericConverter来实现转换的 。 但是我们一般不直接用GenericConversionService,而是用DefaultConversionService或者是ApplicationConversionService(SpringBoot环境底下使用) 。 因为DefaultConversionService和ApplicationConversionService在创建的时候,会添加很多Spring自带的Converter和GenericConverter,就不需要我们手动添加了. TypeConverter其实也是算是一个门面接口,他也定义了转换方法 。 他是将PropertyEditor和ConversionService进行整合,方便我们同时使用PropertyEditor和ConversionService 。 convertIfNecessary方法会去调用PropertyEditor和ConversionService进行类型转换,值得注意的是, 优先使用PropertyEditor进行转换,如果没有找到对应的PropertyEditor,会使用ConversionService进行转换 。 TypeConverter有个简单的实现SimpleTypeConverter,这里来个简单的demo 。 这里需要注意,ConversionService需要我们手动设置,但是PropertyEditor不需要,因为SimpleTypeConverter默认会去添加PropertyEditor的实现. 到这就讲完了类型转换的常见的几个api,这里再简单总结一下: 画张图来总结他们之间的关系 。 前面在举@Value的例子时说,类型转换是根据PropertyEditor来的,其实只说了一半,因为底层实际上是根据TypeConverter来转换的,所以@Value类型转换时也能使用ConversionService类转换,所以那张图实际上应该这么画才算对 。 这里我们回到开头提到的话题,Environment中到底是如何进行类型转换的,让我们看看Environment类的接口体系 。 Environment有个子接口ConfigurableEnvironment中,前面也提到过 。 它继承了ConfigurablePropertyResolver接口 。 而ConfigurablePropertyResolver有一个 setConversionService 方法 。 所以从这可以看出,Environment底层实际上是通过ConversionService实现类型转换的 。 这其实也就造成了一个问题,因为ConversionService和PropertyEditor属于并列关系,那么就会导致Environment无法使用PropertyEditor来进行类型转换,也就会丧失部分Spring提供的类型转换功能,就比如无法通过Environment将String转换成Resource对象,因为Spring没有实现String转换成Resource的Converter 。 当然你可以自己实现一个String转换成Resource的Converter,然后添加到ConversionService,之后Environment就支持String转换成Resource了. 上一节我们讲了类型转换,而既然提到了类型转换,那么就不得不提到数据绑定了,他们是密不可分的,因为在数据绑定时,往往都会伴随着类型转换, 。 数据绑定的意思就是将一些配置属性跟我们的Bean对象的属性进行绑定. 不知你是否记得,在远古的ssm时代,我们一般通过xml方式声明Bean的时候,可以通过 <property/> 来设置Bean的属性 。 然后Spring在创建User的过程中,就会给 username 属性设置为 三友的java日记 . 这就是数据绑定,将 三友的java日记 绑定到username这个属性上. 数据绑定的核心api主要包括以下几个: 这里我们先来讲一下PropertyValue(注意没有s) 。 顾明思议,PropertyValue就是就是封装了属性名和对应的属性值,它就是数据绑定时属性值的来源. 以前面的提到的xml创建Bean为例,Spring在启动的时候会去解析xml中的 <property/> 标签,然后将 name 和 value 封装成PropertyValue 。 当创建User这个Bean的时候,到了属性绑定的阶段的时候,就会取出PropertyValue,设置到User的username属性上. 而PropertyValues,比PropertyValue多了一个s,也就是复数的意思,所以其实PropertyValues本质上就是PropertyValue的一个集合 。 因为一个Bean可能有多个属性配置,所以就用PropertyValues来保存. BeanWrapper其实就数据绑定的核心api了,因为在Spring中涉及到数据绑定都是通过BeanWrapper来完成的,比如前面提到的Bean的属性的绑定,就是通过BeanWrapper来的 。 BeanWrapper是一个接口,他有一个唯一的实现BeanWrapperImpl. 先来个demo 。 结果 。 成功获取到,说明设置成功 。 BeanWrapperImpl也间接实现了TypeConverter接口 。 当然底层还是通过前面提到的ConversionService和PropertyEditor实现的 。 所以当配置的类型跟属性的类型不同时,就可以对配置的类型进行转换,然后再绑定到属性上 。 这里简单说一下数据绑定和@Value的异同,因为这两者看起来好像是一样的,但实际还是有点区别的 。 相同点: 不同点: DataBinder也是用来进行数据绑定的,它的底层也是间接通过BeanWrapper来实现的数据绑定的 。 但是他相比于BeanWrapper多了一些功能,比如在数据绑定之后,可以对数据校验,比如可以校验字段的长度等等 。 说到数据校验,是不是想到了SpringMVC中的参数校验,通过@Valid配合一些诸如@NotBlank、@NotNull等注解,实现优雅的参数校验. 其实SpringMVC的参数校验就是通过DataBinder来的,所以DataBinder其实在SpringMVC中用的比较多,但是在Spring中确用的很少. 如果你有兴趣,可以翻一下SpringMVC中关于请求参数处理的HandlerMethodArgumentResolver的实现,里面有的实现会用到DataBinder(WebDataBinder)来进行数据请求参数跟实体类的数据绑定、类型转换、数据校验等等. 不知道你有没有注意过,平时写接口的时候,前端传来的参数String类型的时间字符串无法通过Spring框架本身转换成Date类型,有部分原因就是前面提到的Spring没有相关的Converter实现 。 总的来说,数据绑定在xml配置和SpringMVC中用的比较多的,并且数据绑定也是Spring Bean生命周期中一个很重要的环节. Spring为了方便操作和处理泛型类型,提供了一个强大的工具类——ResolvableType. 泛型处理其实是一块相对独立的东西,因为它就只是一个工具类,只还不过这个工具类在Spring中却是无处不在! 。 ResolvableType提供了有一套灵活的API,可以在运行时获取和处理泛型类型等信息. 接下来就通过一个案例,来看一看如何通过ResolvableType快速简单的获取到泛型的 。 首先我声明了一个MyMap类,继承HashMap,第一个泛型参数是Integer类型,第二个泛型参数是List类型,List的泛型参数又是String 。 接下来就来演示一下如何获取到HashMap的泛型参数以及List的泛型参数 。 第一步,先来通过 ResolvableType#forClass 方法创建一个MyMap类型对应的ResolvableType 。 因为泛型参数是在父类HashMap中,所以我们得获取到父类HashMap对应的ResolvableType,通过 ResolvableType#getSuperType() 方法获取 。 接下来需要获取HashMap的泛型参数对应的ResolvableType类型,可以通过 ResolvableType#getGeneric(int... indexes) 就可以获取指定位置的泛型参数ResolvableType,方法参数就是指第几个位置的泛型参数,从0开始 。 比如获取第一个位置的对应的ResolvableType类型 。 现在有了第一个泛型参数的ResolvableType类型,只需要通过 ResolvableType#resolve() 方法就可以获取到ResolvableType类型对应的class类型,这样就可以获取到一个泛型参数的class类型 。 如果你想获取到HashMap第二个泛型参数的泛型类型,也就是List泛型类型就可以这么写 。 从上面的演示下来可以发现,其实每变化一步,其实就是获取对应泛型或者是父类等等对应的ResolvableType,父类或者是泛型参数又可能有泛型之类的,只需要一步一步获取就可以了,当需要获取到具体的class类型的时候,通过 ResolvableType#resolve() 方法就行了. 除了上面提到的通过 ResolvableType#forClass 方法创建ResolvableType之外,还可以通过一下几个方法创建: 通过上面解释可以看出,对于一个类方法参数,方法返回值,字段等等都可以获取到对应的ResolvableType 。 国际化(Internationalization,简称i18n)也是Spring提供的一个核心功能,它其实也是一块相对独立的功能. 所谓的国际化,其实理解简单点就是对于不同的地区国家,输出的文本内容语言不同. Spring的国际化其实主要是依赖Java中的国际化和文本处理方式. Locale是Java提供的一个类,它可以用来标识不同的语言和地区,如en_US表示美国英语,zh_CN表示中国大陆中文等. 目前Java已经穷举了很多国家的地区Locale. 我们可以使用Locale类获取系统默认的Locale,也可以手动设置Locale,以适应不同的语言环境. ResourceBundle是一个加载本地资源的一个类,他可以根据传入的Locale不同,加载不同的资源. 来个demo 。 首先准备资源文件,资源文件通常是.properties文件,文件名命名规则如下: basename_lang_country.properties 。 basename无所谓,叫什么都可以,而lang和country是从Locale中获取的. 举个例子,我们看看英语地区的Locale 。 从上图可以看出,英语Locale的lang为en,country为空字符串,那么此时英语地区对应资源文件就可以命名为:basename_en.properties,由于country为空字符串,可以省略 。 中国大陆Locale如下图 。 此时文件就可以命为:basename_zh_CN.properties 。 好了,现在既然知道了命名规则,我们就创建两个文件,basename就叫message,一个英语,一个中文,放在classpath路径下 。 中文资源文件:message_zh_CN.properties,内容为: 英文资源文件:message_en.properties,内容为: 有了文件之后,就可以通过 ResourceBundle#getBundle(String baseName,Locale locale) 方法来获取获取ResourceBundle 。 测试一下 。 运行结果 。 其实运行结果可以看出,其实是成功获取了,只不过中文乱码了,这主要是因为ResourceBundle底层其实编码是 ISO-8859-1 ,所以会导致乱码. 解决办法最简单就是把中文用Java Unicode序列来表示,之后就可以读出中文了了,比如 三友的java日记 用Java Unicode序列表示为 \u4e09\u53cb\u7684java\u65e5\u8bb0 。 除了这种方式之外,其实还可以继承ResourceBundle内部一个Control类 。 重写newBundle方法 。 newBundle是创建ResourceBundle对应核心方法,重写的时候你就可以随心所欲让它支持其它编码方式. 有了新的Control之后,获取ResourceBundle时只需要通过 ResourceBundle#getBundle(String baseName, Locale targetLocale,Control control) 方法指定Control就可以了. Spring实际上就是通过这种方式扩展,支持不同编码的,后面也有提到. MessageFormat顾明思议就是把消息格式化。它可以接收一条包含占位符的消息模板,并根据 提供的参数 替换 占位符 ,生成最终的消息. MessageFormat对于将动态值插入到消息中非常有用,如欢迎消息、错误消息等. 先来个Demo 。 解释一下上面这段代码: 所以输出结果为: 成功格式化消息. Spring提供了一个国际化接口MessageSource 。 他有一个基于ResourceBundle + MessageFormat的实现ResourceBundleMessageSource 。 他的本质可以在资源文件存储 消息的模板 ,然后通过MessageFormat来替换 占位符 ,MessageSource的getMessage方法就可以传递具体的参数 。 来个demo 。 现在模拟登录欢迎语句,对于不同的人肯定要有不同的名字,所以资源文件需要存模板,需要在不同的资源文件加不同的模板 。 中文资源文件:message_zh_CN.properties 。 英文资源文件:message_en.properties 。 占位符,就是不同人不同名字 。 测试代码 。 运行结果 。 成功根据完成不同国家资源的加载和模板消息的格式化. 这里来简单总结一下这一小节说的内容 。 我们知道Spring的核心就是IOC和AOP,而BeanFactory就是大名鼎鼎的IOC容器,他可以帮我们生产对象. BeanFactory本身是一个接口 。 从上面的接口定义可以看出从可以从BeanFactory获取到Bean. 他也有很多子接口,不同的子接口有着不同的功能 。 ListableBeanFactory 。 从提供的方法可以看出,提供了一些获取集合的功能,比如有的接口可能有多个实现,通过这些方法就可以获取这些实现对象的集合. HierarchicalBeanFactory 。 从接口定义可以看出,可以获取到父容器,说明BeanFactory有子父容器的概念. ConfigurableBeanFactory 。 从命名可以看出,可配置BeanFactory,所以可以对BeanFactory进行配置,比如截图中的方法,可以设置我们前面提到的类型转换的东西,这样在生成Bean的时候就可以类型属性的类型转换了. AutowireCapableBeanFactory 。 提供了自动装配Bean的实现、属性填充、初始化、处理获取依赖注入对象的功能. 比如@Autowired最终就会调用 AutowireCapableBeanFactory#resolveDependency 处理注入的依赖. 其实从这里也可以看出,Spring在BeanFactory的接口设计上面还是基于不同的职责进行接口的划分,其实不仅仅是在BeanFactory,前面提到的那些接口也基本符合这个原则. BeanDefinition是Spring Bean创建环节中很重要的一个东西,它封装了Bean创建过程中所需要的元信息. 如上代码是BeanDefinition接口的部分方法,从这方法的定义名称可以看出,一个Bean所创建过程中所需要的一些信息都可以从BeanDefinition中获取,比如这个Bean的class类型,这个Bean是否是懒加载,这个Bean是否是单例的等等,因为有了这些信息,Spring才知道要创建一个什么样的Bean. 读取BeanDefinition大致分为以下几类 。 BeanDefinitionReader 。 BeanDefinitionReader可以通过 loadBeanDefinitions(Resource resource) 方法来加载BeanDefinition,方法参数就是我们前面说的资源,比如可以将Bean定义在xml文件中,这个xml文件就是一个资源 。 BeanDefinitionReader的相关实现: ClassPathBeanDefinitionScanner 。 这个作用就是扫描指定包下通过@Component及其派生注解(@Service等等)注解定义的Bean,其实就是@ComponentScan注解的底层实现 。 ClassPathBeanDefinitionScanner这个类其实在很多其它框架中都有使用到,因为这个类可以扫描指定包下,生成BeanDefinition,对于那些需要扫描包来生成BeanDefinition来说,用的很多 。 比如说常见的MyBatis框架,他的注解@MapperScan可以扫描指定包下的Mapper接口,其实他也是通过继承ClassPathBeanDefinitionScanner来扫描Mapper接口的 。 BeanDefinitionRegistry 。 这个从命名就可以看出,是BeanDefinition的注册中心,也就是用来保存BeanDefinition的. 提供了BeanDefinition的增删查的功能. 讲到这里,就可以用一张图来把前面提到东西关联起来 。 前面提到的BeanFactory体系都是一个接口,那么BeanFactory的实现类是哪个类呢? BeanFactory真正底层的实现类,其实就只有一个,那就是DefaultListableBeanFactory这个类,这个类以及父类真正实现了BeanFactory及其子接口的所有的功能. 并且接口的实现上可以看出,他也实现了BeanDefinitionRegistry,也就是说,在底层的实现上,其实BeanFactory跟BeanDefinitionRegistry的实现是同一个实现类. 上面说了这么多,来个demo 。 简单说一下上面代码的意思 。 运行结果 。 成功获取到我们注册的Bean 。 本节主要讲了实现IOC的几个核心的组件 。 BeanFactory及其接口体系: BeanDefinition及其相关组件: BeanFactory核心实现: 终于讲到了ApplicationContext,因为前面说的那么多其实就是为ApplicationContext做铺垫的 。 先来看看ApplicationContext的接口 。 你会惊讶地发现,ApplicationContext继承的几个接口,除了EnvironmentCapable和ApplicationEventPublisher之外,其余都是前面说的. EnvironmentCapable这个接口比较简单,提供了获取Environment的功能 。 说明了可以从ApplicationContext中获取到Environment,所以EnvironmentCapable也算是前面说过了 。 至于ApplicationEventPublisher我们留到下一节说. ApplicationContext也继承了ListableBeanFactory和HierarchicalBeanFactory,也就说明ApplicationContext其实他也是一个BeanFactory,所以说ApplicationContext是IOC容器的说法也没什么毛病,但是由于他还继承了其它接口,功能比BeanFactory多多了. 所以,ApplicationContext是一个集万千功能为一身的接口,一旦你获取到了ApplicationContext(可以@Autowired注入),你就可以用来获取Bean、加载资源、获取环境,还可以国际化一下,属实是个王炸. 虽然ApplicationContext继承了这些接口,但是ApplicationContext对于接口的实现是通过一种委派的方式,而真正的实现都是我们前面说的那些实现 。 什么叫委派呢,咱写一个例子你就知道了 。 如上,其实是一段伪代码 。 因为ApplicationContext继承了ResourcePatternResolver接口,所以我实现了getResources方法,但是真正的实现其实是交给变量中的PathMatchingResourcePatternResolver来实现的,这其实就是委派,不直接实现,而是交给其它真正实现了这个接口的类来处理 。 同理,ApplicationContext对于BeanFactory接口的实现其实最终也是交由DefaultListableBeanFactory来委派处理的. 委派这种方式在Spring内部还是用的非常多的,前面提到的某些接口在的实现上也是通过委派的方式来的 。 ApplicationContext有一个子接口,ConfigurableApplicationContext 。 从提供的方法看出,就是可以对ApplicationContext进行配置,比如设置Environment,同时也能设置parent,说明了ApplicationContext也有子父的概念 。 我们已经看到了很多以Configurable开头的接口,这就是命名习惯,表示了可配置的意思,提供的都是set、add之类的方法 。 ApplicationContext的实现很多,但是他有一个非常重要的抽象实现AbstractApplicationContext,因为其它的实现都是继承这个抽象实现 。 这个类主要是实现了一些继承的接口方法,通过委派的方式,比如对于BeanFactory接口的实现 。 并且AbstractApplicationContext这个类也实现了一个非常核心的refresh方法 。 所有的ApplicationContext在创建之后必须调用这个refresh方法之后才能使用,至于这个方法干了哪些事,后面有机会再写一篇文章来着重扒一扒. 上一小节在说ApplicationContext继承的接口的时候,我们留下了一个悬念,那就是ApplicationEventPublisher的作用,而ApplicationEventPublisher就跟本节要说的事件有关. Spring事件是一种观察者模式的实现,他的作用主要是用来解耦合的. 当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理. 举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于事件的模型来实现,只需要打119、救人监听火灾的发生就行了,当发生了火灾,通知这些打119、救人去触发相应的逻辑操作. Spring Event 事件就是Spring实现了这种事件模型,你只需要基于Spring提供的API进行扩展,就可以轻易地完成事件的发布与订阅 。 Spring事件相关api主要有以下几个: 事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作. 事件监听的接口,泛型是需要监听的事件类型,子类需要实现onApplicationEvent,参数就是监听的事件类型,onApplicationEvent方法的实现就代表了对事件的处理,当事件发生时,Spring会回调onApplicationEvent方法的实现,传入发布的事件. 上一小节留下来的接口,事件发布器,通过publishEvent方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调. ApplicationContext继承了ApplicationEventPublisher,说明只要有ApplicationContext就可以来发布事件了. 就以上面的火灾为例 。 创建一个火灾事件类 。 火灾事件类继承ApplicationEvent 。 创建火灾事件的监听器 。 打119的火灾事件的监听器: 救人的火灾事件的监听器: 事件和对应的监听都有了,接下来进行测试: 将两个事件注册到Spring容器中,然后发布FireEvent事件 。 运行结果: 控制台打印出了结果,触发了监听. 如果现在需要对火灾进行救火,那么只需要去监听FireEvent,实现救火的逻辑,注入到Spring容器中,就可以了,其余的代码根本不用动. Spring内置的事件很多,这里我罗列几个 。 在ApplicationContext(Spring容器)启动的过程中,Spring会发布这些事件,如果你需要这Spring容器启动的某个时刻进行什么操作,只需要监听对应的事件即可. Spring事件的传播是什么意思呢? 前面提到,ApplicationContext有子父容器的概念,而Spring事件的传播就是指当通过子容器发布一个事件之后,不仅可以触发在这个子容器的事件监听器,还可以触发在父容器的这个事件的监听器. 创建了两个容器,父容器注册了打119的监听器,子容器注册了救人的监听器,然后将子父容器通过setParent关联起来,最后通过子容器,发布了着火的事件. 运行结果: 从打印的日志,的确可以看出,虽然是子容器发布了着火的事件,但是父容器的监听器也成功监听了着火事件. 而这种传播特性,从源码中也可以看出来 。 如果父容器不为空,就会通过父容器再发布一次事件. 前面说过,在Spring容器启动的过程,会发布很多事件,如果你需要有相应的扩展,可以监听这些事件. 但是,不知道你有没有遇到过这么一个坑,就是在SpringCloud环境下,你监听这些 Spring事件 的监听器会执行很多次,这其实就是跟传播特性有关. 在SpringCloud环境下,为了使像FeignClient和RibbonClient这些不同服务的配置相互隔离,会为每个FeignClient或者是RibbonClient创建一个Spring容器,而这些容器都有一个公共的父容器,那就是SpringBoot项目启动时创建的容器 。 假设你监听了容器刷新的ContextRefreshedEvent事件,那么你自己写的监听器就在SpringBoot项目启动时创建的容器中 。 每个服务的配置容器他也是Spring容器,启动时也会发布ContextRefreshedEvent,那么由于传播特性的关系,你的事件监听器就会触发执行多次 。 如何解决这个坑呢? 你可以进行判断这些监听器有没有执行过,比如加一个判断的标志;或者是监听类似的事件,比如ApplicationStartedEvent事件,这种事件是在SpringBoot启动中发布的事件,而子容器不是SpringBoot,所以不会多次发这种事件,也就会只执行一次. 到这到这整篇文章终于写完了,这里再来简单地回顾一下本文说的几个核心功能: 当然除了上面,Spring还有很多其它核心功能,就比如AOP、SpEL表达式等等,由于AOP涉及到Bean生命周期,本篇文章也没有涉及到Bean生命周期的讲解,所以这里就不讲了,后面有机会再讲;至于SpEL他是Spring提供的表达式语言,主要是语法,解析语法的一些东西,这里也不讲了. 最后,我怕你文章看得过于入迷,所以再来重复一遍,如果本篇文章对你有所帮助,还请多多点赞、转发、在看,非常感谢!! 。 哦,真差点就忘了,本文所有demo代码关注微信公众号 三友的java日记 ,回复 core 即可获取. 参考资料: [1].《极客时间--小马哥讲Spring核心编程思想》 [2].https://blog.csdn.net/zzuhkp/article/details/119455964 [3].https://blog.csdn.net/zzuhkp/article/details/119455948 [4].https://blog.csdn.net/u010086122/article/details/81566515 。 如何去阅读源码,我总结了18条心法 。 如何写出漂亮代码,我总结了45个小技巧 。 1.5万字+30张图盘点索引常见的11个知识点 。 三万字盘点Spring/Boot的那些常用扩展点 。 两万字盘点那些被玩烂了的设计模式 。 Sentinel为什么这么强,我扒了扒背后的实现原理 。 扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题. 资源管理
Java资源管理
1、来个Demo
public
class JavaResourceDemo
{
public static void main (String[] args) throws IOException
{
//构建URL 指定资源的协议为http协议
URL url =
new
URL(
"http://www.baidu.com"
);
//打开资源连接
URLConnection urlConnection = url.openConnection();
//获取资源输入流
InputStream inputStream = urlConnection.getInputStream();
//通过hutool工具类读取流中数据
String content = IoUtil.read(
new
InputStreamReader(inputStream));
System.out.println(content);
}
}
URL url =
new
URL(
"file://"
+
"文件的路径"
);
2、原理
Spring资源管理
1、资源抽象
Resource
WritableResource
//构建资源
Resource resource =
new
UrlResource(
"http://www.baidu.com"
);
//获取资源输入流
InputStream inputStream = resource.getInputStream();
2、资源加载
//创建ResourceLoader
ResourceLoader resourceLoader =
new
DefaultResourceLoader();
//获取资源
Resource resource = resourceLoader.getResource(
"http://www.baidu.com"
);
3、小结
环境
1、Environment
public
interface PropertyResolver
{
boolean containsProperty (String key)
;
String getProperty (String key)
;
<T>
T getProperty (String key, Class<T> targetType)
;
<T>
T getRequiredProperty (String key, Class<T> targetType) throws IllegalStateException
;
String resolvePlaceholders (String text)
;
}
getProperty(String key)
,很明显是通过配置的key获取对应的value值
getProperty(String key, Class<T> targetType)
,这是获取配置,并转换成对应的类型,比如你获取的是个字符串的
"true"
,这里就可以给你转换成布尔值的
true
,具体的底层实现留到下一节讲
resolvePlaceholders(String text)
,这类方法可以处理
${...}
占位符,也就是先取出
${...}
占位符中的key,然后再通过key获取到值
@SpringBootApplication
public
class EnvironmentDemo
{
public static void main (String[] args)
{
ConfigurableApplicationContext applicationContext = SpringApplication.run(EnvironmentDemo
. class , args )
;
//从ApplicationContext中获取到ConfigurableEnvironment
ConfigurableEnvironment environment = applicationContext.getEnvironment();
//获取name属性对应的值
String name = environment.getProperty(
"name"
);
System.out.println(
"name = "
+ name);
}
}
name = 三友的java日记
2、配置属性源PropertySource
public
class PropertySourceDemo
{
public static void main (String[] args)
{
Map<String, Object> source =
new
HashMap<>();
source.put(
"name"
,
"三友的java日记"
);
PropertySource<Map<String, Object>> propertySource =
new
MapPropertySource(
"myPropertySource"
, source);
Object name = propertySource.getProperty(
"name"
);
System.out.println(
"name = "
+ name);
}
}
3、SpringBoot是如何解析配置文件
类型转换
1、类型转换API
1.1、PropertyEditor
public
interface PropertyEditor
{
void setValue (Object value)
;
Object getValue ()
;
String getAsText ()
;
void setAsText (String text) throws java.lang.IllegalArgumentException
;
}
@Value
配置的key
@Value
配置的key调用Environment的
resolvePlaceholders(String text)
方法,解析占位符,找到配置文件中对应的值
@Value
(
"http://www.baidu.com"
)
private
Resource resource;
1.2、Converter
1.3、GenericConverter
Collection<String>
,取出元素
1.4、ConversionService
1.5、TypeConverter
public
class TypeConverterDemo
{
public static void main (String[] args)
{
SimpleTypeConverter typeConverter =
new
SimpleTypeConverter();
//设置ConversionService
typeConverter.setConversionService(DefaultConversionService.getSharedInstance());
//将字符串"true"转换成Boolean类型的true
Boolean b = typeConverter.convertIfNecessary(
"true"
, Boolean
. class )
;
System.out.println(
"b = "
+ b);
}
}
小结
2、Environment中到底是如何进行类型转换的?
数据绑定
< bean class = "com.sanyou.spring.core.basic.User" >
< property name = "username" value = "三友的java日记" />
</ bean >
@Data
public
class User
{
private
String username;
}
1、PropertyValues
2、BeanWrapper
public
class BeanWrapperDemo
{
public static void main (String[] args)
{
//创建user对象
User user =
new
User();
//创建BeanWrapper对象,把需要进行属性绑定的user对象放进去
BeanWrapper beanWrapper =
new
BeanWrapperImpl(user);
//进行数据绑定,将三友的java日记这个属性值赋值到username这个属性上
beanWrapper.setPropertyValue(
new
PropertyValue(
"username"
,
"三友的java日记"
));
System.out.println(
"username = "
+ user.getUsername());
}
}
3、DataBinder
泛型处理
public
class MyMap extends HashMap < Integer , List < String >>
{
}
//创建MyMap对应的ResolvableType
ResolvableType myMapType = ResolvableType.forClass(MyMap
. class )
;
//获取父类HashMap对应的ResolvableType
ResolvableType hashMapType = myMapType.getSuperType();
//获取第一个泛型参数对应的ResolvableType
ResolvableType firstGenericType = hashMapType.getGeneric(
0
);
//获取第一个泛型参数对应的ResolvableType对应的class类型,也就是Integer的class类型
Class<?> firstGenericClass = firstGenericType.resolve();
//HashMap第二个泛型参数的对应的ResolvableType,也就是List<String>
ResolvableType secondGenericType = hashMapType.getGeneric(
1
);
//HashMap第二个泛型参数List<String>的第一个泛型类型String对应的ResolvableType
ResolvableType secondFirstGenericType = secondGenericType.getGeneric(
0
);
//这样就获取到了List<String>的泛型类型String
Class<?> secondFirstGenericClass = secondFirstGenericType.resolve();
forField(Field field)
:获取字段类型对应的ResolvableType
forMethodReturnType(Method method)
:获取方法返回值类型对应的ResolvableType
forMethodParameter(Method method, int parameterIndex)
:获取方法某个位置方法参数对应的ResolvableType
forConstructorParameter(Constructor<?> constructor, int parameterIndex)
:获取构造方法某个构造参数对应的ResolvableType
国际化
1、Java中的国际化
Locale
ResourceBundle
name=三友的java日记
name=sanyou's java diary
public
class ResourceBundleDemo
{
public static void main (String[] args)
{
//获取ResourceBundle,第一个参数baseName就是我们的文件名称,第二个参数就是地区
ResourceBundle chineseResourceBundle = ResourceBundle.getBundle(
"message"
, Locale.SIMPLIFIED_CHINESE);
//根据name键取值
String chineseName = chineseResourceBundle.getString(
"name"
);
System.out.println(
"chineseName = "
+ chineseName);
ResourceBundle englishResourceBundle = ResourceBundle.getBundle(
"message"
, Locale.ENGLISH);
String englishName = englishResourceBundle.getString(
"name"
);
System.out.println(
"englishName = "
+ englishName);
}
}
MessageFormat
public
class MessageFormatDemo
{
public static void main (String[] args)
{
String message = MessageFormat.format(
"你好:{0}"
,
"张三"
);
System.out.println(
"message = "
+ message);
}
}
你好:{0}
其实就是前面提到的消息的模板,
{0}
就是
占位符 ,中间的0代表消息格式化的时候将
提供的参数 第一个参数替换占位符的值
张三
就是提供的参数,你可以写很多个,但是我们的demo只会取第一个参数,因为是
{0}
message = 你好:张三
2、Spring国际化
welcome=您好:{0}
welcome=hello:{0}
public
class MessageSourceDemo
{
public static void main (String[] args)
{
ResourceBundleMessageSource messageSource =
new
ResourceBundleMessageSource();
//Spring已经扩展了ResourceBundle的Control,支持资源文件的不同编码方式,但是需要设置一下
messageSource.setDefaultEncoding(
"UTF-8"
);
//添加 baseName,就是前面提到的文件中的basename
messageSource.addBasenames(
"message"
);
//中文,传个中文名字
String chineseWelcome = messageSource.getMessage(
"welcome"
,
new
Object[]{
"张三"
}, Locale.SIMPLIFIED_CHINESE);
System.out.println(
"chineseWelcome = "
+ chineseWelcome);
//英文,英语国家肯定是英文名
String englishWelcome = messageSource.getMessage(
"welcome"
,
new
Object[]{
"Bob"
}, Locale.ENGLISH);
System.out.println(
"englishWelcome = "
+ englishWelcome);
}
}
chineseWelcome = 您好:张三
englishWelcome = hello:Bob
小结
basename_lang_country.properties
命名规范
BeanFactory
1、BeanFactory接口体系
2、BeanDefinition及其相关组件
BeanDefinition
public
interface BeanDefinition extends AttributeAccessor , BeanMetadataElement
{
//设置Bean className
void setBeanClassName (@Nullable String beanClassName)
;
//获取Bean className
@Nullable
String getBeanClassName ()
;
//设置是否是懒加载
void setLazyInit ( boolean lazyInit)
;
//判断是否是懒加载
boolean isLazyInit ()
;
//判断是否是单例
boolean isSingleton ()
;
}
读取BeanDefinition
3、BeanFactory核心实现
public
class BeanFactoryDemo
{
public static void main (String[] args)
{
//创建一个BeanFactory
DefaultListableBeanFactory beanFactory =
new
DefaultListableBeanFactory();
//创建一个BeanDefinitionReader,构造参数是一个BeanDefinitionRegistry
//因为DefaultListableBeanFactory实现了BeanDefinitionRegistry,所以直接把beanFactory当做构造参数传过去
AnnotatedBeanDefinitionReader beanDefinitionReader =
new
AnnotatedBeanDefinitionReader(beanFactory);
//读取当前类 BeanFactoryDemo 为一个Bean,让Spring帮我们生成这个Bean
beanDefinitionReader.register(BeanFactoryDemo
. class )
;
//从容器中获取注册的BeanFactoryDemo的Bean
BeanFactoryDemo beanFactoryDemo = beanFactory.getBean(BeanFactoryDemo
. class )
;
System.out.println(
"beanFactoryDemo = "
+ beanFactoryDemo);
}
}
总结
ApplicationContext
public
class MyApplicationContext implements ApplicationContext
{
private
final
ResourcePatternResolver resourcePatternResolver =
new
PathMatchingResourcePatternResolver();
@Override
public
Resource[] getResources(String locationPattern)
throws
IOException {
return
resourcePatternResolver.getResources(locationPattern);
}
}
事件
1、什么是Spring Event 事件
ApplicationEvent
ApplicationListener
ApplicationEventPublisher
话不多说,上代码
// 火灾事件
public
class FireEvent extends ApplicationEvent
{
public FireEvent (String source)
{
super
(source);
}
}
public
class Call119FireEventListener implements ApplicationListener < FireEvent >
{
@Override
public void onApplicationEvent (FireEvent event)
{
System.out.println(
"打119"
);
}
}
public
class SavePersonFireEventListener implements ApplicationListener < FireEvent >
{
@Override
public void onApplicationEvent (FireEvent event)
{
System.out.println(
"救人"
);
}
}
public
class Application
{
public static void main (String[] args)
{
//创建一个Spring容器
AnnotationConfigApplicationContext applicationContext =
new
AnnotationConfigApplicationContext();
//将 事件监听器 注册到容器中
applicationContext.register(Call119FireEventListener
. class )
;
applicationContext.register(SavePersonFireEventListener
. class )
;
applicationContext.refresh();
// 发布着火的事件,触发监听
applicationContext.publishEvent(
new
FireEvent(
"着火了"
));
}
}
打
119
救人
2、Spring内置的事件
事件类型
触发时机
ContextRefreshedEvent
在调用ConfigurableApplicationContext 接口中的refresh()方法时触发
ContextStartedEvent
在调用ConfigurableApplicationContext的start()方法时触发
ContextStoppedEvent
在调用ConfigurableApplicationContext的stop()方法时触发
ContextClosedEvent
当ApplicationContext被关闭时触发该事件,也就是调用close()方法触发
3、Spring事件的传播特性
上代码
public
class EventPropagateApplication
{
public static void main (String[] args)
{
// 创建一个父容器
AnnotationConfigApplicationContext parentApplicationContext =
new
AnnotationConfigApplicationContext();
//将 打119监听器 注册到父容器中
parentApplicationContext.register(Call119FireEventListener
. class )
;
parentApplicationContext.refresh();
// 创建一个子容器
AnnotationConfigApplicationContext childApplicationContext =
new
AnnotationConfigApplicationContext();
//将 救人监听器 注册到子容器中
childApplicationContext.register(SavePersonFireEventListener
. class )
;
childApplicationContext.refresh();
// 设置一下父容器
childApplicationContext.setParent(parentApplicationContext);
// 通过子容器发布着火的事件,触发监听
childApplicationContext.publishEvent(
new
FireEvent(
"着火了"
));
}
}
救人
打
119
传播特性的一个小坑
总结
往期热门文章推荐
最后此篇关于三万字盘点Spring9大核心基础功能的文章就讲到这里了,如果你想了解更多关于三万字盘点Spring9大核心基础功能的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
Linux 有许多跨(假设是 2 个)CPU 内核执行的线程和进程。我希望我的单线程 C/C++ 应用程序成为 CPU0 上的唯一线程。我如何“移动”所有其他线程以使用 CPU1? 我知道我可以使用
我有一个类似于下图的数据库表 Table with 2 columns (UserId and value) 我将传递 UserId 和 2 个字符串。例如:userId: 1, key1: h1,
我想在我的新项目中使用 ASP.NET Core,因为我听说它更快。但是,该项目将使用广泛的数据库访问功能,Entity Framework Core 不支持其中一些功能。我想知道,是否可以使用 En
我已经使用 EntityFrameworkCore.SqlServer 2.0 开发了 asp .net core wep api 2.0 应用程序。它是使用数据库优先方法开发的。当尝试使用 dbco
我已经阅读了很多关于这个主题的文章,但我仍然无法处理这个问题。对不起,如果它是重复的,无论如何! 所以基本上,我正在从头开始构建一个 Angular 应用程序,并且我想按照最佳约定来组织我的代码。我有
我对MPI还是陌生的,所以如果这是一个琐碎的问题,请原谅我。我有一个四核CPU。我想运行一个在单个内核上使用两个进程的OpenMPI C++程序。有什么办法吗?如果是这样,那又如何?我提到了this
下面是一个传播异常处理机制的类问题,所需的输出是异常。任何人都可以解释为什么输出是异常,在此先感谢。 Class Question { public void m1() throws Excep
我想打印每个获得 CPU 时间片的进程的 name 和 pid。可能吗? 最佳答案 对于单个流程,您可以在以下位置获取此信息: /proc//stat 第14和第15个字段分别代表在用户态和内核态花费
我想知道是否可以识别具有特定 thread-id 的线程使用的物理处理器(核心)? 例如,我有一个多线程应用程序,它有两 (2) 个线程(例如,thread-id = 10 和 thread-id =
我有一个需要身份验证的 Solr 核心。假设我有一个用户,密码为password。当我现在尝试在控制台中创建一个 Solr 核心时 bin\solr create -c test 我收到 HTTP 错
我想为与使用它的项目不同的类库中的第二个和后续数据库创建迁移。有皱纹。我永远不会知道连接字符串,直到用户登录并且我可以从目录数据库 (saas) 中获取它。 对于目录数据库,我使用了来自 this 的
我想为一种可以产生 GHC Core 的简单语言创建一个前端。然后我想获取这个输出并通过正常的 GHC 管道运行它。根据this page , 不能直接通过 ghc 命令实现。我想知道是否有任何方法可
阅读文档,我构建了 2 个使用 BLE 连接 2 个 iDevices 的应用程序。 一个设备是中央设备,另一个是外围设备。 Central在寻找Peripheral,当找到它时,探索它的服务和特性,
在我的网络应用程序中,我对长时间运行的任务进行了操作,我想在后台调用此任务。因此,根据文档 .net core 3.1 Queued background tasks我为此使用这样的代码: publi
Solr 1.4 Enterprise Search Server 建议对核心副本进行大量更新,然后将其换成主核心。我正在按照以下步骤操作: 创建准备核心:http://localhost:8983/
它们是否存在,如果存在,文档和代码在哪里? 最佳答案 它们位于 Git 的 test 目录中。 https://github.com/jquery/jquery/tree/master/test 关于
我有一个 Lisp (SBCL 1.0.40.0.debian) 应用程序 (myfitnessdata),它使用以下代码来处理命令行参数: (:use :common-lisp) (:export
Core是GHC的中间语言。阅读Core可以帮助你更好地了解程序的性能。有人向我索要有关阅读 Core 的文档或教程,但我找不到太多。 有哪些文档可用于阅读 GHC Core? 这是我迄今为止发现的内
我有一个核心 WebJob 部署到 Azure Web 应用程序中。我正在使用WebJobs version 3.0.6 . 我注意到,WebJob 代码不会立即拾取对连接字符串和应用程序设置的更改(
我有一个在内部构造和使用 SqlConnection 类的第三方库。我可以从该类继承,但它有大量重载,到目前为止我一直无法找到合适的重载。我想要的是将参数附加到正在使用的连接字符串。 有没有办法在 .
我是一名优秀的程序员,十分优秀!