SpringBoot實現(xiàn)數(shù)據(jù)庫讀寫分離的3種方法小結(jié)
一、數(shù)據(jù)庫讀寫分離概述
在大型應(yīng)用系統(tǒng)中,隨著訪問量的增加,數(shù)據(jù)庫常常成為系統(tǒng)的性能瓶頸。為了提高系統(tǒng)的讀寫性能和可用性,讀寫分離是一種經(jīng)典的數(shù)據(jù)庫架構(gòu)模式。它將數(shù)據(jù)庫讀操作和寫操作分別路由到不同的數(shù)據(jù)庫實例,通常是將寫操作指向主庫(Master),讀操作指向從庫(Slave)。
讀寫分離的主要優(yōu)勢:
- 分散數(shù)據(jù)庫訪問壓力,提高系統(tǒng)的整體吞吐量
- 提升讀操作的性能和并發(fā)量
- 增強系統(tǒng)的可用性和容錯能力
在SpringBoot應(yīng)用中,有多種方式可以實現(xiàn)數(shù)據(jù)庫讀寫分離,本文將介紹三種主實現(xiàn)方案。
二、方案一:基于AbstractRoutingDataSource實現(xiàn)動態(tài)數(shù)據(jù)源
這種方案是基于Spring提供的AbstractRoutingDataSource抽象類,通過重寫其中的determineCurrentLookupKey()方法來實現(xiàn)數(shù)據(jù)源的動態(tài)切換。
2.1 實現(xiàn)原理
AbstractRoutingDataSource的核心原理是在執(zhí)行數(shù)據(jù)庫操作時,根據(jù)一定的策略(通?;诋?dāng)前操作的上下文)動態(tài)地選擇實際的數(shù)據(jù)源。通過在業(yè)務(wù)層或AOP攔截器中設(shè)置上下文標識,讓系統(tǒng)自動判斷是讀操作還是寫操作,從而選擇對應(yīng)的數(shù)據(jù)源。
2.2 具體實現(xiàn)步驟
第一步:定義數(shù)據(jù)源枚舉和上下文持有器
// 數(shù)據(jù)源類型枚舉
public enum DataSourceType {
MASTER, // 主庫,用于寫操作
SLAVE // 從庫,用于讀操作
}
// 數(shù)據(jù)源上下文持有器
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(DataSourceType dataSourceType) {
contextHolder.set(dataSourceType);
}
public static DataSourceType getDataSourceType() {
return contextHolder.get() == null ? DataSourceType.MASTER : contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
第二步:實現(xiàn)動態(tài)數(shù)據(jù)源
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
第三步:配置數(shù)據(jù)源
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
// 設(shè)置默認數(shù)據(jù)源為主庫
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 設(shè)置MyBatis配置
// ...
return sqlSessionFactoryBean.getObject();
}
}
第四步:實現(xiàn)AOP攔截器,根據(jù)方法匹配規(guī)則自動切換數(shù)據(jù)源
@Aspect
@Component
public class DataSourceAspect {
// 匹配所有以select、query、get、find開頭的方法為讀操作
@Pointcut("execution(* com.example.service.impl.*.*(..))")
public void servicePointcut() {}
@Before("servicePointcut()")
public void switchDataSource(JoinPoint point) {
// 獲取方法名
String methodName = point.getSignature().getName();
// 根據(jù)方法名判斷是讀操作還是寫操作
if (methodName.startsWith("select") ||
methodName.startsWith("query") ||
methodName.startsWith("get") ||
methodName.startsWith("find")) {
// 讀操作使用從庫
DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
} else {
// 寫操作使用主庫
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
}
}
@After("servicePointcut()")
public void restoreDataSource() {
// 清除數(shù)據(jù)源配置
DataSourceContextHolder.clearDataSourceType();
}
}
第五步:配置文件application.yml
spring:
datasource:
master:
jdbc-url: jdbc:mysql://master-db:3306/test?useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
jdbc-url: jdbc:mysql://slave-db:3306/test?useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
第六步:使用注解方式靈活控制數(shù)據(jù)源(可選增強)
// 定義自定義注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DataSourceType value() default DataSourceType.MASTER;
}
// 修改AOP攔截器,優(yōu)先使用注解指定的數(shù)據(jù)源
@Aspect
@Component
public class DataSourceAspect {
@Pointcut("@annotation(com.example.annotation.DataSource)")
public void dataSourcePointcut() {}
@Before("dataSourcePointcut()")
public void switchDataSource(JoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
DataSourceContextHolder.setDataSourceType(dataSource.value());
}
}
@After("dataSourcePointcut()")
public void restoreDataSource() {
DataSourceContextHolder.clearDataSourceType();
}
}
// 在Service方法上使用
@Service
public class UserServiceImpl implements UserService {
@Override
@DataSource(DataSourceType.SLAVE)
public List<User> findAllUsers() {
return userMapper.selectAll();
}
@Override
@DataSource(DataSourceType.MASTER)
public void createUser(User user) {
userMapper.insert(user);
}
}
2.3 優(yōu)缺點分析
優(yōu)點:
- 實現(xiàn)簡單,不依賴第三方組件
- 侵入性小,對業(yè)務(wù)代碼影響較小
- 靈活性高,可以根據(jù)業(yè)務(wù)需求靈活切換數(shù)據(jù)源
- 支持多數(shù)據(jù)源擴展,不限于主從兩個庫
缺點:
- 需要手動指定或通過約定規(guī)則判斷讀寫操作
適用場景:
- 中小型項目,讀寫請求分離明確
- 對中間件依賴要求低的場景
- 臨時性能優(yōu)化,快速實現(xiàn)讀寫分離
三、方案二:基于ShardingSphere-JDBC實現(xiàn)讀寫分離
ShardingSphere-JDBC是Apache ShardingSphere項目下的一個子項目,它通過客戶端分片的方式,為應(yīng)用提供了透明化的讀寫分離和分庫分表等功能。
3.1 實現(xiàn)原理
ShardingSphere-JDBC通過攔截JDBC驅(qū)動,重寫SQL解析與執(zhí)行流程來實現(xiàn)讀寫分離。它能夠根據(jù)SQL語義自動判斷讀寫操作,并將讀操作負載均衡地分發(fā)到多個從庫。
3.2 具體實現(xiàn)步驟
第一步:添加依賴
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
第二步:配置文件application.yml
spring:
shardingsphere:
mode:
type: Memory
datasource:
names: master,slave1,slave2
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://master-db:3306/test?useSSL=false
username: root
password: root
slave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://slave1-db:3306/test?useSSL=false
username: root
password: root
slave2:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://slave2-db:3306/test?useSSL=false
username: root
password: root
rules:
readwrite-splitting:
data-sources:
readwrite_ds:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave1,slave2
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
props:
sql-show: true # 開啟SQL顯示,方便調(diào)試
第三步:創(chuàng)建數(shù)據(jù)源配置類
@Configuration
public class DataSourceConfig {
// 無需額外配置,ShardingSphere-JDBC會自動創(chuàng)建并注冊DataSource
@Bean
@ConfigurationProperties(prefix = "mybatis")
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
return sqlSessionFactoryBean;
}
}
第四步:強制主庫查詢的注解(可選)
在某些場景下,即使是查詢操作也需要從主庫讀取最新數(shù)據(jù),ShardingSphere提供了hint機制來實現(xiàn)這一需求。
// 定義主庫查詢注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MasterRoute {
}
// 創(chuàng)建AOP切面攔截器
@Aspect
@Component
public class MasterRouteAspect {
@Around("@annotation(com.example.annotation.MasterRoute)")
public Object aroundMasterRoute(ProceedingJoinPoint joinPoint) throws Throwable {
try {
HintManager.getInstance().setWriteRouteOnly();
return joinPoint.proceed();
} finally {
HintManager.clear();
}
}
}
// 在需要主庫查詢的方法上使用注解
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
@MasterRoute
public Order getLatestOrder(Long userId) {
// 這里的查詢會路由到主庫
return orderMapper.findLatestByUserId(userId);
}
}
3.3 優(yōu)缺點分析
優(yōu)點:
- 自動識別SQL類型,無需手動指定讀寫數(shù)據(jù)源
- 支持多從庫負載均衡
- 提供豐富的負載均衡算法(輪詢、隨機、權(quán)重等)
- 完整的分庫分表能力,可無縫擴展
- 對應(yīng)用透明,業(yè)務(wù)代碼無需修改
缺點:
- 引入額外的依賴和學(xué)習(xí)成本
- 配置相對復(fù)雜
- 性能有輕微損耗(SQL解析和路由)
適用場景:
- 中大型項目,有明確的讀寫分離需求
- 需要負載均衡到多從庫的場景
- 未來可能需要分庫分表的系統(tǒng)
四、方案三:基于MyBatis插件實現(xiàn)讀寫分離
MyBatis提供了強大的插件機制,允許在SQL執(zhí)行的不同階段進行攔截和處理。通過自定義插件,可以實現(xiàn)基于SQL解析的讀寫分離功能。
4.1 實現(xiàn)原理
MyBatis允許攔截執(zhí)行器的query和update方法,通過攔截這些方法,可以在SQL執(zhí)行前動態(tài)切換數(shù)據(jù)源。這種方式的核心是編寫一個攔截器,分析即將執(zhí)行的SQL語句類型(SELECT/INSERT/UPDATE/DELETE),然后根據(jù)SQL類型切換到相應(yīng)的數(shù)據(jù)源。
4.2 具體實現(xiàn)步驟
第一步:定義數(shù)據(jù)源和上下文(與方案一類似)
public enum DataSourceType {
MASTER, SLAVE
}
public class DataSourceContextHolder {
private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(DataSourceType dataSourceType) {
contextHolder.set(dataSourceType);
}
public static DataSourceType getDataSourceType() {
return contextHolder.get() == null ? DataSourceType.MASTER : contextHolder.get();
}
public static void clearDataSourceType() {
contextHolder.remove();
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
第二步:實現(xiàn)MyBatis攔截器
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class ReadWriteSplittingInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
try {
// 判斷是否為事務(wù)
boolean isTransactional = TransactionSynchronizationManager.isActualTransactionActive();
// 如果是事務(wù),則使用主庫
if (isTransactional) {
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
return invocation.proceed();
}
// 根據(jù)SQL類型選擇數(shù)據(jù)源
if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
// 讀操作使用從庫
DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
} else {
// 寫操作使用主庫
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
}
return invocation.proceed();
} finally {
// 清除數(shù)據(jù)源配置
DataSourceContextHolder.clearDataSourceType();
}
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
// 可以從配置文件加載屬性
}
}
第三步:配置數(shù)據(jù)源和MyBatis插件
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(@Autowired ReadWriteSplittingInterceptor interceptor) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 添加MyBatis插件
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
// 其他MyBatis配置
// ...
return sqlSessionFactoryBean.getObject();
}
}
第四步:強制主庫查詢注解(可選)
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public DataSource dynamicDataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(@Autowired ReadWriteSplittingInterceptor interceptor) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
// 添加MyBatis插件
sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
// 其他MyBatis配置
// ...
return sqlSessionFactoryBean.getObject();
}
}
4.3 優(yōu)缺點分析
優(yōu)點:
- 自動識別SQL類型,無需手動指定數(shù)據(jù)源
- 可靈活擴展,支持復(fù)雜的路由規(guī)則
- 基于MyBatis原生插件機制,無需引入額外的中間件
缺點:
- 僅適用于使用MyBatis的項目
- 需要理解MyBatis插件機制
- 沒有內(nèi)置的負載均衡能力,需要額外開發(fā)
- 可能與其他MyBatis插件產(chǎn)生沖突
- 事務(wù)管理較為復(fù)雜
適用場景:
- 純MyBatis項目
- 定制化需求較多的場景
- 對第三方中間件有限制的項目
- 需要對讀寫分離有更精細控制的場景
五、三種方案對比與選型指南
5.1 功能對比
| 功能特性 | 方案一:AbstractRoutingDataSource | 方案二:ShardingSphere-JDBC | 方案三:MyBatis插件 |
|---|---|---|---|
| 自動識別SQL類型 | ? 需要手動或通過規(guī)則指定 | ? 自動識別 | ? 自動識別 |
| 多從庫負載均衡 | ? 需要自行實現(xiàn) | ? 內(nèi)置多種算法 | ? 需要自行實現(xiàn) |
| 與分庫分表集成 | ? 不支持 | ? 原生支持 | ? 需要額外開發(fā) |
| 開發(fā)復(fù)雜度 | ?? 中等 | ? 較低 | ??? 較高 |
| 配置復(fù)雜度 | ? 較低 | ??? 較高 | ?? 中等 |
5.2 選型建議
選擇方案一(AbstractRoutingDataSource)的情況:
- 項目規(guī)模較小,讀寫分離規(guī)則簡單明確
- 對第三方依賴敏感,希望減少依賴
- 團隊對Spring原生機制較為熟悉
- 系統(tǒng)處于早期階段,可能頻繁變動
選擇方案二(ShardingSphere-JDBC)的情況:
- 中大型項目,有復(fù)雜的數(shù)據(jù)庫訪問需求
- 需要多從庫負載均衡能力
- 未來可能需要分庫分表
- 希望盡量減少代碼侵入
- 對開發(fā)效率要求較高
選擇方案三(MyBatis插件)的情況:
- 項目完全基于MyBatis架構(gòu)
- 團隊對MyBatis插件機制較為熟悉
- 有特定的定制化需求
- 希望對SQL路由有更細粒度的控制
- 對框架依賴有嚴格限制
六、實施讀寫分離的最佳實踐
6.1 數(shù)據(jù)一致性處理
從庫數(shù)據(jù)同步存在延遲,這可能導(dǎo)致讀取到過期數(shù)據(jù)的問題。處理方法:
- 提供強制主庫查詢的選項:對于需要最新數(shù)據(jù)的查詢,提供從主庫讀取的機制
- 會話一致性:同一會話內(nèi)的讀寫操作使用相同的數(shù)據(jù)源
- 延遲檢測:定期檢測主從同步延遲,當(dāng)延遲超過閾值時暫停從庫查詢
// 實現(xiàn)延遲檢測的示例
@Component
@Slf4j
public class ReplicationLagMonitor {
@Autowired
private JdbcTemplate masterJdbcTemplate;
@Autowired
private JdbcTemplate slaveJdbcTemplate;
private AtomicBoolean slaveTooLagged = new AtomicBoolean(false);
@Scheduled(fixedRate = 5000) // 每5秒檢查一次
public void checkReplicationLag() {
try {
// 在主庫寫入標記
String mark = UUID.randomUUID().toString();
masterJdbcTemplate.update("INSERT INTO replication_marker(marker, create_time) VALUES(?, NOW())", mark);
// 等待一定時間,給從庫同步的機會
Thread.sleep(1000);
// 從從庫查詢該標記
Integer count = slaveJdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM replication_marker WHERE marker = ?", Integer.class, mark);
// 判斷同步延遲
boolean lagged = (count == null || count == 0);
slaveTooLagged.set(lagged);
if (lagged) {
log.warn("Slave replication lag detected, routing read operations to master");
} else {
log.info("Slave replication is in sync");
}
} catch (Exception e) {
log.error("Failed to check replication lag", e);
slaveTooLagged.set(true); // 發(fā)生異常時,保守地認為從庫延遲過大
} finally{
// 刪除標記數(shù)據(jù)
masterJdbcTemplate.update("DELETE FROM replication_marker WHERE marker = ?", mark);
}
}
public boolean isSlaveTooLagged() {
return slaveTooLagged.get();
}
}
6.2 事務(wù)管理
讀寫分離環(huán)境下的事務(wù)處理需要特別注意:
- 事務(wù)內(nèi)操作都走主庫:確保事務(wù)一致性
- 避免長事務(wù):長事務(wù)會長時間鎖定主庫資源
- 區(qū)分只讀事務(wù):對于只讀事務(wù),可以考慮路由到從庫
6.4 監(jiān)控與性能優(yōu)化
- 監(jiān)控讀寫比例:了解系統(tǒng)的讀寫比例,優(yōu)化資源分配
- 慢查詢監(jiān)控:監(jiān)控各數(shù)據(jù)源的慢查詢
- 連接池優(yōu)化:根據(jù)實際負載調(diào)整連接池參數(shù)
# HikariCP連接池配置示例
spring:
datasource:
master:
# 主庫偏向?qū)懖僮?,連接池可以適當(dāng)小一些
maximum-pool-size: 20
minimum-idle: 5
slave:
# 從庫偏向讀操作,連接池可以適當(dāng)大一些
maximum-pool-size: 50
minimum-idle: 10
七、總結(jié)
在實施讀寫分離時,需要特別注意數(shù)據(jù)一致性、事務(wù)管理和故障處理等方面的問題。
通過合理的架構(gòu)設(shè)計和細致的實現(xiàn),讀寫分離可以有效提升系統(tǒng)的讀寫性能和可擴展性,為應(yīng)用系統(tǒng)的高可用和高性能提供有力支持。
無論選擇哪種方案,請記住讀寫分離是一種架構(gòu)模式,而非解決所有性能問題的萬能藥。在實施前應(yīng)充分評估系統(tǒng)的實際需求和潛在風(fēng)險,確保收益大于成本。
到此這篇關(guān)于SpringBoot實現(xiàn)數(shù)據(jù)庫讀寫分離的3種方法小結(jié)的文章就介紹到這了,更多相關(guān)SpringBoot數(shù)據(jù)庫讀寫分離內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot與spring security的結(jié)合的示例
這篇文章主要介紹了SpringBoot與spring security的結(jié)合的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03
MyBatis 探秘之#{} 與 ${} 參傳差異解碼(數(shù)據(jù)庫連接池筑牢數(shù)據(jù)交互
本文詳細介紹了MyBatis中的`#{}`和`${}`的區(qū)別與使用場景,包括預(yù)編譯SQL和即時SQL的區(qū)別、安全性問題,以及如何正確使用數(shù)據(jù)庫連接池來提高性能,感興趣的朋友一起看看吧2024-12-12
Java中將String轉(zhuǎn)換為int的多種方法
字符串轉(zhuǎn)換為整數(shù)是一個常見需求,本文主要介紹了Java中將String轉(zhuǎn)換為int的多種方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07
詳解Servlet入門級設(shè)置(超詳細 IDEA2020版)
這篇文章主要介紹了詳解Servlet入門級設(shè)置(超詳細 IDEA2020版),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11
EDI中JAVA通過FTP工具實現(xiàn)文件上傳下載實例
這篇文章主要介紹了EDI中JAVA通過FTP工具實現(xiàn)文件上傳下載實例,具有一定的參考價值,有需要的可以了解一下。2016-11-11
Java使用poi組件導(dǎo)出Excel格式數(shù)據(jù)
這篇文章主要介紹了Java使用poi組件導(dǎo)出Excel格式數(shù)據(jù),需要的朋友可以參考下2020-02-02

