version: '3.2' services: redis5: image: redis:5 container_name: redis5 restart: always environment: - ALLOW_EMPTY_PASSWORD=yes command: redis-server --appendonly yes --requirepass 123456 volumes: - redis:/data #数据文件挂载 ports: - 6379:6379 nacos-registry: image: nacos/nacos-server:v2.2.3 container_name: nacos environment: - PREFER_HOST_MODE=hostname - MODE=standalone - SPRING_DATASOURCE_PLATFORM=mysql - MYSQL_SERVICE_HOST=mysql - MYSQL_SERVICE_DB_NAME=nacos - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root - MYSQL_SERVICE_PASSWORD=123456 - MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true # volumes: # - /docker/service/env/nacos:/home/nacos/init.d ports: - "8848:8848" - "9555:9555" - "9848:9848" - "9849:9849" restart: on-failure external_links: - mysql:db #可以用db这个域名访问mysql服务 depends_on: - mysql mysql: image: mysql container_name: mysql command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --lower_case_table_names=1 --log_bin_trust_function_creators=1 --max_allowed_packet=100G --tmp_table_size=256M --key_buffer_size=512M --innodb_log_file_size=1024M restart: always environment: MYSQL_ROOT_PASSWORD: 123456 #设置root帐号密码 ports: - 3306:3306 volumes: - mysql8:/var/lib/mysql #数据文件挂载 volumes: redis: external: true mysql8: external: true
Openvpn搭建及使用方法前置操作echo 1 > /proc/sys/net/ipv4/ip_forward sysctl -p /etc/sysctl.conf搭建步骤拉取镜像docker pull kylemanna/openvpn创建全局变量OVPN_DATA="/data/openvpn" mkdir $OVPN_DATA配置公网ipIP="xxx.xxx.xxx.xxx"初始化将保存配置文件和证书的容器。容器将提示输入密码以保护新生成的证书颁发机构使用的私钥tcp：docker run -v ${OVPN_DATA}:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u tcp://${IP}udp：docker run -v ${OVPN_DATA}:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://${IP}密钥配置docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki例子Enter PEM pass phrase: 输入123456（你是看不见的） Verifying - Enter PEM pass phrase: 输入123456（你是看不见的） Common Name (eg: your user, host, or server name) [Easy-RSA CA]:回车一下 Enter pass phrase for /etc/openvpn/pki/private/ca.key:输入123456启动打开VPN服务器进程tcp：docker run --name openvpn -v ${OVPN_DATA}:/etc/openvpn -d -p 1194:1194 --cap-add=NET_ADMIN --name openvpn kylemanna/openvpnudp：docker run --name openvpn -v ${OVPN_DATA}:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN --name openvpn kylemanna/openvpn生成不带密码的客户端证书docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass检索带有嵌入式证书的客户端配置docker run -v ${OVPN_DATA}:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient example > example.ovpn客户端下载Community Downloads | OpenVPNOpenVPN-2.5.7-I602-amd64.msi删除docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn easyrsa revoke $DNAME docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn easyrsa gen-crl docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn rm -f /etc/openvpn/pki/reqs/"$DNAME".req docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn rm -f /etc/openvpn/pki/private/"$DNAME".key docker run -v ${OVPN_DATA}:/etc/openvpn --rm -it kylemanna/openvpn rm -f /etc/openvpn/pki/issued/"$DNAME".crt
Swagger 枚举字段显示在接口文档中，并自定义枚举返回
Swagger 枚举字段显示在接口文档中，并自定义枚举返回最近使用枚举做为接收参数，接收枚举中的某一个属性，返回对应的枚举，但是发现这种方法不能在swagger文档中体现，故此拓展springfox插件来实现。⚠️ 本次修改兼容 v2 v3先来看下效果：请求参数显示枚举信息响应显示枚举信息接下来实操请求参数和响应显示枚举信息创建枚举对应显示信息注解其中 value 为入参数值，desc 为入参的描述@Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface EnableSwaggerEnum { String value() default "value"; String desc() default "desc"; } @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface EnableSwaggerResponseEnum { String value() default "value"; String desc() default "desc"; }创建一个公共修改model由于要兼容v2 和 v3,故此需要创建一个修改的公共model，用来存放修改的枚举信息。其中，displayValues是用来显示各个枚举值和信息的字符串，allowableListValues 为实际要显示的入场值。@Data public class EnumMidModel { private String displayValues; private AllowableListValues allowableListValues; }封装modelpublic class SwaggerEnumUtils { /** * 封装枚举消息 * * @param type 此解析类型具有的类型擦除 Class<?> * @param enableSwaggerEnum 自定义swagger枚举限制注释 {@link EnableSwaggerEnum} * @return {@link EnumMidModel} * @author Mc * @since 2023/3/1 20:24 */ protected static EnumMidModel getEnumMidModel(Class<?> type, EnableSwaggerEnum enableSwaggerEnum) { EnumMidModel enumMidModel = new EnumMidModel(); String value = enableSwaggerEnum.value(); String desc = enableSwaggerEnum.desc(); Object[] enumConstants = type.getEnumConstants(); // 拼装显示内容 final String displayValues = Arrays.stream(enumConstants) .filter(Objects::nonNull) .map( item -> { Object valueVal = ReflectUtil.getFieldValue(item, value); Object descVal = ReflectUtil.getFieldValue(item, desc); return valueVal.toString() + " : " + descVal.toString(); } ) .collect(Collectors.joining("】\n【", "\n【", "】")); List<String> allowableValue = Arrays.stream(enumConstants) .filter(Objects::nonNull) .map(item -> ReflectUtil.getFieldValue(item, value).toString()) .collect(Collectors.toList()); AllowableListValues allowableListValues = new AllowableListValues(allowableValue, "string"); enumMidModel.setDisplayValues(displayValues); enumMidModel.setAllowableListValues(allowableListValues); return enumMidModel; } }使用插件拓展直接上代码，内容为分别继承ModelPropertyBuilderPlugin、ParameterBuilderPlugin、ModelPropertyBuilderPlugin插件，改写3个插件的apply()方法，supports()方法代表兼容哪种，可以写死为true。静态代码块里的内容为响应添加制定义枚举。@Component @Slf4j @Primary public class EnumPropertyModelOperationBuilder implements ModelPropertyBuilderPlugin, ParameterBuilderPlugin, OperationBuilderPlugin { static Map<String, String> responseMap = new HashMap<>(); static { Class<EnableSwaggerResponseEnum> enableSwaggerResponseEnumClass = EnableSwaggerResponseEnum.class; Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation("com.xinwei", enableSwaggerResponseEnumClass); if (CollUtil.isNotEmpty(classes)) { for (Class<?> aClass : classes) { if (aClass.isEnum()) { Object[] enumConstants = aClass.getEnumConstants(); Class<?>[] interfaces = aClass.getInterfaces(); boolean flag = false; for (Class<?> anInterface : interfaces) { if (anInterface.equals(IErrorCode.class)) { flag = true; break; } } if (!flag) { break; } EnableSwaggerResponseEnum annotation = AnnotationUtil.getAnnotation(aClass, EnableSwaggerResponseEnum.class); String desc = annotation.desc(); String value = annotation.value(); Arrays.stream(enumConstants).filter(Objects::nonNull).forEach(item -> { Object valueVal = ReflectUtil.getFieldValue(item, value); Object descVal = ReflectUtil.getFieldValue(item, desc); responseMap.put(valueVal.toString(), descVal.toString()); }); } } } } /** * {@link ApiModelProperty} 枚举处理 * 可选值必须为 {@link AllowableListValues},否则会带上旧的枚举name * {@link EnumerationElementFacetBuilder#allowedValues(Collection)} 不会清理旧的选项 * {@link EnumerationElementFacetBuilder#allowedValues(AllowableValues)} 会清理旧的选项 * * @param context {@link ParameterContext} * @author Mc * @since 2023/3/1 14:16 */ @Override @Order(Ordered.HIGHEST_PRECEDENCE + 999) public void apply(ModelPropertyContext context) { Optional<ApiModelProperty> annotation = Optional.empty(); if (context.getAnnotatedElement().isPresent()) { annotation = ApiModelProperties.findApiModePropertyAnnotation(context.getAnnotatedElement().get()); } if (context.getBeanPropertyDefinition().isPresent()) { annotation = Annotations.findPropertyAnnotation( context.getBeanPropertyDefinition().get(), ApiModelProperty.class); } final Class<?> rawPrimaryType = context.getBeanPropertyDefinition().get().getRawPrimaryType(); //过滤得到目标类型 if (annotation.isPresent() && Enum.class.isAssignableFrom(rawPrimaryType)) { EnableSwaggerEnum enableSwaggerEnum = AnnotationUtils.findAnnotation(rawPrimaryType, EnableSwaggerEnum.class); if (null != enableSwaggerEnum) { EnumMidModel enumMidModel = SwaggerEnumUtils.getEnumMidModel(rawPrimaryType, enableSwaggerEnum); context.getSpecificationBuilder() .description(annotation.get().value() + enumMidModel.getDisplayValues()) .enumerationFacet(e -> e.allowedValues(enumMidModel.getAllowableListValues())); } } } @Override public boolean supports(@NonNull DocumentationType documentationType) { return true; } /** * {@link Parameter} 枚举处理 * 可选值必须为 {@link AllowableListValues},否则会带上旧的枚举name * {@link EnumerationElementFacetBuilder#allowedValues(Collection)} 不会清理旧的选项 * {@link EnumerationElementFacetBuilder#allowedValues(AllowableValues)} 会清理旧的选项 * * @param context {@link ParameterContext} * @author Mc * @since 2023/3/1 14:16 */ @Override @Order(Ordered.HIGHEST_PRECEDENCE + 2999) public void apply(ParameterContext context) { Optional<Parameter> annotation = context.resolvedMethodParameter().findAnnotation(Parameter.class); if (annotation.isPresent()) { Class<?> type = context.resolvedMethodParameter().getParameterType().getErasedType(); if (Enum.class.isAssignableFrom(type)) { EnableSwaggerEnum enableSwaggerEnum = AnnotationUtils.findAnnotation(type, EnableSwaggerEnum.class); if (enableSwaggerEnum != null) { EnumMidModel enumMidModel = SwaggerEnumUtils.getEnumMidModel(type, enableSwaggerEnum); context.requestParameterBuilder() .description(annotation.get().description() + enumMidModel.getDisplayValues()) .query(a -> a.enumerationFacet(e -> e.allowedValues(enumMidModel.getAllowableListValues()))); } } } } /** * {@link io.swagger.v3.oas.annotations.Operation} 枚举处理 * 可选值必须为 {@link AllowableListValues},否则会带上旧的枚举name * * @param context {@link ParameterContext} * @author Mc * @since 2023/3/1 14:16 */ @Override @Order(Ordered.HIGHEST_PRECEDENCE + 999) public void apply(OperationContext context) { OperationBuilder operationBuilder = context.operationBuilder(); Operation build = operationBuilder.build(); // 添加响应 addResponseInfo(operationBuilder, build); List<ResolvedMethodParameter> parameters = context.getParameters(); parameters.forEach(parameter -> { ResolvedType parameterType = parameter.getParameterType(); Class<?> type = parameterType.getErasedType(); if (Enum.class.isAssignableFrom(type)) { EnableSwaggerEnum enableSwaggerEnum = AnnotationUtils.findAnnotation(type, EnableSwaggerEnum.class); if (enableSwaggerEnum != null) { EnumMidModel enumMidModel = SwaggerEnumUtils.getEnumMidModel(type, enableSwaggerEnum); Set<RequestParameter> requestParameters = build.getRequestParameters(); Set<RequestParameter> newParameters = new HashSet<>(); for (RequestParameter requestParameter : requestParameters) { if (requestParameter.getName().equals(parameter.defaultName().orElse(""))) { ParameterSpecification specification = requestParameter.getParameterSpecification(); Optional<SimpleParameterSpecification> query = specification.getQuery(); if (query.isPresent()) { SimpleParameterSpecification simpleParameterSpecification = query.get(); List<ElementFacet> newFacets = new ArrayList<>(); EnumerationFacet enumerationFacet = new EnumerationFacet(enumMidModel.getAllowableListValues().getValues()); newFacets.add(enumerationFacet); ReflectUtil.setFieldValue(simpleParameterSpecification, "facets", newFacets); } requestParameter = new RequestParameter(requestParameter.getName(), requestParameter.getIn(), enumMidModel.getDisplayValues() , requestParameter.getRequired(), requestParameter.getDeprecated(), requestParameter.getHidden(), specification, requestParameter.getScalarExample(), requestParameter.getExamples(), requestParameter.getPrecedence(), requestParameter.getExtensions(), requestParameter.getParameterIndex() ); } newParameters.add(requestParameter); } operationBuilder.requestParameters(newParameters); } } }); } private static void addResponseInfo(OperationBuilder operationBuilder, Operation build) { if (MapUtil.isNotEmpty(responseMap)) { List<Response> newResponseList = new ArrayList<>(); SortedSet<Response> responses = build.getResponses(); if (CollUtil.isNotEmpty(responses)) { newResponseList.addAll(responses); } for (Map.Entry<String, String> responseInfo : responseMap.entrySet()) { Example someDescription = new ExampleBuilder() .description(responseInfo.getValue()) .mediaType(MediaType.APPLICATION_JSON_VALUE) .id(responseInfo.getKey()) .value(R.failed(Long.valueOf(responseInfo.getKey()), responseInfo.getValue(), null)).build(); Response response = new ResponseBuilder() .code(responseInfo.getKey()) .description(responseInfo.getKey() + "_" + responseInfo.getValue()) .examples(Collections.singleton(someDescription)) .build(); newResponseList.add(response); } operationBuilder.responses(newResponseList); } } }需要添加枚举显示的枚举类，添加注解其中，value="code" 代表对应枚举属性code，入场使用code 接收参数，desc="name" 代表对应描述为name，文档会显示对应code的name描述需要添加响应显示的枚举类，添加注解@EnableSwaggerResponseEnum搞定看效果
QQ炫舞手游传统模式自制制作本篇文章通过讲解qq炫舞手游传统模式的一些参数，使得传统自制歌曲脱离官方编辑器成为可能在线试用： 点击在线试用 本人通过本篇文章制作出来的传统自制歌曲编辑器预览：主页转码歌曲编辑模式选择+可预览按键进度完成制作，自动生成二维码，总计用时，30sCT自制公式注：只针对炫舞手游官方编辑器首先了解一些参数ET：代表歌曲进入时间 EnterTime（Adjust）以下参数以300bpm 举例：节拍：300bpm固定一个节拍为 3200ms ，设为X关卡小节：一个节拍为4个关卡小节，每个关卡小节 800ms ，设为Y ：设为Z关卡小节数全歌时长为公式为：​ET + 关卡时间 - 提前显示小节数 = ET + (关卡小节数 * 节拍时间) - 提前显示小节数 = ET + ( Z * X ) - Y = ET + ( Z * X ) - Z/4 = 歌曲时长ms因此， 300bpm 歌曲计算公式为ET + ( Z * 4 * Y ) - 800 = ET + ( Z * 3200 ) - 800 = 歌曲时长ms若已知一首歌曲时长为 3:12.940 ，则毫秒数为 192940（ 3 * 60 * 1000 + 12940 ）套用公式ET + ( Z * 4 * Y ) - 800 = ET + ( Z * 3200 ) - 800 = 192940故 ET 计算方式ET = （ 192940 + 800 ) / 800 商为Z，即关卡小节数为：242 余数乘4即ET，为： 0.175 * 4 = 140 因此 ET为140ET调节方式关卡小节数Z = 242 - m ET = 140 + 800m 即 ET： 140 + 800 * 2 = 1740 则关卡小节数Z为: 242 - 2 = 240注： 理论上 ，ET为整数较好，所以需要微调ET，但要兼顾歌曲整体时长最终计算方法关卡小节数 = ( 歌曲时长毫秒数 + Y ) / Y 【整数部分】 关卡小节数 = ( 歌曲时长毫秒数 + Y ) / Z * 4 【整数部分】 ET = （ ( 歌曲时长毫秒数 + Y ) % Y ）【小数部分】* Y ET = （ ( 歌曲时长毫秒数 + Y ) % Z * 4 ）【小数部分】* Y综上，ET针对的是全歌曲，变动的是整个歌曲时长，所谓ET找P值，若歌曲源文件无时长改动，修改ET实际对P点意义不大，只是将歌曲在编辑器中裁剪了一番，可能会降低P的手感，也可能让关卡小节时间卡在了歌曲本身的节拍上，从而增加手感，但会造成一定的问题。此方式制作歌曲只需考虑歌曲时长、ET、关卡小节数即可，必填参数为音乐BPM 、ET(毫秒) 、关卡小节数，其他相同。相关常见速度关卡小节值 Y理论值 误差为 ± 1ms bpm关卡小节值 Y （ms）节拍值 Z （ms）380631.5789（非整）2526.3758（非整）360666.6667（非整）2666.6667（非整）340705.8823（非整）2823.5294（非整）32075030003008003200280857.1429（非整）3428.5714（非整）260923.0769（非整）3692.3077（非整）240100040002201090.9091（非整）4363.6364（非整）200120048001801333.3333（非整）5333.3333（非整）16015006000综上，160、180、200、240、300、320歌曲节拍较为稳定。提升建议从一个有效beat到另一个有效beat时间为2Y（2歌关卡小节值），无间断为Y（一个关卡小节值），按键总时长皆为Y（一个关卡小节值）。300bpm，一次为 1.6秒 ，按键时长为 800 ms 左右， 三键单键平均时长为266ms左右，五键为160ms左右，六键为133ms左右，七键为 114 ms 左右 ，键越多，留给单键按键时间越少，难度越高。320bpm，三键单键平均时长为250ms左右，五键为150ms左右，六键为125ms左右，七键为107ms左右340bpm，三键单键平均时长为235ms左右，五键为141ms左右，六键为117ms左右，七键为100ms左右。360bpm，三键单键平均时长为222ms左右，五键为133ms左右，六键为111ms左右，七键为95毫秒左右。由于键位方向不同，故不存在时间重叠的概念。实际单键平均时长要小于以上理论值（大神要P的）。可见，时间递增不多，但难度系数递增。五六七键差距太大是由于按键时长缩减但键数增加导致的大脑反应和手速难度系数倍增，故五键P多不代表能按到六七键故，短键（5键以下）连P增加分数，五键尽量P，great和cool意义不大，六七键尽量绿一点，拉开分数，但建议摆烂，保存体力。
Go 环境国内代理# 安装go依赖 yum install go # 修改go的环境变量添加国内代理 go env -w GOPROXY=https://goproxy.cn # 或者通过以下命令，添加代理 export GO111MODULE=on export GOPROXY=https://goproxy.cn,direct
常用的@Superwarnning 注解大全​ 使用idea的时候，有时避免不了会被编辑器提示警告，虽然不影响代码和程序的正常观看和运行，但总之是不美观，这时便可以用@Superwarnning注解来抑制警告来。尽量在代码编辑无法穷善的时候使用@SuppressWarnings("unchecked")//执行了未检查的转换时的警告，例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型。 @SuppressWarnings("unused") //未使用的变量 @SuppressWarnings("resource") //有泛型未指定类型 @SuppressWarnings("path") //在类路径、源文件路径等中有不存在的路径时的警告 @SuppressWarnings("deprecation") //使用了不赞成使用的类或方法时的警告 @SuppressWarnings("fallthrough") //当 Switch 程序块直接通往下一种情况而没有 break; 时的警告 @SuppressWarnings("serial")//某类实现Serializable(序列化)， 但没有定义 serialVersionUID 时的警告 @SuppressWarnings("rawtypes") //没有传递带有泛型的参数 @SuppressWarnings("finally") //任何 finally 子句不能正常完成时的警告。 @SuppressWarnings("try") // 没有catch时的警告 @SuppressWarnings("DuplicatedCode") //重复代码警告抑制 @SuppressWarnings("SpellCheckingInspection") // 抑制错误拼写 @SuppressWarnings("DuplicatedCode") // 抑制重复代码提示 @SuppressWarnings("all") //所有类型的警告 @SuppressWarnings("UnusedParameters") // 未使用的参数 // 阿里巴巴插件 @SuppressWarnings("AlibabaClassNamingShouldBeCamel") //【xXxXXxX】不符合UpperCamelCase命名风格 @SuppressWarnings（"AlibabaLowerCamelCaseVariableNaming") // 变量名【xxXX】不符合lowerCamelCase命名风格
使用SpringCloud Alibaba结合Springboot3.0 搭建一个demo
Springboot 3 出来也有一段时间了，Springcloud alibaba也在适配当中，本demo使用Springboot 3 + Spingcloud alibaba + nacos等组件，完成一个项目工程的简单搭建组件Nacos : 2.0.3Maven：3.0.8依赖版本springboot :3.0.4springcloud:2022.0.1springcloud alibaba :2.2.10-RC1spring-cloud-starter-alibaba-nacos-discovery:2022.0.0.0-RC1spring-cloud-starter-alibaba-nacos-config：2022.0.0.0-RC1springcloud alibaba 2.2.10-RC1 版本提供的Nacos依赖，经测试无法成功注册，故使用2022.0.0.0-RC1版本项目POM<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.4</version> <relativePath/> </parent> <groupId>com.piemoe</groupId> <artifactId>springboot3</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot3</name> <description>springboot3</description> <properties> <java.version>17</java.version> </properties> ​ <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2022.0.1</version> <type>pom</type> <scope>import</scope> </dependency> ​ <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.10-RC1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> ​ <dependencies> ​ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> ​ <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2022.0.0.0-RC1</version> </dependency> ​ ​ <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2022.0.0.0-RC1</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency> ​ ​ </dependencies> ​ <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> ​ </project> ​启动类@SpringBootApplication public class Springboot3Application { ​ public static void main(String[] args) { SpringApplication.run(Springboot3Application.class, args); } ​ }配置文件bootstrap.ymlserver: port: 13000 ​ spring: main: banner-mode: off application: name: demo profiles: active: local ​ rpc: addr: 127.0.0.1:8848 ​ logging: file: name: logs/${spring.application.name}/all.logbootstrap-local.ymlspring: cloud: nacos: discovery: server-addr: ${rpc.addr} namespace: ${spring.profiles.active} config: server-addr: ${rpc.addr} namespace: ${spring.profiles.active} file-extension: ymlNacos配置demo: name: xxxxx项目已提交到github，有兴趣的可以去看下 仓库
只需将线程计数更改为`1C，然后确认即可。若改为8表示指定8个线程进行打包编译，此处的1C表示cpu线程的倍数，可以有效利用cpu的算力。按40个模块计算，M1 mac max 打包速度从原来的58s缩减到5.23s，这个速度还是很喜人的。
MyBatis-PlusMyBatis-Plus（简称MP）是MyBatis社区的一个增强工具包，它在MyBatis的基础上进行了扩展，封装了很多有用的功能，提高了开发效率。MyBatis-Plus中包含了许多常用的增删改查的方法，还有分页查询、条件构造器、乐观锁、多租户、性能分析等功能。以下是MyBatis-Plus的一些特点：简化开发：MyBatis-Plus可以通过代码自动生成器快速生成CRUD方法的代码，极大地提高了开发效率。增删改查方法：MyBatis-Plus封装了通用的增删改查方法，无需手写SQL语句。集成分页插件：MyBatis-Plus集成了分页插件，使用简单，可以轻松实现分页操作。条件构造器：MyBatis-Plus提供了条件构造器，可以通过代码的形式构造复杂的SQL查询语句。乐观锁：MyBatis-Plus提供了乐观锁的功能，避免了并发操作时的数据冲突。多租户：MyBatis-Plus支持多租户，可以通过配置实现多租户系统的开发。性能分析：MyBatis-Plus集成了性能分析插件，可以分析SQL查询的性能瓶颈，帮助优化SQL语句，提高查询效率。总之，MyBatis-Plus是一个非常优秀的MyBatis增强工具包，其功能强大且易于使用，在实际开发中也能够显著地提高开发效率和工作效率。Java OptionalJava Optional是Java 8中引入的一个类，用于表示一个值可能存在也可能不存在。它的主要目的是减少空指针异常。Java Optional类有两个重要的方法:isPresent()和get()。isPresent()方法用于检查值是否存在，get()方法用于获取值。如果值不存在，get()方法会抛出NoSuchElementException异常。Java Optional还有很多有用的方法，比如orElse()和orElseGet()方法。这些方法可以帮助我们处理值不存在的情况。Java Optional是一个泛型类，因此可以用于处理任何类型的值。使用Java Optional的主要优点是可以避免空指针异常，提高代码健壮性。堵点MyBatis-Plus提供了很好的CURD体验，大大加强了开发的效率，其Lambda表达式式的方式也大大增强了用户的体验，但是MyBatis-Plus返回的参数仍然会在实际项目中被忽略空指针问题，笔者的朋友和同事就经常因为空指针问题（主要是忘记）经常被测试吐槽，故此通过可以使用Optional来增强Mybatis-plus的体验，一定程度上可以避免“忘记”判空带来的空指针问题。代码项目中引入MyBatis-Plus，新建OptionalBaseMapper文件，实现BaseMapper，具体代码如下：@SuppressWarnings({"DuplicatedCode", "unused"}) public interface OptionalBaseMapper<T> extends BaseMapper<T> { // ======================================= 批量插入 ======================================== /** * 批量插入 仅适用于mysql * * @param entityList 实体列表 * @return 影响行数 */ Integer insertBatchSomeColumn(Collection<T> entityList); /** * 逐条插入，适合少量数据插入，或者对性能要求不高的场景 * * 如果大量，请使用 {@link com.baomidou.mybatisplus.extension.service.impl.ServiceImpl#saveBatch(Collection)} 方法 * 使用示例，可见 RoleMenuBatchInsertMapper、UserRoleBatchInsertMapper 类 * * @param entities 实体们 */ default void insertBatch(Collection<T> entities) { entities.forEach(this::insert); } // ======================================= 查询数量 ======================================== /** * 根据 Wrapper 条件，查询总记录数 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ default Optional<Long> getCount(Wrapper<T> queryWrapper) { Long count = selectCount(queryWrapper); return ObjectUtil.isNull(count) ? Optional.empty() : Optional.of(count); } /** * 查询指定字段值数量 * * @param field field * @param value value */ default Optional<Long> selectCount(String field, Object value) { Long count = selectCount(new QueryWrapper<T>().eq(field, value)); return ObjectUtil.isNull(count) ? Optional.empty() : Optional.of(count); } /** * 查询指定字段值数量 * * @param field field * @param value value */ default Optional<Long> selectCount(SFunction<T, ?> field, Object value) { Long count = selectCount(new LambdaQueryWrapper<T>().eq(field, value)); return ObjectUtil.isNull(count) ? Optional.empty() : Optional.of(count); } // ======================================= 查询一个 ======================================== /** * 根据 ID 查询 * * @param id 主键ID */ default Optional<T> getById(Serializable id) { T t = selectById(id); return ObjectUtil.isNull(t) ? Optional.empty() : Optional.of(t); } /** * 根据 entity 条件，查询一条记录 * 查询一条记录，例如 qw.last("limit 1") 限制取一条记录, 注意：多条数据会报异常 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ default Optional<T> getOne(Wrapper<T> queryWrapper) { T t = selectOne(queryWrapper); return ObjectUtil.isNull(t) ? Optional.empty() : Optional.of(t); } /** * 查询一个 * * @param field field * @param value value */ default Optional<T> getOne(String field, Object value) { return getOne(new QueryWrapper<T>().eq(field, value)); } /** * 查询一个 * * @param field field * @param value value */ default Optional<T> getOne(SFunction<T, ?> field, Object value) { return getOne(new LambdaQueryWrapper<T>().eq(field, value)); } /** * 查询一个 * * @param field1 field1 * @param value1 value1 * @param field2 field2 * @param value2 value2 */ default Optional<T> getTwo(String field1, Object value1, String field2, Object value2) { return getOne(new QueryWrapper<T>().eq(field1, value1).eq(field2, value2)); } /** * 查询一个 * * @param field1 field1 * @param value1 value1 * @param field2 field2 * @param value2 value2 */ default Optional<T> getTwo(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) { return getOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)); } // ======================================= 查询多个 ======================================== /** * 查询（根据ID 批量查询） * * @param idList 主键ID列表(不能为 null 以及 empty) */ default Optional<List<T>> listBatchIds(Collection<? extends Serializable> idList) { List<T> list = selectBatchIds(idList); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 根据 entity 条件，查询全部记录 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ default Optional<List<T>> list(Wrapper<T> queryWrapper) { List<T> list = selectList(queryWrapper); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 列表查询 * * @param field field * @param value value */ default Optional<List<T>> list(String field, Object value) { List<T> list = selectList(new QueryWrapper<T>().eq(field, value)); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 列表查询 * * @param field field * @param value value */ default Optional<List<T>> list(SFunction<T, ?> field, Object value) { List<T> list = selectList(new LambdaQueryWrapper<T>().eq(field, value)); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 列表查询 * * @param field field * @param values values */ default Optional<List<T>> list(String field, Collection<?> values) { List<T> list = selectList(new QueryWrapper<T>().in(field, values)); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 列表查询 * * @param field field * @param values values */ default Optional<List<T>> list(SFunction<T, ?> field, Collection<?> values) { List<T> list = selectList(new LambdaQueryWrapper<T>().in(field, values)); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 查询（根据 columnMap 条件） * * @param columnMap 表字段 map 对象 */ default Optional<List<T>> listByMaps(Map<String, Object> columnMap) { List<T> list = selectByMap(columnMap); return CollUtil.isEmpty(list) ? Optional.empty() : Optional.of(list); } /** * 根据 Wrapper 条件，查询全部记录 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ default Optional<List<Map<String, Object>>> maps(Wrapper<T> queryWrapper) { List<Map<String, Object>> maps = selectMaps(queryWrapper); return CollUtil.isEmpty(maps) ? Optional.empty() : Optional.of(maps); } /** * 根据 Wrapper 条件，查询全部记录 * 注意： 只返回第一个字段的值 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ default Optional<List<Object>> listObjs(Wrapper<T> queryWrapper) { List<Object> objects = selectObjs(queryWrapper); return CollUtil.isEmpty(objects) ? Optional.empty() : Optional.of(objects); } // ======================================= 分页 ======================================== /** * 分页 * * @param pageParam 分页参数 需继承PageParam * @param queryWrapper queryWrapper */ default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) { // MyBatis Plus 查询 IPage<T> mpPage = MyBatisUtils.buildPage(pageParam); selectPage(mpPage, queryWrapper); // 转换返回 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal(), mpPage.getPages(), mpPage.getCurrent(), mpPage.getSize()); } /** * 根据 entity 条件，查询全部记录（并翻页） * * @param sortingFields 排序字段 * @param pageParam 分页查询条件 * @param queryWrapper 实体对象封装操作类（可以为 null） */ default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper, Collection<SortingField> sortingFields) { // MyBatis Plus 查询 IPage<T> mpPage = MyBatisUtils.buildPage(pageParam, sortingFields); selectPage(mpPage, queryWrapper); // 转换返回 return new PageResult<>(mpPage.getRecords(), mpPage.getTotal(), mpPage.getPages(), mpPage.getCurrent(), mpPage.getSize()); } }最后，在dao层，继承从BaseMapper换成OptionalBaseMapper即可。
前言数据脱敏是一种数据保护技术，它可以对敏感数据进行处理，使其失去原始意义，从而保护数据隐私。通俗地说，就是将原始数据中敏感信息的部分替换或删除，例如：手机号码中的一部分数字替换为星号，或是姓名中的一些字母用其他字母代替等等。数据脱敏可以保护个人隐私，防止个人信息被泄露、滥用，同时也符合现代法律法规对个人信息保护的要求，是网络安全和信息保护中不可或缺的一环。本文将通过使用注解完成对字段的自定义脱敏功能。正文本文将介绍2种脱敏的方式，分别为：注解方式脱敏和lambda方式脱敏注解脱敏定义脱敏注解@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @JacksonAnnotationsInside @JsonSerialize(using = SensitiveSerialize.class) public @interface Dm { /** * 脱敏信息类型 */ DmTypeEnum type(); /** * 前置不需要打码的长度 * 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用 */ int prefixNoMaskLen() default 0; /** * 后置不需要打码的长度 * 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用 */ int suffixNoMaskLen() default 0; /** * 用什么打码 * 只有 {@link DmTypeEnum} 为 {@link DmTypeEnum#CUSTOMER} 时可用 */ char symbol() default '*'; }type()方法为可以支持实现的脱敏信息类型，通过使用hutool工具，可以看到它支持以下枚举，故拉取出来形成类型枚举：public enum DmTypeEnum { /** * 用户自定义 */ CUSTOMER, /** * 用户id */ BACK_ID, /** * 中文名 */ CHINESE_NAME, /** * 身份证号 */ ID_CARD, /** * 座机号 */ FIXED_PHONE, /** * 手机号 */ MOBILE_PHONE, /** * 地址 */ ADDRESS, /** * 电子邮件 */ EMAIL, /** * 密码 */ PASSWORD, /** * 中国大陆车牌，包含普通车辆、新能源车辆 */ CAR_LICENSE, /** * 银行卡 */ BANK_CARD }关键语句在@JsonSerialize(using = SensitiveSerialize.class)上，@JsonSerialize 是一个注解，它可以用来指定 Java 对象序列化成 JSON 格式时的序列化规则。具体而言，它可以指定序列化时属性名的命名方式、日期格式化等等。它的作用是使得 Java 对象可以方便地转化为 JSON 格式，这对于 Web 开发、移动应用开发等场景非常有用，故此我们在这里自定义一个实现，并在注解中引用。@NoArgsConstructor @AllArgsConstructor @Slf4j public class SensitiveSerialize<T> extends JsonSerializer<T> implements ContextualSerializer { /** * 脱敏类型 */ private DmTypeEnum sensitiveTypeEnum; /** * 前几位不脱敏 */ private Integer prefixNoMaskLen; /** * 最后几位不脱敏 */ private Integer suffixNoMaskLen; /** * 打码字符 */ private char symbol; @Override public void serialize(final T value, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider) throws IOException { Object desensitized = DmUtils.desensitized(value.toString(), sensitiveTypeEnum, prefixNoMaskLen, suffixNoMaskLen, symbol); if (desensitized instanceof String) { jsonGenerator.writeString(desensitized.toString()); return; } if (desensitized instanceof Number) { jsonGenerator.writeNumber(DmUtils.backId()); return; } log.error("[数据脱敏]未知的信息类型：{}", sensitiveTypeEnum); jsonGenerator.writeString(StrUtil.EMPTY); } @Override public JsonSerializer<?> createContextual(final SerializerProvider serializerProvider, final BeanProperty beanProperty) throws JsonMappingException { if (beanProperty != null) { if ( Objects.equals(beanProperty.getType().getRawClass(), String.class) || Objects.equals(beanProperty.getType().getSuperClass().getRawClass(), Number.class) ) { Dm sensitive = beanProperty.getAnnotation(Dm.class); if (sensitive == null) { sensitive = beanProperty.getContextAnnotation(Dm.class); } if (sensitive != null) { return new SensitiveSerialize<T>(sensitive.type(), sensitive.prefixNoMaskLen(), sensitive.suffixNoMaskLen(), sensitive.symbol()); } } return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } return serializerProvider.findNullValueSerializer(null); } }脱敏工具类（对照hutool脱敏方法）@Slf4j public class DmUtils { /** * 脱敏，使用默认的脱敏策略 * * desensitized("100", DesensitizedUtils.DesensitizedType.USER_ID)) = "0" * desensitized("段正淳", DesensitizedUtils.DesensitizedType.CHINESE_NAME)) = "段**" * desensitized("51343620000320711X", DesensitizedUtils.DesensitizedType.ID_CARD)) = "5***************1X" * desensitized("09157518479", DesensitizedUtils.DesensitizedType.FIXED_PHONE)) = "0915*****79" * desensitized("18049531999", DesensitizedUtils.DesensitizedType.MOBILE_PHONE)) = "180****1999" * desensitized("北京市海淀区马连洼街道289号", DesensitizedUtils.DesensitizedType.ADDRESS)) = "北京市海淀区马********" * desensitized("duandazhi-jack@gmail.com.cn", DesensitizedUtils.DesensitizedType.EMAIL)) = "d*************@gmail.com.cn" * desensitized("1234567890", DesensitizedUtils.DesensitizedType.PASSWORD)) = "**********" * desensitized("苏D40000", DesensitizedUtils.DesensitizedType.CAR_LICENSE)) = "苏D4***0" * desensitized("11011111222233333256", DesensitizedUtils.DesensitizedType.BANK_CARD)) = "1101 **** **** **** 3256" * * * @param message 字符串 * @param desensitizedType 脱敏类型;可以脱敏：用户id、中文名、身份证号、座机号、手机号、地址、电子邮件、密码 * @param startInclude 开始排除 类型为 CUSTOMER 时可用 * @param endExclude 结束排除 类型为 CUSTOMER 时可用 * @param symbol 脱敏字符 默认为'*' 类型为 CUSTOMER 时可用 * @return 脱敏之后的字符串 */ public static Object desensitized(Object message, DmTypeEnum desensitizedType, int startInclude, int endExclude, char symbol) { if (message instanceof Number && StrUtil.isBlank(message.toString())) { return -1L; } if (StrUtil.isBlank(message.toString())) { return StrUtil.EMPTY; } String newStr; switch (desensitizedType) { case CUSTOMER: newStr = customer(String.valueOf(message), startInclude, endExclude, symbol); break; case BACK_ID: log.info("[数据脱敏]后台用户id脱敏，值为：{}", message); return backId(); case CHINESE_NAME: newStr = chineseName(String.valueOf(message)); break; case ID_CARD: newStr = idCardNum(String.valueOf(message), 1, 2); break; case FIXED_PHONE: newStr = fixedPhone(String.valueOf(message)); break; case MOBILE_PHONE: newStr = mobilePhone(String.valueOf(message)); break; case ADDRESS: newStr = address(String.valueOf(message), 8); break; case EMAIL: newStr = email(String.valueOf(message)); break; case PASSWORD: newStr = password(String.valueOf(message)); break; case CAR_LICENSE: newStr = carLicense(String.valueOf(message)); break; case BANK_CARD: newStr = bankCard(String.valueOf(message)); break; default: newStr = ""; } return newStr; } public static String replace(CharSequence str, int startInclude, int endExclude, char symbol) { return CharSequenceUtil.replace(str, startInclude, endExclude, symbol); } public static String customer(String customer, int startInclude, int endExclude, char replacedChar) { if (StrUtil.isBlank(customer)) { return ""; } if (startInclude > customer.length() || endExclude > customer.length() || (startInclude + endExclude) > customer.length()) { log.warn("[信息脱敏]自定义长度不正确"); return replace(customer, startInclude, customer.length() - 2, replacedChar); } return replace(customer, startInclude, endExclude, replacedChar); } /** * 【后台用户id】不对外提供backId * * @return 脱敏后的主键 */ public static Long backId() { return -1L; } /** * 【中文姓名】只显示第一个汉字，其他隐藏为2个星号，比如：李** * * @param fullName 姓名 * @return 脱敏后的姓名 */ public static String chineseName(String fullName) { if (StrUtil.isBlank(fullName)) { return StrUtil.EMPTY; } return StrUtil.hide(fullName, 1, fullName.length()); } /** * 【身份证号】前1位 和后2位 * * @param idCardNum 身份证 * @param front 保留：前面的front位数；从1开始 * @param end 保留：后面的end位数；从1开始 * @return 脱敏后的身份证 */ public static String idCardNum(String idCardNum, int front, int end) { //身份证不能为空 if (StrUtil.isBlank(idCardNum)) { return StrUtil.EMPTY; } //需要截取的长度不能大于身份证号长度 if ((front + end) > idCardNum.length()) { return StrUtil.EMPTY; } //需要截取的不能小于0 if (front < 0 || end < 0) { return StrUtil.EMPTY; } return StrUtil.hide(idCardNum, front, idCardNum.length() - end); } /** * 【固定电话 前四位，后两位 * * @param num 固定电话 * @return 脱敏后的固定电话； */ public static String fixedPhone(String num) { if (StrUtil.isBlank(num)) { return StrUtil.EMPTY; } return StrUtil.hide(num, 4, num.length() - 2); } /** * 【手机号码】前三位，后4位，其他隐藏，比如135****2210 * * @param num 移动电话； * @return 脱敏后的移动电话； */ public static String mobilePhone(String num) { if (StrUtil.isBlank(num)) { return StrUtil.EMPTY; } return StrUtil.hide(num, 3, num.length() - 4); } /** * 【地址】只显示到地区，不显示详细地址，比如：北京市海淀区**** * * @param address 家庭住址 * @param sensitiveSize 敏感信息长度 * @return 脱敏后的家庭地址 */ public static String address(String address, int sensitiveSize) { if (StrUtil.isBlank(address)) { return StrUtil.EMPTY; } int length = address.length(); return StrUtil.hide(address, length - sensitiveSize, length); } /** * 【电子邮箱】邮箱前缀仅显示第一个字母，前缀其他隐藏，用星号代替，@及后面的地址显示，比如：d**@126.com * * @param email 邮箱 * @return 脱敏后的邮箱 */ public static String email(String email) { if (StrUtil.isBlank(email)) { return StrUtil.EMPTY; } int index = StrUtil.indexOf(email, '@'); if (index <= 1) { return email; } return StrUtil.hide(email, 1, index); } /** * 【密码】密码的全部字符都用*代替，比如：****** * * @param password 密码 * @return 脱敏后的密码 */ public static String password(String password) { if (StrUtil.isBlank(password)) { return StrUtil.EMPTY; } return StrUtil.repeat('*', password.length()); } /** * 【中国车牌】车牌中间用*代替 * eg1：null -》 "" * eg1："" -》 "" * eg3：苏D40000 -》 苏D4***0 * eg4：陕A12345D -》 陕A1****D * eg5：京A123 -》 京A123 如果是错误的车牌，不处理 * * @param carLicense 完整的车牌号 * @return 脱敏后的车牌 */ public static String carLicense(String carLicense) { if (StrUtil.isBlank(carLicense)) { return StrUtil.EMPTY; } // 普通车牌 if (carLicense.length() == 7) { carLicense = StrUtil.hide(carLicense, 3, 6); } else if (carLicense.length() == 8) { // 新能源车牌 carLicense = StrUtil.hide(carLicense, 3, 7); } return carLicense; } /** * 银行卡号脱敏 * eg: 1101 **** **** **** 3256 * * @param bankCardNo 银行卡号 * @return 脱敏之后的银行卡号 */ public static String bankCard(String bankCardNo) { if (StrUtil.isBlank(bankCardNo)) { return bankCardNo; } bankCardNo = StrUtil.trim(bankCardNo); if (bankCardNo.length() < 9) { return bankCardNo; } final int length = bankCardNo.length(); final int midLength = length - 8; final StringBuilder buf = new StringBuilder(); buf.append(bankCardNo, 0, 4); for (int i = 0; i < midLength; ++i) { if (i % 4 == 0) { buf.append(CharUtil.SPACE); } buf.append('*'); } buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length); return buf.toString(); } }lambda脱敏注解方式脱敏的好处是可以在变量上更方便的进行脱敏，但是这种方式对数据格式支持不高，一般只支持基本的数据类型，无法对实体内嵌套的数据进行脱敏，故此可以通过lambda方式进行脱敏。首先自定义一个函数public interface DmParam<Parent, Children> extends Serializable { /** * 执行函数 * * @param parameter 参数 * @return 函数执行结果 * @throws Exception 自定义异常 */ Children call(Parent parameter) throws Exception; /** * 执行函数，异常包装为RuntimeException * * @param parameter 参数 * @return 函数执行结果 */ default Children callWithServiceException(Parent parameter) { try { return call(parameter); } catch (Exception e) { throw new ServiceException("信息脱敏失败"); } } }既然通过lambda方式获取字段，那么反射也是必须的public class FieldUtil { /** * 通过lambda获取字段 * * @param func 方法 * @return {@link Field} */ public static <T> Field getField(DmParam<T, ?> func) { FieldMeta fieldMeta = getFieldMeta(func); return ReflectUtil.getField(fieldMeta.getGetInstantiatedClass(), fieldMeta.getPropertyName()); } /** * 通过lambda设置字段名称 * * @param func 方法 * @param val 设置的值 */ public static <T> void setField(Object object, DmParam<T, ?> func, String val) { FieldMeta fieldMeta = getFieldMeta(func); ReflectUtil.setFieldValue(object, fieldMeta.getPropertyName(), val); } /** * 通过lambda获取字段名称 * * @param func 方法 * @return {@link String} */ public static <T> FieldMeta getFieldMeta(DmParam<T, ?> func) { // 反射读取 Method writeReplaceMethod; try { writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace"); // 设置可访问 writeReplaceMethod.setAccessible(true); SerializedLambda serializedLambda; serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func); String implMethodName = serializedLambda.getImplMethodName(); String propertyName = methodToProperty(implMethodName); String clazzPath = serializedLambda.getImplClass().replace('/', '.'); Class<String> clazz = ClassUtil.getClass(clazzPath); return new FieldMeta(propertyName, implMethodName, clazz); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.error("[FieldUtil]反射获取字段失败", e); throw new ServiceException(e); } } /** * 通过lambda获取字段名称 * * @param func 方法 * @return {@link String} */ public static <T> String getFieldName(DmParam<T, ?> func) { // 反射读取 Method writeReplaceMethod; try { writeReplaceMethod = func.getClass().getDeclaredMethod("writeReplace"); // 设置可访问 writeReplaceMethod.setAccessible(true); SerializedLambda serializedLambda; serializedLambda = (SerializedLambda) writeReplaceMethod.invoke(func); String implMethodName = serializedLambda.getImplMethodName(); return methodToProperty(implMethodName); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { log.error("[FieldUtil]反射获取字段失败", e); throw new ServiceException(e); } } /** * 根据方法名转换为字段 * * @param implMethodName 方法名 * @return {@link String} */ public static String methodToProperty(String implMethodName) { final String defaultIs = "is"; final String defaultGet = "get"; final String defaultSet = "set"; if (implMethodName.startsWith(defaultIs)) { implMethodName = implMethodName.substring(2); } else if (implMethodName.startsWith(defaultGet) || implMethodName.startsWith(defaultSet)) { implMethodName = implMethodName.substring(3); } else { throw new ServiceException("Error parsing property name '" + implMethodName + "'. Didn't start with 'is', 'get' or 'set'."); } if (implMethodName.length() == 1 || (implMethodName.length() > 1 && !Character.isUpperCase(implMethodName.charAt(1)))) { implMethodName = implMethodName.substring(0, 1).toLowerCase(Locale.ENGLISH) + implMethodName.substring(1); } return implMethodName; } }定义脱敏方法：对于单个实体public class DmLambdaWrapper<T> { T val; @SuppressWarnings("rawtype") public DmLambdaWrapper(T val) { this.val = val; } /** * 获取脱敏实例 * * @param val 待脱敏的实体 * @return {@link DmLambdaWrapper<T>} */ public static <T> DmLambdaWrapper<T> getInstance(T val) { return new DmLambdaWrapper<>(val); } /** * 关联单个字段 * * @param field 关联字段 * @return {@link DmLambdaWrapper<R>} */ public <R> DmLambdaWrapper<R> ivkOne(DmParam<T, R> field) { R call = field.callWithServiceException(val); return new DmLambdaWrapper<>(call); } /** * 关联集合 * * @param listMapper 关联集合字段 * @param predicate 脱敏预言 * @return {@link DmListLambdaWrapper} */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper, Predicate<C> predicate) { S cs = listMapper.callWithServiceException(val); List<C> collect = cs.stream().filter(predicate).collect(Collectors.toList()); return new DmListLambdaWrapper<>(collect); } /** * 关联集合-无预言（集合内所有对应字段都将脱敏） * * @param listMapper 关联集合字段 * @return {@link DmListLambdaWrapper} */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper) { S cs = listMapper.callWithServiceException(val); return new DmListLambdaWrapper<>(cs); } /** * 脱敏字符串 * * @param dmType 脱敏类型 * @param mapper 脱敏字段 * @return {@link DmLambdaWrapper<T>} */ public <R extends String> DmLambdaWrapper<T> dmStr(DmTypeEnum dmType, DmParam<T, R> mapper) { R call = mapper.callWithServiceException(val); if (null != call) { String desensitized = DmUtils.desensitized(call, dmType, 1, 3, '*').toString(); FieldUtil.setField(val, mapper, desensitized); // 反射获取字段名称 } return new DmLambdaWrapper<>(val); } /** * 脱敏多个字段 相同脱敏类型多字段性能要优于 {@link DmLambdaWrapper#dmStr(DmTypeEnum, DmParam)} * * @param dmType 脱敏类型 * @param mappers 脱敏字段 * @return {@link DmLambdaWrapper<T>} * @author Mc * @date 2022/12/16 17:32 */ @SafeVarargs public final <R extends String> DmLambdaWrapper<T> dmStr(DmTypeEnum dmType, final DmParam<T, R>... mappers) { for (DmParam<T, R> mapper : mappers) { R call = mapper.callWithServiceException(val); if (null != call) { String desensitized = DmUtils.desensitized(call, dmType, 1, 3, '*').toString(); FieldUtil.setField(val, mapper, desensitized); // 反射获取字段名称 } } return new DmLambdaWrapper<>(val); } }对于List集合脱敏@Slf4j public class DmListLambdaWrapper<T extends Collection<F>, F> { T listVal; public DmListLambdaWrapper(T listVal) { this.listVal = listVal; } /** * 关联集合 带过滤 * * @param listMapper 集合Mapper * @param predicate 过滤预言 * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:24 */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper, Predicate<C> predicate) { S cs = listMapper.callWithServiceException(listVal); List<C> collect = cs.stream().filter(predicate).collect(Collectors.toList()); return new DmListLambdaWrapper<>(collect); } /** * 挂念集合 不带过滤 * * @param listMapper 集合Mapper * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:26 */ public <C, S extends Collection<C>> DmListLambdaWrapper<Collection<C>, C> ivkList(DmParam<T, S> listMapper) { S cs = listMapper.callWithServiceException(listVal); return new DmListLambdaWrapper<>(cs); } /** * 脱敏字符串字段 * * @param dmType 脱敏类型 * @param mapper 脱敏字段 * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:26 */ public <C extends String> DmListLambdaWrapper<Collection<F>, F> dmList2Str(DmTypeEnum dmType, DmParam<F, C> mapper) { for (F f : listVal) { C message = mapper.callWithServiceException(f); if (null != message) { String desensitized = DmUtils.desensitized(message, dmType, 1, 3, '*').toString(); FieldUtil.setField(f, mapper, desensitized); // 反射获取字段名称 } } return new DmListLambdaWrapper<>(listVal); } /** * 脱敏多个字符串字段，相同类型性能优先于多次使用 {@link DmListLambdaWrapper#dmList2Str(DmTypeEnum, DmParam)} * * @param dmType 脱敏类型 * @param mappers 脱敏字段 * @return {@link DmListLambdaWrapper} * @author Mc * @date 2022/12/16 17:27 */ @SafeVarargs public final <C extends String> DmListLambdaWrapper<Collection<F>, F> dmList2Str(DmTypeEnum dmType, final DmParam<F, C>... mappers) { for (F f : listVal) { for (DmParam<F, C> mapper : mappers) { C message = mapper.callWithServiceException(f); if (null != message) { String desensitized = DmUtils.desensitized(message, dmType, 1, 3, '*').toString(); FieldUtil.setField(f, mapper, desensitized); // 反射获取字段名称 } } } return new DmListLambdaWrapper<>(listVal); } }其他的方式可以自由拓展用法：DmLambdaWrapper.getInstance(user.getUserDetail()) .dmStr(DmTypeEnum.MOBILE_PHONE, UserDetail::getMobile) .dmStr(DmTypeEnum.EMAIL, UserDetail::getEmail);上面的例子将user里的userDetail字段中的mobile、email进行了相对应的数据脱敏。