├── src ├── main │ ├── webapp │ │ ├── assets │ │ │ └── main.css │ │ ├── view │ │ │ ├── hello.jsp │ │ │ ├── route.jsp │ │ │ └── index.jsp │ │ └── WEB-INF │ │ │ ├── web.xml │ │ │ └── route_tag.tld │ ├── resources │ │ ├── simplelogger.properties │ │ └── routes │ └── java │ │ └── com │ │ └── codemacro │ │ └── webdemo │ │ ├── MyWebApp.java │ │ ├── result │ │ ├── Result.java │ │ ├── JSPRouteTag.java │ │ └── JSPResult.java │ │ ├── test │ │ └── TestController.java │ │ ├── BaseController.java │ │ ├── MyServletFilter.java │ │ ├── RouteConf.java │ │ └── ActionManager.java └── test │ └── java │ └── com │ └── codemacro │ └── webdemo │ └── AppTest.java ├── README.md └── pom.xml /src/main/webapp/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | } -------------------------------------------------------------------------------- /src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=debug 2 | -------------------------------------------------------------------------------- /src/main/webapp/view/hello.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 2 | 3 | 4 | hello page 5 | 6 | 7 |

hello ${name}

8 |
9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/webapp/view/route.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 2 | <%@ taglib prefix="route" uri="/myweb-router" %> 3 | 4 | 5 | route test page 6 | 7 | 8 | index 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/routes: -------------------------------------------------------------------------------- 1 | GET /index com.codemacro.webdemo.test.TestController.index 2 | GET /404 com.codemacro.webdemo.test.TestController.code404 3 | GET /obj com.codemacro.webdemo.test.TestController.object 4 | GET /index.jsp com.codemacro.webdemo.test.TestController.template 5 | GET /hello com.codemacro.webdemo.test.TestController.hello 6 | GET /route com.codemacro.webdemo.test.TestController.route 7 | POST /hello com.codemacro.webdemo.test.TestController.sayHello 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/MyWebApp.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class MyWebApp { 7 | private static Logger logger = LoggerFactory.getLogger(MyWebApp.class); 8 | private ActionManager actionMgr; 9 | 10 | public void startup(String appName) { 11 | logger.info("MyWebApp <{}> initializing...", appName); 12 | actionMgr = new ActionManager(appName); 13 | RouteConf.load(actionMgr); 14 | } 15 | 16 | public ActionManager getActionManager() { 17 | return actionMgr; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MyWebFilter 6 | com.codemacro.webdemo.MyServletFilter 7 | 8 | 9 | 10 | MyWebFilter 11 | /* 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A minimum web framework based on Servlet. 2 | 3 | See `TestController` and `routes` for example. 4 | 5 | public class TestController extends BaseController { 6 | 7 | public Result index() { 8 | return ok("hello world"); 9 | } 10 | } 11 | 12 | // routes 13 | GET /index com.codemacro.webdemo.test.TestController.index 14 | 15 | Compile and package to war: 16 | 17 | mvn package 18 | 19 | Deploy a servlet server (Jetty/Tomcat). 20 | 21 | 22 | ## Deploy on Jetty 23 | 24 | 1. download jetty (v9.2) 25 | 2. put .war to `webapps` path 26 | 3. `java -jar start.jar` 27 | 4. locate: `http://localhost:8080/you-war-file-name` 28 | -------------------------------------------------------------------------------- /src/main/webapp/view/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 2 | 3 | 4 | index page 5 | 6 | 7 |

hello ${name}

8 |

9 |

By EL

10 | 11 | ${lang}| 12 | 13 |
14 | 15 | ${lang}| 16 | 17 |

18 |

19 |

By scriptlets

20 | <% String[] langs = (String[]) request.getAttribute("langs"); %> 21 | <% if (langs != null) { %> 22 | <% for (String lang : langs) { %> 23 | <%= lang %>| 24 | <% } } %> 25 |

26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/result/Result.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo.result; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | public class Result { 10 | protected HttpServletResponse response; 11 | protected Object result; 12 | 13 | public Result(HttpServletResponse resp, Object result) { 14 | this.response = resp; 15 | this.result = result; 16 | } 17 | 18 | public void render() throws IOException, ServletException { 19 | PrintWriter writer = response.getWriter(); 20 | writer.append(result.toString()); 21 | writer.close(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/route_tag.tld: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 1.0 7 | 1.1 8 | URLRouteTags 9 | /myweb-router 10 | 11 | 12 | 13 | reverse 14 | com.codemacro.webdemo.result.JSPRouteTag 15 | 16 | 17 | 18 | action 19 | true 20 | 21 | true 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/test/java/com/codemacro/webdemo/AppTest.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo; 2 | 3 | import junit.framework.Test; 4 | import junit.framework.TestCase; 5 | import junit.framework.TestSuite; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest 11 | extends TestCase 12 | { 13 | /** 14 | * Create the test case 15 | * 16 | * @param testName name of the test case 17 | */ 18 | public AppTest( String testName ) 19 | { 20 | super( testName ); 21 | } 22 | 23 | /** 24 | * @return the suite of tests being tested 25 | */ 26 | public static Test suite() 27 | { 28 | return new TestSuite( AppTest.class ); 29 | } 30 | 31 | /** 32 | * Rigourous Test :-) 33 | */ 34 | public void testApp() 35 | { 36 | assertTrue( true ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/result/JSPRouteTag.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo.result; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import javax.servlet.jsp.JspContext; 8 | import javax.servlet.jsp.JspException; 9 | import javax.servlet.jsp.JspWriter; 10 | import javax.servlet.jsp.tagext.DynamicAttributes; 11 | import javax.servlet.jsp.tagext.SimpleTagSupport; 12 | 13 | import com.codemacro.webdemo.ActionManager; 14 | 15 | public class JSPRouteTag extends SimpleTagSupport implements DynamicAttributes { 16 | public static final String ACTION_MGR = "__actionmgr__"; 17 | private String action; 18 | private Map attrMap = new HashMap(); 19 | 20 | @Override 21 | public void doTag() throws IOException { 22 | JspContext context = getJspContext(); 23 | ActionManager actionMgr = (ActionManager) context.findAttribute(ACTION_MGR); 24 | JspWriter out = context.getOut(); 25 | String uri = actionMgr.getReverseAction(action, attrMap); 26 | out.println(uri); 27 | } 28 | 29 | @Override 30 | public void setDynamicAttribute(String uri, String name, Object value) throws JspException { 31 | attrMap.put(name, value); 32 | } 33 | 34 | public void setAction(String action) { 35 | this.action = action; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/test/TestController.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo.test; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.codemacro.webdemo.BaseController; 6 | import com.codemacro.webdemo.result.Result; 7 | 8 | public class TestController extends BaseController { 9 | 10 | public Result index() { 11 | return ok("hello world"); 12 | } 13 | 14 | public Result code404() { 15 | return status(404, "not found"); 16 | } 17 | 18 | public Result object() { 19 | class Obj { 20 | @Override 21 | public String toString() { 22 | return "object: " + hashCode(); 23 | } 24 | } 25 | return ok(new Obj()); 26 | } 27 | 28 | public Result template() { 29 | String[] langs = new String[] {"c++", "java", "python"}; 30 | return ok(jsp("index.jsp") 31 | .put("name", "kevin") 32 | .put("langs", langs) 33 | .put("langList", Arrays.asList(langs)) 34 | ); 35 | } 36 | 37 | public Result hello() { 38 | String name = getQueryString("name"); 39 | return ok(jsp("hello.jsp").put("name", name)); 40 | } 41 | 42 | public Result sayHello() { 43 | String name = getQueryString("name"); 44 | return ok(jsp("hello.jsp").put("name", name)); 45 | } 46 | 47 | public Result route() { 48 | return ok(jsp("route.jsp")); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/result/JSPResult.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo.result; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | import java.util.TreeMap; 6 | 7 | import javax.servlet.ServletException; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | 11 | import com.codemacro.webdemo.ActionManager; 12 | 13 | public class JSPResult extends Result { 14 | private static final String BASE = "/view/"; 15 | private HttpServletRequest request; 16 | private String file; 17 | private Map content; 18 | 19 | public JSPResult(HttpServletRequest req, HttpServletResponse resp, String file, 20 | ActionManager actionMgr) { 21 | super(resp, null); 22 | this.request = req; 23 | this.content = new TreeMap(); 24 | this.file = BASE + file; 25 | put(JSPRouteTag.ACTION_MGR, actionMgr); 26 | } 27 | 28 | public JSPResult put(String key, Object value) { 29 | content.put(key, value); 30 | return this; 31 | } 32 | 33 | @Override 34 | public void render() throws IOException, ServletException { 35 | for (Map.Entry entry : content.entrySet()) { 36 | request.setAttribute(entry.getKey(), entry.getValue()); 37 | } 38 | request.getRequestDispatcher(file).forward(request, response); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/BaseController.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo; 2 | 3 | import java.util.Map; 4 | import javax.servlet.http.HttpServletRequest; 5 | import javax.servlet.http.HttpServletResponse; 6 | 7 | import com.codemacro.webdemo.result.JSPResult; 8 | import com.codemacro.webdemo.result.Result; 9 | 10 | public class BaseController { 11 | private HttpServletRequest request; 12 | private HttpServletResponse response; 13 | private ActionManager actionMgr; 14 | 15 | public void init(HttpServletRequest req, HttpServletResponse resp, ActionManager actionMgr) { 16 | this.request = req; 17 | this.response = resp; 18 | this.actionMgr = actionMgr; 19 | } 20 | 21 | @SuppressWarnings("unchecked") 22 | protected Map getQueryStrings() { 23 | return request.getParameterMap(); 24 | } 25 | 26 | protected String getQueryString(String key) { 27 | return request.getParameter(key); 28 | } 29 | 30 | protected Result status(int code, String text) { 31 | response.setStatus(code); 32 | return new Result(response, text); 33 | } 34 | 35 | protected Result ok(Object obj) { 36 | return new Result(response, obj); 37 | } 38 | 39 | protected Result ok(Result result) { 40 | return result; 41 | } 42 | 43 | protected JSPResult jsp(String file) { 44 | return new JSPResult(request, response, file, actionMgr); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/MyServletFilter.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.Filter; 6 | import javax.servlet.FilterChain; 7 | import javax.servlet.FilterConfig; 8 | import javax.servlet.ServletContext; 9 | import javax.servlet.ServletException; 10 | import javax.servlet.ServletRequest; 11 | import javax.servlet.ServletResponse; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | public class MyServletFilter implements Filter { 19 | private static Logger logger = LoggerFactory.getLogger(MyServletFilter.class); 20 | private MyWebApp app; 21 | 22 | public void destroy() { 23 | logger.info("MyWeb exits"); 24 | } 25 | 26 | public void doFilter(ServletRequest req, ServletResponse res, 27 | FilterChain chain) throws IOException, ServletException { 28 | boolean ret = app.getActionManager().invoke((HttpServletRequest)req, (HttpServletResponse)res); 29 | if (!ret) { 30 | chain.doFilter(req, res); 31 | } 32 | } 33 | 34 | public void init(FilterConfig conf) throws ServletException { 35 | app = new MyWebApp(); 36 | app.startup(getAppName(conf.getServletContext())); 37 | logger.info("MyWeb startup"); 38 | } 39 | 40 | private String getAppName(ServletContext context) { 41 | return context.getServletContextName(); 42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/RouteConf.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | public class RouteConf { 13 | private static final String FILE = "/routes"; 14 | private static final Pattern LineR = Pattern.compile("(\\S+)\\s+(\\S+)\\s+(\\S+)"); 15 | private static Logger logger = LoggerFactory.getLogger(RouteConf.class); 16 | 17 | public static void load(ActionManager actionMgr) { 18 | logger.info("load routes..."); 19 | try (BufferedReader br = new BufferedReader(new InputStreamReader( 20 | RouteConf.class.getResourceAsStream(FILE)))) { 21 | String line; 22 | while ((line = br.readLine()) != null) { 23 | parseLine(actionMgr, line); 24 | } 25 | } catch (IOException e) { 26 | logger.error("load routes failed: {}", e); 27 | } 28 | } 29 | 30 | private static void parseLine(ActionManager actionMgr, String line) { 31 | if (line.startsWith("#")) { 32 | return ; 33 | } 34 | Matcher m = LineR.matcher(line); 35 | if (!m.find()) { 36 | return ; 37 | } 38 | String method = m.group(1); 39 | String uri = m.group(2); 40 | String clazzFunc = m.group(3); 41 | int pos = clazzFunc.lastIndexOf('.'); 42 | String clazz = clazzFunc.substring(0, pos); 43 | String func = clazzFunc.substring(pos + 1); 44 | logger.debug("parse line {} {} {} {}", method, uri, clazz, func); 45 | actionMgr.registerAction(clazz, func, uri, method); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | web-demo 6 | web-demo 7 | 0.0.1-SNAPSHOT 8 | war 9 | 10 | web-demo 11 | http://maven.apache.org 12 | 13 | 14 | UTF-8 15 | 1.7 16 | 1.7 17 | 18 | 19 | 20 | 21 | javax.servlet 22 | servlet-api 23 | 2.5 24 | jar 25 | compile 26 | 27 | 28 | javax.servlet 29 | jsp-api 30 | 2.0 31 | true 32 | 33 | 34 | org.slf4j 35 | slf4j-api 36 | 1.7.5 37 | 38 | 39 | org.slf4j 40 | slf4j-simple 41 | 1.7.5 42 | 43 | 44 | junit 45 | junit 46 | 3.8.1 47 | test 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/main/java/com/codemacro/webdemo/ActionManager.java: -------------------------------------------------------------------------------- 1 | package com.codemacro.webdemo; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | import java.io.UnsupportedEncodingException; 6 | import java.lang.reflect.Method; 7 | import java.net.URLEncoder; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import com.codemacro.webdemo.result.Result; 18 | 19 | public class ActionManager { 20 | private static Logger logger = LoggerFactory.getLogger(ActionManager.class); 21 | public static enum HttpMethod { GET, POST }; 22 | 23 | private static class ActionKey { 24 | public String uri; 25 | public HttpMethod httpMethod; 26 | 27 | public ActionKey(String uri, HttpMethod m) { 28 | this.uri = uri; 29 | this.httpMethod = m; 30 | } 31 | 32 | @Override 33 | public int hashCode() { 34 | return httpMethod.hashCode() | uri.hashCode(); 35 | } 36 | 37 | @Override 38 | public boolean equals(Object that) { 39 | if (that == this) { 40 | return true; 41 | } 42 | if (!(that instanceof ActionKey)) { 43 | return false; 44 | } 45 | ActionKey athat = (ActionKey) that; 46 | return athat.httpMethod.equals(httpMethod) && athat.uri.equals(uri); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "ActionKey[uri=" + uri + ", method=" + httpMethod + "]"; 52 | } 53 | } 54 | 55 | private static class ActionValue { 56 | public Class clazz; 57 | public Method method; 58 | 59 | public ActionValue(Class clazz, Method m) { 60 | this.clazz = clazz; 61 | this.method = m; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return "ActionValue[m=" + clazz.getName() + "." + method.getName() + "]"; 67 | } 68 | } 69 | 70 | private Map actions = new HashMap(); 71 | private String appName; 72 | 73 | public ActionManager(String appName) { 74 | this.appName = appName; 75 | } 76 | 77 | public String getAppName() { 78 | return appName; 79 | } 80 | 81 | @SuppressWarnings("unchecked") 82 | public void registerAction(String clazName, String methodName, String uri, String method) { 83 | try { 84 | uri = "/" + appName + uri; 85 | Class clazz = (Class) loadClass(clazName); 86 | Method m = clazz.getMethod(methodName, (Class[])null); 87 | if (m.getReturnType() != Result.class) { 88 | throw new RuntimeException("action method return type mismatch: " + uri); 89 | } 90 | ActionKey k = new ActionKey(uri, getMethod(method)); 91 | ActionValue v = new ActionValue(clazz, m); 92 | logger.debug("register action {} {} {} {}", clazName, methodName, uri, method); 93 | actions.put(k, v); 94 | } catch (Exception e) { 95 | throw new RuntimeException("registerAction failed: " + uri, e); 96 | } 97 | } 98 | 99 | public boolean invoke(HttpServletRequest req, HttpServletResponse resp) throws IOException { 100 | String uri = req.getRequestURI(); 101 | String method = req.getMethod().toUpperCase(); 102 | try { 103 | ActionValue v = getAction(uri, method); 104 | if (v == null) { 105 | return false; 106 | } 107 | BaseController ctl = (BaseController) v.clazz.newInstance(); 108 | ctl.init(req, resp, this); 109 | logger.debug("invoke action {}", uri); 110 | Result result = (Result) v.method.invoke(ctl, (Object[]) null); 111 | result.render(); 112 | } catch (Exception e) { 113 | PrintWriter writer = resp.getWriter(); 114 | writer.append("action exception: " + uri); 115 | writer.append("\n"); 116 | writer.append(e.getMessage()); 117 | writer.close(); 118 | logger.warn("action exception: {}\n{}", e, actions); 119 | } 120 | return true; 121 | } 122 | 123 | public ActionValue getAction(String uri, String method) { 124 | ActionKey k = new ActionKey(uri, getMethod(method)); 125 | return actions.get(k); 126 | } 127 | 128 | public String getReverseAction(String clazzMethod, Map args) { 129 | String argStr = formatQueryParams(args); 130 | for (Map.Entry action : actions.entrySet()) { 131 | ActionValue av = action.getValue(); 132 | if ((av.clazz.getName() + "." + av.method.getName()).equals(clazzMethod)) { 133 | return action.getKey().uri + (argStr == "" ? "" : "?" + argStr); 134 | } 135 | } 136 | return ""; 137 | } 138 | 139 | static public HttpMethod getMethod(String m) { 140 | switch (m) { 141 | case "GET": return HttpMethod.GET; 142 | case "POST": return HttpMethod.POST; 143 | } 144 | return HttpMethod.GET; 145 | } 146 | 147 | private Class loadClass(String fullName) throws ClassNotFoundException { 148 | return Class.forName(fullName); 149 | } 150 | 151 | private String formatQueryParams(Map args) { 152 | if (args == null) { 153 | return ""; 154 | } 155 | String s = ""; 156 | for (Map.Entry entry : args.entrySet()) { 157 | String kv = encode(entry.getKey()) + "=" + encode(entry.getValue().toString()); 158 | if (s == "") { 159 | s = kv; 160 | } else { 161 | s += "&" + kv; 162 | } 163 | } 164 | return s; 165 | } 166 | 167 | private String encode(String s) { 168 | try { 169 | return URLEncoder.encode(s, "UTF-8"); 170 | } catch (UnsupportedEncodingException e) { 171 | return ""; 172 | } 173 | } 174 | } 175 | 176 | 177 | --------------------------------------------------------------------------------