首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring MVC 參數解析器實戰:優雅解決 CRUD 中非業務字段 (創建人/時間等) 自動賦值

     編輯:Java 綜合教學

痛點:CRUD 代碼中充斥著系統字段賦值

// 控制器/Service 混雜大量樣板代碼
if (request.getId() == 0) { // 新增
    request.setCreatorId(userId);
    request.setCreatorName(userName);
    request.setCreateTime(new Date());
}
request.setModifierId(userId);
request.setModifierName(userName);
request.setModifyTime(new Date());
// 還有默認值處理:null → 0/""/1990-01-01...

問題:業務邏輯被稀釋、重複代碼多、修改字段名需全局搜尋替換、違反單一職責。

解決方案:自定義 HandlerMethodArgumentResolver

Spring MVC 提供 HandlerMethodArgumentResolver 擴展點,攔截控制器方法參數綁定階段,**在參數進入業務邏輯前自動注入系統字段與默認值**。

核心實作步驟

1. 定義標記註解

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ManageModelConfig {
    boolean isSetDefaultFieldsValue() default true; // 是否處理默認值
}

2. 實作 Resolver

@Component
public class ManageModelConfigResolver implements HandlerMethodArgumentResolver {
    
    @Autowired private RequestMappingHandlerAdapter handlerAdapter;
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(ManageModelConfig.class);
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        
        // 1. 先讓 Spring 完成常規綁定 (JSON → POJO)
        RequestResponseBodyMethodProcessor processor = getProcessor();
        Object model = processor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        
        // 2. 獲取當前登入用戶 (假設存於 Request 屬性)
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        Employee currentUser = (Employee) request.getAttribute("CURRENT_USER");
        if (currentUser == null) return model;
        
        // 3. 讀取註解配置
        ManageModelConfig config = parameter.getParameterAnnotation(ManageModelConfig.class);
        
        // 4. 統一處理:默認值 + 系統字段
        ModelHelper.injectSystemAndDefaultFields(model, currentUser, config.isSetDefaultFieldsValue());
        
        return model;
    }
    
    private RequestResponseBodyMethodProcessor getProcessor() {
        // 懶加載構建 Processor,複用 MessageConverter
        // ... 略
    }
}

3. 統一處理工具類 (反射 + 約定優於配置)

public class ModelHelper {
    
    // 系統字段命名約定
    private static final String CREATE_ID = "creatorId";
    private static final String CREATE_NAME = "creatorName";
    private static final String CREATE_TIME = "createTime";
    private static final String MODIFY_ID = "modifierId";
    private static final String MODIFY_NAME = "modifierName";
    private static final String MODIFY_TIME = "modifyTime";
    
    public static  void injectSystemAndDefaultFields(T model, Employee user, boolean setDefault) {
        Class clazz = model.getClass();
        Field[] fields = clazz.getDeclaredFields();
        
        for (Field field : fields) {
            field.setAccessible(true);
            String name = field.getName();
            Class type = field.getType();
            
            try {
                // --- 默認值處理 ---
                if (setDefault && field.get(model) == null) {
                    if (type == String.class) field.set(model, "");
                    else if (type == Integer.class || type == int.class) field.set(model, 0);
                    else if (type == Long.class || type == long.class) field.set(model, 0L);
                    else if (type == BigDecimal.class) field.set(model, BigDecimal.ZERO);
                    else if (type == Date.class) field.set(model, Date.from(LocalDateTime.of(1990,1,1,0,0).atZone(ZoneId.systemDefault()).toInstant()));
                }
                
                // --- 系統字段處理 ---
                boolean isCreate = false;
                try {
                    Field idField = clazz.getDeclaredField("id");
                    idField.setAccessible(true);
                    Object idVal = idField.get(model);
                    isCreate = (idVal == null || (idVal instanceof Number && ((Number)idVal).longValue() <= 0));
                } catch (NoSuchFieldException ignored) {}
                
                if (isCreate) {
                    if (name.equals(CREATE_ID)) field.set(model, user.getId());
                    if (name.equals(CREATE_NAME)) field.set(model, user.getName());
                    if (name.equals(CREATE_TIME)) field.set(model, new Date());
                }
                // 修改字段無論新增/編輯都更新
                if (name.equals(MODIFY_ID)) field.set(model, user.getId());
                if (name.equals(MODIFY_NAME)) field.set(model, user.getName());
                if (name.equals(MODIFY_TIME)) field.set(model, new Date());
                
            } catch (IllegalAccessException e) {
                log.warn("Field inject failed: " + name, e);
            }
        }
    }
}

4. 註冊 Resolver (配置類)

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired private ManageModelConfigResolver resolver;
    
    @Override
    public void addArgumentResolvers(List resolvers) {
        resolvers.add(resolver);
    }
}

或 XML:<mvc:argument-resolvers><bean class="..."/></mvc:argument-resolvers>

控制器代碼極簡化

@PostMapping("/save")
@ResponseBody
public Result save(@ManageModelConfig(isSetDefaultFieldsValue = true) EmployeeDTO dto) {
    if (dto.getId() == 0) employeeService.insert(dto);
    else employeeService.update(dto);
    return Result.ok();
}

**零系統字段代碼、零默認值代碼、純業務邏輯**。

關鍵設計決策

決策點方案理由
識別新增/編輯ID > 0 為編輯,否則新增通用、無需額外標記
系統字段命名約定式 (creatorId/modifyTime...)零配置、統一規範
獲取當前用戶Request 屬性 (Filter/Interceptor 寫入)無侵入、支援集群 Session 共享
默認值處理開關註解參數控制靈活、向後相容
反射效能字段緩存 (ConcurrentHashMap<Class, Field[]>)避免重複 getDeclaredFields

擴展建議

  • 支援巢狀物件遞歸注入
  • 整合驗證框架 (JSR-303) 於注入後執行
  • 审计日誌自動記錄 (AOP 切面)
  • 多租戶字段自動注入 (tenantId)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved