Movatterモバイル変換


[0]ホーム

URL:


Java Servlet 教程-21-自己手写 spring mvc 简单实现

Posted byhoubb on September 27, 2018

整体代码结构

├─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.xml

pom.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.mvc

spring-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 页面。

  • print

页面访问http://localhost:8080/index/print?param=hello

会在控台打印

hello
  • echo

页面访问http://localhost:8080/index/echo?param=hello

会在页面打印

Echo :hello

开源地址

完整代码地址mvc

更多学习

  •  个人 Github
  •  个人公众号
  • 更多实时资讯,前沿技术,生活趣事。尽在【老马啸西风】

    交流社群:交流群信息

    image


    [8]ページ先頭

    ©2009-2025 Movatter.jp