代码Review规范指南及实践案例
在项目开发过程中发现很多代码的坏味道,针对发现的问题进行分析和解答。本文将从异常处理、日志打印、项目分层、包结构划分、DTO使用、面向接口编程、单元测试等多个维度,提供Code Review的规范指南和实践案例。
一、异常处理
1.1 核心问题
在日常开发中,我们经常面临以下异常处理问题:
- 异常要抛出去还是catch处理?
 - 写代码的过程中是否有主动抛出过异常?
 - 要抛出什么类型的异常,Exception、Throwable或其它?
 
1.2 异常处理准则
(1)低层级异常处理
什么是低层级?
这是与第三方代码集成的级别,例如ORM工具或任何执行IO操作的库(通过HTTP访问资源、读取文件、保存到数据库等)。也就是说,您离开应用程序的内部代码以与其他组件交互的级别。
处理准则:
(2)高层级异常处理
什么是高层级?
这将是在将异常直接抛给用户之前您可以处理异常的最后一个地方。
处理准则:
(3)什么时候抛出异常
在开发库的上下文中更容易理解。当您遇到错误时,您应该抛出错误,除了让您的API的使用者知道并让他们决定之外,您无能为力。
假设您是某个数据访问库的开发人员。当您遇到网络错误时,除了抛出异常之外,您无能为力。从数据访问库的角度来看,这是一个不可逆转的错误。
1.3 准则背后的原理
1.4 项目异常处理方法
(1)工具类中的异常处理
// 推荐做法
public class FileUtils {
    public static String readFile(String filePath) throws IOException {
        try {
            return Files.readString(Paths.get(filePath));
        } catch (IOException e) {
            // 添加上下文信息后重新抛出
            throw new IOException("Failed to read file: " + filePath, e);
        }
    }
}
(2)Controller的参数校验
// 推荐做法
@RestController
public class UserController {
    
    @PostMapping("/users")
    public Response createUser(@Valid @RequestBody UserCreateRequest request) {
        try {
            User user = userService.createUser(request);
            return Response.success(user);
        } catch (BusinessException e) {
            log.error("Create user failed: {}", e.getMessage(), e);
            return Response.error(e.getMessage());
        } catch (Exception e) {
            log.error("Unexpected error when creating user", e);
            return Response.error("系统异常,请稍后重试");
        }
    }
}
(3)异常处理的经典案例
// 不推荐的做法
public void processData() {
    try {
        // 业务逻辑
        dataService.process();
    } catch (Exception e) {
        // 吞掉异常,不做任何处理
    }
}
// 推荐的做法
public void processData() throws DataProcessException {
    try {
        dataService.process();
    } catch (DataAccessException e) {
        // 转换为业务异常
        throw new DataProcessException("数据处理失败", e);
    }
}
二、日志打印
2.1 日志输出准则
(1)Good things to log
(2)记录性能数据
@Service
public class OrderService {
    
    public void processOrder(Long orderId) {
        long startTime = System.currentTimeMillis();
        log.info("开始处理订单,orderId: {}", orderId);
        
        try {
            // 业务处理逻辑
            doProcessOrder(orderId);
            
            long duration = System.currentTimeMillis() - startTime;
            log.info("订单处理完成,orderId: {}, 耗时: {}ms", orderId, duration);
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            log.error("订单处理失败,orderId: {}, 耗时: {}ms", orderId, duration, e);
            throw e;
        }
    }
}
(3)Bad things to log
2.2 日志输出案例
(1)系统入口增加日志记录
@RestController
@Slf4j
public class ApiController {
    
    @PostMapping("/api/data")
    public Response processData(@RequestBody DataRequest request) {
        String requestId = UUID.randomUUID().toString();
        log.info("接收到数据处理请求,requestId: {}, request: {}", requestId, request);
        
        try {
            DataResponse response = dataService.process(request);
            log.info("数据处理完成,requestId: {}, response: {}", requestId, response);
            return Response.success(response);
        } catch (Exception e) {
            log.error("数据处理失败,requestId: {}", requestId, e);
            return Response.error("处理失败");
        }
    }
}
(2)请求第三方或远程接口记录日志
@Service
@Slf4j
public class ThirdPartyService {
    
    public ApiResponse callThirdPartyApi(ApiRequest request) {
        log.info("调用第三方API开始,url: {}, request: {}", apiUrl, request);
        
        try {
            ApiResponse response = restTemplate.postForObject(apiUrl, request, ApiResponse.class);
            log.info("调用第三方API成功,url: {}, response: {}", apiUrl, response);
            return response;
        } catch (Exception e) {
            log.error("调用第三方API失败,url: {}, request: {}", apiUrl, request, e);
            throw new ThirdPartyApiException("第三方API调用失败", e);
        }
    }
}
(3)记录系统关键的启动参数
@Component
@Slf4j
public class ApplicationStartupListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Value("${app.version}")
    private String appVersion;
    
    @Value("${spring.profiles.active}")
    private String activeProfile;
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        log.info("应用启动完成");
        log.info("应用版本: {}", appVersion);
        log.info("活动配置: {}", activeProfile);
        log.info("JVM信息: {}", System.getProperty("java.version"));
        log.info("系统时区: {}", TimeZone.getDefault().getID());
    }
}
三、项目分层
3.1 项目分层准则
3.2 项目案例
(1)创建短链接口
// Controller层
@RestController
@RequestMapping("/api/shortlink")
@Slf4j
public class ShortLinkController {
    
    @Autowired
    private ShortLinkService shortLinkService;
    
    @PostMapping("/create")
    public Response<ShortLinkDTO> createShortLink(@Valid @RequestBody CreateShortLinkRequest request) {
        log.info("创建短链请求: {}", request);
        ShortLinkDTO result = shortLinkService.createShortLink(request);
        return Response.success(result);
    }
}
// Service层
@Service
@Transactional
@Slf4j
public class ShortLinkService {
    
    @Autowired
    private ShortLinkDAO shortLinkDAO;
    
    public ShortLinkDTO createShortLink(CreateShortLinkRequest request) {
        // 业务逻辑处理
        String shortCode = generateShortCode();
        
        ShortLink shortLink = new ShortLink();
        shortLink.setOriginalUrl(request.getOriginalUrl());
        shortLink.setShortCode(shortCode);
        shortLink.setCreateTime(new Date());
        
        shortLinkDAO.save(shortLink);
        
        return convertToDTO(shortLink);
    }
    
    private String generateShortCode() {
        // 短码生成逻辑
        return RandomStringUtils.randomAlphanumeric(8);
    }
}
// DAO层
@Repository
public class ShortLinkDAO {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    public void save(ShortLink shortLink) {
        String sql = "INSERT INTO short_link (original_url, short_code, create_time) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, shortLink.getOriginalUrl(), shortLink.getShortCode(), shortLink.getCreateTime());
    }
}
四、包结构划分
4.1 包结构划分准则
4.2 包名命名
- 包名应该使用小写字母
 - 多个单词用点分隔
 - 避免使用下划线或其他特殊字符
 - 包名应该有意义,能够清楚表达包的用途
 
4.3 项目案例
(1)回调接口提取到单独的包中以做区分
// 原有结构
com.example.project.controller.UserController
com.example.project.controller.CallbackController
// 优化后结构
com.example.project.controller.UserController
com.example.project.callback.MessageCallbackController
com.example.project.callback.PaymentCallbackController
(2)使用package-info.java
/**
 * 回调接口包
 * 
 * 包含所有第三方平台的回调接口实现,包括:
 * - 消息状态回调
 * - 支付状态回调
 * - 审核结果回调
 * 
 * @author development team
 * @version 1.0
 * @since 2024-01-01
 */
package com.example.project.callback;
(3)Controller根据业务进行分包
// 按业务领域分包
com.example.project.controller.user.UserController
com.example.project.controller.user.UserProfileController
com.example.project.controller.order.OrderController
com.example.project.controller.order.OrderPaymentController
com.example.project.controller.admin.AdminController
五、DTO的使用
5.1 处理准则
(1)分层领域模型规约(阿里规约)
(2)使用DTO的优点
// Entity - 数据库实体
@Entity
@Table(name = "user")
public class User {
    private Long id;
    private String username;
    private String password;  // 敏感信息
    private String email;
    private Date createTime;
    private Date updateTime;
    // getters and setters
}
// DTO - 数据传输对象
public class UserDTO {
    private Long id;
    private String username;
    private String email;
    private String createTime;  // 格式化后的时间
    // getters and setters
}
// Service层使用
@Service
public class UserService {
    
