Nemo

Nemo 关注TA

路漫漫其修远兮,吾将上下而求索。

Nemo

该文章投稿至Nemo社区   Java  板块 复制链接


基于servlet简单实现一个mvc框架(模仿springmvc)

发布于 2017/12/15 18:30 1,659浏览 6回复 11,887

因为最近在折腾一些比较底层的东西,作为搞java的,不可避免的要折腾下mvc架构了。

这是不久前开始折腾了几天的一个mvc架构,这两天偶尔零零星星的完善了一些,算是有了个雏形,这里稍稍记录下。


这个项目的源码目前托管在github上,地址为:

https://github.com/geeeeeeeeeeeeeeeek/NemoMvc


简单的说明下:

此框架大体只为学习mvc架构方面的东西,简单实现,所以肯定还会存在不少的问题。
框架基于servlet,提供了类似springmvc中Controller,RequestBOdy,ResponseBody,RequestMapping类似的注解支持。
目前数据接收支持json、地址栏参数接收。
数据返回支持html/text及json/application,不过因为没有完成模板引擎,所以html/text暂时只支持页面跳转,不能传参。后续再有空闲时间会添加模板引擎。



简单捋一下这个架构的设计思路:

1、我们需要一个容器(NemoFrameworkCore):

我们都知道,spring中有个Ioc的概念。这个玩意通俗点说就是一个容器,这个容器是在内存中,里面放着各种各样的程序相关的配置/程序实例片段一类的。

所以咱们也需要一个容器,在整个程序的运行期间,用来存放程序相关的东西,方便程序调度。这也是框架的核心所在。

在框架中,就叫它NemoFrameworkCore好了。

2、这个容器里存什么:

配置/程序实例片段一类的。

配置,即程序的一些配置文件中,我们会预设的一些参数。

实例片段通俗来说,也就是java中new Object();后的对象实例了。这些实例片段需要按照一定的规则存放在容器中,方便在程序中调用。这里的规则,我们暂且叫它路由(Route)好了。

3、往容器中放东西需要一个过程:

也就是怎么往这个容器放东西了。

1、配置文件:

我们采用的配置文件是.properties结尾的文件。当然,配置文件可以是多种多样的,类似于xml.ini,text,yml等等,只要是能够易于操作就行。

根据配置文件的位置,读取其中的内容,并把它存放到咱们的容器中,即可。

2、程序实例片段:

这部分需要用到java中反射的内容。包括从磁盘指定位置扫描所有的.class文件,找出我们需要放到容器中的,并把它实例化后放到容器中。

4、怎么用这个容器:

1、配置文件:

直接从容器中读取即可。

2、程序实例片段:

我们在容器中只存放了一些实例,这些实例在程序的运行期间都会保持着,而在java中,我们可以使用反射来调用。

5、框架怎么处理请求:

框架基于servlet,所以我们可以用拦截器或者直接用servlet来拦截我们需要拦截的请求。

根据来者的访问路径,咱们的程序会在容器中根据预设好的路由(Route)找到指定处理请求的实例,然后通过java的反射机制取调用它,并且最终把实例返回的结果再做一些处理,最后返回给调用的客户端即可。


所以这个框架的重点应该是在于java中反射的运用和容器的设计。




有了以上的思路,那么即可以开始动手写这个框架的具体实现了。

1、首先,框架基于servlet,所以咱们需要在tomcat启动的时候就开始做一些操作,并且让这个servlet拦截所有咱们需要做处理的请求:


/**
* 框架核心控制器
* Created by Nemo on 2017/11/23.
*/
@WebServlet(name="/",urlPatterns = "/",loadOnStartup=1)
public class NemoMvcFrameworkCoreServlet extends HttpServlet {

private ServletContext context;

@Override
public void init() throws ServletException {
//TODO 开始扫描配置,加载需要的class等等操作
}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//TODO 判断是否是配置需要处理的请求,开始处理请求
}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //TODO
}

@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//TODO
}

@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//TODO
}
}

配置了loadOnStartup=1,这样咱们的程序便会在开始的时候执行这个servlet的init方法。咱们可以在init方法中做一些配置加载,程序实例加载等等操作。

配置了urlPatterns="/",这样所有访问这个程序的请求都会经过这个servlet的处理。咱们可以在其中判断请求的路径是不是需要框架处理,再做进一步操作。


2、设计一个路由:

因为是mvc层,所以mvc中的程序实例只存放controller中,对应的访问路径、访问的controller实例、以及controller中的方法的对应关系即可。

/**
* 路由实体,用来定义控制器/方法/访问路径
* Created by Nemo on 2017/11/23.
*/
public class RouteBean {

/**
* 访问路径
*/
private String path;

/**
* 需要执行的路由方法
*/
private Method method;

/**
* 路由所在的控制器
*/
private Object controller;

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}

