java?SpringBoot?分布式事務(wù)的解決方案(JTA+Atomic+多數(shù)據(jù)源)
前言
首先,到底啥是分布式事務(wù)呢,比如我們?cè)趫?zhí)行一個(gè)業(yè)務(wù)邏輯的時(shí)候有兩步分別操作A數(shù)據(jù)源和B數(shù)據(jù)源,當(dāng)我們?cè)贏數(shù)據(jù)源執(zhí)行數(shù)據(jù)更改后,在B數(shù)據(jù)源執(zhí)行時(shí)出現(xiàn)運(yùn)行時(shí)異常,那么我們必須要讓B數(shù)據(jù)源的操作回滾,并回滾對(duì)A數(shù)據(jù)源的操作;這種情況在支付業(yè)務(wù)時(shí)常常出現(xiàn);比如買(mǎi)票業(yè)務(wù)在最后支付失敗,那之前的操作必須全部回滾,如果之前的操作分布在多個(gè)數(shù)據(jù)源中,那么這就是典型的分布式事務(wù)回滾;
了解了什么是分布式事務(wù),那分布式事務(wù)在java的解決方案就是JTA(即Java Transaction API);springboot官方提供了 Atomikos or Bitronix的解決思路;
其實(shí),大多數(shù)情況下很多公司是使用消息隊(duì)列的方式實(shí)現(xiàn)分布式事務(wù)。
本篇文章重點(diǎn)講解springboot環(huán)境下,整合 Atomikos +mysql+mybatis+tomcat/jetty;
一、項(xiàng)目依賴
pom.xml中添加atomikos的springboot相關(guān)依賴:
<!--分布式事務(wù)--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency>
點(diǎn)進(jìn)去會(huì)發(fā)現(xiàn)里面整合好了:??transactions-jms??、??transactions-jta??、??transactions-jdbc??、??javax.transaction-api??
二、數(shù)據(jù)源配置
把數(shù)據(jù)源的相關(guān)配置項(xiàng)單獨(dú)提煉到一個(gè)application.yml中:
注意:
- 這回我們的
spring.datasource.type是com.alibaba.druid.pool.xa.DruidXADataSource; - ?
?spring.jta.transaction-manager-id??的值在你的電腦中是唯一的,這個(gè)詳細(xì)請(qǐng)閱讀官方文檔;

