一、JsonUtil 的总结
-
虽然
spring对于controller的返回会自动进行序列化和反序列化,但是架不住项目中其它模块比如说redis、日志等模块也需要用到序列化操作,所以通常都需要手动封装一个JsonUtil工具类 。 -
为了防止
JsonUtil工具类被多次创建导致反复创建ObjectMapper,实际上只需要用单例的JsonUtil即可,而不需要反复去创建JsonUtil,所以通常要将JsonUtil设置为单例模式 。 -
因为在编写
JsonUtil时需要调用ObjectMapper中的序列化和反序列化接口,它们都是需要抛异常的,并且这些异常都是类似的,所以这里借鉴JacksonJsonParser源码中的tryParse,统一对Json处理进行捕获异常 ,源码如下所示:
-
对于
List类型的反序列化,需要特殊处理,因为Jackson无法通过Class<List>+Class<User>推断出List<User>,所以直接调用readValue()传入List类型是不行的!- 解决方法: 使用
ObjectMapper.getTypeFactory().``constructParmetricType``()传入List和要序列化的对象类型,得到JavaType类型,将该JavaType传入readValue()即可,如下所示:
*// 反序列化List* public static <T> T toList(String jsonStr, Class<?> valueType) { *// 因为 Jackson 无法通过 Class<List> + Class<User> 推断出 List<User>,* * // 需要显式告诉它泛型参数类型,否则会报错或反序列化成 List<LinkedHashMap>* * *JavaType javaType = JsonUtil.*getInstance*().getTypeFactory() .constructParametricType(List.class, valueType); return JsonUtil.*tryParse*(() -> { return JsonUtil.*getInstance*().readValue(jsonStr, javaType); }); } - 解决方法: 使用
二、日志配置
-
Spring Boot 项目中推荐使用
logback-spring.xml作为日志配置文件名称,然后在项目配置文件中添加该文件的路径:**logging.config** =classpath:logback-spring.xml -
配置完成后,在使用
Logger的时候,如果报了以下错误:java: 不兼容的类型: java.lang.String无法转换为java.util.function.Supplier<java.lang.String>
则要注意是不是引错包了,可能引入的是 Log4j2,而不是 SLF4J!
-
配置完成后,在使用的时候,也可以使用注解
@Slf4j来代替引入Logger,写起来更加便捷(但本质就是引入了Logger) -
在日志输出的时候,不管对象是否已经重写了
toString,都建议先进行序列化,再输出 ,如下所示。这样子可读性更好、日志分析支持也更好(比如设置密码字段不可被打印,则序列化后不会显示出来,提高安全性)@PostMapping("/register") public RegisterResponseVO register(@RequestBody @Validated RegisterRequestDTO req) { *log*.info("register controller:" + JsonUtil.*toJson*(req)); // 进行序列化再打印 //... }
三、TypeHandler
TypeHandler 是 MyBatis 中的一个 类型转换器接口 ,可以把它理解为 MyBatis 中的 "翻译官",负责处理 Java 对象属性和数据库字段之间的映射转换逻辑 。
🧭 为什么需要 TypeHandler?
默认情况下,MyBatis 已经能自动处理大部分常见类型,比如:
-
String<->VARCHAR -
Integer<->INT -
Date<->TIMESTAMP
但有时候,Java 的类型和数据库类型不匹配,或者你用了一些 自定义类型 ,这时候就需要你自己告诉 MyBatis:"嘿,这两个东西要这么转换" ——这就是 TypeHandler 的职责。
| 比较点 | 直接用 String | 封装类型 + TypeHandler |
|---|---|---|
| 实现复杂度 | ✅ 最简单 | ❌ 稍微复杂一点 |
| 类型语义清晰 | ❌ 不清晰 | ✅ 一眼就知道是加密类型 |
| 解耦业务逻辑 | ❌ 加密逻辑可能到处都是 | ✅ 统一封装、集中管理 |
| 安全性和可维护性 | ⚠️ 容易被误操作泄露 | ✅ 安全性高、可审计 |
| 适合大规模项目 | ❌ 不推荐 | ✅ 推荐 |
⚒️ 如何使用?
-
自定义 TypeHandler 类,继承
BaseTypeHandler<T>(这里的T就是要被转换的类型),重写BaseTypeHandler<T>中的插入、获取值的方法 -
使用注解
@MappedTypes、@MappedJdbcTypes配置该自定义 TypeHandler 类 -
配置文件中指定自定义 TypeHandler 类的包路径**(泪的教训┭┮﹏┭┮)**
首先先给出要被转换的类型:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Encrypt {
private String value;
}然后自定义一个 TypeHandler 类,继承 BaseTypeHandler<T> 类,并且重写里面的方法,使用两个类注解完成配置:
**@MappedTypes(Encrypt.class)**
**@MappedJdbcTypes(JdbcType.** ***VARCHAR** ***)**
public class EncryptTypeHandler **extends BaseTypeHandler<Encrypt>** {
*/***
* * 将Encrypt对象插入到PreparedStatement中去*
* */*
* *@Override
public void setNonNullParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {
if(parameter == null || parameter.getValue() == null) {
*// 为空则插入null*
* *ps.setString(i, null);
return;
}
*// 插入加密后的Encrypt对象*
* *ps.setString(i, SecurityUtil.*aesEncrypt*(parameter.getValue()));
}
*/***
* * 下面三个方法都是获取PreparedStatement中的Encrypt对象*
* */*
* *@Override
public Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {
return decrypt(rs.getString(columnName));
}
@Override
public Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return decrypt(rs.getString(columnIndex));
}
@Override
public Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return decrypt(cs.getString(columnIndex));
}
private Encrypt decrypt(String value) {
if(!StringUtils.*hasText*(value)) {
return null;
}
return new Encrypt(SecurityUtil.*aesDecrypt*(value));
}
}最后记得在配置文件中指定自定义 TypeHandler 类的包路径:
*# 指定自定义 TypeHandler 类的包路径 *
mybatis.type-handlers-package=com.liren.lottery_system.common.handler四、创建奖品功能
① 上传文件细节
通常上传文件的路径,会在配置文件中指定,如下所示:
*## 指定文件上传路径 ##*
pic.local-path=E:\\git_repo\\code\\PM\\temp\\pic
spring.web.resources.static-locations=classpath:/static/,file:${pic.local-path}-
spring.web.resources.static-locations是 springboot 用于配置静态资源加载路径 的属性。 -
classpath指向 Java 类路径 (通常是src/main/resources) -
file指向 操作系统文件系统路径 (如D:\xxx\pic),要使用绝对路径 !
② 三种不同注解在请求时候的区别
| 注解 | 请求来源 | 适合格式 | 常用于 | 支持复杂对象 | 支持文件上传 |
|---|---|---|---|---|---|
| @RequestParam | URL参数 / form表单 | key=value(URL 或表单) | 简单查询、表单 | ❌(需手动封装) | ❌ |
| @RequestBody | 请求体 | JSON、XML 等结构化数据 | API接口 | ✅ | ❌(不能处理文件) |
| @RequestPart | multipart/form-data 的某部分 | 文件 + JSON | 文件上传 + 数据 | ✅ | ✅ |
③ BigDecimal 只能用 NotNull 而不能用 NotBlank
-
对于
String类型,常用@NotBlank -
对于
BigDecimal、Integer等类型,常用@NotNull -
对于
List类型等,常用@NotEmpty
@NotNull(message = "奖品价格不能为空!")
private BigDecimal price; ④ 请求时候出现的序列化处理问题
出现如下问题,本质上就是 spring 找不到合适的转换器来处理对应的请求,导致请求和后端需要的数据类型不匹配。