    public UserDTO getUserById(Long id) {
        User user = userDAO.findById(id);
        return convertToDTO(user);  // 转换时过滤敏感信息
    }
    
    private UserDTO convertToDTO(User user) {
        UserDTO dto = new UserDTO();
        dto.setId(user.getId());
        dto.setUsername(user.getUsername());
        dto.setEmail(user.getEmail());
        dto.setCreateTime(DateUtils.format(user.getCreateTime()));  // 格式化时间
        return dto;
    }
}
5.2 推荐案例
(1)应用平台案例
// 请求DTO
public class SendMessageRequest {
    @NotBlank(message = "接收者不能为空")
    private String receiver;
    
    @NotBlank(message = "消息内容不能为空")
    private String content;
    
    private String messageType;
    // getters and setters
}
// 响应DTO
public class SendMessageResponse {
    private String messageId;
    private String status;
    private String message;
    private Long timestamp;
    // getters and setters
}
// 实体类
@Entity
public class Message {
    private Long id;
    private String messageId;
    private String sender;
    private String receiver;
    private String content;
    private String status;
    private Date createTime;
    private Date sendTime;
    // getters and setters
}
六、面向接口编程
6.1 编码准则
(1)面向接口编程和面向对象编程
(2)接口的本质
接口定义了一个契约,规定了实现类必须提供的方法。这样可以实现多态,同一个接口可以有多种不同的实现。
6.2 项目案例
(1)消息接口的返回值
// 消息发送接口定义
public interface MessageSender {
    SendResult send(SendMessageRequest request);
}
// 短信发送实现
@Component("smsSender")
public class SmsSender implements MessageSender {
    @Override
    public SendResult send(SendMessageRequest request) {
        // 短信发送逻辑
        return new SendResult("SMS", true, "发送成功");
    }
}
// 邮件发送实现
@Component("emailSender")
public class EmailSender implements MessageSender {
    @Override
    public SendResult send(SendMessageRequest request) {
        // 邮件发送逻辑
        return new SendResult("EMAIL", true, "发送成功");
    }
}
// 消息服务
@Service
public class MessageService {
    
    @Autowired
    @Qualifier("smsSender")
    private MessageSender smsSender;
    
    @Autowired
    @Qualifier("emailSender")
    private MessageSender emailSender;
    
    public SendResult sendMessage(String type, SendMessageRequest request) {
        MessageSender sender = "SMS".equals(type) ? smsSender : emailSender;
        return sender.send(request);
    }
}
七、单元测试
7.1 编码准则
(1)单元测试定义
单元测试是对软件中的最小可测试单元进行检查和验证的测试方法。在Java中,通常是对类的方法进行测试。
(2)单元测试的优点
(3)单元测试如何进行
// 被测试的服务类
@Service
public class UserService {
    
    @Autowired
    private UserDAO userDAO;
    
    public User createUser(CreateUserRequest request) {
        if (StringUtils.isBlank(request.getUsername())) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        
        if (userDAO.existsByUsername(request.getUsername())) {
            throw new BusinessException("用户名已存在");
        }
        
        User user = new User();
        user.setUsername(request.getUsername());
        user.setEmail(request.getEmail());
        user.setCreateTime(new Date());
        
        return userDAO.save(user);
    }
}
// 单元测试
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserDAO userDAO;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void createUser_Success() {
        // Given
        CreateUserRequest request = new CreateUserRequest();
        request.setUsername("testuser");
        request.setEmail("test@example.com");
        
        User savedUser = new User();
        savedUser.setId(1L);
        savedUser.setUsername("testuser");
        savedUser.setEmail("test@example.com");
        
        when(userDAO.existsByUsername("testuser")).thenReturn(false);
        when(userDAO.save(any(User.class))).thenReturn(savedUser);
        
        // When & Then
        assertThatThrownBy(() -> userService.createUser(request))
            .isInstanceOf(BusinessException.class)
            .hasMessage("用户名已存在");
        
        verify(userDAO).existsByUsername("existinguser");
        verify(userDAO, never()).save(any(User.class));
    }
}
7.2 项目案例分析
(1)数据回调逻辑单元测试
// 被测试的回调处理类
@Service
public class DataCallbackService {
    
    @Autowired
    private DataService dataService;
    
    @Autowired
    private NotificationService notificationService;
    
    public void handleCallback(DataCallbackRequest request) {
        if (request == null || StringUtils.isBlank(request.getDataId())) {
            throw new IllegalArgumentException("回调参数不能为空");
        }
        
        DataEntity data = dataService.getDataById(request.getDataId());
        if (data == null) {
            throw new BusinessException("数据不存在");
        }
        
        // 更新数据状态
        data.setStatus(request.getStatus());
        data.setUpdateTime(new Date());
        dataService.updateData(data);
        
        // 发送通知
        if ("APPROVED".equals(request.getStatus())) {
            notificationService.sendApprovalNotification(data.getUserId());
        }
    }
}
// 单元测试
@ExtendWith(MockitoExtension.class)
class DataCallbackServiceTest {
    
    @Mock
    private DataService dataService;
    
    @Mock
    private NotificationService notificationService;
    
    @InjectMocks
    private DataCallbackService callbackService;
    
    @Test
    void handleCallback_Success() {
        // Given
        DataCallbackRequest request = new DataCallbackRequest();
        request.setDataId("data123");
        request.setStatus("APPROVED");
        
        DataEntity data = new DataEntity();
        data.setId("data123");
        data.setUserId("user456");
        data.setStatus("PENDING");
        
        when(dataService.getDataById("data123")).thenReturn(data);
        
        // When
        callbackService.handleCallback(request);
        
        // Then
        assertThat(data.getStatus()).isEqualTo("APPROVED");
        assertThat(data.getUpdateTime()).isNotNull();
        
        verify(dataService).getDataById("data123");
        verify(dataService).updateData(data);
        verify(notificationService).sendApprovalNotification("user456");
    }
    
    @Test
    void handleCallback_ThrowException_WhenRequestIsNull() {
        // When & Then
        assertThatThrownBy(() -> callbackService.handleCallback(null))
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("回调参数不能为空");
        
        verify(dataService, never()).getDataById(anyString());
        verify(dataService, never()).updateData(any(DataEntity.class));
        verify(notificationService, never()).sendApprovalNotification(anyString());
    }
    }
}
八、常见代码坏味道
8.1 过长函数
(1)代码的可读性
(2)编码准则
- 函数应该尽可能短小,通常不超过20-30行
 - 一个函数只做一件事情
 - 如果函数需要注释来说明不同部分的功能,考虑拆分
 - 使用有意义的函数名
 
(3)如何把函数变小
// 重构前:过长的函数
public void processOrder(Order order) {
    // 验证订单
    if (order == null) {
        throw new IllegalArgumentException("订单不能为空");
    }
    if (StringUtils.isBlank(order.getOrderId())) {
        throw new IllegalArgumentException("订单ID不能为空");
    }
    
    // 检查库存
    for (OrderItem item : order.getItems()) {
        Product product = productService.getProduct(item.getProductId());
        if (product.getStock() < item.getQuantity()) {
            throw new BusinessException("库存不足");
        }
    }
    
    // 计算价格
    BigDecimal totalPrice = BigDecimal.ZERO;
    for (OrderItem item : order.getItems()) {
        BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getQuantity()));
        totalPrice = totalPrice.add(itemPrice);
    }
    
    // 扣减库存
    for (OrderItem item : order.getItems()) {
        productService.reduceStock(item.getProductId(), item.getQuantity());
    }
    
    // 创建订单记录
    order.setTotalPrice(totalPrice);
    order.setCreateTime(new Date());
    order.setStatus("CREATED");
    orderDAO.save(order);
}
// 重构后:拆分为多个小函数
public void processOrder(Order order) {
    validateOrder(order);
    checkStock(order);
    BigDecimal totalPrice = calculateTotalPrice(order);
    reduceStock(order);
    createOrderRecord(order, totalPrice);
}
private void validateOrder(Order order) {
    if (order == null) {
        throw new IllegalArgumentException("订单不能为空");
    }
    if (StringUtils.isBlank(order.getOrderId())) {
        throw new IllegalArgumentException("订单ID不能为空");
    }
}
private void checkStock(Order order) {
    for (OrderItem item : order.getItems()) {
        Product product = productService.getProduct(item.getProductId());
        if (product.getStock() < item.getQuantity()) {
            throw new BusinessException("库存不足");
        }
    }
}
private BigDecimal calculateTotalPrice(Order order) {
    return order.getItems().stream()
        .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
        .reduce(BigDecimal.ZERO, BigDecimal::add);
}
private void reduceStock(Order order) {
    order.getItems().forEach(item -> 
        productService.reduceStock(item.getProductId(), item.getQuantity()));
}
private void createOrderRecord(Order order, BigDecimal totalPrice) {
    order.setTotalPrice(totalPrice);
    order.setCreateTime(new Date());
    order.setStatus("CREATED");
    orderDAO.save(order);
}
8.2 过大的类
(1)项目案例
// 重构前:职责过多的类
public class UserManager {
    // 用户CRUD操作
    public void createUser(User user) { /* */ }
    public void updateUser(User user) { /* */ }
    public void deleteUser(Long userId) { /* */ }
    public User getUser(Long userId) { /* */ }
    
