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

手把手打造輕量級 Struts 風格 MVC 框架:註解驅動與反射分派實作

     編輯:Java 相關知識

專案背景與目標

Struts 2 透過 struts.xml 將請求 URL 映射至 Action 類別與方法,並根據方法回傳結果字串決定視圖跳轉。本文示範如何使用 Java 註解、反射與 Servlet 機制,從零實作一個極簡版「Struts-like」框架,支援:

  • 註解式 Action 映射(替代 XML)
  • 方法級結果類型與跳轉頁面配置
  • 請求參數自動綁定(基本類型與 POJO)
  • Dispatcher Servlet 統一分派與轉發

核心註解定義

// 結果類型枚舉
public enum ResultType {
    SUCCESS, NONE, LOGIN, INPUT, ERROR
}

// Action 註解:對應 struts.xml 的 <action>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ActionAnnotation {
    String actionName() default "";           // URL 模式,如 "login.action"
    ResultAnnotation[] results() default {};  // 結果映射陣列
}

// 結果註解:對應 <result>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ResultAnnotation {
    ResultType name() default ResultType.SUCCESS; // 邏輯視圖名
    String value() default "index.jsp";           // 實際跳轉頁面
}

Action 上下文模型

public class ActionContext {
    private String url;                           // 請求 URL 模式
    private String methodName;                    // 目標方法名
    private Map<ResultType, String> results;      // 結果映射表
    private Class<?> actionClass;                // Action 類別
    private Object actionInstance;                // Action 實例(單例)
    private Class<?>[] paramTypes;               // 方法參數類型
    private String[] paramNames;                  // 方法參數名稱(用於綁定請求參數)
    private HttpServletRequest request;
    private HttpServletResponse response;

    // Getter/Setter 省略
}

掃描與註冊機制

public class ActionScanner {
    public static Map<String, ActionContext> urlMap = new ConcurrentHashMap<>();

    public static void scanPackage(String basePath, String scanPackage) 
            throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        File dir = new File(basePath);
        File[] files = dir.listFiles();
        if (files == null) return;

        for (File file : files) {
            if (file.isDirectory()) {
                scanPackage(file.getAbsolutePath(), scanPackage);
            } else if (file.getName().endsWith(".class")) {
                String className = file.getAbsolutePath()
                        .substring(file.getAbsolutePath().indexOf(scanPackage))
                        .replace("/", ".")
                        .replace(".class", "");
                Class<?> clazz = Class.forName(className);
                registerActions(clazz);
            }
        }
    }

    private static void registerActions(Class<?> clazz) {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(ActionAnnotation.class)) {
                ActionAnnotation ann = method.getAnnotation(ActionAnnotation.class);
                if (ann.actionName().isEmpty() || ann.results().length == 0) {
                    throw new RuntimeException("Action 註解配置不完整:" + method);
                }

                ActionContext ctx = new ActionContext();
                ctx.setUrl(ann.actionName());
                ctx.setMethodName(method.getName());
                ctx.setActionClass(clazz);
                ctx.setActionInstance(clazz.newInstance());
                ctx.setParamTypes(method.getParameterTypes());
                ctx.setParamNames(getParamNames(method)); // 需編譯參數 -parameters 或用 ASM

                Map<ResultType, String> resultMap = new HashMap<>();
                for (ResultAnnotation r : ann.results()) {
                    resultMap.put(r.name(), r.value());
                }
                ctx.setResults(resultMap);

                urlMap.put(ann.actionName(), ctx);
            }
        }
    }

    // 參數名獲取需編譯時加 -parameters 或使用 Paranamer/ASM 庫
    private static String[] getParamNames(Method method) { /* 實作省略 */ }
}

參數自動綁定工具

public class ParamBinder {
    public static Object[] bind(HttpServletRequest request, 
                                Class<?>[] paramTypes, String[] paramNames) 
            throws Exception {
        Object[] args = new Object[paramTypes.length];
        for (int i = 0; i < paramTypes.length; i++) {
            Class<?> type = paramTypes[i];
            String name = paramNames[i];

            if (isPrimitiveOrWrapper(type) || type == String.class) {
                // 基本類型、包裝類、String 直接綁定
                args[i] = convert(request.getParameter(name), type);
            } else {
                // POJO:實例化後綁定屬性(支援 user.name 格式)
                Object obj = type.newInstance();
                for (Field field : type.getDeclaredFields()) {
                    String paramKey = name + "." + field.getName();
                    String value = request.getParameter(paramKey);
                    if (value != null) {
                        field.setAccessible(true);
                        field.set(obj, convert(value, field.getType()));
                    }
                }
                args[i] = obj;
            }
        }
        return args;
    }

