在项目开发过程中发现很多代码的坏味道,针对发现的问题进行分析和解答。本文将从异常处理、日志打印、项目分层、包结构划分、DTO使用、面向接口编程、单元测试等多个维度,提供Code Review的规范指南和实践案例。

一、异常处理

1.1 核心问题

在日常开发中,我们经常面临以下异常处理问题:

  • 异常要抛出去还是catch处理?
  • 写代码的过程中是否有主动抛出过异常?
  • 要抛出什么类型的异常,Exception、Throwable或其它?

1.2 异常处理准则

核心原则:You should catch the exception when you are in the method that knows what to do

(1)低层级异常处理

什么是低层级?
这是与第三方代码集成的级别,例如ORM工具或任何执行IO操作的库(通过HTTP访问资源、读取文件、保存到数据库等)。也就是说,您离开应用程序的内部代码以与其他组件交互的级别。

处理准则:

  1. 只处理特定的异常,例如SqlTimeoutException或IOException。从不处理一般异常(Exception类型)
  2. 仅当您有一些有意义的事情要做时才处理它,例如重试、触发补偿操作或向异常添加更多数据(例如上下文变量),然后重新抛出它
  3. 不要在此处执行日志记录
  4. 让所有其他异常冒泡,因为它们将由高层级处理

(2)高层级异常处理

什么是高层级?
这将是在将异常直接抛给用户之前您可以处理异常的最后一个地方。

处理准则:

  1. 处理通用异常类
  2. 从当前执行上下文中添加更多信息
  3. 记录错误并通知程序员
  4. 向用户道歉或提示错误信息
  5. 尽快解决

(3)什么时候抛出异常

在开发库的上下文中更容易理解。当您遇到错误时,您应该抛出错误,除了让您的API的使用者知道并让他们决定之外,您无能为力。

假设您是某个数据访问库的开发人员。当您遇到网络错误时,除了抛出异常之外,您无能为力。从数据访问库的角度来看,这是一个不可逆转的错误。

1.3 准则背后的原理

  1. 异常代表不可逆转的错误。它们代表系统中的错误、程序员犯的错误或应用程序无法控制的情况。在这些情况下,用户通常无能为力。
  2. try catch块可以屏蔽应用程序流程。过度使用会让代码逻辑变得复杂和难以理解。
  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

  • 系统启动和关闭信息
  • 用户的重要操作(登录、登出、重要数据修改)
  • 外部系统调用(第三方API、数据库操作)
  • 异常和错误信息
  • 性能关键点的耗时
  • 业务流程的关键节点

(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

  • 敏感信息(密码、身份证号、银行卡号等)
  • 过于频繁的日志(循环中的大量日志)
  • 无意义的日志(如简单的getter/setter调用)
  • 重复的日志信息
  • 调试日志在生产环境中输出

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 项目分层准则

经典三层架构

  • Controller层:接收请求,参数校验,调用Service层
  • Service层:业务逻辑处理,事务控制
  • DAO层:数据访问,与数据库交互

扩展分层

  • DTO层:数据传输对象
  • Entity层:实体对象
  • Utils层:工具类
  • Config层:配置类

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 包结构划分准则

按功能模块划分

com.example.project
├── controller/     # 控制器
├── service/        # 业务逻辑
├── dao/           # 数据访问
├── entity/        # 实体类
├── dto/           # 数据传输对象
├── config/        # 配置类
├── utils/         # 工具类
└── exception/     # 异常类

按业务领域划分

com.example.project
├── user/          # 用户模块
│   ├── controller/
│   ├── service/
│   └── dao/
├── order/         # 订单模块
│   ├── controller/
│   ├── service/
│   └── dao/
└── common/        # 公共模块
    ├── config/
    ├── utils/
    └── exception/

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)分层领域模型规约(阿里规约)

  • DO (Data Object):与数据库表结构一一对应,通过DAO层向上传输数据源对象
  • DTO (Data Transfer Object):数据传输对象,Service或Manager向外传输的对象
  • BO (Business Object):业务对象,由Service层输出的封装业务逻辑的对象
  • VO (View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象

(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)面向接口编程和面向对象编程

面向接口编程的优点

  • 降低耦合度:依赖抽象而不是具体实现
  • 提高可测试性:便于Mock和单元测试
  • 增强可扩展性:新增实现不影响现有代码
  • 符合开闭原则:对扩展开放,对修改关闭

(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关键点

异常处理

  • 明确异常处理的层次和职责
  • 避免吞掉异常或过度捕获
  • 提供有意义的异常信息

日志打印

  • 记录关键业务流程和性能数据
  • 避免记录敏感信息和无意义日志
  • 使用合适的日志级别

代码结构

  • 保持清晰的分层架构
  • 合理划分包结构
  • 使用DTO进行数据传输
  • 面向接口编程

代码质量

  • 编写单元测试
  • 避免过长函数和过大类
  • 遵循单一职责原则
  • 保持代码简洁和可读性

9.2 最佳实践建议

  1. 建立Code Review文化:让Code Review成为开发流程的必要环节
  2. 制定编码规范:团队统一的编码标准和最佳实践
  3. 使用工具辅助:集成静态代码分析工具
  4. 持续学习改进:定期回顾和总结Code Review中发现的问题
  5. 注重代码可读性:代码是给人读的,不仅仅是给机器执行的

通过规范的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