十二條後端開發經驗分享,純乾貨

十二條後端開發經驗分享,純乾貨

一。 優雅的進行執行緒池異常處理

在Java開發中,執行緒池的使用必不可少,使用無返回值 execute() 方法時,執行緒執行發生異常的話,需要記錄日誌,方便回溯,一般做法是線上程執行方法內 try/catch 處理,如下:

@Testpublic void test() throws Exception { ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit。SECONDS, new ArrayBlockingQueue<>(100000)); Future 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 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@Componentpublic 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 Future submit(Callable 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 Callable wrap(final Callable callable, final Map 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 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@Componentpublic 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}

複製程式碼

日誌列印效果如下:

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 = 12022-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: 12022-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 { @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 context) { return new WriteCellData<>(context。getValue()); }}複製程式碼

在以上程式碼中,打個比方想要實現性別欄位得自定義格式轉換,就需要在convertToExcelData方法中,新增如下程式碼

@Overridepublic WriteCellData<?> convertToExcelData(WriteConverterContext 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 { abstract List getArr(); public WriteCellData<?> convertToExcelData(Integer value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { List values = getArr(); Map 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 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 */@Beanpublic 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 事件迴圈,進一步提升系統性能

io。netty netty-all複製程式碼

十。 Springboot web專案優雅停機

web專案配置了優雅停機後,在重啟jar包,或者容器時可以防止正在活動得執行緒被突然停止( kill -9 無解,請不要使用這個引數殺線上程序,docker compose 專案儘量不要用 docker-compose down 命令關閉專案,使用 docker-compose rm -svf 可以觸發優雅停機),造成使用者請求失敗,在此期間允許完成現有請求但不允許新請求,配置如下:

server: shutdown: “graceful”複製程式碼

十一。 nginx配置通用請求字尾

先說下這個配置產生得前提,博主公司pc客戶專案是基於 electron 打包得網頁專案,每次專案大版本更新時,為了做好相容性,防止客戶端網頁快取等,會使用一個新網頁地址,打個比方:

老網頁地址,v1。1。0 版本網頁訪問地址:api。dev。com/pageV110

新網頁地址,v1。2。0 版本網頁訪問地址: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,踩坑點要有記錄,防止下次遇到再掉坑裡

敢於嘗試、擔責,對專案、程式碼裡明確不合理的地方要敢於跟他人溝通,修改問題程式碼,達到最佳化目的。對於自己造成的問題要承擔,不要推卸責任。對於線上問題要重視,優先解決線上問題。

作者:wayn

連結:https://juejin。cn/post/7171770606940061727