public Method getMethod() {
return method;
}

public void setMethod(Method method) {
this.method = method;
}

public Object getController() {
return controller;
}

public void setController(Object controller) {
this.controller = controller;
}
}

咱们还需要一个路由管理器:


/**
* 路由管理器基类,所有的路由管理器都需要继承自它
* Created by Nemo on 2017/11/23.
*/
public abstract class Routers {

/**
* 路由列表,key为路由访问路径,val为路由本身
*/
private Map<String,RouteBean> routes = new HashMap<String,RouteBean>();

/**
* 添加单个路由
* @param route
*/
public void addRoute(RouteBean route){
if(route.getPath() == null) {
route.setPath("/");
}
routes.put(route.getPath(),route);
}

/**
* 批量增加路由
* @param routeBeanList
*/
public void addRoutes(List<RouteBean> routeBeanList){
if(CollectionUtils.isNotEmpty(routeBeanList)){
for(RouteBean route : routeBeanList) {
if(route.getPath() == null){
route.setPath("/");
}
routes.put(route.getPath(), route);
}
}
}

/**
* 移除单个路由
* @param routeBean
*/
public void removeRoute(RouteBean routeBean){
if(routeBean.getPath() == null){
routeBean.setPath("/");
}
routes.remove(routeBean.getPath());
}

/**
* 添加单个路由
* @param path
* @param controller
* @param method
*/
public void addRoute(String path, Object controller, Method method){
if(path == null){
path = "/";
}
RouteBean routeBean = new RouteBean();
routeBean.setPath(path);
routeBean.setController(controller);
routeBean.setMethod(method);

routes.put(routeBean.getPath(),routeBean);
}

public RouteBean getRouteByPath(String path){
if(path == null){
path = "/";
}
return routes.get(path);
}

public abstract Map<String, RouteBean> getRoutes();

public abstract void setRoutes(Map<String, RouteBean> routes);
}

这里的路由管理设计成一个抽象类,是因为目前Restful风格的网络请求会有四种:GET/PUT/POST/DELETE,在容器中,这四类请求对应的路径/方法/controller考虑单独存放,方便调取。而需要扩展的路由可以直接继承自这个基类即可。
例如对应get请求的路由:

/**
* 所有Get方法的路由管理器
* Created by Nemo on 2017/11/23.
*/
public class GetRouters extends Routers {

/**
* 路由列表,key为路由访问路径,val为路由本身
*/
private Map<String,RouteBean> routes = new HashMap<String,RouteBean>();

public Map<String, RouteBean> getRoutes() {
return routes;
}

public void setRoutes(Map<String, RouteBean> routes) {
this.routes = routes;
}
}

只需要单独实现取路由和添加路由的方法,即可完成路由的功能。

3、设计一个用来盛放一些程序运行期间需要的资源的容器:

容器主要是用来存放路由和一些程序运行期间用到的配置。

在servlet启动的时候,程序开始扫描配置,程序文件,添加到容器中,所以同时我们也需要做一个标识,当程序扫描完毕的时候,那么就可以开放访问了。