比如前端请求的 Content-Type 是 application/octet-stream(一种原始类型的二进制流),而请求体其实是 JSON(本质是一个结构体),此时 Spring 默认不会用 MappingJackson2HttpMessageConverter 来解析它,就出错了!
所以需要自定义一个转换器来处理一下 application/octet-stream 这种类型,
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
protected MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
*// MediaType.APPLICATION_OCTET_STREAM 表示这个转换器用于处理二进制流数据,通常用于文件上传。*
// 下面代码表示:如果请求的 Content-Type 是 application/octet-stream,可以用 Jackson 来处理!
* ***super(objectMapper, MediaType.** ***APPLICATION_OCTET_STREAM** ***)** ; // 💥关键之处
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
*// 转换器不用于写入(即不用于响应的序列化)*
* *return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}⑤ 混淆了前端 jquery 的格式字段
混淆了 contentType 和 dataType 两个字段,前者其实是指定发送时候的类型,后者是指定接收时候的类型,如下所示:
$.ajax({
url: '/api/data',
method: 'POST',
contentType: 'application/json', // 告诉服务器我发送的是 JSON
dataType: 'json', // 告诉 jQuery 期待接收的是 JSON
data: JSON.stringify({ name: "lirendada" })
});五、Optional 的使用:搭配 findFirst()
Optional<T> 是一个可能包含值也可能为空的容器对象,它替代了传统的 null 返回,用更安全、明确、链式的方式 处理值的存在或缺失。
常用方法如下所示:
| 方法名 | 说明 |
|---|---|
| of(T value) | 创建非空的 Optional,传 null 会抛异常 |
| ofNullable(T value) | 创建 Optional,允许 null |
| empty() | 创建一个空的 Optional |
| isPresent() | 判断是否有值(true/false) |
| ifPresent(Consumer) | 如果有值就执行某个操作 |
| get() | 获取值,若为空会抛 NoSuchElementException |
| orElse(T other) | 若有值则返回,否则返回默认值**(总会执行传入的表达式,无论是否为空)** |
| orElseGet(Supplier) | 若有值返回,否则调用函数获取默认值**(只有为空才会执行传入的表达式)** |
| orElseThrow() | 若为空抛异常(默认抛 NoSuchElementException) |
| orElseThrow(Supplier) | 若为空抛自定义异常 |
| map(Function) | 对值做转换操作(包装成新的 Optional) |
| flatMap(Function) | 链式调用,避免嵌套 Optional |
| filter(Predicate) | 判断值是否满足条件,不满足则变成 empty |
Optional<String> optionalName = Optional.of("lirendada");
// 1. 是否有值
optionalName.isPresent(); // true
// 2. 如果有值就打印
optionalName.ifPresent(name -> System.out.println("Name: " + name));
// 3. 获取值(不推荐直接用 get)
String name = optionalName.get();
// 4. 如果为空则返回默认值
String value = Optional.ofNullable(null).orElse("默认值"); // "默认值"
// 5. 用 orElseGet 延迟执行
String v = Optional.ofNullable(null).orElseGet(() -> getDefault());
// 6. 为空则抛异常
String error = Optional.ofNullable(null)
.orElseThrow(() -> new IllegalArgumentException("值不能为空"));
// 7. 转换值(map)
Optional<Integer> nameLength = optionalName.map(String::length);
// 8. 链式调用 + 判断
String result = Optional.of("lirendada")
.filter(s -> s.length() > 5)
.map(String::toUpperCase)
.orElse("太短了");
// 输出:LIRENDADA注意事项:
-
❌ 不要用
get()随意取值 ,如果值为 null 会直接抛异常。 -
✅ 推荐始终使用
orElse/ifPresent等方法来处理 Optional。
findFirst() 是 Java Stream 中的一个终止操作(Terminal Operation) ,作用是:
在过滤或处理后的流中,返回“第一个匹配的元素”(包装在
Optional中)。
如果不用 findFirst() 结束的话,就会得到一个 无限的 Stream 流对象 ,而不是最终的 PrizeEntity 元素。
Optional<PrizeEntity> optionalPrizeDTO = prizes.stream()
.filter(y -> y.getId().equals(x.getPrizeId()))
.findFirst();
// 如果 optionalPrizeDTO 不为空,执行该方法
optionalPrizeDTO.ifPresent(y -> {
// 设置详细属性
});六、实体类使用 String 还是枚举来表示状态呢?
-
对于要存入到数据库的实体类 ,优先推荐使用 String 或者 Integer 来表示状态(这样子可以省去转换类型的工作)
-
对于非存入到数据库的实体类 ,优先推荐使用枚举类型来表示状态,安全性等更高!
七、责任链模式 + 策略模式
在扭转活动、奖品、用户的状态时,如果不引入设计模式,则存在以下问题:
-
存在多个处理对象的顺序关系需要维护:奖品+活动状态扭转,活动需要依赖奖品状态改变而改变。可以看出请求依赖于多个决策点,因此处理顺序很重要,不易维护。
-
需要动态改变算法或行为:是否可以扭转状态的条件,若将来会发生改变,在这里不易维护。
-
系统的灵活性和可扩展性无法体现。
-
处理请求的复杂性不易维护。
因此需要对请求处理进行优化:如果请求的处理过程需要根据性能或其他因素进行优化,策略模式可以轻松替换或更新具体的处理策略,而责任链模式可以帮助管理这些策略的应用顺序。
- 策略模式 :(本质就是多态的应用)
-
以项目为例,需要定义不同的状态扭转策略,则先定义一个
AbstractStatusOperator抽象策略类,然后定义其策略实现类PrizeStatusOperator、ActivityStatusOperator和UserStatusOperator。每个具体的操作类都实现了AbstractStatusOperator定义的接口,代表了不同的状态转换策略。 -
以后需要拓展其它状态的扭转策略,只需要定义对应策略实现类,然后实现
AbstractStatusOperator定义的接口即可,大大提高了可拓展性、可维护性!
-