    // 用户认证
    public boolean authenticate(String username, String password) { /* */ }
    public String generateToken(User user) { /* */ }
    public boolean validateToken(String token) { /* */ }
    
    // 用户权限管理
    public void assignRole(Long userId, String role) { /* */ }
    public void removeRole(Long userId, String role) { /* */ }
    public List<String> getUserRoles(Long userId) { /* */ }
    
    // 用户通知
    public void sendWelcomeEmail(User user) { /* */ }
    public void sendPasswordResetEmail(User user) { /* */ }
    public void sendNotification(User user, String message) { /* */ }
}
// 重构后:按职责拆分为多个类
@Service
public class UserService {
    public void createUser(User user) { /* */ }
    public void updateUser(User user) { /* */ }
    public void deleteUser(Long userId) { /* */ }
    public User getUser(Long userId) { /* */ }
}
@Service
public class AuthenticationService {
    public boolean authenticate(String username, String password) { /* */ }
    public String generateToken(User user) { /* */ }
    public boolean validateToken(String token) { /* */ }
}
@Service
public class UserRoleService {
    public void assignRole(Long userId, String role) { /* */ }
    public void removeRole(Long userId, String role) { /* */ }
    public List<String> getUserRoles(Long userId) { /* */ }
}
@Service
public class UserNotificationService {
    public void sendWelcomeEmail(User user) { /* */ }
    public void sendPasswordResetEmail(User user) { /* */ }
    public void sendNotification(User user, String message) { /* */ }
}
九、总结
9.1 Code Review关键点
9.2 最佳实践建议
- 建立Code Review文化:让Code Review成为开发流程的必要环节
 - 制定编码规范:团队统一的编码标准和最佳实践
 - 使用工具辅助:集成静态代码分析工具
 - 持续学习改进:定期回顾和总结Code Review中发现的问题
 - 注重代码可读性:代码是给人读的,不仅仅是给机器执行的
 
通过规范的Code Review流程和标准,可以显著提高代码质量,减少生产环境问题,提升团队整体的开发水平。
        User result = userService.createUser(request);
    // Then
    assertThat(result).isNotNull();
    assertThat(result.getId()).isEqualTo(1L);
    assertThat(result.getUsername()).isEqualTo("testuser");
    
    verify(userDAO).existsByUsername("testuser");
    verify(userDAO).save(any(User.class));
}
@Test
void createUser_ThrowException_WhenUsernameIsBlank() {
    // Given
    CreateUserRequest request = new CreateUserRequest();
    request.setUsername("");
    
    // When & Then
    assertThatThrownBy(() -> userService.createUser(request))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("用户名不能为空");
    
    verify(userDAO, never()).existsByUsername(anyString());
    verify(userDAO, never()).save(any(User.class));
}
@Test
void createUser_ThrowException_WhenUsernameExists() {
    // Given
    CreateUserRequest request = new CreateUserRequest();
    request.setUsername("existinguser");
    
    when(userDAO.existsByUsername("existinguser")).thenReturn(true);
    
    // When
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 逐光の博客!
 评论