整体代码结构
├─java│ └─com│ └─github│ └─houbb│ └─mvc│ │ package-info.java│ ││ ├─annotation│ │ Controller.java│ │ RequestMapping.java│ │ RequestParam.java│ ││ ├─controller│ │ IndexController.java│ ││ ├─exception│ │ MvcRuntimeException.java│ ││ └─servlet│ DispatchServlet.java│├─resources│ application.properties│└─webapp └─WEB-INF web.xmlpom.xml 依赖
引入 servlet-api 相关的包,用户开发。
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.0.1</version><scope>provided</scope></dependency>注解
因为本次主要实现 dispatch 分发这个功能,所有的 ioc 并不是我们实现的重点。
关于 ioc,可以参见spring ioc 实现
所以只实现了以下几个注解:
功能和 spring 保持一致,此处不再赘述。
@Controller
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public@interfaceController{/** * 对象的别名 * @return 路径 * @since 0.0.1 */Stringvalue()default"";}@RequestMapping
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD,ElementType.TYPE})public@interfaceRequestMapping{/** * 映射 url 路徑 * @return 路径 * @since 0.0.1 */Stringvalue();}@RequestParam
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.PARAMETER)public@interfaceRequestParam{/** * 参数别称 * @return 路径 * @since 0.0.1 */Stringvalue();}属性配置文件
web.xml
首先看一下 web 程序最核心的文件 web.xml
<?xml version="1.0" encoding="UTF-8"?><web-appxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns="http://java.sun.com/xml/ns/javaee"xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"version="3.0"><servlet><servlet-name>SpringMvc</servlet-name><servlet-class>com.github.houbb.mvc.servlet.DispatchServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>application.properties</param-value></init-param><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>SpringMvc</servlet-name><url-pattern>/*</url-pattern></servlet-mapping></web-app>配置文件
这里主要有一个配置文件 application.properties
basePackage=com.github.houbb.mvcspring-mvc 中一般是一个 app.xml,其他指定的基本也是扫描包等基础信息。
分发 Servlet
还有一个 DispatchServlet,用于处理各种请求。
也是本次实现的核心内容。
DispatchServlet
继承自 HttpServlet
publicclassDispatchServletextendsHttpServlet{基本属性
后面实现会用到。
/** * 实例 Map * * @since 0.0.1 */privateMap<String,Object>controllerInstanceMap=newHashMap<>();/** * 请求方法 map * * @since 0.0.1 */privateMap<String,Method>requestMethodMap=newHashMap<>();/** * 配置文件 * * @since 0.0.1 */privatePropertiesproperties=newProperties();重载父类方法
@Overridepublicvoidinit(ServletConfigconfig)throwsServletException{}@OverrideprotectedvoiddoGet(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{}@OverrideprotectedvoiddoPost(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{}实现 init 方法
init 方法主要负责解析 web.xml 中的配置,然后进行相关的初始化。
@Overridepublicvoidinit(ServletConfigconfig)throwsServletException{super.init();//1. 加载配置文件信息initConfig(config);//2. 根据配置信息进行相关处理StringbasePackage=properties.getProperty("basePackage");initInstance(basePackage);//3. 初始化映射关系initRequestMappingMap();}我们分开一个个看。
1. 加载配置文件信息
这个就是根据 web.xml 中的配置,初始化一下配置文件信息。
/** * 初始化配置信息 * (1)spring-mvc 一般是指定一个 xml 文件。 * 至于各种 classpath,我们也可以对其进行特殊处理,暂时简单化。 * * @param config 配置信息 * @since 0.0.1 */privatevoidinitConfig(finalServletConfigconfig){finalStringconfigPath=config.getInitParameter("contextConfigLocation");//把web.xml中的contextConfigLocation对应value值的文件加载到流里面try(InputStreamresourceAsStream=this.getClass().getClassLoader().getResourceAsStream(configPath)){properties.load(resourceAsStream);}catch(IOExceptione){thrownewMvcRuntimeException(e);}}- MvcRuntimeException 异常类
一个简单的自定义运行时异常类:
publicclassMvcRuntimeExceptionextendsRuntimeException{//....}2. 根据配置信息进行初始化
根据指定的扫描包,我们初始化所有指定@Controller 注解的类。
当然这里最简单的场景,还有很多复杂的情况,比如 jar 包中引用等等,可以参考spring ioc 实现
/** * 初始化对象实例 * * @param basePackage 基本包 * @since 0.0.1 */privatevoidinitInstance(finalStringbasePackage){Stringpath=basePackage.replaceAll("\\.","/");URLurl=this.getClass().getClassLoader().getResource(path);if(null==url){thrownewMvcRuntimeException("base package can't loaded!");}Filedir=newFile(url.getFile());File[]files=dir.listFiles();if(files!=null){for(Filefile:files){if(file.isDirectory()){//递归读取包initInstance(basePackage+"."+file.getName());}else{StringclassName=basePackage+"."+file.getName().replace(".class","");//实例化处理try{Classclazz=Class.forName(className);if(clazz.isAnnotationPresent(Controller.class)){Objectinstance=clazz.newInstance();controllerInstanceMap.put(className,instance);}}catch(ClassNotFoundException|IllegalAccessException|InstantiationExceptione){thrownewMvcRuntimeException(e);}}}}}3. 初始化映射关系
这个主要是解析@RequestMapping 对应的 url 信息。
/** * 初始化 {@link com.github.houbb.mvc.annotation.RequestMapping} 的方法映射 * * @since 0.0.1 */privatevoidinitRequestMappingMap(){for(Map.Entry<String,Object>entry:controllerInstanceMap.entrySet()){Objectinstance=entry.getValue();Stringprefix="/";finalClasscontrollerClass=instance.getClass();if(controllerClass.isAnnotationPresent(RequestMapping.class)){RequestMappingrequestMapping=(RequestMapping)controllerClass.getAnnotation(RequestMapping.class);prefix=requestMapping.value();}// 暂时只处理当前类的方法Method[]methods=controllerClass.getDeclaredMethods();// 为了简单,只有注解处理的方法才被作为映射。// 当然这里可以加一些限制,比如只处理 public 方法等。// 可以加一些严格的判重,暂不处理。for(Methodmethod:methods){if(method.isAnnotationPresent(RequestMapping.class)){RequestMappingrequestMapping=method.getAnnotation(RequestMapping.class);StringmethodUrl=requestMapping.value();StringfullUrl=prefix+methodUrl;requestMethodMap.put(fullUrl,method);}}}}请求分发
请求主要分为 get/post 两种:
@OverrideprotectedvoiddoGet(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{doDispatch(req,resp);}@OverrideprotectedvoiddoPost(HttpServletRequestreq,HttpServletResponseresp)throwsServletException,IOException{doDispatch(req,resp);}这里我们统一调用了消息分发接口。
doDispatch() 核心实现
/** * 执行消息的分发 * * @param req 请求 * @param resp 响应 * @since 0.0.1 */privatevoiddoDispatch(HttpServletRequestreq,HttpServletResponseresp)throwsIOException{try{if(requestMethodMap.isEmpty()){return;}// 请求信息 url 的处理StringrequestUrl=req.getRequestURI();System.out.println("requestUrl===="+requestUrl);StringcontextPath=req.getContextPath();// 直接替换掉 contextPath,感觉这样不太好。可以选择去掉这种开头。requestUrl=requestUrl.replace(contextPath,"");//404// 这里应该有各种响应码的处理,暂时简单点。if(!requestMethodMap.keySet().contains(requestUrl)){resp.getWriter().write("404 for request "+requestUrl);return;}Methodmethod=requestMethodMap.get(requestUrl);// 参数,这里其实要处理各种基本类型等// 还有各种信息的类型转换Class<?>[]paramTypes=method.getParameterTypes();// 参数为空的情况finalObjectinstance=controllerInstanceMap.get(method.getDeclaringClass().getName());if(paramTypes.length<=0){method.invoke(instance);}// 这里实际上需要对各种类型的参数加以转换,目前只支持 StringObject[]paramValues=newObject[paramTypes.length];List<String>paramNames=getParamNames(method);Map<String,String[]>requestParamMap=req.getParameterMap();for(inti=0;i<paramTypes.length;i++){// 类的简称StringtypeName=paramTypes[i].getSimpleName();if("HttpServletRequest".equals(typeName)){//参数类型已明确,这边强转类型paramValues[i]=req;}elseif("HttpServletResponse".equals(typeName)){paramValues[i]=resp;}elseif("String".endsWith(typeName)){// 为什么是数组 https://www.cnblogs.com/wscit/p/5800147.htmlStringparamName=paramNames.get(i);String[]strings=requestParamMap.get(paramName);// 简单处理,只取第一个。paramValues[i]=strings[0];}else{thrownewMvcRuntimeException("Not support type for "+typeName);}}// 反射调用method.invoke(instance,paramValues);}catch(IOException|IllegalAccessException|InvocationTargetExceptione){resp.getWriter().write("500");}}获取参数名称的方法
/** * 获取参数的名称 * (1)后期可以结合 asm,对于没有注解的也可以处理。 * * @param method 方法 * @return 结果 * @since 0.0.1 */privateList<String>getParamNames(finalMethodmethod){Annotation[][]paramAnnos=method.getParameterAnnotations();List<String>paramNames=newArrayList<>(paramAnnos.length);finalintparamSize=paramAnnos.length;for(inti=0;i<paramSize;i++){Annotation[]annotations=paramAnnos[i];StringparamName=getParamName(i,annotations);paramNames.add(paramName);}returnparamNames;}/** * 获取参数名称 * @param index 参数的下标 * @param annotations 注解信息 * @return 参数名称 * @since 0.0.1 */privatestaticStringgetParamName(finalintindex,finalAnnotation[]annotations){finalStringdefaultName="arg"+index;if(annotations==null){returndefaultName;}for(Annotationannotation:annotations){if(annotation.annotationType().equals(RequestParam.class)){RequestParamparam=(RequestParam)annotation;returnparam.value();}}returndefaultName;}代码自测
IndexController
我们定义个简单的控制类
packagecom.github.houbb.mvc.controller;importcom.github.houbb.mvc.annotation.Controller;importcom.github.houbb.mvc.annotation.RequestMapping;importcom.github.houbb.mvc.annotation.RequestParam;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/** * index 控制器 * * @author binbin.hou * @since 1.0.0 */@Controller@RequestMapping("/index")publicclassIndexController{@RequestMapping("/print")publicvoidprint(@RequestParam("param")Stringparam){System.out.println(param);}@RequestMapping("/echo")publicvoidecho(HttpServletRequestrequest,HttpServletResponseresponse,@RequestParam("param")Stringparam){try{response.getWriter().write("Echo :"+param);}catch(IOExceptione){e.printStackTrace();}}}项目启动
为了方便,此处直接使用 tomcat maven 插件。
<plugin><groupId>org.apache.tomcat.maven</groupId><artifactId>tomcat7-maven-plugin</artifactId><version>2.2</version><configuration><port>8080</port><path>/</path><uriEncoding>${project.build.sourceEncoding}</uriEncoding></configuration></plugin>页面访问
- 404
tomcat7 启动以后,直接页面访问http://localhost:8080/
默认页面显示
404 for request /因为我们没有处理默认的 index 页面。
页面访问http://localhost:8080/index/print?param=hello
会在控台打印
hello- echo
页面访问http://localhost:8080/index/echo?param=hello
会在页面打印
Echo :hello开源地址
完整代码地址mvc
