Skip to content

最佳实践

作者:唐亚峰 | 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()));

下一步