完整的yml文件如下:
spring: datasource: type: com.alibaba.druid.pool.xa.DruidXADataSource druid: systemDB: name: systemDB url: jdbc:mysql://localhost:3306/springboot-mybatis?useUnicode=true&characterEncoding=utf-8 username: root password: root # 下面為連接池的補(bǔ)充設(shè)置,應(yīng)用到上面所有數(shù)據(jù)源中 # 初始化大小,最小,最大 initialSize: 5 minIdle: 5 maxActive: 20 # 配置獲取連接等待超時(shí)的時(shí)間 maxWait: 60000 # 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 minEvictableIdleTimeMillis: 30 validationQuery: SELECT 1 validationQueryTimeout: 10000 testWhileIdle: true testOnBorrow: false testOnReturn: false # 打開(kāi)PSCache,并且指定每個(gè)連接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,wall # 通過(guò)connectProperties屬性來(lái)打開(kāi)mergeSql功能;慢SQL記錄 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 合并多個(gè)DruidDataSource的監(jiān)控?cái)?shù)據(jù) useGlobalDataSourceStat: true businessDB: name: businessDB url: jdbc:mysql://localhost:3306/springboot-mybatis2?useUnicode=true&characterEncoding=utf-8 username: root password: root # 下面為連接池的補(bǔ)充設(shè)置,應(yīng)用到上面所有數(shù)據(jù)源中 # 初始化大小,最小,最大 initialSize: 5 minIdle: 5 maxActive: 20 # 配置獲取連接等待超時(shí)的時(shí)間 maxWait: 60000 # 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 timeBetweenEvictionRunsMillis: 60000 # 配置一個(gè)連接在池中最小生存的時(shí)間,單位是毫秒 minEvictableIdleTimeMillis: 30 validationQuery: SELECT 1 validationQueryTimeout: 10000 testWhileIdle: true testOnBorrow: false testOnReturn: false # 打開(kāi)PSCache,并且指定每個(gè)連接上PSCache的大小 poolPreparedStatements: true maxPoolPreparedStatementPerConnectionSize: 20 filters: stat,wall # 通過(guò)connectProperties屬性來(lái)打開(kāi)mergeSql功能;慢SQL記錄 connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 合并多個(gè)DruidDataSource的監(jiān)控?cái)?shù)據(jù) useGlobalDataSourceStat: true #jta相關(guān)參數(shù)配置 jta: log-dir: classpath:tx-logs transaction-manager-id: txManager
三、數(shù)據(jù)源的注冊(cè)
在DruidConfig.java中實(shí)現(xiàn)多個(gè)數(shù)據(jù)源的注冊(cè);分布式事務(wù)管理器的注冊(cè);druid的注冊(cè)
package com.zjt.config;
import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.transaction.jta.JtaTransactionManager;
import javax.sql.DataSource;
import javax.transaction.UserTransaction;
import java.util.Properties;
/**
* Druid配置
*
*
*/
@Configuration
public class DruidConfig {
@Bean(name = "systemDataSource")
@Primary
@Autowired
public DataSource systemDataSource(Environment env){
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
Properties prop = build(env, "spring.datasource.druid.systemDB.");
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("systemDB");
ds.setPoolSize(5);
ds.setXaProperties(prop);
return ds;
}
@Autowired
@Bean(name = "businessDataSource")
public AtomikosDataSourceBean businessDataSource(Environment env){
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
Properties prop = build(env, "spring.datasource.druid.businessDB.");
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("businessDB");
ds.setPoolSize(5);
ds.setXaProperties(prop);
return ds;
}
/**
* 注入事物管理器
* @return
*/
@Bean(name = "xatx")
public JtaTransactionManager regTransactionManager (){
UserTransactionManager userTransactionManager = new UserTransactionManager();
UserTransaction userTransaction = new UserTransactionImp();
return new JtaTransactionManager(userTransaction, userTransactionManager);
}
private Properties build(Environment env, String prefix){
Properties prop = new Properties();
prop.put("url", env.getProperty(prefix + "url"));
prop.put("username", env.getProperty(prefix + "username"));
prop.put("password", env.getProperty(prefix + "password"));
prop.put("driverClassName", env.getProperty(prefix + "driverClassName", ""));
prop.put("initialSize", env.getProperty(prefix + "initialSize", Integer.class));
prop.put("maxActive", env.getProperty(prefix + "maxActive", Integer.class));
prop.put("minIdle", env.getProperty(prefix + "minIdle", Integer.class));
prop.put("maxWait", env.getProperty(prefix + "maxWait", Integer.class));
prop.put("poolPreparedStatements", env.getProperty(prefix + "poolPreparedStatements", Boolean.class));
prop.put("maxPoolPreparedStatementPerConnectionSize",
env.getProperty(prefix + "maxPoolPreparedStatementPerConnectionSize", Integer.class));
prop.put("maxPoolPreparedStatementPerConnectionSize",
env.getProperty(prefix + "maxPoolPreparedStatementPerConnectionSize", Integer.class));
prop.put("validationQuery", env.getProperty(prefix + "validationQuery"));
prop.put("validationQueryTimeout", env.getProperty(prefix + "validationQueryTimeout", Integer.class));
prop.put("testOnBorrow", env.getProperty(prefix + "testOnBorrow", Boolean.class));
prop.put("testOnReturn", env.getProperty(prefix + "testOnReturn", Boolean.class));
prop.put("testWhileIdle", env.getProperty(prefix + "testWhileIdle", Boolean.class));
prop.put("timeBetweenEvictionRunsMillis",
env.getProperty(prefix + "timeBetweenEvictionRunsMillis", Integer.class));
prop.put("minEvictableIdleTimeMillis", env.getProperty(prefix + "minEvictableIdleTimeMillis", Integer.class));
prop.put("filters", env.getProperty(prefix + "filters"));
return prop;
}
@Bean
public ServletRegistrationBean druidServlet(){
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
//控制臺(tái)管理用戶,加入下面2行 進(jìn)入druid后臺(tái)就需要登錄
//servletRegistrationBean.addInitParameter("loginUsername", "admin");
//servletRegistrationBean.addInitParameter("loginPassword", "admin");
return servletRegistrationBean;
}
@Bean
public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
filterRegistrationBean.addInitParameter("profileEnable", "true");
return filterRegistrationBean;
}
@Bean
public StatFilter statFilter(){
StatFilter statFilter = new StatFilter();
statFilter.setLogSlowSql(true); //slowSqlMillis用來(lái)配置SQL慢的標(biāo)準(zhǔn),執(zhí)行時(shí)間超過(guò)slowSqlMillis的就是慢。
statFilter.setMergeSql(true); //SQL合并配置
statFilter.setSlowSqlMillis(1000);//slowSqlMillis的缺省值為3000,也就是3秒。
return statFilter;
}
@Bean
public WallFilter wallFilter(){
WallFilter wallFilter = new WallFilter();
//允許執(zhí)行多條SQL
WallConfig config = new WallConfig();
config.setMultiStatementAllow(true);
wallFilter.setConfig(config);
return wallFilter;
}
}四、配置數(shù)據(jù)源對(duì)應(yīng)的sqlSessionFactory
分別配置每個(gè)數(shù)據(jù)源對(duì)應(yīng)的sqlSessionFactory,以及MapperScan掃描的包
MybatisDatasourceConfig.java
package com.zjt.config;
import com.zjt.util.MyMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
/**
*
* @description
*/
@Configuration
// 精確到 mapper 目錄,以便跟其他數(shù)據(jù)源隔離
@MapperScan(basePackages = "com.zjt.mapper", markerInterface = MyMapper.class, sqlSessionFactoryRef = "sqlSessionFactory")
public class MybatisDatasourceConfig {
@Autowired
@Qualifier("systemDataSource")
private DataSource ds;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(ds);
//指定mapper xml目錄
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception {
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory()); // 使用上面配置的Factory
return template;
}
//關(guān)于事務(wù)管理器,不管是JPA還是JDBC等都實(shí)現(xiàn)自接口 PlatformTransactionManager
// 如果你添加的是 spring-boot-starter-jdbc 依賴,框架會(huì)默認(rèn)注入 DataSourceTransactionManager 實(shí)例。
//在Spring容器中,我們手工注解@Bean 將被優(yōu)先加載,框架不會(huì)重新實(shí)例化其他的 PlatformTransactionManager 實(shí)現(xiàn)類。
/*@Bean(name = "transactionManager")
@Primary
public DataSourceTransactionManager masterTransactionManager() {
//MyBatis自動(dòng)參與到spring事務(wù)管理中,無(wú)需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的數(shù)據(jù)源
// 與DataSourceTransactionManager引用的數(shù)據(jù)源一致即可,否則事務(wù)管理會(huì)不起作用。
return new DataSourceTransactionManager(ds);
}*/
}MybatisDatasource2Config.java
package com.zjt.config;
import com.zjt.util.MyMapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
/**
*
* @description
*/
@Configuration
// 精確到 mapper 目錄,以便跟其他數(shù)據(jù)源隔離
@MapperScan(basePackages = "com.zjt.mapper2", markerInterface = MyMapper.class, sqlSessionFactoryRef = "sqlSessionFactory2")
public class MybatisDatasource2Config {
@Autowired
@Qualifier("businessDataSource")
private DataSource ds;
@Bean
public SqlSessionFactory sqlSessionFactory2() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(ds);
//指定mapper xml目錄
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath:mapper2/*.xml"));
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate2() throws Exception {
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory2()); // 使用上面配置的Factory
return template;
}
//關(guān)于事務(wù)管理器,不管是JPA還是JDBC等都實(shí)現(xiàn)自接口 PlatformTransactionManager
// 如果你添加的是 spring-boot-starter-jdbc 依賴,框架會(huì)默認(rèn)注入 DataSourceTransactionManager 實(shí)例。
//在Spring容器中,我們手工注解@Bean 將被優(yōu)先加載,框架不會(huì)重新實(shí)例化其他的 PlatformTransactionManager 實(shí)現(xiàn)類。
/*@Bean(name = "transactionManager2")
@Primary
public DataSourceTransactionManager masterTransactionManager() {
//MyBatis自動(dòng)參與到spring事務(wù)管理中,無(wú)需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的數(shù)據(jù)源
// 與DataSourceTransactionManager引用的數(shù)據(jù)源一致即可,否則事務(wù)管理會(huì)不起作用。
return new DataSourceTransactionManager(ds);
}*/
}由于我們本例中只使用一個(gè)事務(wù)管理器:xatx,故就不在使用??TxAdviceInterceptor.java??和??TxAdvice2Interceptor.java??中配置的事務(wù)管理器了;有需求的童鞋可以自己配置其他的事務(wù)管理器;(見(jiàn)DruidConfig.java中查看)
五、測(cè)試接口
新建分布式業(yè)務(wù)測(cè)試接口JtaTestService.java和實(shí)現(xiàn)類JtaTestServiceImpl.java
其實(shí)就是一個(gè)很簡(jiǎn)單的test01()方法,在該方法中我們分別先后調(diào)用??classService.saveOrUpdateTClass(tClass);??和??teacherService.saveOrUpdateTeacher(teacher);??
實(shí)現(xiàn)先后操作兩個(gè)數(shù)據(jù)源:然后我們可以自己debug跟蹤事務(wù)的提交時(shí)機(jī),此外,也可以在在兩個(gè)方法全執(zhí)行結(jié)束之后,手動(dòng)制造一個(gè)運(yùn)行時(shí)異常,來(lái)檢查分布式事務(wù)是否全部回滾;
注意:
在實(shí)現(xiàn)類的方法中我使用的是:
@Transactional(transactionManager = "xatx", propagation = Propagation.REQUIRED, rollbackFor = { java.lang.RuntimeException.class })從而指定了使用哪個(gè)事務(wù)管理器,事務(wù)隔離級(jí)別(一般都用我這個(gè)默認(rèn)的),回滾的條件(一般可以使用Exception),這三個(gè)可以自己根據(jù)業(yè)務(wù)實(shí)際修改;package com.zjt.service3;
import java.util.Map;
public interface JtaTestService {
public Map<String,Object> test01();
}package com.zjt.service3.impl;
import com.zjt.entity.TClass;
import com.zjt.entity.Teacher;
import com.zjt.service.TClassService;
import com.zjt.service2.TeacherService;
import com.zjt.service3.JtaTestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.LinkedHashMap;
import java.util.Map;
@Service("jtaTestServiceImpl")
public class JtaTestServiceImpl implements JtaTestService{
@Autowired
@Qualifier("teacherServiceImpl")
private TeacherService teacherService;
@Autowired
@Qualifier("tclassServiceImpl")
private TClassService tclassService;
@Override
@Transactional(transactionManager = "xatx", propagation = Propagation.REQUIRED, rollbackFor = { java.lang.RuntimeException.class })
public Map<String, Object> test01() {
LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>();
TClass tClass=new TClass();
tClass.setName("8888");
tclassService.saveOrUpdateTClass(tClass);
Teacher teacher=new Teacher();
teacher.setName("8888");
teacherService.saveOrUpdateTeacher(teacher);
System.out.println(1/0);
resultMap.put("state","success");
resultMap.put("message","分布式事務(wù)同步成功");
return resultMap;
}
}六、建立JtaTestContoller.java
建立JtaTestContoller.java,接受一個(gè)來(lái)自前端的http請(qǐng)求,觸發(fā)JtaTestService 的test01方法
package com.zjt.web;
import com.zjt.service3.JtaTestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.LinkedHashMap;
import java.util.Map;
@Controller
@RequestMapping("/jtaTest")
public class JtaTestContoller {
@Autowired
@Qualifier("jtaTestServiceImpl")
private JtaTestService taTestService;
@ResponseBody
@RequestMapping("/test01")
public Map<String,Object> test01(){
LinkedHashMap<String,Object> resultMap=new LinkedHashMap<String,Object>();
try {
return taTestService.test01();
}catch (Exception e){
resultMap.put("state","fail");
resultMap.put("message","分布式事務(wù)同步失敗");
return resultMap;
}
}
}七、在test.ftl中增加一個(gè)按鈕來(lái)測(cè)試
//分布式事務(wù)測(cè)試
$("#JTATest").click(function(){
$.ajax({
type: "POST",
url: "${basePath!}/jtaTest/test01",
data: {} ,
async: false,
error: function (request) {
layer.alert("與服務(wù)器連接失敗/(ㄒoㄒ)/~~");
return false;
},
success: function (data) {
if (data.state == 'fail') {
layer.alert(data.message);
return false;
}else if(data.state == 'success'){
layer.alert(data.message);
}
}
});
});
<button class="layui-btn" id="JTATest">同時(shí)向班級(jí)和老師表插入名為8888的班級(jí)和老師</button>八、啟動(dòng)服務(wù),驗(yàn)證結(jié)果
點(diǎn)擊這個(gè)按鈕,跳轉(zhuǎn)到controller:

