最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringCloud @RefreshScope注解源碼層面深入分析

 更新時(shí)間:2023年04月10日 09:03:35   作者:程序員李哈  
@RefreshScope注解能幫助我們做局部的參數(shù)刷新,但侵入性較強(qiáng),需要開(kāi)發(fā)階段提前預(yù)知可能的刷新點(diǎn),并且該注解底層是依賴于cglib進(jìn)行代理的,所以不要掉入cglib的坑,出現(xiàn)刷了也不更新情況

寫(xiě)在前面

最近在研究Spring Cloud和Spring Cloud Alibaba源碼,在看到Nacos的配置中心的時(shí)候,有注意到自動(dòng)刷新配置的玩法,底層實(shí)現(xiàn)依靠@RefreshScope注解。那么為什么要寫(xiě)這篇文章呢?筆者認(rèn)為@RefreshScope注解源碼實(shí)現(xiàn)跨度特別大,從Spring Cloud Alibaba 到Spring Cloud 到 Spring Boot 再到Spring,筆者認(rèn)為能夠理解它的源碼實(shí)現(xiàn)的話對(duì)Spring全家桶的理解又能上升一個(gè)檔次~

正文

版本如果下:

Spring:5.3.23
Spring Boot:2.6.3
Spring Cloud:3.1.4
Spring Cloud Alibaba:2021.0.4.0
Nacos:2.0.4

先會(huì)用,再深入源碼,所以我們需要從案例出發(fā)。

@RestController
@RefreshScope
public class ConsumerController {
    @Value("${consumer.value:moren}")
    private String value;
    @RequestMapping("/consumer")
    public String consumer(){
        return value;
    }
}

能夠正常使用Nacos服務(wù)端的配置數(shù)據(jù),所以,我們發(fā)現(xiàn)在類(lèi)上存在@RefreshScope注解,所以我們的重心看往@RefreshScope注解。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
	/**
	 * @see Scope#proxyMode()
	 * @return proxy mode
	 */
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

此時(shí),很清楚的看到@RefreshScope注解聚合了@Scope注解,并且賦予了"refresh". 此時(shí),從Spring慣用玩法,我們需要找到Spring何時(shí)解析的@Scope注解,何時(shí)解析的@RefreshScope注解。

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {   
                // 解析當(dāng)前類(lèi)上是否存在@scope注解
				ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
				…………
				if (checkCandidate(beanName, candidate)) {
					BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    // 生成Scope代理類(lèi)
					definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
					beanDefinitions.add(definitionHolder);
					registerBeanDefinition(definitionHolder, this.registry);
				}
			}
		}
		return beanDefinitions;
	}
	@Override
	public ScopeMetadata resolveScopeMetadata(BeanDefinition definition) {
		if (definition instanceof AnnotatedBeanDefinition) {
			AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition;
			AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
					annDef.getMetadata(), this.scopeAnnotationType);
		}
		return metadata;
	}
protected Class<? extends Annotation> scopeAnnotationType = Scope.class;

上面代碼,是解析@ComponentScan時(shí),掃描指定路徑包的@Component注解類(lèi),而我們的@RestController也是一個(gè)@Component,并且我們的@RestController類(lèi)上還有@RefreshScope注解,而@RefreshScope注解又存在@Scope注解。所以,接下來(lái)我們分析如何做Scope的代理。

public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
			BeanDefinitionRegistry registry, boolean proxyTargetClass) {
        // 拿到原類(lèi)的名字,和對(duì)應(yīng)的BeanDefinition
		String originalBeanName = definition.getBeanName();
		BeanDefinition targetDefinition = definition.getBeanDefinition();
		String targetBeanName = getTargetBeanName(originalBeanName);
        // 手動(dòng)創(chuàng)建Scope代理類(lèi)的BeanDefinition
        // 設(shè)置BeanDefinition的BeanClass為ScopedProxyFactoryBean
		RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
		proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
		proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
		proxyDefinition.setSource(definition.getSource());
		proxyDefinition.setRole(targetDefinition.getRole());
        …………
        // 將代理類(lèi)設(shè)置為自動(dòng)注入,并且優(yōu)先級(jí)最高。        
        proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
		proxyDefinition.setPrimary(targetDefinition.isPrimary());
		// 將原類(lèi)設(shè)置為不能自動(dòng)注入
		targetDefinition.setAutowireCandidate(false);
		targetDefinition.setPrimary(false);
		// 將原類(lèi)的BeanDefinition注冊(cè)到工廠中
		registry.registerBeanDefinition(targetBeanName, targetDefinition);
		return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
	}
