Day46
手写Spring-MVC
解决Controller层的方案
思路:监听器在项目启动时DispatherServlet会将controller层的信息记录下来,当前端发送请求的时候DispatherServlet就会根据信息分发给controller层。
准备工作
准备工作的目的是准备好监听器,而监听器的具体实现是获取controlle层的controller类的信息(用类描述类来封装这些信息,并将URI和描述类整合到集合map中)。
步骤1:自定义注解-作用是包含路径信息、URI、标识controller类等。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Configuration {String value(); }@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Controller { }@Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RequestMapping {String value(); }
步骤2:DispatcherServlet如何记录controller层的信息?
-在web.xml配置文件中配置好config配置参数,这个参数可以直接通过请求的getInitParameter()方法获得,于是可以在web项目模块中写一个配置文件的类Appconfig,在web.xml配置文件中直接写明这个配置文件类的路径,这样在项目启动时可以直接访问到这个类。
而写配置文件类的目的是为了通过这个配置文件类拿到web项目中controller层的信息(路径),这样在java模块中就可以访问到controller层了。(具体实现是利用反射,拿到类对象,再拿到注解里写好的路径)。
在这一步中需要写好配置类以及配置文件:
package com.qf.shop.web.config;import com.qf.shop.mvc.annotation.Configuration;/*** 当前项目的配置类*/ @Configuration("com.qf.shop.web.controller") public class AppConfig { }
配置类中是第一步自定义的configuration注解,此注解的目的就是给出当前web项目中controller层的路径。
<context-param><param-name>config</param-name><param-value>com.qf.shop.web.config.AppConfig</param-value></context-param>
步骤3: 为创建监听器准备条件,即写好类描述类,而来描述类中又需要方法描述类,方法描述类中需要参数描述类集合。
package com.qf.shop.mvc.model;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;/*** 类描述类*/ @NoArgsConstructor @AllArgsConstructor @Data public class BeanDefinition<T> {private String requestMappingPath;//父级URiprivate Class<?> clazz;//Controller类的class对象private String name;//类名private T t;//Controller类对象private MethodDefinition methodDefinition;//方法描述类对象 }
package com.qf.shop.mvc.model;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;import java.lang.reflect.Method; import java.util.List;/*** 方法描述类*/ @NoArgsConstructor @AllArgsConstructor @Data public class MethodDefinition {private String requestMappingPath;//子级URiprivate String name;//方法名private Method method;//方法对象private Class<?> returnClazz;//返回值类型private List<ParameterDefinition> parameterDefinitions;//参数描述类对象的集合 }
package com.qf.shop.mvc.model;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;@NoArgsConstructor @AllArgsConstructor @Data public class ParameterDefinition {private String name;//参数名private Class<?> clazz;//参数类型private int index;//参数下标}
注意:此处用到了lombok插件,该插件提供了通过注解自动编写有参、无参、getset方法及toString方法的功能。可以在IDEA中的file->settings->plugin中搜索下载。
步骤4:因为可能出现配置信息有误、配置信息残缺、class文件转换异常、uri映射错误、requestMapping配置错误等等情况,所以写一个错误码接口及其实现枚举类;
对于所有可能出现的异常写一个异常类,继承RuntimeException非受检性异常,抛出枚举类中对应的错误码和错误信息。
又因为监听器最后的目的是将信息存储到集合中,这些信息应该是共享的,所以要将map放进一个静态类的静态map属性中。
错误码接口及其实现类:
package com.qf.shop.mvc.constant;/*** 错误码的接口*/ public interface ResponseCodeInterface {int getCode();void setCode(int code);String getMessage();void setMessage(String message); }
package com.qf.shop.mvc.constant;public enum ResponseCode implements ResponseCodeInterface{CONFIG_EXCEPTION(100,"config的配置信息出错"),CONFIGURATION_EXCEPTION(101,"需要配置Configuration这个注解"),CLASS_FILE_EXCEPTION(102,"class文件转换异常"),REQUEST_MAPPING_PATH_EXCEPTION(103,"RequestMapping地址设置有误"),REQUEST_PATH_EXCEPTION(104,"uri映射错误"),EXCEPTION_CONFIG_EXCEPTION(105,"未配置全局异常的路径"),ADVISER_CONFIG_EXCEPTION(106,"未配置处理器的路径");private int code;private String message;ResponseCode(int code, String message) {this.code = code;this.message = message;}@Overridepublic int getCode() {return code;}@Overridepublic void setCode(int code) {this.code = code;}@Overridepublic String getMessage() {return message;}@Overridepublic void setMessage(String message) {this.message = message;} }
异常信息类:
package com.qf.shop.mvc.exception;import lombok.AllArgsConstructor; import lombok.Data;@AllArgsConstructor @Data public class FrameWorkException extends RuntimeException{private int code;private String message; }
容器类:
package com.qf.shop.mvc.container;import com.qf.shop.mvc.model.BeanDefinition;import java.util.HashMap;public class TypeContainer {private static HashMap<String, BeanDefinition> maps = null;static{maps=new HashMap<>();}public static HashMap<String, BeanDefinition> getMaps() {return maps;}public static void setMaps(HashMap<String, BeanDefinition> maps) {TypeContainer.maps = maps;} }
步骤5:创建监听器。
思路:通过config配置参数获取配置文件类对象,然后获取其注解信息,通过信息找到controller层路径,但是此时不知道controller层下面的类信息。所以要先通过全局域对象获取项目发布的绝对路径,然后和层拼接成层的绝对路径,再封装为File对象,利用listFiles()函数获取文件下面的子文件对象,整合到列表中,再对每个file文件更改路径,利用反射再次转换回Class对象,这样就获得了controller层下面的类对象。
获得类对象后对于每个类对象遍历,依次获得类描述类、方法描述类、参数描述类(多层for循环)的属性,然后进行封装,再调用静态容器整合。
package com.qf.shop.mvc.listerner;import com.qf.shop.mvc.annotation.Configuration; import com.qf.shop.mvc.annotation.Controller; import com.qf.shop.mvc.annotation.RequestMapping; import com.qf.shop.mvc.constant.ResponseCode; import com.qf.shop.mvc.container.TypeContainer; import com.qf.shop.mvc.exception.FrameWorkException; import com.qf.shop.mvc.model.BeanDefinition; import com.qf.shop.mvc.model.MethodDefinition; import com.qf.shop.mvc.model.ParameterDefinition;import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*;public class ApplicationListener implements ServletContextListener {@Overridepublic void contextInitialized(ServletContextEvent sce) {/** 扫描web项目的Controller层* 封装成类描述类的对象* 添加到容器中 -- Key(父级uri+子级uri) Value(类描述类的对象)*/System.out.println("项目启动");//获取web项目的配置文件全路径ServletContext servletContext = sce.getServletContext();String config = servletContext.getInitParameter("config");if(config==null){throw new FrameWorkException(ResponseCode.EXCEPTION_CONFIG_EXCEPTION.getCode(),ResponseCode.EXCEPTION_CONFIG_EXCEPTION.getMessage());}//获取配置文件的class对象Class<?> configClass = getConfigClass(config);//获取扫描Controller层的位置String controllerPosition = getControllerPosition(configClass);//拼接Web项目中Controller层的绝对路径String filePath = getControllerPath(servletContext, controllerPosition);//获取controller层的所有文件:fileListList<File> fileList= new ArrayList<>();getFileList(filePath,fileList);//将fileList转换为classListList<Class<?>> classList = tramToClassList(servletContext, fileList);//将类对象封装为描述类对象,并添加到容器中handlerClassList(classList);Set<Map.Entry<String, BeanDefinition>> entries = TypeContainer.getMaps().entrySet();for (Map.Entry<String, BeanDefinition> entry : entries) {System.out.println(entry);}}public void handlerClassList(List<Class<?>> classList){for (Class<?> clazz : classList) {try {String clazzName = clazz.getName();//类名Object t = clazz.newInstance();//类对象RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);if(requestMapping==null){throw new FrameWorkException(ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getCode(),ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getMessage());}String fatherUri = requestMapping.value();//父级URIMethod[] methods = clazz.getDeclaredMethods();for (Method method : methods) {method.setAccessible(true);String methodName = method.getName();//方法名Class<?> returnType = method.getReturnType();//返回值类型RequestMapping methodAnnotation = method.getAnnotation(RequestMapping.class);if(methodAnnotation==null){throw new FrameWorkException(ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getCode(),ResponseCode.REQUEST_MAPPING_PATH_EXCEPTION.getMessage());}String sonUri = methodAnnotation.value();//获取子级URIList<ParameterDefinition> parameterList = new ArrayList<>();Parameter[] parameters = method.getParameters();for (int i = 0; i < parameters.length; i++) {String parameterName = parameters[i].getName();//获取参数名Class<?> parameterType = parameters[i].getType();//参数类型int index = i;//下标ParameterDefinition parameterDefinition = new ParameterDefinition(parameterName, parameterType, index);//封装参数描述类对象parameterList.add(parameterDefinition);}MethodDefinition methodDefinition = new MethodDefinition(sonUri, methodName, method, returnType, parameterList);//封装方法描述类对象BeanDefinition<Object> beanDefinition = new BeanDefinition<>(fatherUri, clazz, clazzName, t, methodDefinition);//封装类描述类对象String key = sonUri+fatherUri;//拼接uriHashMap<String, BeanDefinition> map = TypeContainer.getMaps();map.put(key,beanDefinition);// Set<Map.Entry<String, BeanDefinition>> entries = map.entrySet(); // for (Map.Entry<String, BeanDefinition> entry : entries) { // System.out.println(entry); // }}} catch (InstantiationException | IllegalAccessException e) {throw new RuntimeException(e);}}}public List<Class<?>> tramToClassList(ServletContext servletContext,List<File> fileList){String realPath = servletContext.getRealPath("WEB-INF\\classes");ArrayList<Class<?>> classList = new ArrayList<>();for (File file : fileList) {//F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classes\com\qf\shop\web\controller\A.classString fileAbsolutePath = file.getAbsolutePath();//\com\qf\shop\web\controller\A.classfileAbsolutePath = fileAbsolutePath.replace(realPath, "");//com\qf\shop\web\controller\AfileAbsolutePath = fileAbsolutePath.substring(1,fileAbsolutePath.lastIndexOf("."));//com.qf.shop.web.controller.AString name = fileAbsolutePath.replace("\\", ".");try {Class<?> clazz = Class.forName(name);Annotation annotation = clazz.getAnnotation(Controller.class);if(annotation!=null){classList.add(clazz);}} catch (ClassNotFoundException e) {throw new RuntimeException(e);}}return classList;}public void getFileList(String filePath,List<File> fileList){File file = new File(filePath);File[] files = file.listFiles();for (File afile : files) {if(afile.isDirectory()){String s = afile.getAbsolutePath();getFileList(s,fileList);}else if(afile.isFile()){fileList.add(afile);}}}public String getControllerPath(ServletContext servletContext,String controllerPosition){//F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classesString realPath = servletContext.getRealPath("WEB-INF\\classes");//发布路径中的编译后的.class文件路径//com\qf\shop\web\controllercontrollerPosition = controllerPosition.replaceAll("\\.", "\\\\");//F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classes/com\qf\shop\web\controllerString filePath = realPath + File.separator +controllerPosition;return filePath;}public String getControllerPosition(Class<?> configClass){Configuration configClassAnnotation = configClass.getAnnotation(Configuration.class);if(configClassAnnotation==null){throw new FrameWorkException(ResponseCode.CONFIGURATION_EXCEPTION.getCode(),ResponseCode.CONFIGURATION_EXCEPTION.getMessage());}String controllerPosition = configClassAnnotation.value();return controllerPosition;}public Class<?> getConfigClass(String config){Class<?> appConfigClass = null;try {appConfigClass = Class.forName(config);} catch (ClassNotFoundException e) {throw new RuntimeException(e);}return appConfigClass;}}
注意:由于JVM在编译时不会将方法名编译进class文件中,所以需要maven-compiler-plugin插件,作用:将参数名编译进class文件。
写完监听器后别忘了在web.xml中进行配置
<listener><listener-class>com.qf.shop.mvc.listerner.ApplicationListener</listener-class></listener>
可能遇到的问题
1.发布路径报错
解决方案:将发布路径改为Tomcat的webApps下的ROOT根路径
2.监听器没有工作
解决方案:在web项目的pom.xml中添加java框架项目的依赖