詳解Spring簡(jiǎn)單容器中的Bean基本加載過(guò)程
本篇將對(duì)定義在 XMl 文件中的 bean,從靜態(tài)的的定義到變成可以使用的對(duì)象的過(guò)程,即 bean 的加載和獲取的過(guò)程進(jìn)行一個(gè)整體的了解,不去深究,點(diǎn)到為止,只求對(duì) Spring IOC 的實(shí)現(xiàn)過(guò)程有一個(gè)整體的感知,具體實(shí)現(xiàn)細(xì)節(jié)留到后面用針對(duì)性的篇章進(jìn)行講解。
首先我們來(lái)引入一個(gè) Spring 入門使用示例,假設(shè)我們現(xiàn)在定義了一個(gè)類 org.zhenchao.framework.MyBean ,我們希望利用 Spring 來(lái)管理類對(duì)象,這里我們利用 Spring 經(jīng)典的 XMl 配置文件形式進(jìn)行配置:
<?xml version="1.0" encoding="UTF-8"?>
<beansxmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- bean的基本配置 -->
<beanname="myBean"class="org.zhenchao.framework.MyBean"/>
</beans>
我們將上面的配置文件命名為 spring-core.xml,則對(duì)象的最原始的獲取和使用示例如下:
// 1. 定義資源
Resource resource = new ClassPathResource("spring-core.xml");
// 2. 利用XmlBeanFactory解析并注冊(cè)bean定義
XmlBeanFactory beanFactory = new XmlBeanFactory(resource);
// 3. 從IOC容器加載獲取bean
MyBean myBean = (MyBean) beanFactory.getBean("myBean");
// 4. 使用bean
myBean.sayHello();
上面 demo 雖然簡(jiǎn)單,但麻雀雖小,五臟俱全,完整的讓 Spring 執(zhí)行了一遍配置文件加載,并獲取 bean 的過(guò)程。雖然從 Spring 3.1 開始 XmlBeanFactory 已經(jīng)被置為 Deprecated ,但是 Spring 并沒有定義出更加高級(jí)的基于 XML 加載 bean 的 BeanFactory,而是推薦采用更加原生的方式,即組合使用 DefaultListableBeanFactory 和 XmlBeanDefinitionReader 來(lái)完成上訴過(guò)程:
Resource resource = new ClassPathResource("spring-core.xml");
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
reader.loadBeanDefinitions(resource);
MyBean myBean = (MyBean) beanFactory.getBean("myBean");
myBean.sayHello();
后面的分析你將會(huì)看到 XmlBeanFactory 實(shí)際上是對(duì) DefaultListableBeanFactory 和 XmlBeanDefinitionReader 組合使用方式的封裝,所以這里我們?nèi)匀粚⒗^續(xù)分析基于 XmlBeanFactory 加載 bean 的過(guò)程。
一. Bean的解析和注冊(cè)

Bean的加載過(guò)程,主要是對(duì)配置文件的解析,并注冊(cè) bean 的過(guò)程,上圖是加載過(guò)程的時(shí)序圖,當(dāng)我們 new XmlBeanFactory(resource) 的時(shí)候,已經(jīng)完成將配置文件包裝成了 Spring 定義的資源,并觸發(fā)解析和注冊(cè)。 new XmlBeanFactory(resource) 調(diào)用的是下面的構(gòu)造方法:
publicXmlBeanFactory(Resource resource)throwsBeansException{
this(resource, null);
}
這個(gè)構(gòu)造方法本質(zhì)上還是繼續(xù)調(diào)用了:
publicXmlBeanFactory(Resource resource, BeanFactory parentBeanFactory)throwsBeansException{
super(parentBeanFactory);
// 加載xml資源
this.reader.loadBeanDefinitions(resource);
}
在這個(gè)構(gòu)造方法里面先是調(diào)用了父類構(gòu)造函數(shù),即 org.springframework.beans.factory.support.DefaultListableBeanFactory 類,這是一個(gè)非常核心的類,它包含了基本 IOC 容器所具有的重要功能,是一個(gè) IOC 容器的基本實(shí)現(xiàn)。然后是調(diào)用了 this.reader.loadBeanDefinitions(resource) ,從這里開始加載配置文件。
Spring 在設(shè)計(jì)采用了許多程序設(shè)計(jì)的基本原則,比如迪米特法則、開閉原則,以及接口隔離原則等等,這樣的設(shè)計(jì)為后續(xù)的擴(kuò)展提供了靈活性,也增強(qiáng)了模塊的復(fù)用性,這也是我看 Spring 源碼的動(dòng)力之一,希望通過(guò)閱讀學(xué)習(xí)的過(guò)程來(lái)提升自己接口設(shè)計(jì)的能力。Spring 使用了專門的資源加載器對(duì)資源進(jìn)行加載,這里的 reader 就是 org.springframework.beans.factory.xml.XmlBeanDefinitionReader 對(duì)象,專門用來(lái)加載基于 XML 文件配置的 bean。這里的加載過(guò)程為:
- 利用 EncodedResource 二次包裝資源文件
- 獲取資源輸入流,并構(gòu)造 InputSource 對(duì)象
- 獲取 XML 文件的實(shí)體解析器和驗(yàn)證模式
- 加載 XML 文件,獲取對(duì)應(yīng)的 Document 對(duì)象
- 由 Document 對(duì)象解析并注冊(cè) bean
1.利用 EncodedResource 二次包裝資源文件
采用 org.springframework.core.io.support.EncodedResource 對(duì)resource 進(jìn)行二次封裝.
2.獲取資源輸入流,并構(gòu)造 InputSource 對(duì)象
對(duì)資源進(jìn)行編碼封裝之后,開始真正進(jìn)入 this.loadBeanDefinitions(new EncodedResource(resource)) 的過(guò)程,該方法源碼如下:
publicintloadBeanDefinitions(EncodedResource encodedResource)throwsBeanDefinitionStoreException{
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
// 標(biāo)記正在加載的資源,防止循環(huán)引用
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// 獲取資源的輸入流
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// 構(gòu)造InputSource對(duì)象
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 真正開始從XML文件中加載Bean定義
return this.doLoadBeanDefinitions(inputSource, encodedResource.getResource());
} finally {
inputStream.close();
}
} catch (IOException ex) {
throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), ex);
} finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
需要知曉的是 org.xml.sax.InputSource 不是 Spring 中定義的類,這個(gè)類來(lái)自 jdk,是 java 對(duì) XML 實(shí)體提供的原生支持。這個(gè)方法主要還是做了一些準(zhǔn)備工作,按照 Spring 方法的命名相關(guān),真正干活的方法一般都是以 “do” 開頭的,這里的 this.doLoadBeanDefinitions(inputSource, encodedResource.getResource()) 就是真正開始加載 XMl 的入口,該方法源碼如下:
protectedintdoLoadBeanDefinitions(InputSource inputSource, Resource resource)throwsBeanDefinitionStoreException{
try {
// 1. 加載xml文件,獲取到對(duì)應(yīng)的Document(包含獲取xml文件的實(shí)體解析器和驗(yàn)證模式)
Document doc = this.doLoadDocument(inputSource, resource);
// 2. 解析Document對(duì)象,并注冊(cè)bean
return this.registerBeanDefinitions(doc, resource);
} catch (BeanDefinitionStoreException ex) {
// 這里是連環(huán)catch,省略
}
}
方面里面的邏輯還是很清晰的,第一步獲取 org.w3c.dom.Document 對(duì)象,第二步由該對(duì)象解析得到 BeanDefinition 對(duì)象,并注冊(cè)到 IOC 容器中。
3.獲取 XML 文件的實(shí)體解析器和驗(yàn)證模式
this.doLoadDocument(inputSource, resource) 包含了獲取實(shí)體解析器、驗(yàn)證模式,以及 Document 對(duì)象的邏輯,源碼如下:
protectedDocumentdoLoadDocument(InputSource inputSource, Resource resource)throwsException{
return this.documentLoader.loadDocument(
inputSource,
this.getEntityResolver(), // 獲取實(shí)體解析器
this.errorHandler,
this.getValidationModeForResource(resource), // 獲取驗(yàn)證模式
this.isNamespaceAware());
}
XML 是半結(jié)構(gòu)化數(shù)據(jù),XML 的驗(yàn)證模式用于保證結(jié)構(gòu)的正確性,常見的驗(yàn)證模式有 DTD 和 XSD 兩種,獲取驗(yàn)證模式的源碼如下:
protectedintgetValidationModeForResource(Resource resource){
int validationModeToUse = this.getValidationMode();
if (validationModeToUse != VALIDATION_AUTO) {
// 手動(dòng)指定了驗(yàn)證模式
return validationModeToUse;
}
// 沒有指定驗(yàn)證模式,則自動(dòng)檢測(cè)
int detectedMode = this.detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// 檢測(cè)驗(yàn)證模式失敗,默認(rèn)采用XSD驗(yàn)證
return VALIDATION_XSD;
}
上面源碼描述了獲取驗(yàn)證模式的執(zhí)行流程,如果沒有手動(dòng)指定,那么 Spring 會(huì)去自動(dòng)檢測(cè)。對(duì)于 XML 文件的解析,SAX 首先會(huì)讀取 XML 文件頭聲明,以獲取對(duì)應(yīng)驗(yàn)證文件地址,并下載對(duì)應(yīng)的文件,如果網(wǎng)絡(luò)不正常,則會(huì)影響下載過(guò)程,這個(gè)時(shí)候可以通過(guò)注冊(cè)一個(gè)實(shí)體解析來(lái)實(shí)現(xiàn)尋找驗(yàn)證文件的過(guò)程。
4.加載 XML 文件,獲取對(duì)應(yīng)的 Document 對(duì)象
獲取對(duì)應(yīng)的驗(yàn)證模式和解析器,解析去就可以加載 Document 對(duì)象了,這里本質(zhì)上調(diào)用的是 org.springframework.beans.factory.xml.DefaultDocumentLoader 的 loadDocument() 方法,源碼如下:
publicDocumentloadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = this.createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = this.createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
整個(gè)過(guò)程類似于我們平常解析 XML 文件的流程。
5.由 Document 對(duì)象解析并注冊(cè) bean
完成了對(duì) XML 文件的到 Document 對(duì)象的解析,我們終于可以解析 Document 對(duì)象,并注冊(cè) bean 了,這一過(guò)程發(fā)生在 this.registerBeanDefinitions(doc, resource) 中,源碼如下:
publicintregisterBeanDefinitions(Document doc, Resource resource)throwsBeanDefinitionStoreException{
// 使用DefaultBeanDefinitionDocumentReader構(gòu)造
BeanDefinitionDocumentReader documentReader = this.createBeanDefinitionDocumentReader();
// 記錄之前已經(jīng)注冊(cè)的BeanDefinition個(gè)數(shù)
int countBefore = this.getRegistry().getBeanDefinitionCount();
// 加載并注冊(cè)bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 返回本次加載的bean的數(shù)量
return getRegistry().getBeanDefinitionCount() - countBefore;
}
這里方法的作用是創(chuàng)建對(duì)應(yīng)的 BeanDefinitionDocumentReader,并計(jì)算返回了過(guò)程中新注冊(cè)的 bean 的數(shù)量,而具體的注冊(cè)過(guò)程,則是由 BeanDefinitionDocumentReader 來(lái)完成的,具體的實(shí)現(xiàn)位于子類 DefaultBeanDefinitionDocumentReader 中:
publicvoidregisterBeanDefinitions(Document doc, XmlReaderContext readerContext){
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
// 獲取文檔的root結(jié)點(diǎn)
Element root = doc.getDocumentElement();
this.doRegisterBeanDefinitions(root);
}
還是按照 Spring 命名習(xí)慣,doRegisterBeanDefinitions 才是真正干活的地方,這也是真正開始解析配置的核心所在:
protectedvoiddoRegisterBeanDefinitions(Element root){
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = this.createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// 處理profile標(biāo)簽(其作用類比pom.xml中的profile)
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles =
StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!this.getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + "] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
// 解析預(yù)處理,留給子類實(shí)現(xiàn)
this.preProcessXml(root);
// 解析并注冊(cè)BeanDefinition
this.parseBeanDefinitions(root, this.delegate);
// 解析后處理,留給子類實(shí)現(xiàn)
this.postProcessXml(root);
this.delegate = parent;
}
方法中顯示處理了 標(biāo)簽,這個(gè)屬性在 Spring 中不是很常用,不過(guò)在 maven 的 pom.xml 中則很常見,意義也是相同的,就是配置多套環(huán)境,從而在部署的時(shí)候可以根據(jù)具體環(huán)境來(lái)選擇使用哪一套配置。方法中會(huì)先去檢測(cè)是否配置了 profile,如果配置了就需要從上下文環(huán)境中確認(rèn)當(dāng)前激活了哪一套 profile。
方法在解析并注冊(cè) BeanDefinition 前后各設(shè)置一個(gè)模板方法,留給子類擴(kuò)展實(shí)現(xiàn),而在 this.parseBeanDefinitions(root, this.delegate) 中執(zhí)行解析和注冊(cè)邏輯:
protectedvoidparseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate){
if (delegate.isDefaultNamespace(root)) {
// 解析默認(rèn)標(biāo)簽
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 解析默認(rèn)標(biāo)簽
this.parseDefaultElement(ele, delegate);
} else {
// 解析自定義標(biāo)簽
delegate.parseCustomElement(ele);
}
}
}
} else {
// 解析自定義標(biāo)簽
delegate.parseCustomElement(root);
}
}
方法中判斷當(dāng)前標(biāo)簽是默認(rèn)標(biāo)簽還是自定義標(biāo)簽,并按照不同的策略去解析,這是一個(gè)復(fù)雜的過(guò)程,后面用文章進(jìn)行針對(duì)性講解,這里不在往下細(xì)究。
到這里我們已經(jīng)完成了靜態(tài)配置到動(dòng)態(tài) BeanDefinition 的解析,這個(gè)時(shí)候 bean 的定義已經(jīng)處于內(nèi)存中,解析去將是探究如何獲取并使用 bean 的過(guò)程。
二. Bean的獲取
在完成了 Bean 的加載過(guò)程之后,我們可以調(diào)用 beanFactory.getBean("myBean") 方法來(lái)獲取目標(biāo)對(duì)象,這里本質(zhì)上調(diào)用的是 org.springframework.beans.factory.support.AbstractBeanFactory 的 getBean() 方法,源碼如下:
publicObjectgetBean(String name)throwsBeansException{
return this.doGetBean(name, null, null, false);
}
這里調(diào)用 this.doGetBean(name, null, null, false) 來(lái)實(shí)現(xiàn)具體邏輯,也符合我們的預(yù)期,該方法可以看做是獲取 bean 的整體框架,一個(gè)函數(shù)完成了整個(gè)過(guò)程的模塊調(diào)度,還是挺復(fù)雜的:
protected <T> TdoGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {
/*
* 轉(zhuǎn)化對(duì)應(yīng)的beanName
*
* 傳入的參數(shù)可能是alias,也可能是FactoryBean,所以需要進(jìn)行解析,主要包含以下內(nèi)容:
* 1. 去除FactoryBean的修飾符“&”
* 2. 取指定alias對(duì)應(yīng)的最終的name
*/
final String beanName = this.transformedBeanName(name);
Object bean;
/*
* 檢查緩存或者實(shí)例工廠中是否有對(duì)應(yīng)的實(shí)例
*
* 為什么會(huì)一開始就進(jìn)行檢查?
* 因?yàn)樵趧?chuàng)建單例bean的時(shí)候會(huì)存在依賴注入的情況,而在創(chuàng)建依賴的時(shí)候?yàn)榱吮苊庋h(huán)依賴
* Spring創(chuàng)建bean的原則是不等bean創(chuàng)建完成就會(huì)將創(chuàng)建bean的ObjectFactory提前曝光,即將對(duì)應(yīng)的ObjectFactory加入到緩存
* 一旦下一個(gè)bean創(chuàng)建需要依賴上一個(gè)bean,則直接使用ObjectFactory
*/
Object sharedInstance = this.getSingleton(beanName); // 獲取單例
if (sharedInstance != null && args == null) {
// 實(shí)例已經(jīng)存在
if (logger.isDebugEnabled()) {
if (this.isSingletonCurrentlyInCreation(beanName)) {
logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");
} else {
logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
}
}
// 返回對(duì)應(yīng)的實(shí)例
bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, null);
} else {
// 單例實(shí)例不存在
if (this.isPrototypeCurrentlyInCreation(beanName)) {
/*
* 只有在單例模式下才會(huì)嘗試解決循環(huán)依賴問(wèn)題
* 對(duì)于原型模式,如果存在循環(huán)依賴,也就是滿足this.isPrototypeCurrentlyInCreation(beanName),拋出異常
*/
throw new BeanCurrentlyInCreationException(beanName);
}
BeanFactory parentBeanFactory = this.getParentBeanFactory();
if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {
// 如果在beanDefinitionMap中(即所有已經(jīng)加載的類中)不包含目標(biāo)bean,則嘗試從parentBeanFactory中檢測(cè)
String nameToLookup = this.originalBeanName(name);
if (args != null) {
// 遞歸到BeanFactory中尋找
return (T) parentBeanFactory.getBean(nameToLookup, args);
} else {
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}
// 如果不僅僅是做類型檢查,則創(chuàng)建bean
if (!typeCheckOnly) {
this.markBeanAsCreated(beanName);
}
try {
/*
* 將存儲(chǔ)XML配置的GenericBeanDefinition轉(zhuǎn)換成RootBeanDefinition
* 如果指定了beanName是子bean的話,同時(shí)會(huì)合并父類的相關(guān)屬性
*/
final RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
this.checkMergedBeanDefinition(mbd, beanName, args);
// 獲取當(dāng)前bean依賴的bean
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
// 存在依賴,遞歸實(shí)例化依賴的bean
for (String dep : dependsOn) {
if (this.isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
// 緩存依賴調(diào)用
this.registerDependentBean(dep, beanName);
this.getBean(dep);
}
}
// 實(shí)例化依賴的bean后,實(shí)例化mbd自身
if (mbd.isSingleton()) {
// scope == singleton
sharedInstance = this.getSingleton(beanName, new ObjectFactory<Object>() {
@Override
publicObjectgetObject()throwsBeansException{
try {
return createBean(beanName, mbd, args);
} catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
}
});
bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
// scope == prototype
Object prototypeInstance;
try {
this.beforePrototypeCreation(beanName);
prototypeInstance = this.createBean(beanName, mbd, args);
} finally {
this.afterPrototypeCreation(beanName);
}
// 返回對(duì)應(yīng)的實(shí)例
bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
// 其它scope
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
@Override
publicObjectgetObject()throwsBeansException{
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
} finally {
afterPrototypeCreation(beanName);
}
}
});
// 返回對(duì)應(yīng)的實(shí)例
bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
} catch (IllegalStateException ex) {
throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex);
}
}
} catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
// 檢查需要的類型是否符合bean的實(shí)際類型,對(duì)應(yīng)getBean時(shí)指定的requireType
if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
try {
return this.getTypeConverter().convertIfNecessary(bean, requiredType);
} catch (TypeMismatchException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
java程序員自己的圖片轉(zhuǎn)文字OCR識(shí)圖工具分享
這篇文章主要介紹了java程序員自己的圖片轉(zhuǎn)文字OCR識(shí)圖工具,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
java8使用流的filter來(lái)篩選數(shù)據(jù)的實(shí)現(xiàn)
這篇文章主要介紹了java8使用流的filter來(lái)篩選數(shù)據(jù),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
java?中的HashMap的底層實(shí)現(xiàn)和元素添加流程
這篇文章主要介紹了java?中的HashMap的底層實(shí)現(xiàn)和元素添加流程,HashMap?是使用頻率最高的數(shù)據(jù)類型之一,同時(shí)也是面試必問(wèn)的問(wèn)題之一,尤其是它的底層實(shí)現(xiàn)原理,下文更多詳細(xì)內(nèi)容,需要的小伙伴可以參考一下2022-05-05
解決微服務(wù)中關(guān)于用戶token處理到的坑
這篇文章主要介紹了解決微服務(wù)中關(guān)于用戶token處理到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08

