java如何设计一个框架_如何设计一个Java Web MVC框架

通过使用Java语言实现一个完整的框架设计,这个框架中主要内容有第一小节介绍的Web框架的结构规划,例如采用MVC模式来进行开发,程序的执行流程设计等内容;第二小节介绍框架的第一个功能:路由,如何让访问的URL映射到相应的处理逻辑;第三小节介绍处理逻辑,如何设计一个公共的调度器,对象继承之后处理函数中如何处理response和request;第四小节至第六小节介绍如何框架的一些辅助功能,例如配置信息,数据库操作等;最后介绍如何基于Web框架实现一个简单的增删改查,包括User的添加、修改、删除、显示列表等操作。

通过这么一个完整的项目例子,我期望能够让读者了解如何开发Web应用,如何搭建自己的目录结构,如何实现路由,如何实现MVC模式等各方面的开发内容。在框架盛行的今天,MVC也不再是神话。经常听到很多程序员讨论哪个框架好,哪个框架不好, 其实框架只是工具,没有好与不好,只有适合与不适合,适合自己的就是最好的,所以教会大家自己动手写框架,那么不同的需求都可以用自己的思路去实现。

e5671a6a9e28630a93210d180c92a01e.png

接下来开始我们的框架之旅。

项目规划

做任何事情都需要做好规划,那么我们在开发博客系统之前,同样需要做好项目的规划,如何设置目录结构,如何理解整个项目的流程图,当我们理解了应用的执行过程,那么接下来的设计编码就会变得相对容易了

创建一个maven项目

约定一下框架基础信息

假设我们的web框架名称是 mario

包名是 com.junicorn.mario

命令行创建

mvn archetype:create -DgroupId=com.junicorn -DartifactId=mario -DpackageName=com.junicorn.mario

Eclipse创建

77f233aa50ff5d9842c5fc48e4d45223.png

f0a586cdf7505719496f9f9bdf8108a5.png

创建好的基本结构是这样的

6ca033262888018a6527c15083dc19c5.png

初始化一下 pom.xml

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

com.junicorn

mario

0.0.1-SNAPSHOT

jar

mario

https://github.com/junicorn/mario

1.6

1.6

UTF-8

3.0.1

javax.servlet

javax.servlet-api

3.1.0

provided

org.apache.maven.plugins

maven-compiler-plugin

3.1

1.6

1.6

UTF-8

OK,项目创建好了,这个将是我们的框架。

框架流程

web程序是基于 M(模型)V(视图)C(控制器)设计的。MVC是一种将应用程序的逻辑层和表现层进行分离的结构方式。在实践中,由于表现层从 Java 中分离了出来,所以它允许你的网页中只包含很少的脚本。

模型 (Model) 代表数据结构。通常来说,模型类将包含取出、插入、更新数据库资料等这些功能。

视图 (View) 是展示给用户的信息的结构及样式。一个视图通常是一个网页,但是在Java中,一个视图也可以是一个页面片段,如页头、页尾。它还可以是一个 RSS 页面,或其它类型的“页面”,Jsp已经很好的实现了View层中的部分功能。

控制器 (Controller) 是模型、视图以及其他任何处理HTTP请求所必须的资源之间的中介,并生成网页。

设计思路

mario 是基于servlet实现的mvc,用一个全局的Filter来做核心控制器,使用sql2o框架作为数据库基础访问。 使用一个接口Bootstrap作为初始化启动,实现它并遵循Filter参数约定即可。

建立路由、数据库、视图相关的包和类,下面是结构:

c591f618e2afc660c417473ea2b4a444.png

路由设计

现代 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 routes = new ArrayList();

public Routers() {

}

public void addRoute(List 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 getRoutes() {

return routes;

}

public void setRoutes(List routes) {

this.routes = routes;

}

}

这里的代码很简单,这个管理器里用List存储所有路由,公有的 addRoute 方法是给外部调用的。

/**

* 路由匹配器,用于匹配路由

* @author biezhi

*/

public class RouteMatcher {

private List routes;

public RouteMatcher(List routes) {

this.routes = routes;

}

public void setRoutes(List routes) {

this.routes = routes;

}

/**

* 根据path查找路由

* @param path 请求地址

* @return 返回查询到的路由

*/

public Route findRoute(String path) {

String cleanPath = parsePath(path);

List matchRoutes = new ArrayList();

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 routes) {

Collections.sort(routes, new Comparator() {

@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());

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

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 get(String sql, Class clazz){

return get(sql, clazz, null);

}

/**

* 查询一个列表

* @param sql

* @param clazz

* @return

*/

public static List getList(String sql, Class clazz){

return getList(sql, clazz, null);

}

/**

* 查询一个对象返回为map类型

* @param sql

* @return

*/

public static Map getMap(String sql){

return getMap(sql, null);

}

/**

* 查询一个列表并返回为list类型

* @param sql

* @return

*/

public static List> 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 map = new HashMap();

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 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 get(String sql, Class clazz, Map 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 getMap(String sql, Map params){

Connection con = sql2o.open();

Query query = con.createQuery(sql);

executeQuery(query, params);

Map t = (Map) query.executeScalar();

con.close();

return t;

}

public static List> getMapList(String sql, Map params){

Connection con = sql2o.open();

Query query = con.createQuery(sql);

executeQuery(query, params);

List> t = query.executeAndFetchTable().asList();

con.close();

return t;

}

public static List getList(String sql, Class clazz, Map params){

Connection con = sql2o.open();

Query query = con.createQuery(sql);

executeQuery(query, params);

List list = query.executeAndFetch(clazz);

con.close();

return list;

}

private static void executeQuery(Query query, Map params){

if (null != params && params.size() > 0) {

Set 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 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 map = new HashMap();

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 map = new HashMap();

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 map = new HashMap();

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("[/]+", "/"));

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值