如何设计一个Java Web MVC框架
作者:biezhi
原文地址:https://github.com/biezhi/java-bible/blob/master/mvc/index.md
通过使用Java语言实现一个完整的框架设计,这个框架中主要内容有第一小节介绍的Web框架的结构规划,例如采用MVC模式来进行开发,程序的执行流程设计等内容;第二小节介绍框架的第一个功能:路由,如何让访问的URL映射到相应的处理逻辑;第三小节介绍处理逻辑,如何设计一个公共的调度器,对象继承之后处理函数中如何处理response和request;第四小节至第六小节介绍如何框架的一些辅助功能,例如配置信息,数据库操作等;最后介绍如何基于Web框架实现一个简单的增删改查,包括User的添加、修改、删除、显示列表等操作。
通过这么一个完整的项目例子,我期望能够让读者了解如何开发Web应用,如何搭建自己的目录结构,如何实现路由,如何实现MVC模式等各方面的开发内容。在框架盛行的今天,MVC也不再是神话。经常听到很多程序员讨论哪个框架好,哪个框架不好, 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。
项目源码:https://github.com/junicorn/mario
示例代码:https://github.com/junicorn/mario-sample
接下来开始我们的框架之旅。
项目规划
做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了
创建一个maven项目
约定一下框架基础信息
- 假设我们的web框架名称是
mario
- 包名是
com.junicorn.mario
命令行创建
mvn archetype:create -DgroupId=com.junicorn -DartifactId=mario -DpackageName=com.junicorn.mario
初始化一下 pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.junicorn</groupId> <artifactId>mario</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>mario</name> <url>https://github.com/junicorn/mario</url> <properties> <maven.compiler.source>1.6</maven.compiler.source> <maven.compiler.target>1.6</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <servlet.version>3.0.1</servlet.version> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>
OK,项目创建好了,这个将是我们的框架。
框架流程
web程序是基于 M(模型)V(视图)C(控制器)
设计的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从 Java 中分离了出来,所以它允许你的网页中只包含很少的脚本。
- 模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。
- 视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Java中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Jsp已经很好的实现了View层中的部分功能。
- 控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。
设计思路
mario 是基于servlet实现的mvc,用一个全局的Filter来做核心控制器,使用sql2o框架作为数据库基础访问。 使用一个接口Bootstrap
作为初始化启动,实现它并遵循Filter参数约定即可。
建立路由、数据库、视图相关的包和类,下面是结构:
路由设计
现代 Web 应用的 URL 十分优雅,易于人们辨识记忆。 路由的表现形式如下:
/resources/:resource/actions/:action http://bladejava.com http://bladejava.com/docs/modules/route
那么我们在java语言中将他定义一个 Route
类, 用于封装一个请求的最小单元, 在Mario中我们设计一个路由的对象如下:
/** * 路由 * @author biezhi */ public class Route { /** * 路由path */ private String path; /** * 执行路由的方法 */ private Method action; /** * 路由所在的控制器 */ private Object controller; public Route() { } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public Method getAction() { return action; } public void setAction(Method action) { this.action = action; } public Object getController() { return controller; } public void setController(Object controller) { this.controller = controller; } }
所有的请求在程序中是一个路由,匹配在 path
上,执行靠 action
,处于 controller
中。
Mario使用一个Filter接收所有请求,因为从Filter过来的请求有无数,如何知道哪一个请求对应哪一个路由呢? 这时候需要设计一个路由匹配器去查找路由处理我们配置的请求, 有了路由匹配器还不够,这么多的路由我们如何管理呢?再来一个路由管理器吧,下面就创建路由匹配器和管理器2个类:
/** * 路由管理器,存放所有路由的 * @author biezhi */ public class Routers { private static final Logger LOGGER = Logger.getLogger(Routers.class.getName()); private List<Route> routes = new ArrayList<Route>(); public Routers() { } public void addRoute(List<Route> routes){ routes.addAll(routes); } public void addRoute(Route route){ routes.add(route); } public void removeRoute(Route route){ routes.remove(route); } public void addRoute(String path, Method action, Object controller){ Route route = new Route(); route.setPath(path); route.setAction(action); route.setController(controller); routes.add(route); LOGGER.info("Add Route:[" + path + "]"); } public List<Route> getRoutes() { return routes; } public void setRoutes(List<Route> routes) { this.routes = routes; } }
这里的代码很简单,这个管理器里用List存储所有路由,公有的 addRoute
方法是给外部调用的。
/** * 路由匹配器,用于匹配路由 * @author biezhi */ public class RouteMatcher { private List<Route> routes; public RouteMatcher(List<Route> routes) { this.routes = routes; } public void setRoutes(List<Route> routes) { this.routes = routes; } /** * 根据path查找路由 * @param path 请求地址 * @return 返回查询到的路由 */ public Route findRoute(String path) { String cleanPath = parsePath(path); List<Route> matchRoutes = new ArrayList<Route>(); for (Route route : this.routes) { if (matchesPath(route.getPath(), cleanPath)) { matchRoutes.add(route); } } // 优先匹配原则 giveMatch(path, matchRoutes); return matchRoutes.size() > 0 ? matchRoutes.get(0) : null; } private void giveMatch(final String uri, List<Route> routes) { Collections.sort(routes, new Comparator<Route>() { @Override public int compare(Route o1, Route o2) { if (o2.getPath().equals(uri)) { return o2.getPath().indexOf(uri); } return -1; } }); } private boolean matchesPath(String routePath, String pathToMatch) { routePath = routePath.replaceAll(PathUtil.VAR_REGEXP, PathUtil.VAR_REPLACE); return pathToMatch.matches("(?i)" + routePath); } private String parsePath(String path) { path = PathUtil.fixPath(path); try { URI uri = new URI(path); return uri.getPath(); } catch (URISyntaxException e) { return null; } } }
路由匹配器使用了正则去遍历路由列表,匹配合适的路由。当然我不认为这是最好的方法, 因为路由的量很大之后遍历的效率会降低,但这样是可以实现的,如果你有更好的方法可以告诉我 🙂
在下一章节我们需要对请求处理做设计了~
控制器设计
一个MVC框架里 C
是核心的一块,也就是控制器,每个请求的接收,都是由控制器去处理的。 在Mario中我们把控制器放在路由对象的controller字段上,实际上一个请求过来之后最终是落在某个方法去处理的。
简单的方法我们可以使用反射实现动态调用方法执行,当然这对性能并不友好,你可以用缓存Method或者更高明的技术去做。 在这里我们不提及太麻烦的东西,因为初步目标是实现MVC框架,所以给大家提醒一下有些了解即可。
控制器的处理部分放在了核心Filter中,代码如下:
/** * Mario MVC核心处理器 * @author biezhi * */ public class MarioFilter implements Filter { private static final Logger LOGGER = Logger.getLogger(MarioFilter.class.getName()); private RouteMatcher routeMatcher = new RouteMatcher(new ArrayList<Route>()); private ServletContext servletContext; @Override public void init(FilterConfig filterConfig) throws ServletException { Mario mario = Mario.me(); if(!mario.isInit()){ String className = filterConfig.getInitParameter("bootstrap"); Bootstrap bootstrap = this.getBootstrap(className); bootstrap.init(mario); Routers routers = mario.getRouters(); if(null != routers){ routeMatcher.setRoutes(routers.getRoutes()); } servletContext = filterConfig.getServletContext(); mario.setInit(true); } } private Bootstrap getBootstrap(String className) { if(null != className){ try { Class<?> clazz = Class.forName(className); Bootstrap bootstrap = (Bootstrap) clazz.newInstance(); return bootstrap; } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } throw new RuntimeException("init bootstrap class error!"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; // 请求的uri String uri = PathUtil.getRelativePath(request); LOGGER.info("Request URI:" + uri); Route route = routeMatcher.findRoute(uri); // 如果找到 if (route != null) { // 实际执行方法 handle(request, response, route); } else{ chain.doFilter(request, response); } } private void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Route route){ // 初始化上下文 Request request = new Request(httpServletRequest); Response response = new Response(httpServletResponse); MarioContext.initContext(servletContext, request, response); Object controller = route.getController(); // 要执行的路由方法 Method actionMethod = route.getAction(); // 执行route方法 executeMethod(controller, actionMethod, request, response); } /** * 获取方法内的参数 */ private Object[] getArgs(Request request, Response response, Class<?>[] params){ int len = params.length; Object[] args = new Object[len]; for(int i=0; i<len; i++){ Class<?> paramTypeClazz = params[i]; if(paramTypeClazz.getName().equals(Request.class.getName())){ args[i] = request; } if(paramTypeClazz.getName().equals(Response.class.getName())){ args[i] = response; } } return args; } /** * 执行路由方法 */ private Object executeMethod(Object object, Method method, Request request, Response response){ int len = method.getParameterTypes().length; method.setAccessible(true); if(len > 0){ Object[] args = getArgs(request, response, method.getParameterTypes()); return ReflectUtil.invokeMehod(object, method, args); } else { return ReflectUtil.invokeMehod(object, method); } } }
这里执行的流程是酱紫的:
- 接收用户请求
- 查找路由
- 找到即执行配置的方法
- 找不到你看到的应该是404
看到这里也许很多同学会有点疑问,我们在说路由、控制器、匹配器,可是我怎么让它运行起来呢? 您可说到点儿上了,几乎在任何框架中都必须有配置这项,所谓的零配置都是扯淡。不管硬编码还是配置文件方式, 没有配置,框架的易用性和快速开发靠什么完成,又一行一行编写代码吗? 如果你说机器学习,至少现在好像没人用吧。
扯淡完毕,下一节来进入全局配置设计 ->
配置设计
Mario中所有的配置都可以在 Mario
全局唯一对象完成,将它设计为单例。
要运行起来整个框架,Mario对象是核心,看看里面都需要什么吧!
- 添加路由
- 读取资源文件
- 读取配置
- 等等
由此我们简单的设计一个Mario全局对象:
/** * Mario * @author biezhi * */ public final class Mario { /** * 存放所有路由 */ private Routers routers; /** * 配置加载器 */ private ConfigLoader configLoader; /** * 框架是否已经初始化 */ private boolean init = false; private Mario() { routers = new Routers(); configLoader = new ConfigLoader(); } public boolean isInit() { return init; } public void setInit(boolean init) { this.init = init; } private static class MarioHolder { private static Mario ME = new Mario(); } public static Mario me(){ return MarioHolder.ME; } public Mario addConf(String conf){ configLoader.load(conf); return this; } public String getConf(String name){ return configLoader.getConf(name); } public Mario addRoutes(Routers routers){ this.routers.addRoute(routers.getRoutes()); return this; } public Routers getRouters() { return routers; } /** * 添加路由 * @param path 映射的PATH * @param methodName 方法名称 * @param controller 控制器对象 * @return 返回Mario */ public Mario addRoute(String path, String methodName, Object controller){ try { Method method = controller.getClass().getMethod(methodName, Request.class, Response.class); this.routers.addRoute(path, method, controller); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } return this; } }
这样在系统中永远保持一个Mario实例,我们用它来操作所有配置即可。
在Boostrap
的init
方法中使用
@Override public void init(Mario mario) { Index index = new Index(); mario.addRoute("/", "index", index); mario.addRoute("/html", "html", index); }
这样,一个简单的MVC后端已经形成了!接下来我们要将结果展现在JSP文件中,要做视图的渲染设计 LET’S GO!
视图设计
我们已经完成了MVC中的C层,还有M和V没有做呢。这一小节来对视图进行设计,从后台到前台的渲染是这样的 后台给定一个视图位置,输出到前端JSP或者其他模板引擎上,做一个非常简单的接口:
/** * 视图渲染接口 * @author biezhi * */ public interface Render { /** * 渲染到视图 * @param view 视图名称 * @param writer 写入对象 */ public void render(String view, Writer writer); }
具体的实现我们先写一个JSP的,当你在使用Servlet进行开发的时候已经习惯了这句语法:
servletRequest.getRequestDispatcher(viewPath).forward(servletRequest, servletResponse);
那么一个JSP的渲染实现就很简单了
/** * JSP渲染实现 * @author biezhi * */ public class JspRender implements Render { @Override public void render(String view, Writer writer) { String viewPath = this.getViewPath(view); HttpServletRequest servletRequest = MarioContext.me().getRequest().getRaw(); HttpServletResponse servletResponse = MarioContext.me().getResponse().getRaw(); try { servletRequest.getRequestDispatcher(viewPath).forward(servletRequest, servletResponse); } catch (ServletException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } private String getViewPath(String view){ Mario mario = Mario.me(); String viewPrfix = mario.getConf(Const.VIEW_PREFIX_FIELD); String viewSuffix = mario.getConf(Const.VIEW_SUFFIX_FIELD); if (null == viewSuffix || viewSuffix.equals("")) { viewSuffix = Const.VIEW_SUFFIX; } if (null == viewPrfix || viewPrfix.equals("")) { viewPrfix = Const.VIEW_PREFIX; } String viewPath = viewPrfix + "/" + view; if (!view.endsWith(viewSuffix)) { viewPath += viewSuffix; } return viewPath.replaceAll("[/]+", "/"); } }
配置 JSP 视图的位置和后缀可以在配置文件或者硬编码中进行,当然这看你的习惯, 默认设置了 JSP 在 /WEB-INF/
下,后缀是 .jsp
你懂的!
怎么用可以参考 mario-sample
这个项目,因为真的很简单 相信你自己。
在下一节中我们就要和数据库打交道了,尝试新的旅程吧 🙂
数据库操作
这一小节是对数据库操作做一个简单的封装,不涉及复杂的事务操作等。
我选用了Sql2o作为底层数据库框架作为支持,它的简洁易用性让我刮目相看,后面我们也会写如何实现一个ORM框架。
/** * 数据库支持 * @author biezhi * */ public final class MarioDb { private static Sql2o sql2o = null; private MarioDb() { } /** * 初始化数据库配置 * @param url * @param user * @param pass */ public static void init(String url, String user, String pass){ sql2o = new Sql2o(url, user, pass); } /** * 初始化数据库配置 * @param dataSource */ public static void init(DataSource dataSource){ sql2o = new Sql2o(dataSource); } /** * 查询一个对象 * @param sql * @param clazz * @return */ public static <T> T get(String sql, Class<T> clazz){ return get(sql, clazz, null); } /** * 查询一个列表 * @param sql * @param clazz * @return */ public static <T> List<T> getList(String sql, Class<T> clazz){ return getList(sql, clazz, null); } /** * 查询一个对象返回为map类型 * @param sql * @return */ public static Map<String, Object> getMap(String sql){ return getMap(sql, null); } /** * 查询一个列表并返回为list<map>类型 * @param sql * @return */ public static List<Map<String, Object>> getMapList(String sql){ return getMapList(sql, null); } /** * 插入一条记录 * @param sql * @param params * @return */ public static int insert(String sql, Object ... params){ StringBuffer sqlBuf = new StringBuffer(sql); sqlBuf.append(" values ("); int start = sql.indexOf("(") + 1; int end = sql.indexOf(")"); String a = sql.substring(start, end); String[] fields = a.split(","); Map<String, Object> map = new HashMap<String, Object>(); int i=0; for(String name : fields){ sqlBuf.append(":" + name.trim() + " ,"); map.put(name.trim(), params[i]); i++; } String newSql = sqlBuf.substring(0, sqlBuf.length() - 1) + ")"; Connection con = sql2o.open(); Query query = con.createQuery(newSql); executeQuery(query, map); int res = query.executeUpdate().getResult(); con.close(); return res; } /** * 更新 * @param sql * @return */ public static int update(String sql){ return update(sql, null); } /** * 带参数更新 * @param sql * @param params * @return */ public static int update(String sql, Map<String, Object> params){ Connection con = sql2o.open(); Query query = con.createQuery(sql); executeQuery(query, params); int res = query.executeUpdate().getResult(); con.close(); return res; } public static <T> T get(String sql, Class<T> clazz, Map<String, Object> params){ Connection con = sql2o.open(); Query query = con.createQuery(sql); executeQuery(query, params); T t = query.executeAndFetchFirst(clazz); con.close(); return t; } @SuppressWarnings("unchecked") public static Map<String, Object> getMap(String sql, Map<String, Object> params){ Connection con = sql2o.open(); Query query = con.createQuery(sql); executeQuery(query, params); Map<String, Object> t = (Map<String, Object>) query.executeScalar(); con.close(); return t; } public static List<Map<String, Object>> getMapList(String sql, Map<String, Object> params){ Connection con = sql2o.open(); Query query = con.createQuery(sql); executeQuery(query, params); List<Map<String, Object>> t = query.executeAndFetchTable().asList(); con.close(); return t; } public static <T> List<T> getList(String sql, Class<T> clazz, Map<String, Object> params){ Connection con = sql2o.open(); Query query = con.createQuery(sql); executeQuery(query, params); List<T> list = query.executeAndFetch(clazz); con.close(); return list; } private static void executeQuery(Query query, Map<String, Object> params){ if (null != params && params.size() > 0) { Set<String> keys = params.keySet(); for(String key : keys){ query.addParameter(key, params.get(key)); } } } }
设计MVC框架部分已经完成,下一节是一个增删改查的例子
增删改查
/** * 用户控制器 */ public class UserController { /** * 用户列表 * @param request * @param response */ public void users(Request request, Response response){ List<User> users = MarioDb.getList("select * from t_user", User.class); request.attr("users", users); response.render("users"); } /** * 添加用户界面 * @param request * @param response */ public void show_add(Request request, Response response){ response.render("user_add"); } /** * 保存方法 * @param request * @param response * @throws ParseException */ public void save(Request request, Response response) throws ParseException{ String name = request.query("name"); Integer age = request.queryAsInt("age"); String date = request.query("birthday"); if(null == name || null == age || null == date){ request.attr("res", "error"); response.render("user_add"); return; } Date bir = new SimpleDateFormat("yyyy-MM-dd").parse(date); int res = MarioDb.insert("insert into t_user(name, age, birthday)", name, age, bir); if(res > 0){ String ctx = MarioContext.me().getContext().getContextPath(); String location = ctx + "/users"; response.redirect(location.replaceAll("[/]+", "/")); } else { request.attr("res", "error"); response.render("user_add"); } } /** * 编辑页面 * @param request * @param response */ public void edit(Request request, Response response){ Integer id = request.queryAsInt("id"); if(null != id){ Map<String, Object> map = new HashMap<String, Object>(); map.put("id", id); User user = MarioDb.get("select * from t_user where id = :id", User.class, map); request.attr("user", user); response.render("user_edit"); } } /** * 修改信息 * @param request * @param response */ public void update(Request request, Response response){ Integer id = request.queryAsInt("id"); String name = request.query("name"); Integer age = request.queryAsInt("age"); if(null == id || null == name || null == age ){ request.attr("res", "error"); response.render("user_edit"); return; } Map<String, Object> map = new HashMap<String, Object>(); map.put("id", id); map.put("name", name); map.put("age", age); int res = MarioDb.update("update t_user set name = :name, age = :age where id = :id", map); if(res > 0){ String ctx = MarioContext.me().getContext().getContextPath(); String location = ctx + "/users"; response.redirect(location.replaceAll("[/]+", "/")); } else { request.attr("res", "error"); response.render("user_edit"); } } /** * 删除 * @param request * @param response */ public void delete(Request request, Response response){ Integer id = request.queryAsInt("id"); if(null != id){ Map<String, Object> map = new HashMap<String, Object>(); map.put("id", id); MarioDb.update("delete from t_user where id = :id", map); } String ctx = MarioContext.me().getContext().getContextPath(); String location = ctx + "/users"; response.redirect(location.replaceAll("[/]+", "/")); } }
原文地址:https://www.jianshu.com/p/d20b4890c437
相关推荐
-
全方位详解Service Mesh(服务网格) Java基础
2019-10-4
-
什么是 JWT — JSON WEB TOKEN Java基础
2019-2-2
-
shiro自定义异常无法被捕获总是抛出AuthenticationException解决方案 Java基础
2019-9-15
-
重写equals就必须重写hashCode的原理分析 Java基础
2019-9-4
-
Java PDF生成方案之iText Java基础
2020-5-30
-
Redis 持久化(persistence)技术口袋书 Java基础
2018-10-10
-
Elasticsearch实现复合查询,高亮结果等技巧 Java基础
2019-9-10
-
利用Activity实现一个简单的可输入对话框 Java基础
2020-6-16
-
Golang命名规范和开发规范 Java基础
2019-9-17
-
Java 8原生API也可以开发响应式代码? Java基础
2019-9-1