Java 参数校验:只能从指定的值中取其一

有时候参数校验的需求是只允许从指定的几个数据中选用一个,否则抛出异常。 如果是 String 类型的可以用 @Pattern 注解写正则表达式,例如: @Pattern(regexp = "^(CENTURIES|YEARS|MONTHS|DAYS)$", message = "持续时间单位错误") private String durationUnit; 数字类型可以通过自定义注解 + 校验规则。 OneOf.java @Target({FIELD, PARAMETER}) @Retention(RUNTIME) @Constraint(validatedBy = OneOfValidator.class) @Documented @Repeatable(List.class) public @interface OneOf { String message() default "Invalid parameter"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; // 用 double 接收可用值,兼容整数和小数 double[] value(); @Target({FIELD, PARAMETER}) @Retention(RUNTIME) @Documented @interface List { OneOf[] value(); } } OneOfValidator.java ...

2022 年 十一月 17 日 · 1 分钟 · 115 字 · K.T

Nacos 元数据增加启动日期、版本号和 PID

Spring Cloud 项目下 Nacos 默认的元数据只有 preserved.register.source=SPRING_CLOUD 这一项,可以通过增加配置项手动添加想要的数据,方便追溯一些启动相关的信息。 比如:启动日期、文件版本号(因为打成 jar 包时 maven 插件会将文件名自动命名为 xxx-{version}.jar,所以这里是获取的 jar 文件路径)和 PID。 代码如下: @Configuration public class NacosConfig { /** * Nacos 元数据配置 * * @return */ @Bean @ConditionalOnNacosDiscoveryEnabled public NacosDiscoveryProperties nacosDiscoveryProperties() { NacosDiscoveryProperties nacosDiscoveryProperties = new NacosDiscoveryProperties(); Map<String, String> metadata = nacosDiscoveryProperties.getMetadata(); // 启动时间 metadata.put("boot.startup.time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis())); // 文件路径 // String.valueOf 代替 toString 避免空指针 metadata.put("boot.path", String.valueOf(new ApplicationHome(this.getClass()).getSource())); // PID metadata.put("boot.pid", new ApplicationPid().toString()); return nacosDiscoveryProperties; } } Nacos 控制台的实例可以看到类似如下的元数据: ...

2022 年 九月 19 日 · 1 分钟 · 78 字 · K.T

Spring Boot 带附件的邮件发送配置

虽然 Spring Boot 提供了邮件相关的功能,但在缺少部分配置的情况下或多或少会有一些问题,如附件的文件名乱码、文件格式等问题。本文提供统一解决这类问题的配置。 Maven 依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> 1. 配置文件 以 QQ 邮箱为例: spring: mail: host: smtp.qq.com port: 465 username: [email protected] # 发送邮箱账户 password: exmaplepassword # 申请的授权码 properties: mail: smtp: socketFactory: port: 465 class: javax.net.ssl.SSLSocketFactory ssl: enable: true starttls: enable: true required: true 2. 封装统一工具类 /** * @author Kytrun * @version 1.0 * @date 2022/07/25 16:14 */ @RequiredArgsConstructor @Component public class MailUtil { private final JavaMailSender mailSender; @Value("${spring.mail.username}") private String mailFrom; // 长文件名不截断 static { System.getProperties().setProperty("mail.mime.splitlongparameters", "false"); } /** * 发送纯文字邮件 * * @param to 收件地址 * @param subject 主题 * @param text 内容 */ public void send(String to, String subject, String text) { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setTo(to); mailMessage.setSubject(subject); mailMessage.setSentDate(new Date()); mailMessage.setText(text); mailMessage.setFrom(mailFrom); mailSender.send(mailMessage); } /** * 发送带多个附件的邮件 * * @param to 收件地址 * @param subject 主题 * @param text 文字内容 * @param fileList 文件列表 * @throws MessagingException * @throws UnsupportedEncodingException */ public void send(String to, String subject, String text, List<MultipartFile> fileList) throws MessagingException, UnsupportedEncodingException { MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true); messageHelper.setFrom(mailFrom); messageHelper.setTo(to); messageHelper.setSubject(subject); messageHelper.setText(text); if (fileList != null && !fileList.isEmpty()) { for (MultipartFile file : fileList) { String fileName = file.getOriginalFilename() == null ? "文件" : file.getOriginalFilename(); //解决中文附件名的问题 fileName = MimeUtility.encodeText(fileName); messageHelper.addAttachment(fileName, file); } } mailSender.send(mimeMessage); } }

2022 年 七月 25 日 · 1 分钟 · 208 字 · K.T

Spring Cloud Feign 去除全局包装、统一异常处理

2022 年关于 Spring Cloud 服务间调用组件 Feign 配置的总结。 Maven 依赖: <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> 1. 全局响应包装的拆解 一般来说,我们服务接口响应内容都有一层全局的包装,比如: { "code": 0, "message": "成功", "data": { ... } } 在服务间调用时如果在每个 Feign 的接口处定义同样的类比如 Result<XxVO> 再 getData() 不是不行,但是比较繁琐,而且在获取想要的数据前可能需要根据 code、message 判断得到的结果是否正确。通过自定义 decoder 可以比较好地解决前一个问题: // 注入 Jackson 的 ObjectMapper,如果不喜欢 Lombok 可以自行修改 @RequiredArgsConstructor public class UnwrapDecoder implements Decoder { private final ObjectMapper objectMapper; @SneakyThrows @Override public Object decode(Response response, Type type) { Reader reader = response.body().asReader(Charset.defaultCharset()); Result<?> result = objectMapper.readValue(reader, Result.class); // 根据 code 判断操作是否成功 if (ResultCode.isSuccess(result.getCode())) { Object data = result.getData(); JavaType javaType = TypeFactory.defaultInstance().constructType(type); return objectMapper.convertValue(data, javaType); } // 若不成功,抛出业务异常,注意此处的异常会在 DecodeException 中被捕获,后文会处理 throw new BusinessException(result.getCode(), result.getMessage()); } } 使用方式: ...

2022 年 七月 13 日 · 2 分钟 · 266 字 · K.T

Spring Cloud 不停机更新 – shell 脚本

本文简单介绍关于 Spring Cloud 技术栈的对外无感知的优雅停机、更新和重启,仅通过一些微服务组件和 shell 脚本实现,不依赖容器技术。 1. 实现原理 实现对外透明的优雅更新的基本原理就是在原服务没有停机的时候启动一个新的进程,通过负载均衡让访问可以到达两个节点,然后将旧的服务“优雅”地下线,其本质是不接收新的请求,同时将正在处理中的请求完成,然后停机,之后的请求全部路由到新的服务上。 2. 服务架构 实现以上方案主要依靠 Spring Gateway 和 Nacos。 Spring Gateway 聚合内部的服务接口向外暴露,所有服务注册在 Nacos 上,在 Spring Gateway 上配置好 Nacos 使其动态路由到具体的服务而非写死 IP 和端口。 3. 具体细节 首先,优雅下线的配置是最简单的,在 Spring Boot 的配置文件中添加 server: shutdown: graceful (Spring Boot 2.3+ 支持) 然后在停机时用 kill 命令就可以了,配置了优雅下线后会关闭各种连接、释放资源并主动从 Nacos 中注销。需要特别注意的是不能使用 kill -9 强制杀进程。 然后,因为需要能够在同一时间内运行两个服务,所以我们需要给它们指定不同的端口,一种较为简单的方式是在配置文件中将 server.port 配置为 0,这样启动时就会在本机没有占用的端口中随机使用一个,但缺点是无法指定端口的范围,因而我使用的是另一种方式,即在 shell 脚本中寻找一个端口然后在 jar 包的启动参数中指定。 shell 编写的大体思路是: 找到一个在指定范围内没被占用的端口,开启新的服务。 找到旧版本的进程然后 kill 关闭。 按道理说这样就行了,但是我在实际测试中发现在旧服务关闭的短暂时间内会有 Gateway 仍然路由到旧服务然后报错的情况,当前的解决方案是在关闭服务前先调用 Nacos 的 API 将旧实例从中注销,过一段时间后再杀掉进程,还需要一段时间考察。 ...

2021 年 十二月 28 日 · 2 分钟 · 356 字 · K.T

Spring Boot 打包为 exe 安装文件

在工作中可能会遇到单机使用的 Java Web 项目。从可移植性、可扩展性、学习成本等各方面来看,使用 Spring Boot 搭建都是一个不错的选择,而且如果后期调整为云部署也是非常容易。 虽然 Spring Boot 天然支持打成 jar 包,但是对于用户来说,启动方式还是不够“优雅”。如果封装为一个独立的安装包,有快捷方式,双击运行,那么体验就更接近 APP 了。 在调研了多种方式后,使用以下的方案是我认为还算 OK 的一种,遂记录下来。 创建一个文件夹,放入 jar 包和 jre 文件夹; 这里有两种方式,我是直接将 jre 一起打包,也可以独立安装 Java。 编写一个 bat 批处理文件,内容如下: @echo off start jre1.8.0_251\bin\java.exe -jar swagx-0.0.1-SNAPSHOT.jar explorer http://localhost:8080 其中 jre1.8.0_251\bin\java.exe 为 Java 的路径,swagx-0.0.1-SNAPSHOT.jar 为 jar 的文件名称,http://localhost:8080 为项目启动主页,也可以添加静态 html 文件等,启动时改为 index.html 之类的。 然后将 bat 文件放入目录,建议命名为应用名称; 使用 Inno Setup 软件打包,网上教程很多; 注意执行文件选择 bat 脚本: 在文件夹中放入 .ico 格式的图标文件,在 Inno Setup 生成的代码中添加图标配置: ...

2020 年 七月 6 日 · 1 分钟 · 69 字 · K.T