一、Spring Boot 接口三种入参请求和接收方式
Spring Boot 接口常见的入参方式主要有三种:
路径参数
查询参数
JSON 请求体
1. 路径参数:@PathVariable
请求地址:
GET /users/1001
Controller:
@GetMapping("/users/{id}")
public UserVO getUser(
@PathVariable Long id
) {
return userService.getUser(id);
}
也可以明确指定参数名:
@GetMapping("/users/{userId}")
public UserVO getUser(
@PathVariable("userId") Long id
) {
return userService.getUser(id);
}
适合场景:
根据 ID 查询
根据编号删除
访问明确的单个资源
2. 查询参数:@RequestParam
请求地址:
GET /users?page=1&size=10&status=1
Controller:
@GetMapping("/users")
public List<UserVO> list(
@RequestParam Integer page,
@RequestParam Integer size,
@RequestParam(required = false) Integer status
) {
return userService.list(page, size, status);
}
设置默认值:
@GetMapping("/users")
public List<UserVO> list(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size
) {
return userService.list(page, size);
}
参数较多时,可以直接使用对象接收:
@Data
public class UserQuery {
private Integer page;
private Integer size;
private Integer status;
}
@GetMapping("/users")
public List<UserVO> list(UserQuery query) {
return userService.list(query);
}
适合场景:
分页查询
条件筛选
简单表单参数
3. JSON 请求体:@RequestBody
请求:
{
"username": "zhangsan",
"age": 30
}
DTO:
@Data
public class UserCreateDTO {
private String username;
private Integer age;
}
Controller:
@PostMapping("/users")
public Long create(
@RequestBody UserCreateDTO dto
) {
return userService.create(dto);
}
适合场景:
新增数据
修改数据
复杂对象
嵌套对象
数组和集合
4. 三种方式怎么选
@PathVariable:资源标识
@RequestParam:查询条件
@RequestBody:复杂业务数据
例如:
@PutMapping("/users/{id}")
public void update(
@PathVariable Long id,
@RequestBody UserUpdateDTO dto
) {
}
这里:
id 表示修改哪个用户
dto 表示修改哪些内容
二、Comparable 和 Comparator 实现排序
Java 中对象排序常见有两种方式:
Comparable:对象自己定义排序规则
Comparator:外部定义排序规则
1. Comparable
实体类实现 Comparable:
@Data
@AllArgsConstructor
public class User implements Comparable<User> {
private Long id;
private String username;
private Integer age;
@Override
public int compareTo(User other) {
return Integer.compare(this.age, other.age);
}
}
排序:
List<User> users = new ArrayList<>();
users.add(new User(1L, "zhangsan", 30));
users.add(new User(2L, "lisi", 20));
users.add(new User(3L, "wangwu", 25));
Collections.sort(users);
按照年龄升序排列。
降序:
@Override
public int compareTo(User other) {
return Integer.compare(other.age, this.age);
}
2. Comparator
不修改实体类,在外部指定排序规则:
users.sort(
Comparator.comparing(User::getAge)
);
降序:
users.sort(
Comparator.comparing(User::getAge)
.reversed()
);
多字段排序:
users.sort(
Comparator.comparing(User::getAge)
.thenComparing(User::getUsername)
);
处理 null:
users.sort(
Comparator.comparing(
User::getAge,
Comparator.nullsLast(
Integer::compareTo
)
)
);
3. 两者区别
Comparable:类本身只有一种主要排序规则
Comparator:可以定义多种排序规则
实际开发中,Comparator 使用更加灵活。
三、遍历集合时 remove 或 add 的注意事项
遍历集合时直接执行 remove() 或 add(),很容易出现:
ConcurrentModificationException
1. 错误示例
List<String> list =
new ArrayList<>(
List.of("A", "B", "C")
);
for (String item : list) {
if ("B".equals(item)) {
list.remove(item);
}
}
增强 for 底层使用迭代器。
遍历过程中直接修改原集合,会破坏迭代器状态。
2. 使用 Iterator 删除
Iterator<String> iterator =
list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if ("B".equals(item)) {
iterator.remove();
}
}
这是遍历时删除元素的标准写法。
3. 使用 removeIf
条件删除可以直接写:
list.removeIf(
item -> "B".equals(item)
);
这种写法更简洁。
4. 普通 for 循环倒序删除
for (int i = list.size() - 1; i >= 0; i--) {
if ("B".equals(list.get(i))) {
list.remove(i);
}
}
倒序删除不会影响前面还没有遍历的下标。
5. 遍历时新增
不建议在增强 for 中直接新增:
for (String item : list) {
list.add("D");
}
可以先保存待新增数据:
List<String> newItems =
new ArrayList<>();
for (String item : list) {
if ("A".equals(item)) {
newItems.add("D");
}
}
list.addAll(newItems);
四、BeanUtils.copyProperties 复制对象避坑
Spring 提供的:
BeanUtils.copyProperties(source, target);
可以复制两个对象中名称和类型相同的属性。
1. 基本使用
UserEntity entity = new UserEntity();
entity.setId(1001L);
entity.setUsername("zhangsan");
UserVO vo = new UserVO();
BeanUtils.copyProperties(entity, vo);
注意参数顺序:
BeanUtils.copyProperties(源对象, 目标对象);
很容易写反。
2. 只会浅拷贝
假设对象中有一个集合:
private List<String> roles;
复制以后:
source.getRoles() == target.getRoles()
结果可能是:
true
也就是说两个对象引用的是同一个集合。
修改其中一个,另一个也可能受到影响。
3. 属性名称相同但类型不同,不会复制
例如:
UserEntity:
private Long status;
UserVO:
private String status;
虽然字段名一样,但类型不同,不会自动转换。
需要手动处理:
vo.setStatus(
String.valueOf(entity.getStatus())
);
4. null 会覆盖原值
例如更新对象:
UserUpdateDTO dto = new UserUpdateDTO();
dto.setUsername(null);
执行:
BeanUtils.copyProperties(dto, entity);
会把原来的 username 覆盖成 null。
可以忽略指定字段:
BeanUtils.copyProperties(
dto,
entity,
"username"
);
但如果需要自动忽略所有 null,通常要自己封装工具方法,或者使用 MapStruct。
5. 不要过度依赖 BeanUtils
字段少时,手动赋值更加清晰:
UserVO vo = new UserVO();
vo.setId(entity.getId());
vo.setUsername(entity.getUsername());
字段很多、转换复杂时,可以考虑:
MapStruct
五、Optional 常用方法实践
Optional 主要用于处理可能为 null 的返回值,减少简单的空判断。
但它不能完全替代 if-else。
1. of
对象不能为空:
Optional<String> optional =
Optional.of("hello");
如果传入 null,会抛出:
NullPointerException
2. ofNullable
允许传入 null:
Optional<String> optional =
Optional.ofNullable(username);
这是最常用的创建方式。
3. isPresent
判断是否有值:
if (optional.isPresent()) {
System.out.println(optional.get());
}
不过这种写法和普通 if 区别不大。
4. ifPresent
有值时执行:
optional.ifPresent(
value -> System.out.println(value)
);
例如:
Optional.ofNullable(user)
.ifPresent(this::saveLog);
5. orElse
为空时返回默认值:
String username =
optional.orElse("默认用户");
注意:
optional.orElse(createDefaultUser());
即使 optional 有值,createDefaultUser() 也可能被执行。
6. orElseGet
为空时才执行:
String username =
optional.orElseGet(
() -> createDefaultUsername()
);
如果默认值需要计算或查询,优先使用:
orElseGet()
7. orElseThrow
为空时抛异常:
User user =
Optional.ofNullable(
userMapper.selectById(id)
).orElseThrow(
() -> new RuntimeException(
"用户不存在"
)
);
这是业务查询中很常见的写法。
8. map
对内部值进行转换:
String username =
Optional.ofNullable(user)
.map(User::getUsername)
.orElse("未知用户");
避免写成:
if (user != null) {
if (user.getUsername() != null) {
}
}
9. filter
根据条件过滤:
User user =
Optional.ofNullable(
userMapper.selectById(id)
).filter(
item -> item.getStatus() == 1
).orElseThrow(
() -> new RuntimeException(
"用户不可用"
)
);
10. flatMap
当转换结果本身也是 Optional 时使用:
Optional<String> result =
optionalUser.flatMap(
this::getUsernameOptional
);
避免出现:
Optional<Optional<String>>
11. Optional 不适合滥用
不建议用于实体字段:
private Optional<String> username;
也不建议方法参数大量使用:
public void save(Optional<User> user)
更适合作为:
方法返回值
六、serialVersionUID 为什么要手动赋值
实现 Serializable 后,通常会看到:
private static final long serialVersionUID = 1L;
完整示例:
@Data
public class User
implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
}
1. serialVersionUID 的作用
它相当于序列化类的版本号。
序列化时会记录版本号。
反序列化时会检查:
当前类版本号
是否和序列化数据中的版本号一致
不一致可能抛出:
InvalidClassException
2. 不手动赋值的问题
如果没有手动定义,Java 会根据类结构自动生成一个值。
当类发生变化时,例如:
新增字段
删除字段
修改方法
调整结构
自动生成的版本号可能变化。
之前序列化的数据就可能无法反序列化。
3. 手动定义的意义
private static final long serialVersionUID = 1L;
表示开发者主动控制版本。
普通新增字段时,可以继续保持:
1L
如果类结构发生重大变化,并且不希望兼容旧数据,可以改成:
2L
4. 常见使用场景
Java 原生序列化
Session 持久化
对象写入文件
RPC
缓存对象
如果只是普通 DTO,并且项目完全不使用 Java 原生序列化,实际影响可能不大。
七、引用拷贝、浅拷贝和深拷贝
Java 对象复制时,需要区分:
引用拷贝
浅拷贝
深拷贝
1. 引用拷贝
User user1 = new User();
User user2 = user1;
这里没有创建新对象。
user1 == user2
结果是:
true
两个变量指向同一个对象。
修改:
user2.setUsername("lisi");
user1 也会变化。
2. 浅拷贝
浅拷贝会创建一个新对象,但对象内部的引用类型字段仍然共享。
例如:
@Data
public class User implements Cloneable {
private String username;
private Address address;
@Override
protected User clone()
throws CloneNotSupportedException {
return (User) super.clone();
}
}
使用:
User user1 = new User();
user1.setUsername("zhangsan");
user1.setAddress(new Address("昆明"));
User user2 = user1.clone();
此时:
user1 == user2
是:
false
但:
user1.getAddress() == user2.getAddress()
可能是:
true
修改 user2 的地址,会影响 user1。
3. 深拷贝
深拷贝不仅创建外层对象,也会复制内部引用对象。
手动实现:
@Override
protected User clone()
throws CloneNotSupportedException {
User user = (User) super.clone();
user.setAddress(
this.address.clone()
);
return user;
}
这样:
user1.getAddress()
== user2.getAddress()
结果是:
false
4. BeanUtils 属于哪种
BeanUtils.copyProperties(
source,
target
);
通常属于浅拷贝。
基本类型和不可变对象看不出问题,但集合、自定义对象仍可能共享引用。
5. 常见深拷贝方式
手动创建新对象
实现 clone
JSON 序列化再反序列化
MapStruct 自定义转换
复制构造方法
实际业务中,推荐使用:
手动转换
复制构造方法
MapStruct
逻辑更加清晰。
八、@Async 使用避坑
Spring 的 @Async 可以让方法在线程池中异步执行。
1. 开启异步
启动类或配置类添加:
@EnableAsync
@Configuration
public class AsyncConfig {
}
2. 基本使用
@Service
public class MessageService {
@Async
public void sendMessage() {
System.out.println(
Thread.currentThread().getName()
);
}
}
调用:
messageService.sendMessage();
调用线程不会等待方法执行结束。
3. 建议指定线程池
不要长期依赖默认线程池。
@Bean("messageExecutor")
public Executor messageExecutor() {
ThreadPoolTaskExecutor executor =
new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("message-");
executor.initialize();
return executor;
}
使用:
@Async("messageExecutor")
public void sendMessage() {
}
4. 同类内部调用会失效
错误示例:
@Service
public class OrderService {
public void createOrder() {
this.sendMessage();
}
@Async
public void sendMessage() {
}
}
this.sendMessage() 没有经过 Spring 代理对象,异步不会生效。
推荐拆分到另一个 Bean:
@Service
@RequiredArgsConstructor
public class OrderService {
private final MessageService messageService;
public void createOrder() {
messageService.sendMessage();
}
}
5. private 方法不会生效
错误写法:
@Async
private void sendMessage() {
}
代理对象无法正常代理私有方法。
一般应该使用:
public
6. 异常不容易被调用者捕获
返回 void 时:
@Async
public void execute() {
throw new RuntimeException("异步异常");
}
调用方无法通过普通 try-catch 捕获。
可以返回:
CompletableFuture<T>
例如:
@Async("messageExecutor")
public CompletableFuture<String> execute() {
return CompletableFuture.completedFuture(
"success"
);
}
7. 事务上下文不会自动传递
异步方法运行在另一个线程。
原线程中的:
事务
ThreadLocal
请求上下文
部分安全上下文
不一定自动传递。
因此不要默认认为异步方法和调用方处于同一个事务。
8. 异步任务可能丢失
如果应用刚提交异步任务就关闭,任务可能没有完成。
重要业务不要只依赖内存异步线程。
例如:
支付通知
订单状态同步
重要消息发送
更适合使用消息队列。
九、MyBatis 一级缓存避坑
MyBatis 一级缓存默认存在,作用范围是:
同一个 SqlSession
在 Spring 项目中,通常可以理解为:
同一个事务中的同一个 SqlSession
1. 基本效果
User user1 =
userMapper.selectById(1001L);
User user2 =
userMapper.selectById(1001L);
如果处于同一个 SqlSession,第二次查询可能直接从一级缓存获取,不再执行 SQL。
2. 增删改会清空缓存
例如:
User user1 =
userMapper.selectById(1001L);
userMapper.updateById(user);
User user2 =
userMapper.selectById(1001L);
执行更新后,一级缓存通常会被清空。
第二次查询会重新访问数据库。
3. 不同 SqlSession 不共享
两个不同请求、两个不同事务,通常不共享一级缓存。
因此一级缓存不能当成全局缓存使用。
它和 Redis 完全不是一个概念。
4. 同一个对象引用可能带来误解
第一次查询:
User user1 =
userMapper.selectById(1001L);
修改对象但没有执行更新:
user1.setUsername("lisi");
再次查询:
User user2 =
userMapper.selectById(1001L);
在某些情况下,可能直接从一级缓存返回对象。
此时:
user1 == user2
可能是:
true
看起来像数据库已经变化,实际上可能只是内存中的缓存对象被修改。
5. 批量操作和循环查询要注意
例如:
for (Long id : ids) {
userMapper.selectById(id);
}
如果数据量很大,会产生大量查询,也可能让本地缓存占用更多内存。
更合理的方式是批量查询:
userMapper.selectBatchIds(ids);
6. 手动清理一级缓存
可以调用:
sqlSession.clearCache();
但一般不需要手动清理。
如果业务频繁依赖手动清缓存,通常说明事务边界或查询设计需要调整。
7. 一级缓存的核心注意点
可以简单记住:
作用域是 SqlSession
不能跨请求共享
增删改后通常清空
返回的可能是同一个对象引用
不能替代 Redis
MyBatis 一级缓只在当前 SqlSession 内生效,不能当成业务缓存
```
这些知识本身不复杂,但如果不了解边界,就很容易在实际项目中出现一些比较隐蔽的问题。
评论
填写昵称与邮箱即可评论,无需登录。