- 责任链模式 :定义
ConvertStatusService接口类,在ConvertStatusServiceImpl实现中,通过遍历statusOperator中的所有策略(多态性),并按照一定的顺序执行,形成了一个责任链,每个策略判断是否为自己的责任,如果是则处理请求。
八、Map 注入 Bean 时候的细节
在上面实现策略模式的时候,使用 Map 进行管理:
@Autowired
private final Map<String, AbstractStatusOperator> statusOperator = new HashMap<>();这个 Map<String, AbstractStatusOperator> 中的 String 不是必须要是类名(虽然可以是),它的实际含义取决于 Spring 容器中注入这些 AbstractStatusOperator实现时使用的 Bean 名称 。
九、邮箱发送
https://springdoc.cn/spring-boot-email/
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>配置文件:
*## 邮件 ##
spring.mail.host=smtp.qq.com
spring.mail.port=465
spring.mail.username=2916776007@qq.com
# 你的授权码:邮箱设置-》第三方服务-》开启IMAP/SMTP服务-》获取授权码
spring.mail.password=tlgccbesycdldfbi
spring.mail.default-encoding=UTF-8
spring.mail.protocol=smtps
spring.mail.properties.mail.smtp.auth=true
**spring.mail.properties.mail.smtp.ssl.enable=true*编写工具类:
@Component
@Slf4j
public class MailUtil {
@Value(value = "${spring.mail.username}")
private String from;
@Autowired
private JavaMailSender mailSender;
public Boolean sendSampleMail(String to, String subject, String context) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(context);
try {
mailSender.send(message);
} catch (Exception e) {
*log*.error("向{}发送邮件失败!", to, e);
return false;
}
return true;
}
}十、spring 项目使用线程池
ThreadPoolTaskExecutor 是 Spring 框架提供的线程池执行器,是对 JDK 原生 ThreadPoolExecutor 的一个 友好封装 ,实现了 TaskExecutor 接口(Spring 的线程执行框架核心接口)。
首先进行配置:
@Configuration
@EnableAsync
@Data
public class ExecutorConfig {
@Value("${async.executor.thread.core_pool_size}")
private int corePoolSize;
@Value("${async.executor.thread.max_pool_size}")
private int maxPoolSize;
@Value("${async.executor.thread.queue_capacity}")
private int queueCapacity;
@Value("${async.executor.thread.name.prefix}")
private String namePrefix;
@Bean(name = "asyncServiceExecutor")
public ThreadPoolTaskExecutor asyncServiceExecutor(){
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
threadPoolTaskExecutor.setQueueCapacity(queueCapacity);
threadPoolTaskExecutor.setKeepAliveSeconds(3);
threadPoolTaskExecutor.setThreadNamePrefix(namePrefix);
*// rejection-policy:当pool已经达到max size的时候,如何处理新任务*
* // CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行*
* *threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
*// 加载*
* *threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}使用时候直接引入 Bean 即可:
@Autowired
private ThreadPoolTaskExecutor asyncServiceExecutor;
public void process(Map<String, String> msg) {
// ...
* *asyncServiceExecutor.execute(() -> func()); // 执行并发任务
// ...
}
private void func() {
// 要处理的并发任务
}