作者: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("[/]+", "/"));
}
}
网友评论