public static String getTargetBeanName(String originalBeanName) {
	return TARGET_NAME_PREFIX + originalBeanName;
}
private static final String TARGET_NAME_PREFIX = "scopedTarget.";

這里就是非常關(guān)鍵的部分, 有沒(méi)有感覺(jué)是在"偷天換日"。手動(dòng)創(chuàng)建BeanDefintion,把ScopedProxyFactoryBean作為BeanClass,并且把原類(lèi)的name作為手動(dòng)創(chuàng)建BeanDefintion的name。把原類(lèi)的BeanDefinition的name加上前綴scopedTarget. 相信Spring底子好的讀者很容易看明白。下面是改變的流程圖。

那么,說(shuō)了這么多的意義在哪里呢?我們的主題不是動(dòng)態(tài)刷新么,怎么連注冊(cè)中心的影子都沒(méi)見(jiàn)到?年輕人,不要著急,文章開(kāi)頭就說(shuō)了本篇文章跨度很大~

此時(shí),我們需要從Spring Cloud規(guī)范包入手,相信各位讀者知道,Spring-Cloud-Commons和Spring-Cloud-Context 這兩個(gè)Spring Cloud規(guī)范包。從他們的spring.factories自動(dòng)裝配包可以得知以下信息。

@Bean
@ConditionalOnMissingBean(RefreshScope.class)
public static RefreshScope refreshScope() {
	return new RefreshScope();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBootstrapEnabled
public LegacyContextRefresher legacyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope,
			RefreshProperties properties) {
	return new LegacyContextRefresher(context, scope, properties);
}
@Bean
public RefreshEventListener refreshEventListener(ContextRefresher contextRefresher) {
	return new RefreshEventListener(contextRefresher);
}

從spring.factroies自動(dòng)裝配規(guī)范文件中我們能看到RefreshAutoConfiguration類(lèi),從類(lèi)名也能獲取到很多信息。再?gòu)钠渲械腀Bean中可以看到RefreshScope 、LegacyContextRefresher 、RefreshEventListener三個(gè)類(lèi)。那么下面從筆者的解釋和源碼深入理解這三個(gè)類(lèi)~!

  • RefreshScope:擴(kuò)展@Scope注解,并且CRUD名字為refresh的@Scope注解類(lèi),之前介紹的@RefreshScope注解中存在value為refresh的@Scope注解。
  • LegacyContextRefresher:用來(lái)刷新Environment。
  • RefreshEventListener:用來(lái)監(jiān)聽(tīng)ApplicationReadyEvent和RefreshEvent事件。
public class RefreshScope extends GenericScope
		implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered 
// RefreshScope的父類(lèi)
// 實(shí)現(xiàn)了BeanFactoryPostProcessor擴(kuò)展接口
public class GenericScope
		implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean 
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
	this.beanFactory = beanFactory;
    // 把當(dāng)前Scope注冊(cè)到BeanFacotry中。
	beanFactory.registerScope(this.name, this);
	setSerializationId(beanFactory);
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
	for (String name : registry.getBeanDefinitionNames()) {
		BeanDefinition definition = registry.getBeanDefinition(name);
		if (definition instanceof RootBeanDefinition) {
			RootBeanDefinition root = (RootBeanDefinition) definition;
			if (root.getDecoratedDefinition() != null && root.hasBeanClass()
					&& root.getBeanClass() == ScopedProxyFactoryBean.class) {
				if(getName().equals(root.getDecoratedDefinition().getBeanDefinition().getScope())) {
                    // 再一次的偷天換日
					root.setBeanClass(LockedScopedProxyFactoryBean.class);
					root.getConstructorArgumentValues().addGenericArgumentValue(this);
					root.setSynthetic(true);
				}
			}
		}
	}
}

這里注意到RefreshScope自動(dòng)注入的類(lèi),這里需要區(qū)分@RefreshScope注解和RefreshScope類(lèi)。它的父類(lèi)GenericScope實(shí)現(xiàn)了BeanFactroyPostProcessor。在postProcessBeanDefinitionRegistry回掉中可以清楚的看到再一次上演了"偷天換日",把ScopedProxyFactoryBean換成了LockedScopedProxyFactoryBean。并且在postProcessBeanFactory回掉中把RefreshScope作為Scope注冊(cè)到BeanFacotry工廠中。

// SmartApplicationListener子類(lèi)監(jiān)聽(tīng)器
public class RefreshEventListener implements SmartApplicationListener 
// 事件回掉。
@Override
public void onApplicationEvent(ApplicationEvent event) {
	if (event instanceof ApplicationReadyEvent) {
		handle((ApplicationReadyEvent) event);
	}
    // 一定要注意到RefreshEvent事件,非常非常非常重要!??!
    // 后續(xù)我們只需要找到哪里發(fā)出的這個(gè)事件即可。
	else if (event instanceof RefreshEvent) {
		handle((RefreshEvent) event);
	}
}
public void handle(RefreshEvent event) {
	if (this.ready.get()) { 
		Set<String> keys = this.refresh.refresh();
	}
}
public synchronized Set<String> refresh() {
    // 刷新Environment上下文。而我們知道配置數(shù)據(jù)是放在Environment中的。
	Set<String> keys = refreshEnvironment();
    // 調(diào)用scope的refreshAll,從方法就可以知道,要刷新所有的數(shù)據(jù)
	this.scope.refreshAll();
	return keys;
}
  • RefreshEventListener作為SmartApplicationListener的子類(lèi)實(shí)現(xiàn)onApplicationEvent方法
  • 監(jiān)聽(tīng)ApplicationReadyEvent和RefreshEvent事件
  • 調(diào)用ContextRefresh類(lèi)(LegacyContextRefresher)的refresh方法
  • refreshEnvironment方法刷新Environment,返回發(fā)生改變的配置數(shù)據(jù)
  • 調(diào)用RefreshScope類(lèi)的refreshAll方法,把整個(gè)RefreshScope中存放的Bean給destroy。
public synchronized Set<String> refreshEnvironment() {
    // 拿到更新前的Environment
	Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
    // 更新Environment。
	updateEnvironment();
    // 對(duì)比更新后和更新前,返回發(fā)生變化的數(shù)據(jù)。
	Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
	this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
	return keys;
}
@Override
protected void updateEnvironment() {
	addConfigFilesToEnvironment();
}
ConfigurableApplicationContext addConfigFilesToEnvironment() {
	ConfigurableApplicationContext capture = null;
	try {
		StandardEnvironment environment = copyEnvironment(getContext().getEnvironment());
		…………
		// 創(chuàng)建一個(gè)Spring Boot的啟動(dòng)器,目的:為了創(chuàng)建出Spring的上下文(這樣整個(gè)上下文刷新就可以得到最新的environment)。
		// 這里要注意,刷新Spring上下文的environment是手動(dòng)放入的,
		// 也即重新刷新Spring上下文的環(huán)境變量會(huì)加載到手動(dòng)創(chuàng)建的environment中
		SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class).bannerMode(Banner.Mode.OFF)
		.web(WebApplicationType.NONE).environment(environment);
		builder.application().setListeners(
			Arrays.asList(new BootstrapApplicationListener(), new BootstrapConfigFileApplicationListener()));
		capture = builder.run();
		MutablePropertySources target = getContext().getEnvironment().getPropertySources();
		String targetName = null;
		// 把再次刷新Spring上下文environment得到的數(shù)據(jù)賦值到原有的environment環(huán)境變量中(這不就完成了配置數(shù)據(jù)的刷新么)
		for (PropertySource<?> source : environment.getPropertySources()) {
			if (!this.standardSources.contains(name)) {
				if (target.contains(name)) {
					target.replace(name, source);
				}
				else {
					if (targetName != null) {
						target.addAfter(targetName, source);
							// update targetName to preserve ordering
						targetName = name;
					}
					else {
							// targetName was null so we are at the start of the list
						target.addFirst(source);
						targetName = name;
					}
				}
			}
		}
	}
	// finally中把臨時(shí)創(chuàng)建出的Application上下文給關(guān)閉。
	finally {
		ConfigurableApplicationContext closeable = capture;
		while (closeable != null) {
			closeable.close();
			if (closeable.getParent() instanceof ConfigurableApplicationContext) {
				closeable = (ConfigurableApplicationContext) closeable.getParent();
			}
			else {
				break;
			}
		}
	}
	return capture;
}

這里就是重點(diǎn)所在。一言以蔽之:手動(dòng)創(chuàng)建Environment對(duì)象,然后重新走一遍上下文刷新,這樣可以得到最新的配置文件,然后把手動(dòng)創(chuàng)建Environment對(duì)象賦值給當(dāng)前舊的上下文,這樣就完成了動(dòng)態(tài)刷新配置。詳細(xì)流程如下:

  • 創(chuàng)建出Environment對(duì)象。
  • 創(chuàng)建SpringApplicationBuilder對(duì)象,也即Spring Boot的啟動(dòng)器(手動(dòng)傳入創(chuàng)建出的Environment對(duì)象,這樣刷新上下文的時(shí)候使用的是這里創(chuàng)建的Environment對(duì)象)
  • 調(diào)用SpringApplication的run方法,刷新Spring Boot和Spring上下文(刷新完成后返回Spring上下文)。
  • 把刷新Spring Boot和Spring上下文得到的Environment對(duì)象的屬性拷貝到當(dāng)前Spring上下文中
  • close掉刷新完成后返回Spring上下文的。
public void refreshAll() {
	super.destroy();
	this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
@Override
public void destroy() {
	List<Throwable> errors = new ArrayList<Throwable>();
    // 清空緩存
	Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
    // 調(diào)用摧毀的回掉函數(shù)
	for (BeanLifecycleWrapper wrapper : wrappers) {
		try {
			Lock lock = this.locks.get(wrapper.getName()).writeLock();
			lock.lock();
			try {
				wrapper.destroy();
			}
			finally {
				lock.unlock();
			}
		}
		catch (RuntimeException e) {
			errors.add(e);
		}
	}
}

這里就是把RefreshScope中所有的緩存的給clear,并且回掉Bean的destroy方法。這里我必須再次強(qiáng)調(diào)RefreshScope的作用,就是CRUD 類(lèi)上標(biāo)有value為"refresh"的@Scope注解(@RefreshScope不就是么,所以我們的ConsumerController這個(gè)Bean就是交給RefreshScope管理,可能到這里筆者有點(diǎn)懵逼,為什么ConsumerController交給RefreshScope管理?他不是Spring的Bean么,不是要進(jìn)入三級(jí)緩存中么???那么下面就是為了解答這個(gè)問(wèn)題。)

我們看到getBean的doGetBean方法創(chuàng)建Bean的流程中。

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
throws BeansException {
    // 創(chuàng)建singleton的bean
	if (mbd.isSingleton()) {
        …………
	}
    // 創(chuàng)建Prototype的bean
	else if (mbd.isPrototype()) {
		…………
	}
    // 創(chuàng)建其他Scope作用域的bean
	else {
		String scopeName = mbd.getScope();
		if (!StringUtils.hasLength(scopeName)) {
			throw new IllegalStateException("No scope name defined for bean '" + beanName + "'");
		}
        // 根據(jù)name拿到對(duì)應(yīng)的Scope。
		Scope scope = this.scopes.get(scopeName);
		if (scope == null) {
			throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
		}
		try {
            // 調(diào)用scope的get方法,所以對(duì)應(yīng)的scope可以緩存。
			Object scopedInstance = scope.get(beanName, () -> {
				beforePrototypeCreation(beanName);
				try {
					return createBean(beanName, mbd, args);
				}
				finally {
					afterPrototypeCreation(beanName);
				}
			});
			beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
		}
		catch (IllegalStateException ex) {
			throw new ScopeNotActiveException(beanName, scopeName, ex);
		}
	}
}

在else代碼塊中會(huì)去處理不同Scope作用域的Bean,而在RefreshScope的父類(lèi)GenericScope中的postProcessBeanFactory回掉方法中(前面有介紹)會(huì)把RefreshScope作為一個(gè)Scope注冊(cè)到BeanFactory中(所以上文的refreshAll方法把RefreshScope的緩存全部clear掉了,下次就會(huì)去createBean,就會(huì)重新走一遍Spring創(chuàng)建Bean的過(guò)程,而環(huán)境變量已經(jīng)更改了,@Value注解的注入就會(huì)注入到新的環(huán)境變量中的配置數(shù)據(jù))。

這里把RefreshScope的緩存全部clear掉了,那么總要有一個(gè)地方每次都來(lái)拿一遍數(shù)據(jù)(這樣緩存在就拿緩存的,緩存不在(緩存不在就代表被clear了,而clear掉了代表有地方發(fā)生了RefreshEvent事件,執(zhí)行了refreshAll方法和refreshEnvironment,緩存被清除了,環(huán)境變量被更改了)就重新createBean,拿到最新的環(huán)境變量)

此時(shí),我們是不是忘了,我們的ConsumerController被代理了呢?上文介紹RefreshScope的父類(lèi)GenericScope中postProcessBeanDefinitionRegistry方法注冊(cè)了LockedScopedProxyFactoryBean。

public static class LockedScopedProxyFactoryBean<S extends GenericScope> extends ScopedProxyFactoryBean
			implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
	    // 反射的Method
        Method method = invocation.getMethod();
	    …………
	    try {
		    if (proxy instanceof Advised) {
			    Advised advised = (Advised) proxy;
			    ReflectionUtils.makeAccessible(method);
			    // advised.getTargetSource().getTarget()方法會(huì)去getBean
			    return ReflectionUtils.invokeMethod(method, advised.getTargetSource().getTarget(),
				invocation.getArguments());
		    }
		    return invocation.proceed();
	    }
    }
}
public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
	@Override
	public Object getTarget() throws Exception {
		// 看到熟悉的getBean方法了。
		return getBeanFactory().getBean(getTargetBeanName());
	}
}

