Struts 2 透過 struts.xml 將請求 URL 映射至 Action 類別與方法,並根據方法回傳結果字串決定視圖跳轉。本文示範如何使用 Java 註解、反射與 Servlet 機制,從零實作一個極簡版「Struts-like」框架,支援:
// 結果類型枚舉
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"; // 實際跳轉頁面
}
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) { /* 判斷邏輯 */ }
}
@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);
}
}
}
@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);
}
}
}
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;
}
}
<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 映射 → 反射調用 → 參數綁定 → 結果轉發。生產級框架還需考慮:
理解此核心流程有助於深入掌握 Struts 2、Spring MVC 等主流框架的運作原理。