前言

  在开发系统过程中,经常会接触到大量的数据信息,这些数据信息可能包含身份证号、手机号、姓名、卡号等各种敏感信息。而有些用户需求是不允许,这些敏感信息数据为了保护用户个人信息的安全。对这些数据需要进行特殊处理。

一、什么是数据脱敏?

数据脱敏又称数据去隐私化或数据变形,是在给定的规则、策略下对敏感数据进行变换、修改的技术机制,能够在很大程度上解决敏感数据在非可信环境中使用的问题。根据数据保护规范和脱敏策略.对业务数据中的敏感信息实施自动变形.实现对敏感信息的隐藏。

二、实现

适合单个字段

1、枚举需要脱敏的字段类型


/*** 需要脱敏的字段类型** @author xh* @Date 2022/7/20*/
public enum SensitiveTypeEnum {/*** 中文名*/CHINESE_NAME,/*** 身份证号*/ID_CARD,/*** 座机号*/FIXED_PHONE,/*** 手机号*/MOBILE_PHONE,/*** 地址*/ADDRESS,/*** 电子邮件*/EMAIL,/*** 银行卡*/BANK_CARD,/*** 虚拟账号*/ACCOUNT,/*** 密码*/PASSWORD;
}

2、创建注解,标识需要脱敏的字段

import com.artboy.project.common.enums.SensitiveTypeEnum;import java.lang.annotation.*;/*** @author xh* @Date 2022/7/20*/
// 指定在注解使用的位置
@Target({ElementType.FIELD, ElementType.METHOD})
// 指定注解保存的范围
@Retention(RetentionPolicy.RUNTIME)
// 可以被子类继承
@Inherited
// 设置为文档说明
@Documented
public @interface Desensitized {/*脱敏类型(规则)*/SensitiveTypeEnum type();/*判断注解是否生效的方法*/String isEffictiveMethod() default "";
}

创建Object工具类

用于复制对象和对对象的其他操作,注意使用fastjson实现深拷贝对于复杂的对象会出现栈溢出,这里修改为了Gson

package com.artboy.project.common.utils;import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.*;/*** @author xh* @Date 2022/7/26*/
public class DesensitizedObjectUtils {/*** 用序列化-反序列化方式实现深克隆* 缺点:1、被拷贝的对象必须要实现序列化** @param obj* @return*/@SuppressWarnings("unchecked")public static <T> T deepCloneObject(T obj) {T t = (T) new Object();try {ByteArrayOutputStream byteOut = new ByteArrayOutputStream();ObjectOutputStream out = new ObjectOutputStream(byteOut);out.writeObject(obj);out.close();ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());ObjectInputStream in = new ObjectInputStream(byteIn);t = (T) in.readObject();in.close();} catch (IOException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();}return t;}/*** 用序列化-反序列化的方式实现深克隆* 这里使用GSON来取代FAST-JSON* 缺点:1、当实体中存在接口类型的参数,并且这个接口指向的实例为枚举类型时,会报错"com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 171, fieldName iLimitKey"** @param objSource* @return*/public static Object deepCloneByFastJson(Object objSource) {
//        String tempJson = JSON.toJSONString(objSource);
//        Object clone = JSON.parseObject(tempJson, objSource.getClass());Gson gson = new Gson();String tempJson = gson.toJson(objSource);Object clone = gson.fromJson(tempJson, objSource.getClass());return clone;}/*** 深度克隆对象** @throws IllegalAccessException* @throws InstantiationException*/public static Object deepClone(Object objSource) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {if (null == objSource) return null;//是否jdk类型、基础类型、枚举类型if (isJDKType(objSource.getClass())|| objSource.getClass().isPrimitive()|| objSource instanceof Enum<?>) {if ("java.lang.String".equals(objSource.getClass().getName())) {//目前只支持String类型深复制return new String((String) objSource);} else {return objSource;}}// 获取源对象类型Class<?> clazz = objSource.getClass();Object objDes = clazz.newInstance();// 获得源对象所有属性Field[] fields = getAllFields(objSource);// 循环遍历字段,获取字段对应的属性值for (Field field : fields) {field.setAccessible(true);if (null == field) continue;Object value = field.get(objSource);if (null == value) continue;Class<?> type = value.getClass();if (isStaticFinal(field)) {continue;}try {//遍历集合属性if (type.isArray()) {//对数组类型的字段进行递归过滤int len = Array.getLength(value);if (len < 1) continue;Class<?> c = value.getClass().getComponentType();Array newArray = (Array) Array.newInstance(c, len);for (int i = 0; i < len; i++) {Object arrayObject = Array.get(value, i);Array.set(newArray, i, deepClone(arrayObject));}} else if (value instanceof Collection<?>) {Collection newCollection = (Collection) value.getClass().newInstance();Collection<?> c = (Collection<?>) value;Iterator<?> it = c.iterator();while (it.hasNext()) {Object collectionObj = it.next();newCollection.add(deepClone(collectionObj));}field.set(objDes, newCollection);continue;} else if (value instanceof Map<?, ?>) {Map newMap = (Map) value.getClass().newInstance();Map<?, ?> m = (Map<?, ?>) value;Set<?> set = m.entrySet();for (Object o : set) {Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;Object mapVal = entry.getValue();newMap.put(entry.getKey(), deepClone(mapVal));}field.set(objDes, newMap);continue;}//是否jdk类型或基础类型if (isJDKType(field.get(objSource).getClass())|| field.getClass().isPrimitive()|| isStaticType(field)|| value instanceof Enum<?>) {if ("java.lang.String".equals(value.getClass().getName())) {//目前只支持String类型深复制field.set(objDes, new String((String) value));} else {field.set(objDes, field.get(objSource));}continue;}//是否枚举if (value.getClass().isEnum()) {field.set(objDes, field.get(objSource));continue;}//是否自定义类if (isUserDefinedType(value.getClass())) {field.set(objDes, deepClone(value));continue;}} catch (Exception e) {e.printStackTrace();}}return objDes;}/*** 是否静态变量** @param field* @return*/private static boolean isStaticType(Field field) {return field.getModifiers() == 8 ? true : false;}private static boolean isStaticFinal(Field field) {return Modifier.isFinal(field.getModifiers()) && Modifier.isStatic(field.getModifiers());}/*** 是否jdk类型变量** @param clazz* @return* @throws IllegalAccessException*/private static boolean isJDKType(Class clazz) throws IllegalAccessException {//Class clazz = field.get(objSource).getClass();return org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "javax.")|| org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "java.")|| org.apache.commons.lang3.StringUtils.startsWith(clazz.getName(), "javax.")|| org.apache.commons.lang3.StringUtils.startsWith(clazz.getName(), "java.");}/*** 是否用户自定义类型** @param clazz* @return*/private static boolean isUserDefinedType(Class<?> clazz) {returnclazz.getPackage() != null&& !org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "javax.")&& !org.apache.commons.lang3.StringUtils.startsWith(clazz.getPackage().getName(), "java.")&& !org.apache.commons.lang3.StringUtils.startsWith(clazz.getName(), "javax.")&& !StringUtils.startsWith(clazz.getName(), "java.");}/*** 获取包括父类所有的属性** @param objSource* @return*/public static Field[] getAllFields(Object objSource) {/*获得当前类的所有属性(private、protected、public)*/List<Field> fieldList = new ArrayList<Field>();Class tempClass = objSource.getClass();while (tempClass != null && !tempClass.getName().toLowerCase().equals("java.lang.object")) {//当父类为null的时候说明到达了最上层的父类(Object类).fieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));tempClass = tempClass.getSuperclass(); //得到父类,然后赋给自己}Field[] fields = new Field[fieldList.size()];fieldList.toArray(fields);return fields;}/*** 深度克隆对象** @throws IllegalAccessException* @throws InstantiationException*/@Deprecatedpublic static Object copy(Object objSource) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {if (null == objSource) return null;// 获取源对象类型Class<?> clazz = objSource.getClass();Object objDes = clazz.newInstance();// 获得源对象所有属性Field[] fields = getAllFields(objSource);// 循环遍历字段,获取字段对应的属性值for (Field field : fields) {field.setAccessible(true);// 如果该字段是 static + final 修饰if (field.getModifiers() >= 24) {continue;}try {// 设置字段可见,即可用get方法获取属性值。field.set(objDes, field.get(objSource));} catch (Exception e) {e.printStackTrace();}}return objDes;}
}

创建脱敏工具类

package com.artboy.project.common.utils;import com.artboy.project.common.annotation.Desensitized;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;/*** @author xh* @Date 2022/7/26*/
public class DesensitizedUtils {/*** 获取脱敏json串(递归引用会导致java.lang.StackOverflowError)** @param javaBean* @return*/public static String getJson(Object javaBean) {String json = null;if (null != javaBean) {try {if (javaBean.getClass().isInterface()) return json;/* 克隆出一个实体进行字段修改,避免修改原实体 *///Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);Object clone = DesensitizedObjectUtils.deepClone(javaBean);/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */Set<Integer> referenceCounter = new HashSet<Integer>();/* 对克隆实体进行脱敏操作 */DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);/* 利用fastjson对脱敏后的克隆对象进行序列化 */
//                json = JSON.toJSONString(clone, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty);json = new Gson().toJson(clone);/* 清空计数器 */referenceCounter.clear();referenceCounter = null;} catch (Throwable e) {e.printStackTrace();}}return json;}public static <T> T getObj(T javaBean) {T clone = null;if (null != javaBean) {try {if (javaBean.getClass().isInterface()) return null;/* 克隆出一个实体进行字段修改,避免修改原实体 *///Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);clone = (T) DesensitizedObjectUtils.deepClone(javaBean);/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */Set<Integer> referenceCounter = new HashSet<Integer>();/* 对克隆实体进行脱敏操作 */DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);/* 清空计数器 */referenceCounter.clear();referenceCounter = null;} catch (Throwable e) {e.printStackTrace();}}return clone;}/*** 对需要脱敏的字段进行转化** @param fields* @param javaBean* @param referenceCounter* @throws IllegalArgumentException* @throws IllegalAccessException*/private static void replace(Field[] fields, Object javaBean, Set<Integer> referenceCounter) throws IllegalArgumentException, IllegalAccessException {if (null != fields && fields.length > 0) {for (Field field : fields) {field.setAccessible(true);if (null != field && null != javaBean) {Object value = field.get(javaBean);if (null != value) {Class<?> type = value.getClass();//处理子属性,包括集合中的if (type.isArray()) {//对数组类型的字段进行递归过滤int len = Array.getLength(value);for (int i = 0; i < len; i++) {Object arrayObject = Array.get(value, i);if (isNotGeneralType(arrayObject.getClass(), arrayObject, referenceCounter)) {replace(DesensitizedObjectUtils.getAllFields(arrayObject), arrayObject, referenceCounter);}}} else if (value instanceof Collection<?>) {//对集合类型的字段进行递归过滤Collection<?> c = (Collection<?>) value;Iterator<?> it = c.iterator();while (it.hasNext()) {// TODO: 待优化Object collectionObj = it.next();if (isNotGeneralType(collectionObj.getClass(), collectionObj, referenceCounter)) {replace(DesensitizedObjectUtils.getAllFields(collectionObj), collectionObj, referenceCounter);}}} else if (value instanceof Map<?, ?>) {//对Map类型的字段进行递归过滤Map<?, ?> m = (Map<?, ?>) value;Set<?> set = m.entrySet();for (Object o : set) {Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;Object mapVal = entry.getValue();if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) {replace(DesensitizedObjectUtils.getAllFields(mapVal), mapVal, referenceCounter);}}} else if (value instanceof Enum<?>) {continue;}/*除基础类型、jdk类型的字段之外,对其他类型的字段进行递归过滤*/else {if (!type.isPrimitive()&& type.getPackage() != null&& !StringUtils.startsWith(type.getPackage().getName(), "javax.")&& !StringUtils.startsWith(type.getPackage().getName(), "java.")&& !StringUtils.startsWith(field.getType().getName(), "javax.")&& !StringUtils.startsWith(field.getName(), "java.")&& referenceCounter.add(value.hashCode())) {replace(DesensitizedObjectUtils.getAllFields(value), value, referenceCounter);}}}//脱敏操作setNewValueForField(javaBean, field, value);}}}}/*** 排除基础类型、jdk类型、枚举类型的字段** @param clazz* @param value* @param referenceCounter* @return*/private static boolean isNotGeneralType(Class<?> clazz, Object value, Set<Integer> referenceCounter) {return !clazz.isPrimitive()&& clazz.getPackage() != null&& !clazz.isEnum()&& !StringUtils.startsWith(clazz.getPackage().getName(), "javax.")&& !StringUtils.startsWith(clazz.getPackage().getName(), "java.")&& !StringUtils.startsWith(clazz.getName(), "javax.")&& !StringUtils.startsWith(clazz.getName(), "java.")&& referenceCounter.add(value.hashCode());}/*** 脱敏操作(按照规则转化需要脱敏的字段并设置新值)* 目前只支持String类型的字段,如需要其他类型如BigDecimal、Date等类型,可以添加** @param javaBean* @param field* @param value* @throws IllegalAccessException*/public static void setNewValueForField(Object javaBean, Field field, Object value) throws IllegalAccessException {//处理自身的属性Desensitized annotation = field.getAnnotation(Desensitized.class);if (field.getType().equals(String.class) && null != annotation && executeIsEffictiveMethod(javaBean, annotation)) {String valueStr = (String) value;if (StringUtils.isNotBlank(valueStr)) {switch (annotation.type()) {case CHINESE_NAME: {field.set(javaBean, DesensitizedUtils.chineseName(valueStr));break;}case ID_CARD: {field.set(javaBean, DesensitizedUtils.idCardNum(valueStr));break;}case FIXED_PHONE: {field.set(javaBean, DesensitizedUtils.fixedPhone(valueStr));break;}case MOBILE_PHONE: {field.set(javaBean, DesensitizedUtils.mobilePhone(valueStr));break;}case ADDRESS: {field.set(javaBean, DesensitizedUtils.address(valueStr, 7));break;}case EMAIL: {field.set(javaBean, DesensitizedUtils.email(valueStr));break;}case BANK_CARD: {field.set(javaBean, DesensitizedUtils.bankCard(valueStr));break;}case PASSWORD: {field.set(javaBean, DesensitizedUtils.password(valueStr));break;}case ACCOUNT:{field.set(javaBean, DesensitizedUtils.account(valueStr));break;}}}}}/*** 执行某个对象中指定的方法** @param javaBean     对象* @param desensitized* @return*/private static boolean executeIsEffictiveMethod(Object javaBean, Desensitized desensitized) {boolean isAnnotationEffictive = true;//注解默认生效if (desensitized != null) {String isEffictiveMethod = desensitized.isEffictiveMethod();if (isNotEmpty(isEffictiveMethod)) {try {Method method = javaBean.getClass().getMethod(isEffictiveMethod);method.setAccessible(true);isAnnotationEffictive = (Boolean) method.invoke(javaBean);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}}return isAnnotationEffictive;}private static boolean isNotEmpty(String str) {return str != null && !"".equals(str);}private static boolean isEmpty(String str) {return !isNotEmpty(str);}/*** 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**** @param fullName* @return*/public static String chineseName(String fullName) {if (StringUtils.isBlank(fullName)) {return "";}String name = StringUtils.left(fullName, 1);return StringUtils.rightPad(name, StringUtils.length(fullName), "*");}/*** 【身份证号】显示第一位和最后一位** @param id* @return*/public static String idCardNum(String id) {if (StringUtils.isBlank(id)) {return "";}return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));}/*** 【虚拟账号】显示第一位和最后一位** @param id* @return*/public static String account(String id) {if (StringUtils.isBlank(id)) {return "";}return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));}/*** 【固定电话 后四位,其他隐藏,比如1234** @param num* @return*/public static String fixedPhone(String num) {if (StringUtils.isBlank(num)) {return "";}return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");}/*** 【手机号码】前三位,后四位,其他隐藏,比如135****6810** @param num* @return*/public static String mobilePhone(String num) {if (StringUtils.isBlank(num)) {return "";}return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***"));}/*** 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****** @param address* @param sensitiveSize 敏感信息长度* @return*/public static String address(String address, int sensitiveSize) {if (StringUtils.isBlank(address)) {return "";}int length = StringUtils.length(address);return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");}/*** 【电子邮箱 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com>** @param email* @return*/public static String email(String email) {if (StringUtils.isBlank(email)) {return "";}int index = StringUtils.indexOf(email, "@");if (index <= 1)return email;elsereturn StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));}/*** 【银行卡号】前4位,后3位,其他用星号隐藏每位1个星号,比如:6217 **** **** **** 567>** @param cardNum* @return*/public static String bankCard(String cardNum) {if (StringUtils.isBlank(cardNum)) {return "";}return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 3), StringUtils.length(cardNum), "*"), "****"));}/*** 【密码】密码的全部字符都用*代替,比如:******** @param password* @return*/public static String password(String password) {if (StringUtils.isBlank(password)) {return "";}String pwd = StringUtils.left(password, 0);return StringUtils.rightPad(pwd, StringUtils.length(password), "*");}/*** 遍历List脱敏数据* @param content* @return*/public static <T> List getList(List<T> content){if (content == null || content.size() <= 0) {return content;}List list = new ArrayList<T>();for (T t : content) {list.add(getObj(t));}return list;}
}

创建脱敏工具类

package com.artboy.project.common.utils;import com.artboy.project.common.annotation.Desensitized;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;/*** @author xh* @Date 2022/7/26*/
public class DesensitizedUtils {/*** 获取脱敏json串(递归引用会导致java.lang.StackOverflowError)** @param javaBean* @return*/public static String getJson(Object javaBean) {String json = null;if (null != javaBean) {try {if (javaBean.getClass().isInterface()) return json;/* 克隆出一个实体进行字段修改,避免修改原实体 *///Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);Object clone = DesensitizedObjectUtils.deepClone(javaBean);/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */Set<Integer> referenceCounter = new HashSet<Integer>();/* 对克隆实体进行脱敏操作 */DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);/* 利用fastjson对脱敏后的克隆对象进行序列化 */
//                json = JSON.toJSONString(clone, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty);json = new Gson().toJson(clone);/* 清空计数器 */referenceCounter.clear();referenceCounter = null;} catch (Throwable e) {e.printStackTrace();}}return json;}public static <T> T getObj(T javaBean) {T clone = null;if (null != javaBean) {try {if (javaBean.getClass().isInterface()) return null;/* 克隆出一个实体进行字段修改,避免修改原实体 *///Object clone =DesensitizedObjectUtils.deepCloneObject(javaBean);//Object clone =DesensitizedObjectUtils.deepCloneByFastJson(javaBean);clone = (T) DesensitizedObjectUtils.deepClone(javaBean);/* 定义一个计数器,用于避免重复循环自定义对象类型的字段 */Set<Integer> referenceCounter = new HashSet<Integer>();/* 对克隆实体进行脱敏操作 */DesensitizedUtils.replace(DesensitizedObjectUtils.getAllFields(clone), clone, referenceCounter);/* 清空计数器 */referenceCounter.clear();referenceCounter = null;} catch (Throwable e) {e.printStackTrace();}}return clone;}/*** 对需要脱敏的字段进行转化** @param fields* @param javaBean* @param referenceCounter* @throws IllegalArgumentException* @throws IllegalAccessException*/private static void replace(Field[] fields, Object javaBean, Set<Integer> referenceCounter) throws IllegalArgumentException, IllegalAccessException {if (null != fields && fields.length > 0) {for (Field field : fields) {field.setAccessible(true);if (null != field && null != javaBean) {Object value = field.get(javaBean);if (null != value) {Class<?> type = value.getClass();//处理子属性,包括集合中的if (type.isArray()) {//对数组类型的字段进行递归过滤int len = Array.getLength(value);for (int i = 0; i < len; i++) {Object arrayObject = Array.get(value, i);if (isNotGeneralType(arrayObject.getClass(), arrayObject, referenceCounter)) {replace(DesensitizedObjectUtils.getAllFields(arrayObject), arrayObject, referenceCounter);}}} else if (value instanceof Collection<?>) {//对集合类型的字段进行递归过滤Collection<?> c = (Collection<?>) value;Iterator<?> it = c.iterator();while (it.hasNext()) {// TODO: 待优化Object collectionObj = it.next();if (isNotGeneralType(collectionObj.getClass(), collectionObj, referenceCounter)) {replace(DesensitizedObjectUtils.getAllFields(collectionObj), collectionObj, referenceCounter);}}} else if (value instanceof Map<?, ?>) {//对Map类型的字段进行递归过滤Map<?, ?> m = (Map<?, ?>) value;Set<?> set = m.entrySet();for (Object o : set) {Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;Object mapVal = entry.getValue();if (isNotGeneralType(mapVal.getClass(), mapVal, referenceCounter)) {replace(DesensitizedObjectUtils.getAllFields(mapVal), mapVal, referenceCounter);}}} else if (value instanceof Enum<?>) {continue;}/*除基础类型、jdk类型的字段之外,对其他类型的字段进行递归过滤*/else {if (!type.isPrimitive()&& type.getPackage() != null&& !StringUtils.startsWith(type.getPackage().getName(), "javax.")&& !StringUtils.startsWith(type.getPackage().getName(), "java.")&& !StringUtils.startsWith(field.getType().getName(), "javax.")&& !StringUtils.startsWith(field.getName(), "java.")&& referenceCounter.add(value.hashCode())) {replace(DesensitizedObjectUtils.getAllFields(value), value, referenceCounter);}}}//脱敏操作setNewValueForField(javaBean, field, value);}}}}/*** 排除基础类型、jdk类型、枚举类型的字段** @param clazz* @param value* @param referenceCounter* @return*/private static boolean isNotGeneralType(Class<?> clazz, Object value, Set<Integer> referenceCounter) {return !clazz.isPrimitive()&& clazz.getPackage() != null&& !clazz.isEnum()&& !StringUtils.startsWith(clazz.getPackage().getName(), "javax.")&& !StringUtils.startsWith(clazz.getPackage().getName(), "java.")&& !StringUtils.startsWith(clazz.getName(), "javax.")&& !StringUtils.startsWith(clazz.getName(), "java.")&& referenceCounter.add(value.hashCode());}/*** 脱敏操作(按照规则转化需要脱敏的字段并设置新值)* 目前只支持String类型的字段,如需要其他类型如BigDecimal、Date等类型,可以添加** @param javaBean* @param field* @param value* @throws IllegalAccessException*/public static void setNewValueForField(Object javaBean, Field field, Object value) throws IllegalAccessException {//处理自身的属性Desensitized annotation = field.getAnnotation(Desensitized.class);if (field.getType().equals(String.class) && null != annotation && executeIsEffictiveMethod(javaBean, annotation)) {String valueStr = (String) value;if (StringUtils.isNotBlank(valueStr)) {switch (annotation.type()) {case CHINESE_NAME: {field.set(javaBean, DesensitizedUtils.chineseName(valueStr));break;}case ID_CARD: {field.set(javaBean, DesensitizedUtils.idCardNum(valueStr));break;}case FIXED_PHONE: {field.set(javaBean, DesensitizedUtils.fixedPhone(valueStr));break;}case MOBILE_PHONE: {field.set(javaBean, DesensitizedUtils.mobilePhone(valueStr));break;}case ADDRESS: {field.set(javaBean, DesensitizedUtils.address(valueStr, 7));break;}case EMAIL: {field.set(javaBean, DesensitizedUtils.email(valueStr));break;}case BANK_CARD: {field.set(javaBean, DesensitizedUtils.bankCard(valueStr));break;}case PASSWORD: {field.set(javaBean, DesensitizedUtils.password(valueStr));break;}case ACCOUNT:{field.set(javaBean, DesensitizedUtils.account(valueStr));break;}}}}}/*** 执行某个对象中指定的方法** @param javaBean     对象* @param desensitized* @return*/private static boolean executeIsEffictiveMethod(Object javaBean, Desensitized desensitized) {boolean isAnnotationEffictive = true;//注解默认生效if (desensitized != null) {String isEffictiveMethod = desensitized.isEffictiveMethod();if (isNotEmpty(isEffictiveMethod)) {try {Method method = javaBean.getClass().getMethod(isEffictiveMethod);method.setAccessible(true);isAnnotationEffictive = (Boolean) method.invoke(javaBean);} catch (NoSuchMethodException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}}return isAnnotationEffictive;}private static boolean isNotEmpty(String str) {return str != null && !"".equals(str);}private static boolean isEmpty(String str) {return !isNotEmpty(str);}/*** 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**** @param fullName* @return*/public static String chineseName(String fullName) {if (StringUtils.isBlank(fullName)) {return "";}String name = StringUtils.left(fullName, 1);return StringUtils.rightPad(name, StringUtils.length(fullName), "*");}/*** 【身份证号】显示第一位和最后一位** @param id* @return*/public static String idCardNum(String id) {if (StringUtils.isBlank(id)) {return "";}return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));}/*** 【虚拟账号】显示第一位和最后一位** @param id* @return*/public static String account(String id) {if (StringUtils.isBlank(id)) {return "";}return StringUtils.left(id,1).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(id,1), StringUtils.length(id),"*"),"*"));}/*** 【固定电话 后四位,其他隐藏,比如1234** @param num* @return*/public static String fixedPhone(String num) {if (StringUtils.isBlank(num)) {return "";}return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");}/*** 【手机号码】前三位,后四位,其他隐藏,比如135****6810** @param num* @return*/public static String mobilePhone(String num) {if (StringUtils.isBlank(num)) {return "";}return StringUtils.left(num, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*"), "***"));}/*** 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****** @param address* @param sensitiveSize 敏感信息长度* @return*/public static String address(String address, int sensitiveSize) {if (StringUtils.isBlank(address)) {return "";}int length = StringUtils.length(address);return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");}/*** 【电子邮箱 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com>** @param email* @return*/public static String email(String email) {if (StringUtils.isBlank(email)) {return "";}int index = StringUtils.indexOf(email, "@");if (index <= 1)return email;elsereturn StringUtils.rightPad(StringUtils.left(email, 1), index, "*").concat(StringUtils.mid(email, index, StringUtils.length(email)));}/*** 【银行卡号】前4位,后3位,其他用星号隐藏每位1个星号,比如:6217 **** **** **** 567>** @param cardNum* @return*/public static String bankCard(String cardNum) {if (StringUtils.isBlank(cardNum)) {return "";}return StringUtils.left(cardNum, 4).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(cardNum, 3), StringUtils.length(cardNum), "*"), "****"));}/*** 【密码】密码的全部字符都用*代替,比如:******** @param password* @return*/public static String password(String password) {if (StringUtils.isBlank(password)) {return "";}String pwd = StringUtils.left(password, 0);return StringUtils.rightPad(pwd, StringUtils.length(password), "*");}/*** 遍历List脱敏数据* @param content* @return*/public static <T> List getList(List<T> content){if (content == null || content.size() <= 0) {return content;}List list = new ArrayList<T>();for (T t : content) {list.add(getObj(t));}return list;}
}

测试

这里我们以UserEntity为例:(一定要序列化)

package com.artboy.project.model.entity;import com.artboy.project.common.annotation.Desensitized;
import com.artboy.project.common.enums.SensitiveTypeEnum;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;/*** 用户** @TableName user*/
@TableName(value = "user")
@Data
public class UserEntity implements Serializable {/*** id*/@TableId(type = IdType.AUTO)private Integer id;/*** 用户昵称*/private String username;/*** 账号*/private String userAccount;/*** 用户头像*/private String avatarUrl;/*** 性别*/private Integer gender;/*** 密码*/@Desensitized(type = SensitiveTypeEnum.PASSWORD)private String userPassword;/*** 电话*/@Desensitized(type = SensitiveTypeEnum.MOBILE_PHONE)private String phone;/*** 邮箱*/@Desensitized(type = SensitiveTypeEnum.EMAIL)private String email;/*** 状态 0 - 正常*/private Integer userStatus;/*** 创建时间,数据库已设置自动更新*/private Date createTime;/*** 更新时间,数据库已设置自动更新*/private Date updateTime;/*** 是否删除*/@TableLogicprivate Integer isDelete;/*** 用户角色 0 - 普通用户 1 - 管理员*/private Integer userRole;@TableField(exist = false)private static final long serialVersionUID = 1L;
}

测试方法:

QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
UserEntity user = userMapper.selectOne(queryWrapper);
UserEntity safetyUser = DesensitizedUtils.getObj(user);

Springboot切面+注解实现数据脱敏

处理步骤

新建脱敏的枚举类

package com.fwy.common.config.dataRule;import lombok.Getter;/*** @description:隐私数据类型枚举* @author: xiaYZ* @createDate: 2022/6/21*/@Getter
public enum PrivacyTypeEnum {/** 自定义(此项需设置脱敏的范围)*/CUSTOMER,/** 姓名 */NAME,/** 身份证号 */ID_CARD,/** 手机号 */PHONE,/** 邮箱 */EMAIL,
}

新建脱敏操作的工具类

package com.fwy.common.config.dataRule;/*** @description:* @author: xiaYZ* @createDate: 2022/6/21*/
public class PrivacyUtil {/*** 隐藏手机号中间四位*/public static String hidePhone(String phone) {return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}/*** 隐藏邮箱*/public static String hideEmail(String email) {return email.replaceAll("(\\w?)(\\w+)(\\w)(@\\w+\\.[a-z]+(\\.[a-z]+)?)", "$1****$3$4");}/*** 隐藏身份证*/public static String hideIDCard(String idCard) {return idCard.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1*****$2");}/*** 【中文姓名】只显示第一个汉字,其他隐藏为星号,比如:任***/public static String hideChineseName(String chineseName) {if (chineseName == null) {return null;}return desValue(chineseName, 1, 0, "*");}//    /**
//     * 【身份证号】显示前4位, 后2位
//     */
//    public static String hideIdCard(String idCard) {
//        return desValue(idCard, 4, 2, "*");
//    }//    /**
//     * 【手机号码】前三位,后四位,其他隐藏。
//     */
//    public static String hidePhone(String phone) {
//        return desValue(phone, 3, 4, "*");
//    }/*** 对字符串进行脱敏操作* @param origin          原始字符串* @param prefixNoMaskLen 左侧需要保留几位明文字段* @param suffixNoMaskLen 右侧需要保留几位明文字段* @param maskStr         用于遮罩的字符串, 如'*'* @return 脱敏后结果*/public static String desValue(String origin, int prefixNoMaskLen, int suffixNoMaskLen, String maskStr) {if (origin == null) {return null;}StringBuilder sb = new StringBuilder();for (int i = 0, n = origin.length(); i < n; i++) {if (i < prefixNoMaskLen) {sb.append(origin.charAt(i));continue;}if (i > (n - suffixNoMaskLen - 1)) {sb.append(origin.charAt(i));continue;}sb.append(maskStr);}return sb.toString();}}

申明注解类

package com.fwy.common.config.dataRule;import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @description: 自定义数据脱敏注解* @author: xiaYZ* @createDate: 2022/6/21*/@Target(ElementType.FIELD) // 作用在字段上
@Retention(RetentionPolicy.RUNTIME) // class文件中保留,运行时也保留,能通过反射读取到
@JacksonAnnotationsInside // 表示自定义自己的注解PrivacyEncrypt
@JsonSerialize(using = PrivacySerializer.class) // 该注解使用序列化的方式
public @interface PrivacyEncrypt {/*** 脱敏数据类型(没给默认值,所以使用时必须指定type)*/PrivacyTypeEnum type();/*** 前置不需要打码的长度*/int prefixNoMaskLen() default 1;/*** 后置不需要打码的长度*/int suffixNoMaskLen() default 1;/*** 用什么打码*/String symbol() default "*";
}

注解的AOP操作

package com.fwy.common.config.dataRule;import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;import java.io.IOException;
import java.util.Objects;/*** @description:* @author: xiaYZ* @createDate: 2022/6/21*/@NoArgsConstructor
@AllArgsConstructor
public class PrivacySerializer extends JsonSerializer<String> implements ContextualSerializer {// 脱敏类型private PrivacyTypeEnum privacyTypeEnum;// 前几位不脱敏private Integer prefixNoMaskLen;// 最后几位不脱敏private Integer suffixNoMaskLen;// 用什么打码private String symbol;@Overridepublic void serialize(final String origin, final JsonGenerator jsonGenerator,final SerializerProvider serializerProvider) throws IOException {if (StringUtils.isNotBlank(origin) && null != privacyTypeEnum) {switch (privacyTypeEnum) {case CUSTOMER:jsonGenerator.writeString(PrivacyUtil.desValue(origin, prefixNoMaskLen, suffixNoMaskLen, symbol));break;case NAME:jsonGenerator.writeString(PrivacyUtil.hideChineseName(origin));break;case ID_CARD:jsonGenerator.writeString(PrivacyUtil.hideIDCard(origin));break;case PHONE:jsonGenerator.writeString(PrivacyUtil.hidePhone(origin));break;case EMAIL:jsonGenerator.writeString(PrivacyUtil.hideEmail(origin));break;default:throw new IllegalArgumentException("unknown privacy type enum " + privacyTypeEnum);}}}@Overridepublic JsonSerializer<?> createContextual(final SerializerProvider serializerProvider,final BeanProperty beanProperty) throws JsonMappingException {if (beanProperty != null) {if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {PrivacyEncrypt privacyEncrypt = beanProperty.getAnnotation(PrivacyEncrypt.class);if (privacyEncrypt == null) {privacyEncrypt = beanProperty.getContextAnnotation(PrivacyEncrypt.class);}if (privacyEncrypt != null) {return new PrivacySerializer(privacyEncrypt.type(), privacyEncrypt.prefixNoMaskLen(),privacyEncrypt.suffixNoMaskLen(), privacyEncrypt.symbol());}}return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);}return serializerProvider.findNullValueSerializer(null);}
}

实际使用

基于jackson,通过自定义注解的方式实现数据脱敏

添加依赖

spring-web、spring-boot-starter-web已经集成了jackson相关包,不用添加

<!--jackson依赖-->
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId>
</dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId>
</dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId>
</dependency>

脱敏注解
DesensitizationJsonSerializer.class: 脱敏序列化类

@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationJsonSerializer.class)
public @interface Desensitization {Class<? extends AbstractDesensitization> value();}

脱敏序列化

/*** 脱敏序列化*/
public class DesensitizationJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {private AbstractDesensitization desensitization;public DesensitizationJsonSerializer() {}public DesensitizationJsonSerializer(AbstractDesensitization desensitization) {this.desensitization = desensitization;}@Overridepublic void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {jsonGenerator.writeString(desensitization.serialize(s));;}@Overridepublic JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {JsonSerializer<?> jsonSerializer = null;if(null == beanProperty) jsonSerializer = serializerProvider.findNullValueSerializer(beanProperty);if(!Objects.equals(beanProperty.getType().getRawClass(), String.class))jsonSerializer = serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);if(Objects.equals(beanProperty.getType().getRawClass(), String.class)){jsonSerializer = setDesensitization(jsonSerializer, beanProperty);}return jsonSerializer;}/*** 设置脱敏* @param beanProperty* @return*/private JsonSerializer<?> setDesensitization(JsonSerializer<?> jsonSerializer, BeanProperty beanProperty) {Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);if (desensitization == null) desensitization = beanProperty.getContextAnnotation(Desensitization.class);if (desensitization != null) {//设置脱敏实例try {jsonSerializer = new DesensitizationJsonSerializer(desensitization.value().newInstance());} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}return jsonSerializer;}
}

脱敏类
脱敏父类
子类通过继承AbstractDesensitization实现扩展

public abstract class AbstractDesensitization {/*** 脱敏* @param value* @return*/public abstract String serialize(String value);}

中文姓名脱敏

public class ChineseNameDesensitization extends AbstractDesensitization {@Overridepublic String serialize(String value) {String serializeValue = "";if(value.length() < 3){serializeValue = value.replaceAll(".*(?=[\\u4e00-\\u9fa5])","*");}else{serializeValue = value.replaceAll("(?<=[\\u4e00-\\u9fa5]).*(?=[\\u4e00-\\u9fa5])","*");}return serializeValue;}}

手机号脱敏

public class MobilePhoneDesensitization extends AbstractDesensitization {@Overridepublic String serialize(String value) {return value.replaceAll("(\\d{3})\\d{4}(\\d{4})","$1****$2");}}

身份证脱敏

public class IdCardDesensitization extends AbstractDesensitization {@Overridepublic String serialize(String value) {return value.replaceAll("(?<=\\w{3})\\w(?=\\w{4})","*");}}

测试
添加脱敏注解

public class User {@Desensitization(ChineseNameDesensitization.class)private String name;private Integer age;@Desensitization(IdCardDesensitization.class)private String idCard;@Desensitization(MobilePhoneDesensitization.class)private String mobilePhone;//...get and set
}

新建UserController,查询用户信息

@RestController
public class UserController {@GetMapping("/users")private List<User> users() throws Exception {List<User> girls = new ArrayList<>();User user = new User();user.setName("西施");user.setAge(18);user.setIdCard("123456789123456202");user.setMobilePhone("12345678901");User user2 = new User();user2.setName("杨贵妃");user2.setAge(18);user2.setIdCard("123456789123456202");user2.setMobilePhone("12345678901");User user3 = new User();user3.setName("古代四大美女之一 * 貂蝉");user3.setAge(18);user3.setIdCard("123456789123456202");user3.setMobilePhone("12345678901");User user4 = new User();user4.setName("古代四大美女之一 * 王昭君");user4.setAge(18);user4.setIdCard("123456789123456202");user4.setMobilePhone("12345678901");User user5 = new User();user5.setName(null);user5.setAge(18);user5.setIdCard(null);user5.setMobilePhone(null);girls.add(user);girls.add(user2);girls.add(user3);girls.add(user4);girls.add(user5);return girls;}
}

Java实现数据脱敏相关推荐

  1. Java 实现数据脱敏的技术方案

    数据脱敏是保护个人隐私的一种重要手段,它通过对敏感信息进行处理,将敏感信息转换为不敏感的信息,以保护个人隐私不被泄漏.在Java中,数据脱敏也是一项非常重要的技术,本文将从数据脱敏的概念.Java中的 ...

  2. java web 数据脱敏

    java web 数据脱敏 参考上文: java日志脱敏实现 1. 题记 在交易管理系统中,由于数据库存储客户人脸图片和客户名称.客户证件号.手机号.银行卡号等相关敏感字段,为了防止数据泄露现根据用户 ...

  3. java 数据库数据脱敏_Sharding-JDBC-数据脱敏

    数据脱敏 该章节主要介绍如何使用数据脱敏功能,如何进行相关配置.数据脱敏功能即可与数据分片功能共同使用,又可作为单独功能组件,独立使用. 与数据分片功能共同使用时,会创建ShardingDataSou ...

  4. Java自定义数据脱敏注解

    应用场景 数据库中密文存储身份证.手机号等敏感信息时,Java需要将密文数据转换为明文并脱敏返回给前端. 脱敏方式枚举类 public enum DesensitizationTypeEnum {/* ...

  5. 一文玩转 Java 日志数据脱敏,就是如此简单!

    自定义Layout 编写log4j配置 正则匹配说明 注意事项 脱敏测试 许多系统为了安全需要对敏感信息(如手机号.邮箱.姓名.身份证号.密码.卡号.住址等)的日志打印要求脱敏后才能输出,本文将结合个 ...

  6. 一文玩转 Java 日志数据脱敏

  7. 【Java-数据脱敏】使用Java对姓名、手机号码、证件号码进行数据脱敏

    [Java-数据脱敏]使用Java对姓名.手机号码.证件号码进行数据脱敏 1)姓名脱敏 2)手机号码脱敏 3)证件号码脱敏(md5) 1)姓名脱敏 //定义脱敏后的姓名 String resultNa ...

  8. Java Springboot切面+注解实现数据脱敏

    Java Springboot切面+注解实现数据脱敏 1. 业务概述 2. 设计编码 2.1 脱敏类型枚举 2.2 脱敏注解 2.3 脱敏工具类 2.4 统一拦截器 2.5 统一结果集 2.6 用户实 ...

  9. 数据脱敏——基于Java自定义注解实现日志字段脱敏

    上文说了数据过敏主要有两个思路:第一个就是在序列化实体之前先把需要脱敏的字段进行处理,之后正常序列化:第二个就是在实体序列化的时候,对要脱敏的字段进行处理. 脱敏实现思路 这里探讨第一种方法,用基于自 ...

最新文章

  1. 逻辑模型设计步骤-分析主题域
  2. Android-MeasureSpec那些事
  3. 关于MySql中自增长id设置初始值
  4. 主板usb模式启动快捷键大全
  5. 为什么Kubernetes要引入pod的概念,而不直接操作Docker容器
  6. ActiveMQ 发送和接收消息
  7. UNICODE与ANSI的区别
  8. MYSQL启动后报:ERROR! The server quit without updating PID file错误的问题解决
  9. 经典商业模式案例第1例:校园O2O
  10. Arduino:光立方的开发与实现
  11. FPGA(七) PWM波
  12. javaweb户籍管理系统
  13. 运筹优化算法工程师面试问题汇总
  14. 计算机 高级软件工程师考试试题,高级软件工程师8月考试试题A卷带答案.doc
  15. 计算机 桌面 休眠,电脑自动休眠在哪设置的_教你让电脑自动休眠状态的方法-系统城...
  16. 数据中心为什么需要大二层网络
  17. 弗曼学习方法-终极快速学习方法
  18. 游戏后台生成唯一ID
  19. Win10 或者Win7 64位系统安装32位的CAD2010教程
  20. 安卓的WebView的使用

热门文章

  1. 3D图形的概念和渲染管线(Render Pipeline)
  2. 蛇呆在树上,整天无所事事_指南:如何在无所事事的盒子上安装OroCRM
  3. 2017年1月/4月新番表中文版
  4. 2020年数字货币市场分析报告
  5. 模具设计难学吗?掌握ug和cad软件还不够,还有更重要的一点!
  6. photoshop cs6安装包
  7. java.text.ParseException: Unparseable date: 2015-06-09 hh:56:19
  8. ann神经网络模型评价指标,神经网络模型参数辨识
  9. mac查找jdk安装位置
  10. QMS-云质-质量管理软件-供应链质量管理