LockedScopedProxyFactoryBean實(shí)現(xiàn)了MethodInterceptor接口,所以只要調(diào)用ConsumerController類(lèi)中的方法就會(huì)走到LockedScopedProxyFactoryBean的invoke方法。恰好在invoke方法中會(huì)去調(diào)用BeanFactory的getBean方法。而getBean再到doGetBean,再到上面介紹的else代碼塊中,就走到RefreshScope類(lèi)中的get方法了。

@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
    // 拿緩存,如果緩存中沒(méi)有就創(chuàng)建
	BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
	this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
	try {
        // 拿緩存,如果緩存中沒(méi)有就回掉createBean方法。
		return value.getBean();
	}
	catch (RuntimeException e) {
		this.errors.put(name, e);
		throw e;
	}
}

看到get方法其實(shí)就恍然大悟了,因?yàn)樵贚ockedScopedProxyFactoryBean的invoke方法中會(huì)調(diào)用getBean,getBean調(diào)用doGetBean中會(huì)調(diào)用scope.get方法,而在get方法中會(huì)去拿緩存,如果沒(méi)有緩存就會(huì)創(chuàng)建一個(gè)新的,而新的就會(huì)去執(zhí)行createBean方法創(chuàng)建一個(gè)新的Bean出來(lái)。恰好在這之前緩存已經(jīng)被清除了,環(huán)境變量更新了。最終createBean方法創(chuàng)建的時(shí)候@Value注入的就是最新的環(huán)境變量中的配置數(shù)據(jù)。

所以,RefreshScope類(lèi)+@RefreshScope注解控制了Bean的創(chuàng)建,RefreshEvent事件控制了緩存的clear和環(huán)境變量的更新。但是我們似乎還沒(méi)有閉環(huán)RefreshEvent事件在哪里發(fā)出的。

public class NacosContextRefresher
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
	private void registerNacosListener(final String groupKey, final String dataKey) {
		// 一個(gè)group、dataid對(duì)應(yīng)一組事件。
		String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
		// 創(chuàng)建一個(gè)Nacos事件監(jiān)聽(tīng)器
		Listener listener = listenerMap.computeIfAbsent(key,
			lst -> new AbstractSharedListener() {
				@Override
				public void innerReceive(String dataId, String group,
					String configInfo) {
					// 這里發(fā)送了RefreshEvent事件。
					applicationContext.publishEvent(
						new RefreshEvent(this, null, "Refresh Nacos config"));
				}
			});
		try {
			// 添加Nacos的內(nèi)部事件
			configService.addListener(dataKey, groupKey, listener);
		}
	}
}

對(duì)于Nacos的配置中心代碼不過(guò)細(xì)講,我們能夠知道RefreshEvent事件是從Nacos內(nèi)部發(fā)出的即可

