SpringMvc本质上就是对Servlet的封装。html
由于建立一个Maven项目,而后在pom文件中增长一个依赖:java
<dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <!-- 部署在服务器时,不使用这个servlet-api 而使用tomcat的--> <scope>provided</scope> </dependency>
2,建立DispatcherServlet,并注册到web.xml中web
package com.dxh.edu.mvcframework.servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class DxhDispatcherServlet extends HttpServlet { /** * 接收处理请求 */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } }
web.xml:spring
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <display-name>Archetype Created Web Application</display-name> <servlet> <servlet-name>dxhmvc</servlet-name> <servlet-class>com.dxh.edu.mvcframework.servlet.DxhDispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>springmvc.properties</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dxhmvc</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
由于要使用到注解,因此首先要自定义几个注解:apache
这里就不赘述如何自定义注解了,详情请看:https://www.cnblogs.com/peida/archive/2013/04/24/3036689.htmlapi
Controller注解:数组
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DxhController { String value() default ""; }
Service注解:浏览器
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DxhService { String value() default ""; }
RequestMapping注解:缓存
@Documented @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DxhRequestMapping { String value() default ""; }
Autowired注解:tomcat
@Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface DxhAutowired { String value() default ""; }
测试代码咱们放在同项目中的com.dxh.demo包中:
package com.dxh.demo.controller; import com.dxh.demo.service.IDemoService; import com.dxh.edu.mvcframework.annotations.DxhAutowired; import com.dxh.edu.mvcframework.annotations.DxhController; import com.dxh.edu.mvcframework.annotations.DxhRequestMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @DxhController @DxhRequestMapping("/demo") public class DemoController { @DxhAutowired private IDemoService demoService; /** * URL:/demo/query */ @DxhRequestMapping("/query") public String query(HttpServletRequest request, HttpServletResponse response, String name){ return demoService.get(name); } }
package com.dxh.demo.service; public interface IDemoService { String get(String name); }
package com.dxh.demo.service.impl; import com.dxh.demo.service.IDemoService; import com.dxh.edu.mvcframework.annotations.DxhService; @DxhService("demoService") public class IDemoServiceImpl implements IDemoService { @Override public String get(String name) { System.out.println("Service实现类中的Name:"+ name); return name; } }
在建立好的DxhDispatcherServlet中重写init()方法,并在init方法中作初始化配置:
@Override public void init(ServletConfig config) throws ServletException { //1. 加载配置文件 springmvc.properties String contextConfigLocation = config.getInitParameter("contextConfigLocation"); doLoadConfig(contextConfigLocation); //2. 扫描相关的类——扫描注解 doScan(""); //3. 初始化Bean对象(实现IOC容器,基于注解) doInstance(); //4. 实现依赖注入 doAutoWired(); //5. 构造一个handleMapping处理器映射器,将配置好的url和method创建映射关系 initHandleMapping(); System.out.println("MVC 初始化完成"); //6. 等待请求进入处理请求 }
以及5个空方法,这篇文章自定义MVC框架其实就是须要对这5个步骤的编写。
//TODO 5,构造一个映射器 private void initHandleMapping() { } //TODO 4,实现依赖注入 private void doAutoWired() { } //TODO 3,IOC容器 private void doInstance() { } //TODO 2,扫描类 private void doScan(String scanPackage) { } //TODO 1,加载配置文件 private void doLoadConfig(String contextConfigLocation) { }
首先在resource目录中建立一个配置文件——springmvc.properties
表示要扫描com.dxh.demo下的全部注解。
而后在web.xml中进行配置:
这样,就能够经过config.getInitParameter("contextConfigLocation")得到这个路径。
private Properties properties = new Properties();;
//1,加载配置文件 private void doLoadConfig(String contextConfigLocation) { //根据指定路径加载成流: InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation); try { properties.load(resourceAsStream); } catch (IOException e) { e.printStackTrace(); } }
//2. 扫描相关的类——扫描注解 doScan(properties.getProperty("scanPackage"));
//缓存扫描到的类的全类名 private List<String> classNames = new ArrayList<>();
//2,扫描类 //scanPackage :com.dxh.demo package--->磁盘的文件夹(File) private void doScan(String scanPackage) { //1.得到classPath路径 String clasPath = Thread.currentThread().getContextClassLoader().getResource("").getPath(); //2.拼接,获得scanPackage在磁盘上的路径 String scanPackagePath= clasPath + scanPackage.replaceAll("\\.","/"); File pack = new File(scanPackagePath); File[] files = pack.listFiles(); for (File file : files) { if (file.isDirectory()){ //子 package //递归 doScan(scanPackage+"."+file.getName()); //com.dxh.demo.controller }else if(file.getName().endsWith(".class")){ String className = scanPackage + "." + file.getName().replace(".class", ""); classNames.add(className); } } }
上一步骤咱们把扫描到的类的全类名放到了,list中,那么本次步骤须要遍历整个list:
代码实现:
//IOC容器 private Map<String,Object> ioc = new HashMap<>();
//3,IOC容器 //基于classNames缓存的类的全限定类名,以及反射技术,完成对象建立和管理 private void doInstance() { if (classNames.size()==0) return; try{ for (int i = 0; i < classNames.size(); i++) { String className = classNames.get(i); //com.dxh.demo.controller.DemoController //反射 Class<?> aClass = Class.forName(className); //区分controller ,区分service if (aClass.isAnnotationPresent(DxhController.class)){ //controller的id此处不作过多处理,不取value了,用类的首字母小写做为id,保存到IOC容器中 String simpleName = aClass.getSimpleName();//DemoController String lowerFirstSimpleName = lowerFirst(simpleName); //demoController Object bean = aClass.newInstance(); ioc.put(lowerFirstSimpleName,bean); }else if (aClass.isAnnotationPresent(DxhService.class)){ DxhService annotation = aClass.getAnnotation(DxhService.class); //获取注解的值 String beanName = annotation.value(); //指定了id就以指定的id为准 if (!"".equals(beanName.trim())){ ioc.put(beanName,aClass.newInstance()); }else{ //没有指定id ,首字母小写 String lowerFirstSimpleName = lowerFirst(aClass.getSimpleName()); ioc.put(lowerFirstSimpleName,aClass.newInstance()); } //service层每每是有接口的,再以接口名为id再存入一分bean到ioc,便于后期根据接口类型注入 Class<?>[] interfaces = aClass.getInterfaces(); for (Class<?> anInterface : interfaces) { //以接口的类名做为id放入。 ioc.put(anInterface.getName(),aClass.newInstance()); } }else { continue; } } }catch (Exception e){ e.printStackTrace(); } }
上一步骤把全部须要加载的bean,存入了ioc Map中,此时,咱们就须要遍历这个map而后依次获得每一个bean对象,而后判断对象中有没有被@****DxhAutowired修饰的属性。
代码实现:
//4,实现依赖注入 private void doAutoWired() { if (ioc.isEmpty()){return;} //1,判断容器中有没有被@DxhAutowried注解的属性,若是有须要维护依赖注入关系 for (Map.Entry<String,Object> entry: ioc.entrySet()){ //获取bean对象中的字段信息 Field[] declaredFields = entry.getValue().getClass().getDeclaredFields(); for (Field declaredField : declaredFields) { if (!declaredField.isAnnotationPresent(DxhAutowired.class)){ continue; } //有该注解: DxhAutowired annotation = declaredField.getAnnotation(DxhAutowired.class); String beanName = annotation.value(); //须要注入的bean的Id if ("".equals(beanName.trim())){ //没有配置具体的beanId,须要根据当前字段的类型注入(接口注入) IDemoService beanName = declaredField.getType().getName(); } //开启赋值 declaredField.setAccessible(true); try { //字段调用,两个参数:(哪一个对象的字段,传入什么) declaredField.set(entry.getValue(),ioc.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
构造一个handleMapping处理器映射器,将配置好的url和method创建映射关系****。
手写MVC框架最关键的环节
假设有一个:
那么如何经过/demo/query定位到 DemoController类中的query这个方法 ?
以前咱们全部被@DxhController(自定义Controller注解)的类,都存在了ioc 这个map中。
咱们能够遍历这个map,获得每一个bean对象
而后判断是否被@DxhController所修饰(排除@DxhService所修饰的bean)
而后判断是否被@DxhRequestMapping所修饰,有的话,就取其value值,做为baseUrl
而后遍历该bean对象中的全部方法,获得被@DxhRequestMapping修饰的方法。获得其value值,做为methodUrl。
baseUrl + methodUrl = url
咱们把url和当前method绑定起来,存在map中,也就是创建了url和method创建映射关系。
代码实现:
//handleMapping ,存储url和method直接的映射关系 private Map<String,Object> handleMapping = new HashMap<>();
//5,构造一个映射器,将url和method进行关联 private void initHandleMapping() { if (ioc.isEmpty()){return;} for (Map.Entry<String,Object> entry: ioc.entrySet()){ //获取ioc中当前遍历对象的class类型 Class<?> aClass = entry.getValue().getClass(); //排除非controller层的类 if (!aClass.isAnnotationPresent(DxhController.class)){ continue; } String baseUrl = ""; if (aClass.isAnnotationPresent(DxhRequestMapping.class)){ //Controller层 类上 注解@DxhRequestMapping中的value值 baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value(); } //获取方法 Method[] methods = aClass.getMethods(); for (Method method : methods) { //排除没有@DxhRequestMapping注解的方法 if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;} //Controller层 类中方法上 注解@DxhRequestMapping中的value值 String methodUrl = method.getAnnotation(DxhRequestMapping.class).value(); String url = baseUrl+methodUrl; //创建url和method之间的映射关系,用map缓存起来 handleMapping.put(url,method); } } }
到目前位置,尚未彻底写完,可是不妨碍咱们测试一下看看刚才写的那部份内容有没有什么问题:
<?xml version="1.0" encoding="UTF-8"?> <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.dxh.edu</groupId> <artifactId>mvc</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>mvc Maven Webapp</name> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <!-- 部署在服务器时,不使用这个servlet-api 而使用tomcat的--> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugins> <!-- 编译插件定义编译细节--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>11</source> <target>11</target> <encoding>utf-8</encoding> <!-- 告诉编译器,编译的时候记录下形参的真实名称--> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/</path> </configuration> </plugin> </plugins> </build> </project>
pom文件中加入了一个tomcat插件,并设置端口为8080,所以咱们经过tomcat启动项目:
启动完成后,打开浏览器url中输入:
http://localhost:8080/demo/query
浏览器中什么都没返回(咱们的代码还没真正的完成,还没有编写处理请求步骤),同时控制台中打印了MVC初始化完成,能够认为,目前的代码没有明显的缺陷。 咱们继续~~~~~
DxhDispatcherServlet这个类继承了HttpServlet,并重写了doGet和doPost方法,在doGet中调用了doPost方法,当咱们使用反射调用方法时(method.invoke(......))发现少了一部分参数:
所以咱们要改造initHandleMapping(),修改url和method的映射关系(不简简单单的存入map中)。
package com.dxh.edu.mvcframework.pojo; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; public class Handler { //method.invoke(obj,)须要 private Object controller; private Method method; //spring中url支持正则 private Pattern pattern; //参数的顺序,为了进行参数绑定 ,Key 参数名, Value 表明第几个参数 private Map<String,Integer> paramIndexMapping; public Handler(Object controller, Method method, Pattern pattern) { this.controller = controller; this.method = method; this.pattern = pattern; this.paramIndexMapping = new HashMap<>(); } //getset方法这里省略,实际代码中须要... }
在Handler类中编写了4个属性:
首先,就不能直接经过Map<url,Method>的得方式进行关系映射了,使用一个list,泛型是刚才建立的Handler。
//handleMapping ,存储url和method直接的映射关系 // private Map<String,Method> handleMapping = new HashMap<>(); private List<Handler> handlerMapping = new ArrayList<>();
改动前,改动后代码对比:
改动后的initHandleMapping():
//5,构造一个映射器,将url和method进行关联 private void initHandleMapping() { if (ioc.isEmpty()){return;} for (Map.Entry<String,Object> entry: ioc.entrySet()){ //获取ioc中当前遍历对象的class类型 Class<?> aClass = entry.getValue().getClass(); //排除非controller层的类 if (!aClass.isAnnotationPresent(DxhController.class)){ continue; } String baseUrl = ""; if (aClass.isAnnotationPresent(DxhRequestMapping.class)){ //Controller层 类上 注解@DxhRequestMapping中的value值 baseUrl = aClass.getAnnotation(DxhRequestMapping.class).value(); } //获取方法 Method[] methods = aClass.getMethods(); for (Method method : methods) { //排除没有@DxhRequestMapping注解的方法 if (!method.isAnnotationPresent(DxhRequestMapping.class)){continue;} //Controller层 类中方法上 注解@DxhRequestMapping中的value值 String methodUrl = method.getAnnotation(DxhRequestMapping.class).value(); String url = baseUrl+methodUrl; //把method全部信息以及url封装为Handler Handler handler = new Handler(entry.getValue(),method, Pattern.compile(url)); //处理计算方法的参数位置信息 Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { Parameter parameter = parameters[i]; //不作太多的参数类型判断,只作:HttpServletRequest request, HttpServletResponse response和基本类型参数 if (parameter.getType()==HttpServletRequest.class||parameter.getType()==HttpServletResponse.class){ //若是时request和response对象,那么参数名称存 HttpServletRequest 和 HttpServletResponse handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),i); }else{ handler.getParamIndexMapping().put(parameter.getName(),i); } } handlerMapping.add(handler); } } }
上一步骤,咱们配置了 uri和method的映射关系,并封装到了Handler中存入list,那么接下来,就要经过HttpServletRequest,取出uri,而后找到具体的Handler:
private Handler getHandler(HttpServletRequest req) { if (handlerMapping.isEmpty()){return null;} String url = req.getRequestURI(); //遍历 handlerMapping for (Handler handler : handlerMapping) { Matcher matcher = handler.getPattern().matcher(url); if (!matcher.matches()){continue;} return handler; } return null; }
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { //根据uri获取到可以处理当前请求的Handler(从handlerMapping中(list)) Handler handler = getHandler(req); if (handler==null){ resp.getWriter().write("404 not found"); return; } //参数绑定 //该方法全部参数得类型数组 Class<?>[] parameterTypes = handler.getMethod().getParameterTypes(); //根据上述数组长度建立一个新的数组(参数数组,传入反射调用的) Object[] paramValues = new Object[parameterTypes.length]; //如下就是为了向参数数组中设值,并且还得保证参数得顺序和方法中形参顺序一致。 Map<String,String[]> parameterMap = req.getParameterMap(); //遍历request中全部的参数 ,(填充除了request、response以外的参数) for (Map.Entry<String,String[]> entry: parameterMap.entrySet()){ //name=1&name=2 name[1,2] String value = StringUtils.join(entry.getValue(), ",");// 如同 1,2 //若是参数和方法中的参数匹配上了,填充数据 if (!handler.getParamIndexMapping().containsKey(entry.getKey())){continue;} //方法形参确实有该参数,找到它得索引位置,对应得把参数值放入paramValues Integer index = handler.getParamIndexMapping().get(entry.getKey()); //把前台传递过来的参数值,填充到对应得位置去 paramValues[index] = value; } Integer requestIndex = handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName()); paramValues[requestIndex] = req; Integer responseIndex = handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName()); paramValues[responseIndex] = resp; //最终调用handler得method属性 try { Object invoke = handler.getMethod().invoke(handler.getController(), paramValues); //简单操做,把方法返回的数据以字符串的形式写出 resp.getWriter().write(invoke.toString()); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
打开浏览器,url中输入:http://localhost:8080/demo/query?name=lisi
返回:
控制台中打印出:
OK完成~