最佳实践
作者:唐亚峰 | battcn
字数统计:1.6k 字
学习目标
掌握 Wemirr Platform 开发中的最佳实践和常见问题解决方案
代码组织
分层架构
Controller - 参数校验、调用 Service、结果转换
│
Service - 业务逻辑、事务管理、调用其他 Service/Mapper
│
Mapper - 数据访问、SQL 操作
│
Entity - 数据库映射实体包结构规范
com.wemirr.xxx/
├── controller/ # 控制器
│ └── XxxController.java
├── service/ # 服务层
│ ├── XxxService.java
│ └── impl/
│ └── XxxServiceImpl.java
├── mapper/ # 数据访问层
│ └── XxxMapper.java
├── entity/ # 实体类
│ └── Xxx.java
├── dto/ # 数据传输对象
│ ├── XxxDTO.java
│ └── XxxQuery.java
├── vo/ # 视图对象
│ └── XxxVO.java
├── convert/ # 对象转换器
│ └── XxxConvert.java
├── enums/ # 枚举
│ └── XxxStatusEnum.java
└── constants/ # 常量
└── XxxConstants.java对象转换
java
// 使用 MapStruct 进行对象转换
@Mapper(componentModel = "spring")
public interface OrderConvert {
OrderConvert INSTANCE = Mappers.getMapper(OrderConvert.class);
OrderVO toVO(Order order);
List<OrderVO> toVOList(List<Order> orders);
Order toEntity(OrderDTO dto);
@Mapping(target = "statusName", expression = "java(order.getStatus().getDesc())")
OrderDetailVO toDetailVO(Order order);
}
// 使用
OrderVO vo = OrderConvert.INSTANCE.toVO(order);异常处理
统一异常体系
java
// 业务异常基类
public class BizException extends RuntimeException {
private Integer code;
private String message;
public BizException(String message) {
this(ResultCode.BIZ_ERROR.getCode(), message);
}
public BizException(ResultCode resultCode) {
this(resultCode.getCode(), resultCode.getMessage());
}
public BizException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
}
// 结果码枚举
public enum ResultCode {
SUCCESS(0, "成功"),
BIZ_ERROR(10000, "业务异常"),
PARAM_ERROR(10001, "参数错误"),
NOT_FOUND(10002, "资源不存在"),
UNAUTHORIZED(10003, "未授权"),
FORBIDDEN(10004, "禁止访问"),
;
private final Integer code;
private final String message;
}断言式异常
java
// 工具类
public class BizAssert {
public static void notNull(Object obj, String message) {
if (obj == null) {
throw new BizException(message);
}
}
public static void isTrue(boolean expression, String message) {
if (!expression) {
throw new BizException(message);
}
}
public static void notEmpty(Collection<?> collection, String message) {
if (collection == null || collection.isEmpty()) {
throw new BizException(message);
}
}
}
// 使用
public void updateOrder(Long orderId, OrderDTO dto) {
Order order = orderMapper.selectById(orderId);
BizAssert.notNull(order, "订单不存在");
BizAssert.isTrue(order.canUpdate(), "订单状态不允许修改");
// 业务逻辑
}事务管理
事务注解使用
java
@Service
@RequiredArgsConstructor
public class OrderService {
// 基本事务
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 所有操作在同一事务中
}
// 只读事务
@Transactional(readOnly = true)
public OrderVO getDetail(Long id) {
// 只读操作
}
// 新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String message) {
// 无论外层事务是否回滚,日志都会保存
}
}事务失效场景
java
// ❌ 同类方法调用,事务失效
@Service
public class OrderService {
public void process() {
this.createOrder(); // 事务失效
}
@Transactional
public void createOrder() {
// ...
}
}
// ✅ 解决方案1:注入自己
@Service
public class OrderService {
@Autowired
private OrderService self;
public void process() {
self.createOrder(); // 事务生效
}
}
// ✅ 解决方案2:使用 AopContext
public void process() {
((OrderService) AopContext.currentProxy()).createOrder();
}
// ❌ 异常被捕获,事务失效
@Transactional
public void createOrder() {
try {
// 业务逻辑
} catch (Exception e) {
log.error("error", e);
// 异常被吃掉,事务不会回滚
}
}
// ✅ 正确做法
@Transactional
public void createOrder() {
try {
// 业务逻辑
} catch (Exception e) {
log.error("error", e);
throw e; // 重新抛出
}
}安全实践
参数校验
java
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20个字符")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字、下划线")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度6-20个字符")
private String password;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
}
// Controller
@PostMapping
public Result<Void> create(@RequestBody @Valid UserDTO dto) {
// 参数已校验
}SQL 注入防护
java
// ❌ 危险:字符串拼接
String sql = "SELECT * FROM user WHERE name = '" + name + "'";
// ✅ 安全:使用参数绑定
@Select("SELECT * FROM user WHERE name = #{name}")
User selectByName(@Param("name") String name);
// ✅ 安全:使用 Wrapper
lambdaQuery().eq(User::getName, name).list();XSS 防护
java
// 配置 XSS 过滤器
@Configuration
public class XssConfig {
@Bean
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new XssFilter());
registration.addUrlPatterns("/*");
registration.setOrder(1);
return registration;
}
}
// 或使用 Hutool 的 HtmlUtil
String safe = HtmlUtil.escape(userInput);敏感数据脱敏
java
// 返回 VO 时脱敏
@Data
public class UserVO {
private Long id;
private String username;
@JsonSerialize(using = MobileSerializer.class)
private String mobile; // 138****8888
@JsonSerialize(using = IdCardSerializer.class)
private String idCard; // 110***********1234
}
// 自定义序列化器
public class MobileSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (StrUtil.isNotBlank(value) && value.length() == 11) {
gen.writeString(value.substring(0, 3) + "****" + value.substring(7));
} else {
gen.writeString(value);
}
}
}日志规范
日志级别
| 级别 | 使用场景 |
|---|---|
| ERROR | 影响业务的错误 |
| WARN | 潜在问题、可恢复的错误 |
| INFO | 重要业务节点、状态变化 |
| DEBUG | 开发调试信息 |
| TRACE | 非常详细的调试信息 |
日志格式
java
// ✅ 好的日志
log.info("创建订单成功, orderId={}, userId={}, amount={}", orderId, userId, amount);
log.error("调用支付接口失败, orderId={}, errorCode={}", orderId, errorCode, e);
// ❌ 不好的日志
log.info("创建订单成功"); // 缺少关键信息
log.info("创建订单成功, 订单: " + order); // 字符串拼接性能差
log.error(e.getMessage()); // 丢失堆栈信息日志配置
xml
<!-- logback-spring.xml -->
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</configuration>接口设计
RESTful 规范
GET /api/orders # 列表查询
GET /api/orders/{id} # 单个查询
POST /api/orders # 创建
PUT /api/orders/{id} # 全量更新
PATCH /api/orders/{id} # 部分更新
DELETE /api/orders/{id} # 删除
# 子资源
GET /api/orders/{id}/items # 获取订单项
POST /api/orders/{id}/items # 添加订单项
# 操作
POST /api/orders/{id}/cancel # 取消订单
POST /api/orders/{id}/pay # 支付订单统一响应格式
java
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(0);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> fail(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}分页响应
java
// 分页查询
@GetMapping
public Result<IPage<OrderVO>> page(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
OrderQuery query
) {
IPage<OrderVO> page = orderService.page(current, size, query);
return Result.success(page);
}
// 响应示例
{
"code": 0,
"message": "success",
"data": {
"records": [...],
"total": 100,
"size": 10,
"current": 1,
"pages": 10
}
}测试实践
单元测试
java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderMapper orderMapper;
@Mock
private StockService stockService;
@InjectMocks
private OrderServiceImpl orderService;
@Test
void createOrder_Success() {
// Given
OrderDTO dto = new OrderDTO();
dto.setProductId(1L);
dto.setQuantity(2);
when(stockService.checkStock(1L, 2)).thenReturn(true);
// When
orderService.createOrder(dto);
// Then
verify(orderMapper).insert(any(Order.class));
verify(stockService).deduct(1L, 2);
}
@Test
void createOrder_StockNotEnough() {
// Given
OrderDTO dto = new OrderDTO();
dto.setProductId(1L);
dto.setQuantity(100);
when(stockService.checkStock(1L, 100)).thenReturn(false);
// When & Then
assertThrows(BizException.class, () -> orderService.createOrder(dto));
}
}接口测试
java
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void createOrder_Success() throws Exception {
OrderDTO dto = new OrderDTO();
dto.setProductId(1L);
dto.setQuantity(2);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtil.toJson(dto)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0));
}
}常见问题
1. 循环依赖
java
// ❌ 循环依赖
@Service
public class AService {
@Autowired
private BService bService;
}
@Service
public class BService {
@Autowired
private AService aService;
}
// ✅ 解决方案:使用 @Lazy
@Service
public class AService {
@Lazy
@Autowired
private BService bService;
}2. N+1 查询
java
// ❌ N+1 问题
List<Order> orders = orderMapper.selectList(null);
for (Order order : orders) {
User user = userMapper.selectById(order.getUserId()); // N 次查询
order.setUserName(user.getName());
}
// ✅ 批量查询
List<Order> orders = orderMapper.selectList(null);
Set<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toSet());
Map<Long, User> userMap = userMapper.selectBatchIds(userIds).stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
orders.forEach(order -> order.setUserName(userMap.get(order.getUserId()).getName()));