到此這篇關(guān)于SpringCloud @RefreshScope注解源碼層面深入分析的文章就介紹到這了,更多相關(guān)SpringCloud @RefreshScope內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • mybatis自動(dòng)建表的實(shí)現(xiàn)方法

    mybatis自動(dòng)建表的實(shí)現(xiàn)方法

    這篇文章主要介紹了mybatis自動(dòng)建表的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • 關(guān)于JDK8中的字符串拼接示例詳解

    關(guān)于JDK8中的字符串拼接示例詳解

    字符串拼接問(wèn)題應(yīng)該是每個(gè)Java程序員都熟知的事情了,幾乎每個(gè)Java程序員都讀過(guò)關(guān)于StringBuffer/StringBuilder來(lái)拼接字符串。下面這篇文章主要給大家介紹了關(guān)于JDK8中的字符串拼接的相關(guān)資料,需要的朋友可以參考下。
    2018-04-04
  • 基于java時(shí)區(qū)轉(zhuǎn)換夏令時(shí)的問(wèn)題及解決方法

    基于java時(shí)區(qū)轉(zhuǎn)換夏令時(shí)的問(wèn)題及解決方法

    下面小編就為大家分享一篇基于java時(shí)區(qū)轉(zhuǎn)換夏令時(shí)的問(wèn)題及解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2017-11-11
  • 基于SpringBoot多線程@Async的使用體驗(yàn)

    基于SpringBoot多線程@Async的使用體驗(yàn)

    這篇文章主要介紹了SpringBoot多線程@Async的使用體驗(yàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-12-12
  • Java點(diǎn)餐小程序之黑心商人

    Java點(diǎn)餐小程序之黑心商人

    這篇文章主要介紹了一個(gè)Java編程的小程序-點(diǎn)餐系統(tǒng),算是對(duì)之前所學(xué)習(xí)的Java基礎(chǔ)知識(shí)作了一個(gè)匯總,需要的朋友可以參考下
    2017-09-09
  • Java編程實(shí)現(xiàn)基于用戶的協(xié)同過(guò)濾推薦算法代碼示例

    Java編程實(shí)現(xiàn)基于用戶的協(xié)同過(guò)濾推薦算法代碼示例

    這篇文章主要介紹了Java編程實(shí)現(xiàn)基于用戶的協(xié)同過(guò)濾推薦算法代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11
  • 那些年用httpclient時(shí)踩過(guò)的一些坑

    那些年用httpclient時(shí)踩過(guò)的一些坑

    這篇文章主要給大家介紹了關(guān)于那些年用httpclient時(shí)踩過(guò)的一些坑,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用httpclient具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05
  • JVM?運(yùn)行時(shí)數(shù)據(jù)區(qū)與JMM?內(nèi)存模型

    JVM?運(yùn)行時(shí)數(shù)據(jù)區(qū)與JMM?內(nèi)存模型

    這篇文章主要介紹了JVM?運(yùn)行時(shí)數(shù)據(jù)區(qū)與JMM?內(nèi)存模型,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值。需要的朋友可以參考一下
    2022-07-07
  • 淺談java的TCP和UDP編程(附實(shí)例講解)

    淺談java的TCP和UDP編程(附實(shí)例講解)

    下面小編就為大家?guī)?lái)一篇淺談java的TCP和UDP編程(附實(shí)例講解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-05-05
  • Java數(shù)據(jù)類(lèi)型之引用數(shù)據(jù)類(lèi)型解讀

    Java數(shù)據(jù)類(lèi)型之引用數(shù)據(jù)類(lèi)型解讀

    這篇文章主要介紹了Java數(shù)據(jù)類(lèi)型之引用數(shù)據(jù)類(lèi)型,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-07-07

最新評(píng)論

定远县| 太康县| 太仓市| 永新县| 聊城市| 饶平县| 衢州市| 克什克腾旗| 策勒县| 江口县| 光山县| 朝阳县| 长丰县| 林西县| 炉霍县| 思南县| 康平县| 惠来县| 沽源县| 建水县| 泉州市| 图们市| 福海县| 鲁山县| 花莲县| 灌阳县| 吉隆县| 太仓市| 宜章县| 博乐市| 嘉兴市| 肃南| 鄂托克旗| 田东县| 华亭县| 新巴尔虎右旗| 紫阳县| 海城市| 扎兰屯市| 抚州市| 黄石市|