代码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 许可协议。转载请注明来自 逐光の博客!
评论