/**
* 框架核心处理类,用来加载路由 + 配置 + 资源文件等一系列的处理
* note:目前先处理路由
* Created by Nemo on 2017/11/23.
*/
public class NemoFrameworkCore {
/**
* 路由管理器
*/
private GetRouters getRouters;
private PutRouters putRouters;
private PostRouters postRouters;
private DeleteRouters deleteRouters;

/**
* 是否已经初始化的标志
*/
private boolean isInited = false;

private static NemoFrameworkCore nemoFrameworkCore = null;

private NemoFrameworkCore(){
getRouters = new GetRouters();
putRouters = new PutRouters();
deleteRouters = new DeleteRouters();
postRouters = new PostRouters();
}

public static NemoFrameworkCore core(){
if(nemoFrameworkCore == null){
nemoFrameworkCore = new NemoFrameworkCore();
}
return nemoFrameworkCore;
}

/**
* 添加路由
* @param path 访问路径
* @param controller 控制器对象
* @param method 方法
* @return
*/
public NemoFrameworkCore addRoute(String path, Object controller, Method method){
try {
UrlMapping urlMapping = method.getAnnotation(UrlMapping.class);
MappingMethod mappingMethod = urlMapping.method();
if(mappingMethod == null || mappingMethod.name().equals(MappingMethod.GET.name())){
this.getRouters.addRoute(path, controller , method);
}else if(mappingMethod.name().equals(MappingMethod.POST.name())){
this.postRouters.addRoute(path, controller , method);
}else if(mappingMethod.name().equals(MappingMethod.DELETE.name())){
this.deleteRouters.addRoute(path, controller , method);
}else if(mappingMethod.name().equals(MappingMethod.PUT.name())){
this.putRouters.addRoute(path, controller , method);
}
} catch (Exception e){
e.printStackTrace();
}
return this;
}

/**
* 根据路径得到路由
* @param path
* @return
*/
public RouteBean getRouteByPath(String path){
return this.getRouters.getRouteByPath(path);
}

/**
* 根据访问地址+访问方法获取路由
* @param path
* @param mappingMethod
* @return
*/
public RouteBean getRouteByPathAndMappingMethod(String path,MappingMethod mappingMethod){
if(mappingMethod == null || mappingMethod.name().equals(MappingMethod.GET.name())){
return this.getRouters.getRouteByPath(path);
}else if(mappingMethod.name().equals(MappingMethod.POST.name())){
return this.postRouters.getRouteByPath(path);
}else if(mappingMethod.name().equals(MappingMethod.DELETE.name())){
return this.deleteRouters.getRouteByPath(path);
}else if(mappingMethod.name().equals(MappingMethod.PUT.name())){
return this.putRouters.getRouteByPath(path);
}
return null;
}

/**
* 从请求中获取需要访问的路由
* @param request
* @return
*/
public RouteBean getRoute(HttpServletRequest request,MappingMethod mappingMethod){
//用户访问的路径
String path = NemoFrameworkUrlUtils.getFullRequestUrl(request);

//得到路由
return this.getRouteByPathAndMappingMethod(path, mappingMethod);
}

public boolean isInited() {
return isInited;
}

public void setInited(boolean inited) {
isInited = inited;
}

/**
* 得到配置
* @param key
* @return
*/
public static String getProp(String key){
return NemoFrameworkPropertiesUtils.getProp(key);
}

}

通过这个容器,可以很方便的得到路由相关,配置相关的信息。


4、设计一个配置扫描器,扫描程序的可变配置:

程序的配置文件固定为:nemo.framework.properties,servlet启动的时候,可以读取这个文件的数据,并且把读取到的结果保存在一个静态空间中,方便程序接下来的调用。


/**
* Created by Nemo on 2017/11/27.
*/
public class NemoFrameworkPropertiesUtils {

/**
* 初始化加载配置文件信息
*/
public static void loadProperties(){
Properties pps = new Properties();
try {
ClassLoader classLoader = NemoFrameworkPropertiesUtils.class.getClassLoader();
//系统配置文件固定为这个
pps.load( classLoader.getResourceAsStream("nemo.framework.properties"));
} catch (IOException e) {
e.printStackTrace();
}
//得到配置文件的名字
Enumeration enumeration = pps.propertyNames();
while(enumeration.hasMoreElements()) {
String strKey = (String) enumeration.nextElement();
String strValue = pps.getProperty(strKey);
//加入缓存中
NemoFrameworkCacheUtils.addCache(strKey,strValue);
}
}

/**
* 得到配置信息
* @param key
* @return
*/
public static String getProp(String key) {
Object prop = NemoFrameworkCacheUtils.getCache(key);
return prop==null?null:prop.toString();
}

}

/**
* Created by Nemo on 2017/11/27.
*/
public class NemoFrameworkCacheUtils {

private static Map<String,Object> props = new HashMap<String, Object>();

private static String DEFAULT_PREFIX = "nemo.framework.cache.";

/**
* 得到缓存
* @return
*/
public static Object getCache(String key){
if(key != null){
return props.get(DEFAULT_PREFIX+key);
}
return null;
}

/**
* 删除缓存
* @param key
*/
public static void removeCache(String key){
if(key!=null){
props.remove(DEFAULT_PREFIX+key);
}
}

/**
* 添加缓存
* @param key
* @param value
*/
public static void addCache(String key,Object value){
if(key!=null){
props.put(DEFAULT_PREFIX+key,value);
}
}

}


5、设计一个包扫描器:

这个包扫描器做的工作也就是把配置的指定目录下的所有.class文件都扫描一遍,找到咱们需要放到路由中的代码片段(controller和UrlMapping),并且把它实例化,建立对应的路由关系后存到咱们的容器中。


/**
* 包扫描器,扫描报下的指定注解,存放到核心框架内存中
* Created by Nemo on 2017/11/23.
*/
public class NemoFrameworkCorePackageScaner {

/**
* 开始扫描指定包下面的所有注解为Controller的类及其下面全部注解为UrlMapping的方法
* @param packageName
*/
public static void scan(String packageName) throws Exception {
NemoFrameworkCore core = NemoFrameworkCore.core();
if(!core.isInited()) {
//得到所有的controller
List<Object> classList = NemoFrameworkAnnotationUtils.getAllController(packageName);
if (CollectionUtils.isNotEmpty(classList)) {
for (Object clss : classList) {
//所有controller下的mapping
点赞(0)
点了个评