Java項(xiàng)目日志脫敏解決方案
應(yīng)合規(guī)要求。。。。巴拉、巴拉、巴拉。故對日志中客戶的敏感信息進(jìn)行脫敏便提上了日程。
一、通過歷史項(xiàng)目所使用技術(shù),以及網(wǎng)上高頻方法,大概是4種方案
- 單脫敏工具類,對日志輸出的地方單獨(dú)進(jìn)行脫敏處理
- 脫敏工具類+日志框架的切面方法轉(zhuǎn)換器上統(tǒng)一脫敏
- 脫敏框架-----注解模式
- 脫敏框架-----工具類+配置模式
二、日志脫敏的難度描述:
- 參數(shù)命名、數(shù)據(jù)庫字段命名未標(biāo)準(zhǔn)化,或者標(biāo)準(zhǔn)化不高
- 日志輸出非單一字段輸出
- 無法明確日志輸出包含哪些脫敏范圍
- 日志輸出代碼量很大,很難一處一處進(jìn)行評估
三、各方案優(yōu)缺點(diǎn)
1.方案一:單脫敏工具類,對日志輸出的地方單獨(dú)進(jìn)行脫敏處理
優(yōu)點(diǎn):效率最高
缺點(diǎn):侵入性最高、且工作量最大
總結(jié):可以解決幾乎所有項(xiàng)目的問題,但是侵入性太高,耦合性太高,幾乎不再使用
2.方案二:脫敏工具類+日志框架的切面方法轉(zhuǎn)換器上統(tǒng)一脫敏
優(yōu)點(diǎn):侵入性最低、且工作量最小
缺點(diǎn):效率最低,需對所有可能的情況進(jìn)行脫敏判斷,且誤殺風(fēng)險(xiǎn)最高
總結(jié):可以解決幾乎所有項(xiàng)目的問題,日志超長,一般會(huì)進(jìn)行截取,使用率最高,但是若日志量巨大可能會(huì)使用日志框架進(jìn)行輔助
3.方案三:脫敏框架-----注解模式(如:sensitive)
優(yōu)點(diǎn):方法簡單,使用方便,效率較高,可以解決大部分場景的脫敏
缺點(diǎn):侵入性較高,需對所有可能的情況進(jìn)行脫敏判斷
總結(jié):新項(xiàng)目,或者項(xiàng)目重構(gòu)以及數(shù)據(jù)標(biāo)準(zhǔn)化程度較高會(huì)考慮使用,對于自定義日志輸出、請求日志等很不友好(加了注解才脫敏嘛)
4.方案四:脫敏框架-----工具類+配置模式(如:desensitize)
優(yōu)點(diǎn):侵入性低、且工作量最小
缺點(diǎn):易漏殺,需所有日志輸出按框架特定格式進(jìn)行輸出
總結(jié):可以解決幾乎所有項(xiàng)目的問題,對于項(xiàng)目標(biāo)準(zhǔn)化要求較高,新項(xiàng)目或者重構(gòu),推薦指數(shù)比方案三高。但是針對多為字段(如證件號碼字段,不同證件類型,證件號碼規(guī)范不一樣),多釋義字段(如:number:號碼,可能代表證件號碼、還可能代表數(shù)量)不太友好。
四、方案二的代碼演示
(方案一絕對不會(huì)用的,方案三、方案四有很多框架的詳解,就不贅述
所以這里重點(diǎn)介紹方案二。)
1.不吹不黑
市場上大多數(shù)項(xiàng)目面對合規(guī)時(shí),項(xiàng)目基本成型,特別是較大的項(xiàng)目,一般都是會(huì)分成不同的大項(xiàng)目團(tuán)隊(duì),不同團(tuán)隊(duì)針對的業(yè)務(wù)重疊的部分,字段命名也時(shí)常不一樣,日志輸出對不同的開發(fā)者有不同的習(xí)慣,故想要使用方案三和方案四,很難全面、低耦合、低工作量的完成。那么方案二的缺點(diǎn)可能是最能接受的。
2.示例背景
- 數(shù)據(jù)有做標(biāo)準(zhǔn)化,但程度相對較低
- 項(xiàng)目相對龐大,且較為完善的基礎(chǔ)上進(jìn)行日志脫敏改造
- 整個(gè)項(xiàng)目結(jié)構(gòu)分為 前臺(tái)門戶 + 后臺(tái)門戶 + 多個(gè)后端服務(wù) + 多個(gè)第三方服務(wù)
- 日志輸出有{對象名:對象}方式、{對象}方式、字符串拼接對象方式、第三方請求體、xml、sql等
- 日志框架都是logback
3.示例要求
- 合規(guī)要求的所有類型數(shù)據(jù)不管字段名,必須全部脫敏
- 代碼侵入最小
- 不影響服務(wù)正常功能
- 對純數(shù)字類型脫敏,要求能夠解碼(本項(xiàng)目特殊要求,合規(guī)是不想允許這樣的)
4.示例方案設(shè)計(jì)
logback.xml配置
<conversionRule conversionWord="msg" converterClass="com.test.mask.SensitiveDataConverter"> </conversionRule>
轉(zhuǎn)換器(SensitiveDataConverter)代碼
public class SensitiveConverter extends MessageConverter {
@Override
public String convert(ILoggingEvent event){
// 獲取原始日志
String requestLogMsg = event.getFormattedMessage();
// 獲取返回脫敏后的日志 isLogMaskEnabled全局公共配置,是否脫敏開關(guān)
return GlobalConfig.isLogMaskEnabled() ? LogSensitiveUtils.filterSensitive(requestLogMsg) : requestLogMsg;
}
}實(shí)際處理工具類(LogSensitiveUtils)代碼-可以提取到公共包中進(jìn)行依賴管理即可
public class LogSensitiveUtils {
/**
* [郵箱]@前隱藏<例子:138******1234>
* 字段加數(shù)字的可逆掩碼 以及純數(shù)字采取可逆掩碼
* 其他的對脫敏部分采取根據(jù)字段長度取中間的進(jìn)行脫敏
*
* @param content
* @return
*/
public static String filterSensitive(String content) {
try {
if (!StringUtils.isBlank(content)) {
for (Map.Entry<String, List<Pattern>> entry : LogSensitiveConstants.SENSITIVE_SEQUENCE.entrySet()) {
content = filter(content, entry.getKey(), entry.getValue());
}
}
return content;
} catch (Exception e) {
return content;
}
}
/**
* 數(shù)字的可逆掩碼
*
* @param content 需脫敏字符串
* @param type 采取的脫敏方式
* @param patterns 該方式下需匹配的正則
* @return
* @author hh
* @date 2021年10月18日
*/
public static String filter(String content, String type, List<Pattern> patterns) {
for (Pattern pattern : patterns) {
Matcher matcher = pattern.matcher(content);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, Matcher.quoteReplacement(basesensitive(matcher.group(), type)));
}
matcher.appendTail(sb);
content = sb.toString();
}
return content;
}
/**
* 基礎(chǔ)純鏇字脫敏處理 指定起止展示長度 剩余用"KEY"中字符替換
* 非純數(shù)字脫敏處理
* [郵箱] @前隱藏<例亍:******@.163>
*
* @param str 待脫敏的字符串
* @return
* @author hh
* @date 2021年10月18日
*/
private static String basesensitive(String str, String type) {
int startLength, endLength = 0;
if (StringUtils.isBlank(str)) {
return StringUtils.EMPTY;
}
// 默認(rèn)脫敏從第4個(gè)字符開始掩碼
startLength = 3;
endLength = getEndLength(str.length());
if (LogSensitiveConstants.EMAIL.equals(type)) {
endLength = str.length() - str.indexOf('@');
}
if (LogSensitiveConstants.FIELD_NUM.equals(type)) {
Matcher matcher = LogSensitiveConstants.NUMBER_PATTERN.matcher(str);
int start = -1;
int end = -1;
String ss = "";
if (matcher.find()) {
ss = matcher.group();
}
start = str.indexOf(ss);
while (matcher.find()) {
ss = matcher.group();
}
end = str.lastIndexOf(ss);
int length = end - start;
endLength = getEndLength(length);
startLength = start + startLength;
}
String replacement = str.substring(startLength, str.length() - endLength);
StringBuilder sb = new StringBuilder();
if (LogSensitiveConstants.NUM.equals(type) || LogSensitiveConstants.FIELD_NUM.equals(type)) {
for (int i = 0; i < replacement.length(); i++) {
char ch;
if (replacement.charAt(i) >= '0' && replacement.charAt(i) <= '?') {
ch = LogSensitiveConstants.KEY.charAt((int) (replacement.charAt(i) - '0'));
} else {
ch = replacement.charAt(i);
}
sb.append(ch);
}
} else {
for (int i = 0; i < replacement.length(); i++) {
sb.append("*");
}
}
return StringUtils.left(str, startLength).concat(StringUtils.leftPad(StringUtils.right(str, endLength), str.length() - startLength, sb.toString()));
}正則表達(dá)式常量類(LogSensitiveConstants)-- 可以提取到公共包中進(jìn)行依賴管理即可
public class LogSensitiveConstants {
/**
* 數(shù)字脫敏掩碼字符
*/
public static final String KEY = "oiZeAsGTbQ";
public static final Pattern NUMBER_PATTERN = Pattern.compile("\\d");
/**
* 脫敏掩碼類型標(biāo)識
*/
public static final String EMAIL = "email";
public static final String FIELD_NUM = "field_num";
public static final String FIELD = "field";
public static final String NOT_NUM = "not_num";
public static final String NUM = "num";
/**
* 過濾先后順序:郵箱-->字段加數(shù)字的可逆掩碼-->其他非數(shù)字掩碼-->字段加非數(shù)字掩碼-->數(shù)字可逆掩碼
* 順序原因:
* 1.郵箱@前可能被其他正則先脫敏,但是郵箱有特殊脫敏要求,故優(yōu)先進(jìn)行脫敏
* 2.純數(shù)字和非字段前綴校驗(yàn)的非純數(shù)字放最后是因?yàn)?,純?shù)字涵蓋范圍與其他的有重疊,減少誤殺的方式,最好就是范圍大的放最后
* 3.字段前綴校驗(yàn)的兩種情況,其實(shí)不分順序,同理,非字段前綴的兩種類型,也不分前后
*/
public static final Map<String,List<Pattern>> SENSITIVE_SEQUENCE = new TreeMap<String, List<Pattern>>();
/**
* 數(shù)字:手機(jī)號、身份證號
*/
public static final List<Pattern> SENSITIVE_NUM_KEY = new ArrayList<Pattern>(4);
/**
* 過濾順序:身份證號-->手機(jī)號-->座機(jī)號-->QQ-->營業(yè)執(zhí)照-->稅務(wù)登記號+臺(tái)灣往來通行證+戶口簿+身份證號(純數(shù)字)-->回鄉(xiāng)證港澳往來內(nèi)地通行證
*/
public static final List<Pattern> SENSITIVE_NOT_NUM_KEY = new ArrayList<Pattern>(7);
/**
* 字段過濾(非純數(shù)字):
*/
public static final List<Pattern> SENSITIVE_FIELD_KEY = new ArrayList<Pattern>(6);
/**
* 字段過濾(純數(shù)字):
*/
public static final List<Pattern> SENSITIVE_FIELD_NUM = new ArrayList<Pattern>(3);
/**
* 郵箱過濾:
*/
public static final List<Pattern> SENSITIVE_EMAIL_KEY = new ArrayList<Pattern>(1);
/**
* 手機(jī)號正則匹配
*/
public static final String TEL_REGEX = "^1[23456789]\\d{9}$";
/**
* 電話號碼正則匹配
*/
public static final String PHONE_REGEX = "^0\\d{2,3}-\\d{7,8}$";
/**
* 身份證號正則匹配
*/
public static final String IDENTIFY_REGEX = "(^[1-9]\\d{7}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}$)|(^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([0-9]|X)$)";
/**
* 郵箱正則匹配
*/
public static final String EMAIL_REGEX = "([^a-zA-Z0-9._%-]|^)([a-zA-Z0-9_\\.-]+)@([\\da-zA-Z\\.-]+)\\.([a-zA-Z\\.]{2,6})" + "([^a-zA-Z\\d]|$)|([^a-zA-Z\\d]|^)[a-zA-Z\\d]+(\\.[a-z\\d]+)*@([\\da-zA-Z](-[\\da-zA-Z])?)+(\\.{1,2}[a-zA-Z]+)+$/([^a-zA-Z]|$)";
/**
* 護(hù)照號
* 護(hù)照號根據(jù)護(hù)照類型,規(guī)則不同,其中部分規(guī)則納入到手機(jī)號、LICENSE_NO_REGEX
*/
private static final String PASSPORT_REGEX = "(\\D|^)[P|pS|s]\\d{7}(\\b)";
/**
* 統(tǒng)一社會(huì)信用代碼: ^([0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXV]{10}1[1-9]\d{14})$
*/
private static final String USCC_REGEX = "([^0-9A-HJ-NPQRTUWXY]|^)([0-9A-HJ-NPQRTUWXV]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}|[1-9]\\d{14})(\\b)";
/**
* 組織機(jī)構(gòu)代碼證: [a-zA-Z0-9]{8}-[a-ZA-Z0-9]
*/
private static final String OCC_REGEX = "([^a-zA-Z0-9]|^)([a-zA-Z0-9]{8})-[a-zA-Z0-9](\\b)";
/**
* 警官證: ([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})
*/
private static final String POLICE_REGEX = "([^a-zA-Z0-9_\\.\\-]|^)(([a-zA-Z0-9_\\.\\-])+\\@(([a-zA-Z0-9\\-])+\\.)+([a-zA-Z0-9]{2,4}))(\\b)";
/**
* 軍人/武警身份證件 ^[\u4E00-\u9FA5](字第)([0-9a-zA-Z]{4,8})(號?)$/
*/
private static final String SOLDIER_REGEX = "([^\\u4E00-\\u9FA5]|^)([\\u4E00-\\u9FA5](字第)([0-9a-zA-Z]{4,8})(號?))";
/**
* mac
*/
private static final String MAC_REGEX = "[A-F0-9]{2}([-:][A-F0-9]{2})([-:.][A-F0-9]{2})([-:][A-F0-9]{2})([-:.][A-F0-9]{2})([-:][A-F-9]{2})(\\b)";
/**
* 車牌: ([京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領(lǐng)A-Z]{1}[A-Z]{1}(([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領(lǐng)A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9掛學(xué)警港澳]{1})
* 普通汽車:[京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領(lǐng)A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9掛學(xué)警港澳]{1}
* 新能源車:[京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領(lǐng)A-Z]{1}[A-Z]{1}(([0-9]{5}[DF])|([DF][A-HJ-NP-Z0-9][0-9]{4}))
*/
private static final String LICENSEE_CAR_REGEX = "[京津滬渝冀豫云遼黑湘皖魯新蘇浙贛鄂桂甘晉蒙陜吉閩貴粵青藏川寧瓊使領(lǐng)A-Z]{1}[A-Z]{1}(([0-9]{5}[DF])|([DF][A-HJ-NP-Z0-9][0-9]{4})|([A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9掛學(xué)警港澳]{1}))";
/**
* IP
* IPV4: ((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9][01]?[0-9][0-9]?)
* IPV6: ([0-9a-fA-F]{1,4}::?){1,7}([0-9a-fA-F]{1,4})
*/
private static final String IP_REGEX = "(((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9][01]?[0-9][0-9]?))|(([0-9a-fA-F]{1,4}::?){1,7}([0-9a-fA-F]{1,4}))";
/**
* 姓名: 用戶/客戶名稱/創(chuàng)建人/報(bào)告人 [\u4E00-\u9FA5]{2,12}
* cust_name/repr_client_name/USER_NAME/ACCT_NAME/CREATER_NAME
*/
private static final String USER_REGEX = "(\"?)((cust(_?)name)|(repr(_?)client(_?)name)|(repr(_?)client(_?)name)|(USER(_?)NAME)|(ACCT(_?)NAME)|(CREATER(_?)NAME))(\"?)(:|=)(\"?)[\\u4E00-\\u9FA5]{2,12}(\"?)";
/**
* 微信正則匹配
*/
private static final String WECHART_REGEX = "(\"?)wechat(\"?)(:|=)(\"?)[a-zA-Z]([-_a-zA-Z0-9]{5,19})(\"?)";
/**
* 生日正則匹配 屏蔽有效數(shù)據(jù) 即1900-01-01~2099-12-31時(shí)間段的數(shù)據(jù)
*/
private static final String BIRTH_DATE_REGEX = "(\"?)birth(_?)date(\"?)(:|=)(\"?)(19|20)\\d{2}([.|_|年]?)(1[0-2]|0?[1-9])([.|_|年]?(0?[1-9]|[1-2]|[0-9]|3[0-1])(\"?))";
/**
* 畢業(yè)院校正則匹配
*/
private static final String GRADUATE_INSTITUTIONS_REGEX = "(\"?)gradate(_?)institutions(\"?)(:|=)(\"?)[\\u4E00-\\u9FA5]{4,18}(\"?)";
/**
* 籍貫
*/
private static final String NATIVE_PLACE_REGEX = "(\"?)native(_?)place(\"?)(:|=)(\"?)[\\u4E00-\\u9FA5]{2,18}(\"?)";
/**
* 地址:
* ADDR\ADDRESS\ADDRDETAILS\addressLines
*/
private static final String ADDR_REGEX = "[\\u4E00-\\u9FA5][#()()A-Z0-9\\u4E00-\\u9FA5]{1,20}(省|市|區(qū)|鎮(zhèn)|縣|鄉(xiāng)|村|屯|路|街|組|號|小區(qū)|室|單元)" +
"|[#()()A-Z0-9\\u4E00-\\u9FA5]{1,20}(省|市|區(qū)|鎮(zhèn)|縣|鄉(xiāng)|村|屯|路|街|組1號|小區(qū)|室|單元)[#()()0-9a-z\\u4E00-\\u9FA5]{0,20}";
/**
* 銀行賬號: ([1-9]{1})(\\d{14,18})
* acctno\accountno
*/
private static final String ACCTNO_REGEX = "((\"?)((ACCT(_?)NO)|(ACCOUNT(_?)NO))(\"?)(:|=)(\"?)([1-9](\\d{14,18}))(\"?))";
/**
* QQ正則匹配: [1-9][8-9]{4,12}
* qq
*/
private static final String QQ_REGEX = "(\"?)qq(\"?)(:|=)(\"?)[1-9][8-9]{4,12}(\"?)";
/**
* 營業(yè)執(zhí)照號和稅務(wù)登記證 ([A-Z0-9]{15}|[A-Z0-9]{18}|[A-Z0-9]{20})
* licence_no\tax_no
*/
private static final String LICENSE_NO_REGEX = "(\"?)((licence(_?)no)|(tax(_?)no))(\"?)(:|=)(\"?)([A-Z0-9]{15}|[A-Z0-9]{18}|[A-Z0-9]{20})(\"?)";
static {
SENSITIVE_NUM_KEY.add(Pattern.compile(TEL_REGEX));
SENSITIVE_NUM_KEY.add(Pattern.compile(PHONE_REGEX));
SENSITIVE_NUM_KEY.add(Pattern.compile(IDENTIFY_REGEX));
SENSITIVE_NUM_KEY.add(Pattern.compile(PASSPORT_REGEX));
}
static {
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(USCC_REGEX));
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(OCC_REGEX));
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(POLICE_REGEX));
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(SOLDIER_REGEX));
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(MAC_REGEX));
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(LICENSEE_CAR_REGEX));
SENSITIVE_NOT_NUM_KEY.add(Pattern.compile(IP_REGEX));
}
static {
SENSITIVE_FIELD_KEY.add(Pattern.compile(USER_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_KEY.add(Pattern.compile(WECHART_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_KEY.add(Pattern.compile(BIRTH_DATE_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_KEY.add(Pattern.compile(GRADUATE_INSTITUTIONS_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_KEY.add(Pattern.compile(NATIVE_PLACE_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_KEY.add(Pattern.compile(ADDR_REGEX, Pattern.CASE_INSENSITIVE));
}
static {
SENSITIVE_FIELD_NUM.add(Pattern.compile(QQ_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_NUM.add(Pattern.compile(LICENSE_NO_REGEX, Pattern.CASE_INSENSITIVE));
SENSITIVE_FIELD_NUM.add(Pattern.compile(ACCTNO_REGEX, Pattern.CASE_INSENSITIVE));
}
static {
SENSITIVE_EMAIL_KEY.add(Pattern.compile(EMAIL_REGEX));
}
static {
SENSITIVE_SEQUENCE.put(EMAIL, SENSITIVE_EMAIL_KEY);
SENSITIVE_SEQUENCE.put(FIELD_NUM, SENSITIVE_FIELD_NUM);
SENSITIVE_SEQUENCE.put(FIELD, SENSITIVE_FIELD_KEY);
SENSITIVE_SEQUENCE.put(NOT_NUM, SENSITIVE_NOT_NUM_KEY);
SENSITIVE_SEQUENCE.put(NUM, SENSITIVE_NUM_KEY);
}
private LogSensitiveConstants() {
}
}結(jié)束!
如果是日志輸出相對規(guī)范,絕大部分輸出需脫敏的字段的場景,都使用了key:value 或key = value的情況下非常建議方案四
相關(guān)文章
關(guān)于Java集合框架Collection接口詳解
這篇文章主要介紹了關(guān)于Java集合框架Collection接口詳解,Collection接口是Java集合框架中的基礎(chǔ)接口,定義了一些基本的集合操作,包括添加元素、刪除元素、遍歷集合等,需要的朋友可以參考下2023-05-05
SpringBoot整合MyBatis-Plus樂觀鎖不生效的問題及解決方法
這篇文章主要介紹了SpringBoot整合MyBatis-Plus樂觀鎖不生效的問題解決方案,通過實(shí)例代碼介紹了SpringBoot各個(gè)層次的操作,需要的朋友可以參考下2022-04-04
Redis原子計(jì)數(shù)器incr,防止并發(fā)請求操作
這篇文章主要介紹了Redis原子計(jì)數(shù)器incr,防止并發(fā)請求操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11
JAVA8 STREAM COLLECT GROUPBY分組實(shí)例解析
這篇文章主要介紹了JAVA8 STREAM COLLECT GROUPBY分組實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01
SpringBoot中EasyExcel實(shí)現(xiàn)execl導(dǎo)入導(dǎo)出
本文主要介紹了SpringBoot中EasyExcel實(shí)現(xiàn)execl導(dǎo)入導(dǎo)出,實(shí)現(xiàn)了如何準(zhǔn)備環(huán)境、創(chuàng)建實(shí)體類、自定義轉(zhuǎn)換器以及編寫導(dǎo)入邏輯的步驟和示例代碼,感興趣的可以了解下2023-06-06

