前言 本文是博主從事後端開發以來,對公司、個人項目的經驗總結,包含代碼編寫、功能推薦、第三方庫使用及優雅配置等,希望大家看到都能有所收穫 博主github地址: https://github.com/wayn111 一. 優雅的進行線程池異常處理 在Java開發中,線程池的使用必不可少,使用無返回值 ...
前言
本文是博主從事後端開發以來,對公司、個人項目的經驗總結,包含代碼編寫、功能推薦、第三方庫使用及優雅配置等,希望大家看到都能有所收穫
- 博主github地址: https://github.com/wayn111
一. 優雅的進行線程池異常處理
在Java開發中,線程池的使用必不可少,使用無返回值 execute()
方法時,線程執行發生異常的話,需要記錄日誌,方便回溯,一般做法是線上程執行方法內 try/catch
處理,如下:
@Test
public void test() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000));
Future<Integer> submit = threadPoolExecutor.execute(() -> {
try {
int i = 1 / 0;
return i;
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
});
}
但是當線程池調用方法很多時,那麼每個線程執行方法內都要 try/catch
處理,這就不優雅了,其實ThreadPoolExecutor
類還支持傳入 ThreadFactory
參數,自定義線程工廠,在創建 thread
時,指定 setUncaughtExceptionHandler
異常處理方法,這樣就可以做到全局處理異常了,代碼如下:
ThreadFactory threadFactory = r -> {
Thread thread = new Thread(r);
thread.setUncaughtExceptionHandler((t, e) -> {
// 記錄線程異常
log.error(e.getMessage(), e);
});
return thread;
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(100000),
threadFactory);
threadPoolExecutor.execute(() -> {
log.info("---------------------");
int i = 1 / 0;
});
二. 線程池決絕策略設置錯誤導致業務介面執行超時
先介紹下線程池得四種決絕策略
- AbortPolicy:丟棄任務並拋出RejectedExecutionException異常,這是線程池預設的拒絕策略
- DiscardPolicy:丟棄任務,但是不拋出異常。如果線程隊列已滿,則後續提交的任務都會被丟棄,且是靜默丟棄。
使用此策略,可能會使我們無法發現系統的異常狀態。建議是一些無關緊要的業務採用此策略 - DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新提交被拒絕的任務。此拒絕策略,是一種喜新厭舊的拒絕策略。是否要採用此種拒絕策略,還得根據實際業務是否允許丟棄老任務來認真衡量。
- CallerRunsPolicy:由調用線程處理該任務
如下是一個線上業務介面使用得線程池配置,決絕策略採用 CallerRunsPolicy
// 某個線上線程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
50, // 最小核心線程數
50, // 最大線程數,當隊列滿時,能創建的最大線程數
60L, TimeUnit.SECONDS, // 空閑線程超過核心線程時,回收該線程的最大等待時間
new LinkedBlockingQueue<>(5000), // 阻塞隊列大小,當核心線程使用滿時,新的線程會放進隊列
new CustomizableThreadFactory("task"), // 自定義線程名
new ThreadPoolExecutor.CallerRunsPolicy() // 線程執行的拒絕策略
);
在某些情況下,子線程任務調用第三方介面超時,導致核心線程數、最大線程數占滿、阻塞隊列占滿的情況下執行拒絕策略時,由於使用 CallerRunsPolicy
策略,導致業務線程執行子任務時繼續超時,進而導致介面執行異常,這種情況下,考慮到子線程任務得重要性,不是很重要得話,可以使用 DiscardPolicy
策略,要是很重要,可以發送到消息隊列中持久化子線程任務數據待後續處理
三. 優雅的單例模式懶載入幫助類代碼實現
博主推薦通過靜態內部類實現單例模式,並實現懶載入效果,代碼如下
// 使用靜態內部類完成單例模式封裝,避免線程安全問題,避免重覆初始化成員屬性
@Slf4j
public class FilterIpUtil {
private FilterIpUtil() {
}
private List<String> strings = new ArrayList<>();
// 代碼塊在FilterIpUtil實例初始化時才會執行
{
// 在代碼塊中完成文件的第一次讀寫操作,後續不再讀這個文件
System.out.println("FilterIpUtil init");
try (InputStream resourceAsStream = FilterIpUtil.class.getClassLoader().getResourceAsStream("filterIp.txt")) {
// 將文件內容放到string集合中
IoUtil.readUtf8Lines(resourceAsStream, strings);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
public static FilterIpUtil getInstance() {
return InnerClassInstance.instance;
}
// 使用內部類完成單例模式,由jvm保證線程安全
private static class InnerClassInstance {
private static final FilterIpUtil instance = new FilterIpUtil();
}
// 判斷集合中是否包含目標參數
public boolean isFilter(String arg) {
return strings.contains(arg);
}
}
四. 使用ip2region實現請求地址解析
在博主之前公司得項目中,ip解析是調用淘寶IP還有聚合IP介面獲取結果,通常耗時200毫秒左右,並且介面不穩定時而會掛。都會影響業務介面耗時,後來在 github
上瞭解到 ip2region
這個項目,使用本地ip庫查詢,查詢速度微秒級別, 精準度能達到90%,但是ip庫還是有少部分ip信息不准,建議資料庫中把請求ip地址保存下來。簡介如下:
ip2region
v2.0 - 是一個離線IP地址定位庫和IP定位數據管理框架,10微秒級別的查詢效率,提供了眾多主流編程語言的 xdb
數據生成和查詢客戶端實現基於 xdb
文件的查詢,下麵是一個 Spring
項目中 ip2region
幫助類來實現ip地址解析
/**
* ip2region工具類
*/
@Slf4j
@Component
public class Ip2region {
private Searcher searcher = null;
@Value("${ip2region.path:}")
private String ip2regionPath = "";
@PostConstruct
private void init() {
// 1、從 dbPath 載入整個 xdb 到記憶體。
String dbPath = ip2regionPath;
// 1、從 dbPath 載入整個 xdb 到記憶體。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
log.error("failed to create content cached searcher: {}", e.getMessage(), e);
}
}
public IpInfoBean getIpInfo(String ip) {
if (StringUtils.isBlank(ip)) {
return null;
}
// 3、查詢
try {
long sTime = System.nanoTime();
// 國家|區域|省份|城市|ISP
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
log.info("{region: {}, ioCount: {}, took: {} μs}", region, searcher.getIOCount(), cost);
if (StringUtils.isNotBlank(region)) {
String[] split = region.split("\|");
IpInfoBean ipInfo = new IpInfoBean();
ipInfo.setIp(ip);
if (!"".equals(split[0])) {
ipInfo.setCountry(split[0]);
}
if (!"".equals(split[2])) {
ipInfo.setProvince(split[2]);
}
if (!"".equals(split[3])) {
ipInfo.setCity(split[3]);
}
if (!"".equals(split[4])) {
ipInfo.setIsp(split[4]);
}
return ipInfo;
}
} catch (Exception e) {
log.error("failed to search({}): {}", ip, e);
return null;
}
// 4、關閉資源 - 該 searcher 對象可以安全用於併發,等整個服務關閉的時候再關閉 searcher
// searcher.close();
// 備註:併發使用,用整個 xdb 數據緩存創建的查詢對象可以安全的用於併發,也就是你可以把這個 searcher 對象做成全局對象去跨線程訪問。
return null;
}
}
要註意得就是 ip2region
v2.0 版本使用的xdb文件不建議放在項目 resources
下一起打包,存在編碼格式問題,建議通過指定路徑載入得方式單獨放在伺服器目錄下
五. 優雅得Springboot + mybatis配置多數據源方式
Springboot + mybatis
得項目中一般通過 @MapperScan
註解配置 dao
層包目錄,來實現 dao
層增強,其實項目中配置一個@MapperScan
是指定一個數據源,配置兩個@MapperScan
就可以指定兩個數據源,通過不同得 dao
層包目錄區分,來實現不同數據源得訪問隔離。
比如下麵代碼中,com.xxx.dao.master
目錄下為主數據源 dao
文件,com.xxx.dao.slave
為從數據源 dao
文件,這個方式比網上得基於 aop
加註解得方式更加簡潔好用,也沒有單個方法中使用不同數據源切換得問題,因此推薦這種寫法
/**
* 主數據源
*/
@Slf4j
@Configuration
@MapperScan(basePackages = {"com.xxx.dao.master"},
sqlSessionFactoryRef = "MasterSqlSessionFactory")
public class MasterDataSourceConfig {
@Bean(name = "MasterDataSource")
@Qualifier("MasterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource clickHouseDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "MasterSqlSessionFactory")
public SqlSessionFactory getSqlSessionFactory(@Qualifier("MasterDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/master/*.xml"));
log.info("------------------------------------------MasterDataSource 配置成功");
return sessionFactoryBean.getObject();
}
}
/**
* 從數據源
*/
@Slf4j
@Configuration
@MapperScan(basePackages = {"com.xxx.dao.slave"},
sqlSessionFactoryRef = "SlaveSqlSessionFactory")
public class MasterDataSourceConfig {
@Bean(name = "SlaveDataSource")
@Qualifier("SlaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource clickHouseDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "SlaveSqlSessionFactory")
public SqlSessionFactory getSqlSessionFactory(@Qualifier("SlaveDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/slave/*.xml"));
log.info("------------------------------------------SlaveDataSource 配置成功");
return sessionFactoryBean.getObject();
}
}
數據源yml配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
# 主庫數據源
master:
url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password:
slave:
url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password:
博主剛開始編碼一、兩年得時候一個項目中遇到了多數據源使用得問題,那時候題主便在網上搜索Spring 多數據源
得帖子,大多數都是基於Spring提供得AbstractRoutingDataSource + AOP + 註解
來做動態切換,包括現在流行得 Mybatis plus
官方得多數據源解決方案也是這種做法,這種做法解決了博主當時得多數據源使用問題,後來加了一個需求,在一個定時任務中,查詢兩個數據源得數據,才發現動態切換在單個方法中不好用了,最後使用得原生jdbc數據源解決。多年後,博主在另一家公司得項目中又遇到了多數據源問題,但是這次博主在網上搜索得是Mybatis 多數據源
,才發現了這個優雅得解決方案,進而推薦給大家
六. Spring Security項目中,使用MDC實現介面請求調用追蹤,以及用戶ID記錄
MDC介紹
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 、logback及log4j2 提供的一種方便在多線程條件下記錄日誌的功能。MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日誌時,只需要從 MDC 中獲取所需的信息即可。
雖然MDC能夠方便得實現介面請求調用追蹤功能,但是它在子線程中會丟失父線程中添加得鍵值對信息,解決方法是通過父線程中調用線程池前調用 MDC.getCopyOfContextMap()
,然後在子線程中第一個調用 MDC.setConextMap()
獲取鍵值對信息,完整實現代碼如下:
/**
* 自定義Spring線程池,解決子線程丟失reqest_id問題
*/
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
/**
* MDC幫助類,添加reqest_id
*/
public class ThreadMdcUtil {
public static final String REQUEST_ID = "request_id";
/**
* 設置請求唯一ID
*/
public static void setTraceIdIfAbsent() {
if (MDC.get(REQUEST_ID) == null) {
MDC.put(REQUEST_ID, IdUtil.getUid());
}
}
/**
* 存在userId則添加到REQUEST_ID中
* @param userId
*/
public static void setUserId(String userId) {
String s = MDC.get(REQUEST_ID);
if (s != null) {
MDC.put(REQUEST_ID, s + "_" + userId);
}
}
public static void removeTraceId() {
MDC.remove(REQUEST_ID);
}
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
// 設置traceId
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
在 Spring Security
中添加 token
過濾器
/**
* token過濾器 驗證token有效性
*
* @author ruoyi
*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
try {
// 入口傳入請求ID
ThreadMdcUtil.setTraceIdIfAbsent();
LoginUserDetail loginUser = tokenService.getLoginUser(request);
if (Objects.nonNull(loginUser) && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {
// 記錄userId
ThreadMdcUtil.setUserId(String.valueOf(loginUser.getMember().getId()));
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
} finally {
// 出口移除請求ID
ThreadMdcUtil.removeTraceId();
}
}
}
最後在 logback.xml
中添加 %X{request_id}
<property name="pattern"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{request_id}] [%thread] [%-5level] %logger{36}:%L %M - %msg%n"/>
日誌列印效果如下:
2022-11-27 21:29:48.008 [86c76336100c414dbe9217aeb099ccd5_12] [http-nio-82-exec-2] [INFO ] c.w.m.a.s.impl.IHomeServiceImpl:56 getHomeIndexDataCompletableFuture - getHomeIndexDataCompletableFuture:com.wayn.common.util.R@701f7b8e[code=200,msg=操作成功,map={bannerList=[{"createTime":"2020-06-26 19:56:03","delFlag":false,"id":14,"imgUrl":"https://m.360buyimg.com/mobilecms/s700x280_jfs/t1/117335/39/13837/263099/5f291a83E8ba761d0/5c0460445cb28248.jpg!cr_1125x449_0_166!q70.jpg.dpg","jumpUrl":"http://82.157.141.70/mall/#/detail/1155015","sort":0,"status":0,"title":"hh2","updateTime":"2022-06-19 09:16:46"},{"createTime":"2020-06-26 19:56:03","delFlag":false,"id":15,"imgUrl":"https://m.360buyimg.com/mobilecms/s700x280_jfs/t1/202096/26/11652/265782/616acb67E4fcdc9ac/8d7cdfbb6c934e67.jpg!cr_1125x449_0_166!q70.jpg.dpg","jumpUrl":"#/detail/1155015","sort":0,"status":0,"title":"hh","updateTime":"2022-06-19 09:04:58"}], newGoodsList=[{"actualSales":0,"brandId":0,"brief":"酥脆奶香,甜酸回味","categoryId":1008015,"counterPrice":56.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1116011","id":1116011,"isHot":true,"isNew":true,"isOnSale":true,"keywords":"","name":"蔓越莓曲奇 200克","picUrl":"http://yanxuan.nosdn.127.net/767b370d07f3973500db54900bcbd2a7.png","retailPrice":36.00,"shareUrl":"","sort":5,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"粉彩色澤,記錄生活","categoryId":1012003,"counterPrice":49.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1127047","id":1127047,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"趣味粉彩系列筆記本","picUrl":"http://yanxuan.nosdn.127.net/6c03ca93d8fe404faa266ea86f3f1e43.png","retailPrice":29.00,"shareUrl":"","sort":2,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"慢回彈海綿,時尚設計。","categoryId":1008002,"counterPrice":66.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1134030","id":1134030,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"簡約知性記憶棉坐墊","picUrl":"http://yanxuan.nosdn.127.net/aa49dfe878becf768eddc4c1636643a6.png","retailPrice":46.00,"shareUrl":"","sort":12,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"慢回彈海綿的呵護,萌趣添彩。","categoryId":1008002,"counterPrice":69.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1134032","id":1134032,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"趣味粉彩系列記憶棉坐墊","picUrl":"http://yanxuan.nosdn.127.net/8b30eeb17c831eba08b97bdcb4c46a8e.png","retailPrice":49.00,"shareUrl":"","sort":13,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"100%桑蠶絲,絲滑潤膚","categoryId":1008009,"counterPrice":2619.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1135002","id":1135002,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"宮廷奢華真絲四件套","picUrl":"http://yanxuan.nosdn.127.net/45548f26cfd0c7c41e0afc3709d48286.png","retailPrice":2599.00,"shareUrl":"","sort":1,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"自由海軍領探索未來夢","categoryId":1020003,"counterPrice":89.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1135072","id":1135072,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"經典海魂紋水手裙(嬰童)","picUrl":"http://yanxuan.nosdn.127.net/43e57d4208cdc78ac9c088f9b3e798f5.png","retailPrice":69.00,"shareUrl":"","sort":3,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"經典海魂紋自由海軍領","categoryId":1020003,"counterPrice":89.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1135073","id":1135073,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"海魂紋哈衣水手服(嬰童)","picUrl":"http://yanxuan.nosdn.127.net/53052b04ae001d289c040e09ea92231c.png","retailPrice":69.00,"shareUrl":"","sort":4,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":5,"brandId":0,"brief":"差旅好伴侶","categoryId":1032000,"counterPrice":119.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1152031","id":1152031,"isHot":true,"isNew":true,"isOnSale":true,"keywords":"","name":"魔獸世界-伊利丹頸枕眼罩套裝","picUrl":"http://yanxuan.nosdn.127.net/fd6e78a397bd9e9804116a36f0270b0a.png","retailPrice":99.00,"shareUrl":"","sort":4,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":5,"brandId":0,"brief":"桌面整理神器","categoryId":1032000,"counterPrice":519.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1152095","id":1152095,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"魔獸世界 聯盟·暴風城 堡壘收納盒","picUrl":"http://yanxuan.nosdn.127.net/c86b49f635fa141decebabbd0966a6ef.png","retailPrice":499.00,"shareUrl":"","sort":6,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":0,"brandId":0,"brief":"3重透氣,清爽柔滑","categoryId":1008009,"counterPrice":479.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1152161","id":1152161,"isHot":false,"isNew":true,"isOnSale":true,"keywords":"","name":"竹語絲麻印花四件套","picUrl":"http://yanxuan.nosdn.127.net/977401e75113f7c8334c4fb5b4bf6215.png","retailPrice":459.00,"shareUrl":"","sort":6,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10}], categoryList=[{"createTime":"2020-12-08 23:09:12","delFlag":false,"iconUrl":"http://cdn.wayn.xin/9fc3c52571aa38a1466f114c2dc892fc.png","id":2,"jumpType":0,"name":"大牌手機","picUrl":"http://cdn.wayn.xin/2545dd00ca4575759024af2811949a9d.jpg","sort":1,"status":0,"updateTime":"2020-12-12 19:26:48","valueId":5,"valueUrl":"http://baidu.com"},{"createTime":"2020-12-06 13:27:54","delFlag":false,"iconUrl":"http://82.157.141.70/upload/2022/02/21/a23fa32c8f4004a8c9fbbb1784462163.jpg","id":1,"jumpType":0,"name":"滋補保健2","picUrl":"http://cdn.wayn.xin/d4de7172eb7ae4178ae4dafd6a973d8f.jpg","sort":2,"status":0,"updateTime":"2022-06-19 09:17:20","valueId":2},{"createTime":"2020-12-08 23:26:15","delFlag":false,"iconUrl":"http://cdn.wayn.xin/6bc0f8a131d2d16b8fc2004d4aa4860c.png","id":3,"jumpType":1,"name":"不鏽鋼鍋","picUrl":"http://cdn.wayn.xin/314d87257f7a2ff03d5f4c5183797912.jpg","sort":2,"status":0,"updateTime":"2020-12-12 19:27:24","valueId":1036000},{"createTime":"2020-12-12 19:28:08","delFlag":false,"iconUrl":"http://cdn.wayn.xin/5a90531d901529967885279d7dc826e1.png","id":14,"jumpType":0,"name":"進口牛奶","picUrl":"http://cdn.wayn.xin/0b1f6ab63d5e222c52c83a2d0581e44c.jpg","sort":3,"status":0,"valueId":5},{"createTime":"2020-12-12 19:28:33","delFlag":false,"iconUrl":"http://cdn.wayn.xin/33530951827ca7e59940d51cda537d84.png","id":15,"jumpType":0,"name":"量販囤貨","picUrl":"http://cdn.wayn.xin/bb6daee3b3e51c3008db97585249f513.jpg","sort":4,"status":0,"valueId":2},{"createTime":"2020-12-12 19:28:50","delFlag":false,"iconUrl":"http://cdn.wayn.xin/7d337f25111b263b29d5d12589015c46.png","id":16,"jumpType":0,"name":"清潔用品","picUrl":"http://cdn.wayn.xin/be8995bda39d03b17349b8ec0dcab3d5.jpg","sort":5,"status":0,"valueId":2},{"createTime":"2020-12-12 19:29:10","delFlag":false,"iconUrl":"http://cdn.wayn.xin/2e632ec0173bb477dcdb601495e0412a.png","id":17,"jumpType":0,"name":"洗護用品","picUrl":"http://cdn.wayn.xin/53fb88c9d1245caa882aa3fc29187d0b.jpg","sort":6,"status":0,"valueId":4},{"createTime":"2020-12-12 19:29:28","delFlag":false,"iconUrl":"http://cdn.wayn.xin/942323c0e74677cf2aa15f09a1e63bca.png","id":18,"jumpType":0,"name":"日用百貨","picUrl":"http://cdn.wayn.xin/8587f91db2edcb43e57da9835cc7ec76.jpg","sort":7,"status":0,"valueId":2},{"createTime":"2020-12-12 19:29:46","delFlag":false,"iconUrl":"http://cdn.wayn.xin/18d9d860ba9b8b28522e050f11a8a8e0.png","id":19,"jumpType":0,"name":"明星乳膠","picUrl":"http://cdn.wayn.xin/65273c7fb2273e90958e92626248a90a.jpg","sort":8,"status":0,"valueId":6},{"createTime":"2020-12-12 19:30:15","delFlag":false,"iconUrl":"http://cdn.wayn.xin/7c790577afda91eebc3c95586e190957.png","id":20,"jumpType":0,"name":"口碑好物","picUrl":"http://cdn.wayn.xin/210011b35be4ceee39e6a466b40b8e22.jpg","sort":9,"status":0,"updateTime":"2021-04-01 20:13:08","valueId":5}], expire_time=1669549170235, hotGoodsList=[{"actualSales":1,"brandId":1001045,"brief":"一級桑蠶絲,吸濕透氣柔軟","categoryId":1036000,"counterPrice":719.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1006013","id":1006013,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"雙宮繭桑蠶絲被 空調被","picUrl":"http://yanxuan.nosdn.127.net/583812520c68ca7995b6fac4c67ae2c7.png","retailPrice":699.00,"shareUrl":"","sort":7,"unit":"件","updateTime":"2021-08-08 11:19:36","virtualSales":10},{"actualSales":1,"brandId":1001045,"brief":"雙層子母被,四季皆可使用","categoryId":1008008,"counterPrice":14199.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1006014","id":1006014,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"雙宮繭桑蠶絲被 子母被","picUrl":"http://yanxuan.nosdn.127.net/2b537159f0f789034bf8c4b339c43750.png","retailPrice":1399.00,"shareUrl":"","sort":15,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":1001000,"brief":"加大加厚,雙色精彩","categoryId":1036000,"counterPrice":219.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1011004","id":1011004,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"色織精梳AB紗格紋空調被","picUrl":"http://yanxuan.nosdn.127.net/0984c9388a2c3fd2335779da904be393.png","retailPrice":199.00,"shareUrl":"","sort":2,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":0,"brief":"共用親密2人時光","categoryId":1008008,"counterPrice":219.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1019002","id":1019002,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"升級款護頸雙人記憶枕","picUrl":"http://yanxuan.nosdn.127.net/0118039f7cda342651595d994ed09567.png","retailPrice":199.00,"shareUrl":"","sort":10,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":0,"brief":"健康保護枕","categoryId":1008008,"counterPrice":119.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1019006","id":1019006,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"植物填充護頸夜交藤枕","picUrl":"http://yanxuan.nosdn.127.net/60c3707837c97a21715ecc3986a744ce.png","retailPrice":99.00,"shareUrl":"","sort":7,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":0,"brief":"厚實舒適","categoryId":1008001,"counterPrice":59.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1021000","id":1021000,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"被","name":"埃及進口長絨棉毛巾","picUrl":"http://yanxuan.nosdn.127.net/7191f2599c7fe44ed4cff7a76e853154.png","retailPrice":39.00,"shareUrl":"","sort":7,"unit":"條","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":6,"brandId":1001020,"brief":"浪漫毛線繡球,簡約而不簡單","categoryId":1008009,"counterPrice":319.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1022000","id":1022000,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"意式毛線繡球四件套","picUrl":"http://yanxuan.nosdn.127.net/5350e35e6f22165f38928f3c2c52ac57.png","retailPrice":299.00,"shareUrl":"","sort":18,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":32,"brandId":1001000,"brief":"柔軟紗布,嬰童可用","categoryId":1036000,"counterPrice":269.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1027004","id":1027004,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"色織六層紗布夏涼被","picUrl":"http://yanxuan.nosdn.127.net/6252f53aaf36c072b6678f3d8c635132.png","retailPrice":249.00,"shareUrl":"","sort":3,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":32,"brandId":0,"brief":"原生苦蕎,健康護頸","categoryId":1008008,"counterPrice":119.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1036002","id":1036002,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"高山苦蕎麥枕","picUrl":"http://yanxuan.nosdn.127.net/ffd7efe9d5225dff9f36d5110b027caa.png","retailPrice":99.00,"shareUrl":"","sort":5,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10},{"actualSales":32,"brandId":0,"brief":"5cm記憶綿的親密包裹","categoryId":1008008,"counterPrice":619.00,"createTime":"2018-02-01 00:00:00","delFlag":false,"goodsSn":"1037011","id":1037011,"isHot":true,"isNew":false,"isOnSale":true,"keywords":"","name":"安睡慢回彈記憶綿床墊","picUrl":"http://yanxuan.nosdn.127.net/a03ea6f4509439acdafcb7ceba1debe0.png","retailPrice":599.00,"shareUrl":"","sort":22,"unit":"件","updateTime":"2018-02-01 00:00:00","virtualSales":10}]}]
2022-11-27 21:29:48.336 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug - ==> Preparing: SELECT COUNT(*) AS total FROM shop_goods WHERE del_flag = 0 AND is_on_sale = 1
2022-11-27 21:29:48.387 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug - ==> Parameters:
2022-11-27 21:29:48.426 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage_mpCount:137 debug - <== Total: 1
2022-11-27 21:29:48.430 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug - ==> Preparing: select id, goods_sn, name, pic_url, counter_price, retail_price, actual_sales, virtual_sales from shop_goods WHERE del_flag = 0 and is_on_sale = 1 order by create_time desc LIMIT ?
2022-11-27 21:29:48.452 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug - ==> Parameters: 6(Long)
2022-11-27 21:29:48.476 [9d919fb6d33c4652ba28ff87ae210809_12] [http-nio-82-exec-3] [DEBUG] c.w.c.c.m.s.G.selectGoodsListPage:137 debug - <== Total: 6
最後分析上訴日誌:通過86c76336100c414dbe9217aeb099ccd5
實現介面調用追蹤,通過12
用戶ID,實現用戶調用追蹤
七. alibaba excel導出時自定義格式轉換優雅實現
官網介紹:EasyExcel
是一個基於Java的簡單、省記憶體的讀寫Excel的開源項目。在儘可能節約記憶體的情況下支持讀寫百M的Excel。
EasyExcel
是 alibaba
出的一個基於 java poi
得excel通用處理類庫,他的優勢在於記憶體消耗。對比 easypoi
方案,EasyExcel
在記憶體消耗、知名度(大廠光環)上更出眾些。
博主在使用過程中發現導出excel,官網對自定義格式欄位提供了 converter
介面,但只簡單提供了CustomStringStringConverter
類代碼,達不到博主想要得優雅要求,如下:
public class CustomStringStringConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 這裡讀的時候會調用
*
* @param context
* @return
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) {
return "自定義:" + context.getReadCellData().getStringValue();
}
/**
* 這裡是寫的時候會調用 不用管
*
* @return
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
return new WriteCellData<>(context.getValue());
}
}
在以上代碼中,打個比方想要實現性別欄位得自定義格式轉換,就需要在convertToExcelData方法中,添加如下代碼
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
String value = context.getValue();
if ("man".equals(value)) {
return new WriteCellData<>("男");
} else {
return new WriteCellData<>("女");
}
}
可以看到,非常得不優雅,對於這種類型欄位,博主習慣使用枚舉類來定義欄位所有類型,然後將枚舉類轉換為 map(value,desc)
結構,就可以優雅得實現這個自定義格式得需求
/**
* 一、先定義int欄位抽象轉換類,實現通用轉換邏輯
*/
public abstract class AbstractIntConverter implements Converter<Integer> {
abstract List<ConverterDTO> getArr();
public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
List<ConverterDTO> values = getArr();
Map<Integer, String> map = values.stream().collect(toMap(ConverterDTO::getType, ConverterDTO::getDesc));
String result = map.getOrDefault(value, "");
return new WriteCellData<>(result);
}
static class ConverterDTO {
private Integer type;
private String desc;
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public ConverterDTO(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
}
}
/**
* 二、定義通用狀態欄位轉換類
*/
public class StatusConverter extends AbstractIntConverter {
@Override
List<ConverterDTO> getArr() {
StatusEnum[] values = StatusEnum.values();
return Arrays.stream(values).map(sexEnum -> new ConverterDTO(sexEnum.getType(), sexEnum.getDesc())).toList();
}
/**
* 狀態枚舉
*/
enum StatusEnum {
MAN(0, "啟用"),
WOMAN(1, "禁用");
private Integer type;
private String desc;
StatusEnum(Integer type, String desc) {
this.type = type;
this.desc = desc;
}
public Integer getType() {
return type;
}
public String getDesc() {
return desc;
}
}
}
最後再導出 ExcelProperty
中甜膩加 StatusConverter
,就優雅得實現了自定義格式得需求
public class User extends BaseEntity {
...
/**
* 用戶狀態 0 啟用 1 禁用
*/
@ExcelProperty(value = "用戶狀態", converter = StatusConverter.class)
private Integer userStatus;
...
}
八. Springboot 預設redis客戶端lettuce經常連接超時解決方案
不知道大家有沒有遇到這種情況,線上項目使用 lettuce
客戶端,當操作 redis
得介面一段時間沒有調用後(比如30分鐘),再次調用 redis
操作後,就會遇到連接超時得問題,導致介面異常。博主直接給出分析過程:
- 通過wireshark抓包工具,發現項目中
redis
連接創建後,一段時間未傳輸數據後,客戶端發送psh
包,未收到服務端ack
包,觸發tcp得超時重傳機制,在重傳次數重試完後,最終客戶端主動關閉了連接。
到這裡我們就知道這個問題,主要原因在於服務端沒有回覆客戶端(比如tcp參數設置、防火牆主動關閉等,都是針對一段時間內沒有數據傳輸得tcp連接會做關閉處理),造成了客戶端得連接超時
面對這個問題有三種解決方案:
- redis操作異常後進行重試,這篇文章有介紹 生產環境Redis連接,長時間無響應被伺服器斷開問題
- 啟用一個心跳定時任務,定時訪問
redis
,保持redis
連接不被關閉,簡而言之,就是寫一個定時任務,定時調用redis
得get
命令,進而保活redis
連接 - 基於Springboot 提供得
LettuceClientConfigurationBuilderCustomizer
自定義客戶端配置,博主這裡主要針對第三種自定義客戶端配置來講解一種優雅得方式
Springboot
項目中關於 lettuce
客戶端得自動配置是沒有啟用保活配置得,要啟用得話代碼如下:
/**
* 自定義lettuce客戶端配置
*
* @return LettuceClientConfigurationBuilderCustomizer
*/
@Bean
public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
return clientConfigurationBuilder -> {
LettuceClientConfiguration clientConfiguration = clientConfigurationBuilder.build();
ClientOptions clientOptions = clientConfiguration.getClientOptions().orElseGet(ClientOptions::create);
ClientOptions build = clientOptions.mutate().build();
SocketOptions.KeepAliveOptions.Builder builder = build.getSocketOptions().getKeepAlive().mutate();
// 保活配置
builder.enable(true);
builder.idle(Duration.ofSeconds(30));
SocketOptions.Builder socketOptionsBuilder = clientOptions.getSocketOptions().mutate();
SocketOptions.KeepAliveOptions keepAliveOptions = builder.build();
socketOptionsBuilder.keepAlive(keepAliveOptions);
SocketOptions socketOptions = socketOptionsBuilder.build();
ClientOptions clientOptions1 = ClientOptions.builder().socketOptions(socketOptions).build();
clientConfigurationBuilder.clientOptions(clientOptions1);
};
}
添加 lettuce
客戶端的自定義配置,在 KeepAliveOptions
中啟用 enable
,這樣 lettuce
客戶端就會在tcp協議規範上啟用 keep alive
機制自動發送心跳包
九. redis客戶端lettuce啟用epoll
直接給 官網連接,配置很簡單,添加一個 netty-all
得依賴,lettuce
會自動檢測項目系統是否支持 epoll
(linux
系統支持),並且是否有netty-transport-native-epoll
依賴( netty-all
包含 netty-transport-native-epoll
),都滿足得話就會自動啟用 epoll
事件迴圈,進一步提升系統性能
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
十. Springboot web項目優雅停機
web項目配置了優雅停機後,在重啟jar包,或者容器時可以防止正在活動得線程被突然停止( kill -9
無解,請不要使用這個參數殺線上進程,docker compose
項目儘量不要用 docker-compose down
命令關閉項目,使用 docker-compose rm -svf
可以觸發優雅停機),造成用戶請求失敗,在此期間允許完成現有請求但不允許新請求,配置如下:
server: shutdown: "graceful"
十一. nginx配置通用請求尾碼
先說下這個配置產生得前提,博主公司pc客戶項目是基於 electron
打包得網頁項目,每次項目大版本更新時,為了做好相容性,防止客戶端網頁緩存等,會使用一個新網頁地址,打個比方:
老網頁地址,v1.1.0
版本網頁訪問地址:http://api.dev.com/pageV110
新網頁地址,v1.2.0
版本網頁訪問地址:http://api.dev.com/pageV120
那麼項目得nginx配置則則需要新加一個 v1.2.0
得配置如下:
server {
listen 80;
server_name api.dev.com;
client_max_body_size 10m;
# 老網頁v1.1.0配置
location ~ ^/pageV110 {
alias /home/wwwroot/api.dev.com/pageV110;
index index.html index.htm;
}
# 新網頁v1.2.0配置
location ~ ^/pageV120 {
alias /home/wwwroot/api.dev.com/pageV120;
index index.html index.htm;
}
}
那麼博主在每次項目發佈得時候就需要配合前端發版,配置一個新網頁,故產生了這個通用配置得需求,如下:
server {
listen 80;
server_name api.dev.com;
client_max_body_size 10m;
# 配置正則localtion
location ~ ^/pageV(.*) {
set $s $1; # 定義尾碼變數
alias /home/wwwroot/api.dev.com/pageV$s;
index index.html index.htm;
}
}
在 nginx
配置文件語法中,location
語句可以使用正則表達式,定義 set $s $1
變數,實現了通用配置
十二. 關於開發人員的自我提升和突破
博主這裡主要總結了四點:
- 多和他人溝通,溝通能把複雜問題簡單化,有時候開發階段一個需求多問幾句,可以減少因為個人理解差異導致的需求不一致問題,進而減少開發時間
- 建立長短期目標,觀看技術視頻、書籍給自己充電,比如7天利用業餘時間看完一本電子書,三十天從零開始一個新項目等
- 善於總結,對於項目中的疑難bug,踩坑點要有記錄,防止下次遇到再掉坑裡
- 敢於嘗試、擔責,對項目、代碼里明確不合理的地方要敢於跟他人溝通,修改問題代碼,達到優化目的。對於自己造成的問題要承擔,不要推卸責任。對於線上問題要重視,優先解決線上問題。