SpringBoot中動(dòng)態(tài)注入Bean的技巧分享
在 Spring Boot 開(kāi)發(fā)中,動(dòng)態(tài)注入 Bean 是一種強(qiáng)大的技術(shù),它允許我們根據(jù)特定條件或運(yùn)行時(shí)環(huán)境靈活地創(chuàng)建和管理 Bean。
相比于傳統(tǒng)的靜態(tài) Bean 定義,動(dòng)態(tài)注入提供了更高的靈活性和可擴(kuò)展性,特別適合構(gòu)建可插拔的模塊化系統(tǒng)和處理復(fù)雜的業(yè)務(wù)場(chǎng)景。
本文將介紹 Spring Boot 中三種動(dòng)態(tài) Bean 注入技巧。
一、條件化 Bean 配置
1.1 基本原理
條件化 Bean 配置是 Spring Boot 中最常用的動(dòng)態(tài)注入方式,它允許我們根據(jù)特定條件決定是否創(chuàng)建 Bean。
Spring Boot 提供了豐富的條件注解,可以基于類路徑、Bean 存在情況、屬性值、系統(tǒng)環(huán)境等因素動(dòng)態(tài)決定 Bean 的創(chuàng)建。
1.2 常用條件注解
Spring Boot 提供了多種條件注解,最常用的包括:
@ConditionalOnProperty:基于配置屬性的條件@ConditionalOnBean:基于特定 Bean 存在的條件@ConditionalOnMissingBean:基于特定 Bean 不存在的條件@ConditionalOnClass:基于類路徑上有指定類的條件@ConditionalOnMissingClass:基于類路徑上沒(méi)有指定類的條件@ConditionalOnExpression:基于 SpEL 表達(dá)式的條件@ConditionalOnWebApplication:基于是否是 Web 應(yīng)用的條件@ConditionalOnResource:基于資源是否存在的條件
1.3 代碼示例
下面是一個(gè)綜合示例,展示如何使用條件注解動(dòng)態(tài)注入不同的數(shù)據(jù)源 Bean:
@Configuration
public class DataSourceConfig {
@Bean
@ConditionalOnProperty(name = "datasource.type", havingValue = "mysql", matchIfMissing = true)
public DataSource mysqlDataSource() {
// 創(chuàng)建 MySQL 數(shù)據(jù)源
return new MySQLDataSource();
}
@Bean
@ConditionalOnProperty(name = "datasource.type", havingValue = "postgresql")
public DataSource postgresqlDataSource() {
// 創(chuàng)建 PostgreSQL 數(shù)據(jù)源
return new PostgreSQLDataSource();
}
@Bean
@ConditionalOnProperty(name = "datasource.type", havingValue = "mongodb")
@ConditionalOnClass(name = "com.mongodb.client.MongoClient")
public DataSource mongodbDataSource() {
// 創(chuàng)建 MongoDB 數(shù)據(jù)源,但前提是類路徑中有 MongoDB 驅(qū)動(dòng)
return new MongoDBDataSource();
}
@Bean
@ConditionalOnMissingBean(DataSource.class)
public DataSource defaultDataSource() {
// 如果沒(méi)有其他數(shù)據(jù)源 Bean,創(chuàng)建默認(rèn)數(shù)據(jù)源
return new H2DataSource();
}
}在上面的例子中:
- 通過(guò)
datasource.type屬性值決定創(chuàng)建哪種數(shù)據(jù)源 - 如果屬性不存在,默認(rèn)創(chuàng)建 MySQL 數(shù)據(jù)源
- MongoDB 數(shù)據(jù)源只有在同時(shí)滿足屬性值條件和類路徑條件時(shí)才會(huì)創(chuàng)建
- 如果所有條件都不滿足,則創(chuàng)建默認(rèn)的 H2 數(shù)據(jù)源
1.4 自定義條件注解
我們還可以創(chuàng)建自定義條件注解來(lái)滿足特定業(yè)務(wù)需求:
// 自定義條件判斷邏輯
public class OnEnvironmentCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 獲取注解屬性
Map<String, Object> attributes = metadata.getAnnotationAttributes(
ConditionalOnEnvironment.class.getName());
String[] envs = (String[]) attributes.get("value");
// 獲取當(dāng)前環(huán)境
String activeEnv = context.getEnvironment().getProperty("app.environment");
// 檢查是否匹配
for (String env : envs) {
if (env.equalsIgnoreCase(activeEnv)) {
return true;
}
}
return false;
}
}
// 自定義條件注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnEnvironmentCondition.class)
public @interface ConditionalOnEnvironment {
String[] value() default {};
}使用自定義條件注解:
@Configuration
public class EnvironmentSpecificConfig {
@Bean
@ConditionalOnEnvironment({"dev", "test"})
public SecurityConfig developmentSecurityConfig() {
return new DevelopmentSecurityConfig();
}
@Bean
@ConditionalOnEnvironment({"prod", "staging"})
public SecurityConfig productionSecurityConfig() {
return new ProductionSecurityConfig();
}
}1.5 優(yōu)缺點(diǎn)與適用場(chǎng)景
優(yōu)點(diǎn):
- 配置簡(jiǎn)單直觀,易于理解和維護(hù)
- Spring Boot 原生支持,無(wú)需額外依賴
- 可組合多個(gè)條件,實(shí)現(xiàn)復(fù)雜的條件邏輯
缺點(diǎn):
- 條件邏輯主要在編譯時(shí)確定,運(yùn)行時(shí)靈活性有限
- 對(duì)于非常復(fù)雜的條件邏輯,代碼可能變得冗長(zhǎng)
適用場(chǎng)景:
- 基于配置屬性選擇不同的實(shí)現(xiàn)
- 根據(jù)環(huán)境(開(kāi)發(fā)、測(cè)試、生產(chǎn))加載不同的 Bean
- 處理可選依賴和功能的條件性啟用
- 構(gòu)建可插拔的模塊化系統(tǒng)
二、BeanDefinitionRegistryPostProcessor 動(dòng)態(tài)注冊(cè)
2.1 基本原理
BeanDefinitionRegistryPostProcessor 是 Spring 容器的擴(kuò)展點(diǎn)之一,它允許我們?cè)诔R?guī) Bean 定義加載完成后、Bean 實(shí)例化之前,動(dòng)態(tài)修改應(yīng)用上下文中的 Bean 定義注冊(cè)表。
通過(guò)實(shí)現(xiàn)此接口,我們可以編程式地注冊(cè)、修改或移除 Bean 定義。
2.2 接口說(shuō)明
BeanDefinitionRegistryPostProcessor 接口繼承自 BeanFactoryPostProcessor,并添加了一個(gè)額外的方法:
public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}2.3 代碼示例
以下是一個(gè)使用 BeanDefinitionRegistryPostProcessor 動(dòng)態(tài)注冊(cè)服務(wù)實(shí)現(xiàn)的例子:
@Component
public class ServiceRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Autowired
private Environment environment;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
// 獲取服務(wù)配置
String[] serviceTypes = environment.getProperty("app.services.enabled", String[].class, new String[0]);
// 動(dòng)態(tài)注冊(cè)服務(wù) Bean
for (String serviceType : serviceTypes) {
registerServiceBean(registry, serviceType);
}
}
private void registerServiceBean(BeanDefinitionRegistry registry, String serviceType) {
// 根據(jù)服務(wù)類型確定具體實(shí)現(xiàn)類
Class<?> serviceClass = getServiceClassByType(serviceType);
if (serviceClass == null) {
return;
}
// 創(chuàng)建 Bean 定義
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(serviceClass)
.setScope(BeanDefinition.SCOPE_SINGLETON)
.setLazyInit(false);
// 注冊(cè) Bean 定義
String beanName = serviceType + "Service";
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
private Class<?> getServiceClassByType(String serviceType) {
switch (serviceType.toLowerCase()) {
case "email":
return EmailServiceImpl.class;
case "sms":
return SmsServiceImpl.class;
case "push":
return PushNotificationServiceImpl.class;
default:
return null;
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 可以進(jìn)一步處理已注冊(cè)的 Bean 定義
}
}在上面的例子中,我們通過(guò)配置屬性 app.services.enabled 來(lái)確定需要啟用哪些服務(wù),然后在 postProcessBeanDefinitionRegistry 方法中動(dòng)態(tài)注冊(cè)相應(yīng)的 Bean 定義。
2.4 高級(jí)應(yīng)用:動(dòng)態(tài)模塊加載
我們可以利用 BeanDefinitionRegistryPostProcessor 實(shí)現(xiàn)動(dòng)態(tài)模塊加載,例如:
@Component
public class DynamicModuleLoader implements BeanDefinitionRegistryPostProcessor {
@Autowired
private ResourceLoader resourceLoader;
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
try {
// 獲取模塊目錄
Resource[] resources = resourceLoader.getResource("classpath:modules/")
.getURL().listFiles();
if (resources != null) {
for (Resource moduleDir : resources) {
// 加載模塊配置
Properties moduleProps = loadModuleProperties(moduleDir);
if (Boolean.parseBoolean(moduleProps.getProperty("module.enabled", "false"))) {
// 加載模塊配置類
String configClassName = moduleProps.getProperty("module.config-class");
if (configClassName != null) {
Class<?> configClass = Class.forName(configClassName);
// 注冊(cè)模塊配置類
registerConfigurationClass(registry, configClass);
}
}
}
}
} catch (Exception e) {
throw new BeanCreationException("Failed to load dynamic modules", e);
}
}
private void registerConfigurationClass(BeanDefinitionRegistry registry, Class<?> configClass) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(configClass);
String beanName = configClass.getSimpleName();
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
private Properties loadModuleProperties(Resource moduleDir) throws IOException {
Properties props = new Properties();
Resource propFile = resourceLoader.getResource(moduleDir.getURL() + "/module.properties");
if (propFile.exists()) {
try (InputStream is = propFile.getInputStream()) {
props.load(is);
}
}
return props;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 空實(shí)現(xiàn)
}
}這個(gè)例子展示了如何掃描 modules 目錄下的各個(gè)模塊,根據(jù)模塊配置文件決定是否啟用該模塊,并動(dòng)態(tài)注冊(cè)模塊的配置類。
2.5 優(yōu)缺點(diǎn)與適用場(chǎng)景
優(yōu)點(diǎn):
- 提供完全編程式的 Bean 注冊(cè)控制
- 可以在運(yùn)行時(shí)根據(jù)外部條件動(dòng)態(tài)創(chuàng)建 Bean
- 能夠處理復(fù)雜的動(dòng)態(tài)注冊(cè)邏輯
缺點(diǎn):
- 實(shí)現(xiàn)相對(duì)復(fù)雜,需要理解 Spring 容器的生命周期
- 難以調(diào)試
- 不當(dāng)使用可能導(dǎo)致不可預(yù)測(cè)的行為
適用場(chǎng)景:
- 插件系統(tǒng)或模塊化架構(gòu)
- 基于配置動(dòng)態(tài)加載組件
- 根據(jù)外部系統(tǒng)狀態(tài)動(dòng)態(tài)調(diào)整應(yīng)用結(jié)構(gòu)
- 高度定制化的框架和中間件開(kāi)發(fā)
三、ImportBeanDefinitionRegistrar 實(shí)現(xiàn)動(dòng)態(tài)注入
3.1 基本原理
ImportBeanDefinitionRegistrar 是 Spring 框架提供的另一個(gè)強(qiáng)大機(jī)制,它允許我們?cè)谑褂?nbsp;@Import 注解導(dǎo)入配置類時(shí),動(dòng)態(tài)注冊(cè) Bean 定義。
與 BeanDefinitionRegistryPostProcessor 不同,ImportBeanDefinitionRegistrar 更加專注于配置類導(dǎo)入場(chǎng)景,是實(shí)現(xiàn)自定義注解驅(qū)動(dòng)功能的理想選擇。
3.2 接口說(shuō)明
ImportBeanDefinitionRegistrar 接口只有一個(gè)方法:
public interface ImportBeanDefinitionRegistrar {
void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry);
}其中:
importingClassMetadata提供了導(dǎo)入該注冊(cè)器的類的元數(shù)據(jù)信息registry允許注冊(cè)額外的 Bean 定義
3.3 代碼示例
下面我們通過(guò)一個(gè)案例展示如何使用 ImportBeanDefinitionRegistrar 實(shí)現(xiàn)一個(gè)自定義的 @EnableHttpClients 注解,自動(dòng)為指定的接口生成 HTTP 客戶端實(shí)現(xiàn):
首先,定義自定義注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(HttpClientRegistrar.class)
public @interface EnableHttpClients {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] clients() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface HttpClient {
String value() default ""; // API 基礎(chǔ)URL
String name() default ""; // Bean名稱
}然后,實(shí)現(xiàn) ImportBeanDefinitionRegistrar:
public class HttpClientRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 解析 @EnableHttpClients 注解屬性
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHttpClients.class.getName());
// 獲取要掃描的包和類
List<String> basePackages = new ArrayList<>();
for (String pkg : (String[]) attributes.get("basePackages")) {
if (StringUtils.hasText(pkg)) {
basePackages.add(pkg);
}
}
for (Class<?> clazz : (Class<?>[]) attributes.get("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
// 如果沒(méi)有指定包,使用導(dǎo)入類的包
if (basePackages.isEmpty()) {
basePackages.add(ClassUtils.getPackageName(metadata.getClassName()));
}
// 創(chuàng)建類路徑掃描器
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(HttpClient.class));
// 掃描 @HttpClient 注解的接口
for (String basePackage : basePackages) {
for (BeanDefinition beanDef : scanner.findCandidateComponents(basePackage)) {
String className = beanDef.getBeanClassName();
try {
Class<?> interfaceClass = Class.forName(className);
registerHttpClient(registry, interfaceClass);
} catch (ClassNotFoundException e) {
throw new BeanCreationException("Failed to load HTTP client interface: " + className, e);
}
}
}
// 處理直接指定的客戶端接口
for (Class<?> clientClass : (Class<?>[]) attributes.get("clients")) {
registerHttpClient(registry, clientClass);
}
}
private void registerHttpClient(BeanDefinitionRegistry registry, Class<?> interfaceClass) {
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("HTTP client must be an interface: " + interfaceClass.getName());
}
// 獲取 @HttpClient 注解信息
HttpClient annotation = interfaceClass.getAnnotation(HttpClient.class);
if (annotation == null) {
return;
}
// 確定 Bean 名稱
String beanName = StringUtils.hasText(annotation.name())
? annotation.name()
: StringUtils.uncapitalize(interfaceClass.getSimpleName());
// 創(chuàng)建動(dòng)態(tài)代理工廠的 Bean 定義
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(HttpClientFactoryBean.class)
.addPropertyValue("interfaceClass", interfaceClass)
.addPropertyValue("baseUrl", annotation.value());
// 注冊(cè) Bean 定義
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
}最后,實(shí)現(xiàn) HTTP 客戶端工廠:
public class HttpClientFactoryBean implements FactoryBean<Object>, InitializingBean {
private Class<?> interfaceClass;
private String baseUrl;
private Object httpClient;
@Override
public Object getObject() throws Exception {
return httpClient;
}
@Override
public Class<?> getObjectType() {
return interfaceClass;
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void afterPropertiesSet() throws Exception {
// 創(chuàng)建接口的動(dòng)態(tài)代理實(shí)現(xiàn)
httpClient = Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class<?>[] { interfaceClass },
new HttpClientInvocationHandler(baseUrl)
);
}
// Getter and Setter
public void setInterfaceClass(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
// 實(shí)際處理 HTTP 請(qǐng)求的 InvocationHandler
private static class HttpClientInvocationHandler implements InvocationHandler {
private final String baseUrl;
public HttpClientInvocationHandler(String baseUrl) {
this.baseUrl = baseUrl;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 實(shí)際實(shí)現(xiàn)會(huì)處理 HTTP 請(qǐng)求,這里簡(jiǎn)化為打印日志
System.out.println("Executing HTTP request to " + baseUrl + " for method " + method.getName());
// 根據(jù)方法返回類型創(chuàng)建模擬響應(yīng)
return createMockResponse(method.getReturnType());
}
private Object createMockResponse(Class<?> returnType) {
// 簡(jiǎn)化實(shí)現(xiàn),實(shí)際代碼應(yīng)根據(jù)返回類型創(chuàng)建適當(dāng)?shù)捻憫?yīng)對(duì)象
if (returnType == String.class) {
return "Mock response";
}
if (returnType == Integer.class || returnType == int.class) {
return 200;
}
return null;
}
}
}使用自定義注解創(chuàng)建 HTTP 客戶端:
// 接口定義
@HttpClient(value = "https://api.example.com", name = "userClient")
public interface UserApiClient {
User getUser(Long id);
List<User> getAllUsers();
void createUser(User user);
}
// 啟用 HTTP 客戶端
@Configuration
@EnableHttpClients(basePackages = "com.example.api.client")
public class ApiClientConfig {
}
// 使用生成的客戶端
@Service
public class UserService {
@Autowired
private UserApiClient userClient;
public User getUserById(Long id) {
return userClient.getUser(id);
}
}3.4 Spring Boot 自動(dòng)配置原理
Spring Boot 的自動(dòng)配置功能就是基于 ImportBeanDefinitionRegistrar 實(shí)現(xiàn)的。@EnableAutoConfiguration 注解通過(guò) @Import(AutoConfigurationImportSelector.class) 導(dǎo)入了一個(gè)選擇器,該選擇器讀取 META-INF/spring.factories 文件中的配置類列表,并動(dòng)態(tài)導(dǎo)入符合條件的自動(dòng)配置類。
我們可以參考這種模式實(shí)現(xiàn)自己的模塊自動(dòng)配置:
// 自定義模塊啟用注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ModuleConfigurationImportSelector.class)
public @interface EnableModules {
String[] value() default {};
}
// 導(dǎo)入選擇器
public class ModuleConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableModules.class.getName());
String[] moduleNames = (String[]) attributes.get("value");
List<String> imports = new ArrayList<>();
for (String moduleName : moduleNames) {
String configClassName = getModuleConfigClassName(moduleName);
if (isModuleAvailable(configClassName)) {
imports.add(configClassName);
}
}
return imports.toArray(new String[0]);
}
private String getModuleConfigClassName(String moduleName) {
return "com.example.module." + moduleName + ".config." +
StringUtils.capitalize(moduleName) + "ModuleConfiguration";
}
private boolean isModuleAvailable(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}3.5 優(yōu)缺點(diǎn)與適用場(chǎng)景
優(yōu)點(diǎn):
- 與 Spring 的注解驅(qū)動(dòng)配置模式無(wú)縫集成
- 支持復(fù)雜的條件注冊(cè)邏輯
- 便于實(shí)現(xiàn)可重用的配置模塊
- 是實(shí)現(xiàn)自定義啟用注解的理想選擇
缺點(diǎn):
- 需要深入理解 Spring 的配置機(jī)制
- 配置類導(dǎo)入順序可能帶來(lái)問(wèn)題
- 不如
BeanDefinitionRegistryPostProcessor靈活,僅限于配置導(dǎo)入場(chǎng)景
適用場(chǎng)景:
- 開(kāi)發(fā)自定義的"啟用"注解(如
@EnableXxx) - 實(shí)現(xiàn)可重用的配置模塊
- 框架集成,如 ORM、消息隊(duì)列等
- 基于注解的自動(dòng)代理生成
四、方案對(duì)比
| 技巧 | 運(yùn)行時(shí)動(dòng)態(tài)性 | 實(shí)現(xiàn)復(fù)雜度 | 靈活性 | 與注解配合 | 使用場(chǎng)景 |
| 條件化Bean配置 | 低 | 低 | 中 | 好 | 簡(jiǎn)單條件判斷、環(huán)境區(qū)分 |
| BeanDefinitionRegistryPostProcessor | 高 | 高 | 高 | 一般 | 插件系統(tǒng)、高度動(dòng)態(tài)場(chǎng)景 |
| ImportBeanDefinitionRegistrar | 中 | 中 | 高 | 極好 | 自定義注解、模塊化配置 |
五、總結(jié)
通過(guò)合理選擇和組合這些技巧,我們可以構(gòu)建更加靈活、模塊化和可擴(kuò)展的 Spring Boot 應(yīng)用。
關(guān)鍵是根據(jù)實(shí)際需求選擇合適的技術(shù),保持代碼的簡(jiǎn)潔和可維護(hù)性。
到此這篇關(guān)于SpringBoot中動(dòng)態(tài)注入Bean的技巧分享的文章就介紹到這了,更多相關(guān)SpringBoot動(dòng)態(tài)注入Bean內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談Java中Spring Boot的優(yōu)勢(shì)
在本篇文章中小編給大家分析了Java中Spring Boot的優(yōu)勢(shì)以及相關(guān)知識(shí)點(diǎn)內(nèi)容,興趣的朋友們可以學(xué)習(xí)參考下。2018-09-09
Spring Boot 2.4配置特定環(huán)境時(shí)spring: profiles提示被棄用的原
這篇文章主要介紹了Spring Boot 2.4配置特定環(huán)境時(shí)spring: profiles提示被棄用的原因,本文給大家分享詳細(xì)解決方案,需要的朋友可以參考下2023-04-04
SpringBoot動(dòng)態(tài)表操作服務(wù)的實(shí)現(xiàn)代碼
在現(xiàn)代的應(yīng)用開(kāi)發(fā)中,尤其是在數(shù)據(jù)庫(kù)設(shè)計(jì)不斷變化的情況下,動(dòng)態(tài)操作數(shù)據(jù)庫(kù)表格成為了不可或缺的一部分,在本篇文章中,我們將以一個(gè)典型的動(dòng)態(tài)表操作服務(wù)為例,詳細(xì)介紹如何在 Spring Boot 中使用 JdbcTemplate 實(shí)現(xiàn)動(dòng)態(tài)表管理,需要的朋友可以參考下2025-01-01
Android 單例模式 Singleton 簡(jiǎn)單實(shí)例設(shè)計(jì)模式解析
這篇文章主要介紹了單例模式 Singleton 簡(jiǎn)單實(shí)例設(shè)計(jì)模式解析的相關(guān)資料,需要的朋友可以參考下2016-12-12
基于UDP協(xié)議實(shí)現(xiàn)聊天系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了基于UDP協(xié)議實(shí)現(xiàn)聊天系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-04-04