當(dāng)正常執(zhí)行了sql語(yǔ)句之后,我們可以發(fā)現(xiàn)數(shù)據(jù)庫(kù)并沒(méi)有變化,因?yàn)檎麄€(gè)方法的事務(wù)還沒(méi)有走完,當(dāng)我們走到1/0這步時(shí):

拋出運(yùn)行時(shí)異常,并被spring事務(wù)攔截器攔截,并捕獲異常:

在??this.completeTransactionAfterThrowing(txInfo, var16);??方法中會(huì)將事務(wù)全部回滾:
22:09:04.243 logback [http-nio-8080-exec-5] INFO c.a.i.imp.CompositeTransactionImp - rollback() done of transaction 192.168.1.103.tm0000400006
此時(shí),當(dāng)我們?cè)俅未蜷_(kāi)數(shù)據(jù)庫(kù)驗(yàn)證,依舊沒(méi)有變化,證明分布式事務(wù)配置成功;
大家可以基于我的代碼自己練習(xí)一下,自己嘗試著使用多事務(wù)管理器的情況下的靈活配置;
到此這篇關(guān)于java SpringBoot 分布式事務(wù)的解決方案(JTA+Atomic+多數(shù)據(jù)源)的文章就介紹到這了,更多相關(guān)java SpringBoot 分布式事務(wù) 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(10)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07
SpringBoot部署到Linux讀取resources下的文件及遇到的坑
本文主要給大家介紹SpringBoot部署到Linux讀取resources下的文件,在平時(shí)業(yè)務(wù)開(kāi)發(fā)過(guò)程中,很多朋友在獲取到文件內(nèi)容亂碼或者文件讀取不到的問(wèn)題,今天給大家分享小編遇到的坑及處理方案,感興趣的朋友跟隨小編一起看看吧2021-06-06
Java 生成任意長(zhǎng)度的驗(yàn)證碼過(guò)程解析
這篇文章主要介紹了Java 生成任意長(zhǎng)度的驗(yàn)證碼過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
一文詳解Spring中ResponseEntity包裝器的使用
在?Spring?中,ResponseEntity?是?HTTP?響應(yīng)的包裝器,這篇文章主要為大家詳細(xì)介紹了ResponseEntity包裝器的使用,感興趣的可以了解一下2025-02-02
使用GenericObjectPool避免泄漏設(shè)置方法
這篇文章主要為大家介紹了使用GenericObjectPool避免泄漏的設(shè)置方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
java實(shí)現(xiàn)/創(chuàng)建線程的幾種方式小結(jié)
在JAVA中,用Thread類代表線程,所有線程對(duì)象都必須是Thread類或者Thread類子類的實(shí)例,下面這篇文章主要介紹了java實(shí)現(xiàn)/創(chuàng)建線程的幾種方式,需要的朋友可以參考下2021-08-08
關(guān)于SpringBoot整合RabbitMQ實(shí)現(xiàn)死信隊(duì)列
這篇文章主要介紹了關(guān)于SpringBoot整合RabbitMQ實(shí)現(xiàn)死信隊(duì)列,死信隊(duì)列實(shí)際上就是一個(gè)普通的隊(duì)列,只是這個(gè)隊(duì)列跟死信交換機(jī)進(jìn)行了綁定,用來(lái)存放死信而已,需要的朋友可以參考下2023-05-05
Java后端調(diào)用微信支付和支付寶支付的詳細(xì)步驟
這篇文章主要介紹了Java后端如何調(diào)用微信支付和支付寶支付,涵蓋了基本概念、配置步驟、代碼示例以及注意事項(xiàng),文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-04-04