    private static Object convert(String value, Class<?> targetType) { /* 類型轉換邏輯 */ }
    private static boolean isPrimitiveOrWrapper(Class<?> type) { /* 判斷邏輯 */ }
}

Dispatcher Servlet 處理請求

@WebServlet("*.action")
public class DispatcherServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        String url = req.getServletPath().substring(1); // 移除首字 '/'

        ActionContext ctx = ActionScanner.urlMap.get(url);
        if (ctx == null) {
            resp.sendError(404, "Action not found: " + url);
            return;
        }

        try {
            ctx.setRequest(req);
            ctx.setResponse(resp);

            Object[] args = ParamBinder.bind(req, ctx.getParamTypes(), ctx.getParamNames());
            Method method = ctx.getActionClass().getMethod(ctx.getMethodName(), ctx.getParamTypes());
            ResultType result = (ResultType) method.invoke(ctx.getActionInstance(), args);

            String target = ctx.getResults().get(result);
            if (target != null) {
                req.getRequestDispatcher(target).forward(req, resp); // 預設 dispatcher
            } else {
                throw new RuntimeException("未找到結果映射:" + result);
            }
        } catch (Exception e) {
            throw new ServletException("Action 執行失敗", e);
        }
    }
}

初始化 Servlet 載入掃描

@WebServlet(value = "/init", loadOnStartup = 1)
public class FrameworkInitServlet extends HttpServlet {
    @Override
    public void init() throws ServletException {
        String scanPackage = getInitParameter("scan_package"); // 從 web.xml 讀取
        String realPath = getServletContext().getRealPath("/WEB-INF/classes");
        try {
            ActionScanner.scanPackage(realPath, scanPackage);
            System.out.println("框架初始化完成,註冊 Action 數:" + ActionScanner.urlMap.size());
        } catch (Exception e) {
            throw new ServletException("框架初始化失敗", e);
        }
    }
}

Action 使用範例

public class LoginAction {
    @ActionAnnotation(
        actionName = "login.action",
        results = {
            @ResultAnnotation(name = ResultType.SUCCESS, value = "index.jsp"),
            @ResultAnnotation(name = ResultType.LOGIN, value = "login.jsp")
        }
    )
    public ResultType login(String username, String password) {
        if ("admin".equals(username) && "123456".equals(password)) {
            return ResultType.SUCCESS;
        }
        return ResultType.LOGIN;
    }

    @ActionAnnotation(
        actionName = "loginUser.action",
        results = {
            @ResultAnnotation(name = ResultType.SUCCESS, value = "index.jsp"),
            @ResultAnnotation(name = ResultType.LOGIN, value = "login.jsp")
        }
    )
    public ResultType loginUser(int userId, UserForm form) { // 支援 POJO 綁定
        if ("admin".equals(form.getUsername()) && "123456".equals(form.getPassword())) {
            return ResultType.SUCCESS;
        }
        return ResultType.LOGIN;
    }
}

web.xml 配置(或使用 Servlet 3.0+ 註解替代)

<servlet>
    <servlet-name>FrameworkInitServlet</servlet-name>
    <servlet-class>com.framework.FrameworkInitServlet</servlet-class>
    <init-param>
        <param-name>scan_package</param-name>
        <param-value>com.myapp.action</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet>
    <servlet-name>DispatcherServlet</servlet-name>
    <servlet-class>com.framework.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>DispatcherServlet</servlet-name>
    <url-pattern>*.action</url-pattern>
</servlet-mapping>

擴展方向與總結

此極簡框架展示了 MVC 分派核心機制:URL 映射 → 反射調用 → 參數綁定 → 結果轉發。生產級框架還需考慮:

  • 攔截器鏈(前後處理、權限、事務)
  • 類型轉換與驗證框架
  • 多種 Result Type(redirect、chain、stream、JSON)
  • 異常統一處理與國際化
  • 插件化架構與模組隔離

理解此核心流程有助於深入掌握 Struts 2、Spring MVC 等主流框架的運作原理。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved