Day46
手写Spring-MVC
解决Controller层的方案
思路:监听器在项目启动时DispatherServlet会将controller层的信息记录下来,当前端发送请求的时候DispatherServlet就会根据信息分发给controller层。
准备工作
准备工作的目的是准备好监听器,而监听器的具体实现是获取controlle层的controller类的信息(用类描述类来封装这些信息,并将URI和描述类整合到集合map中)。
步骤1:自定义注解-作用是包含路径信息、URI、标识controller类等。
java@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层了。(具体实现是利用反射,拿到类对象,再拿到注解里写好的路径)。
在这一步中需要写好配置类以及配置文件:
javapackage com.qf.shop.web.config; import com.qf.shop.mvc.annotation.Configuration; /** * 当前项目的配置类 */ @Configuration("com.qf.shop.web.controller") public class AppConfig { }
配置类中是第一步自定义的configuration注解,此注解的目的就是给出当前web项目中controller层的路径。
xml<context-param> <param-name>config</param-name> <param-value>com.qf.shop.web.config.AppConfig</param-value> </context-param>
步骤3: 为创建监听器准备条件,即写好类描述类,而来描述类中又需要方法描述类,方法描述类中需要参数描述类集合。
javapackage com.qf.shop.mvc.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 类描述类 */ @NoArgsConstructor @AllArgsConstructor @Data public class BeanDefinition<T> { private String requestMappingPath;//父级URi private Class<?> clazz;//Controller类的class对象 private String name;//类名 private T t;//Controller类对象 private MethodDefinition methodDefinition;//方法描述类对象 }
javapackage 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;//子级URi private String name;//方法名 private Method method;//方法对象 private Class<?> returnClazz;//返回值类型 private List<ParameterDefinition> parameterDefinitions;//参数描述类对象的集合 }
javapackage 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属性中。
错误码接口及其实现类:
javapackage com.qf.shop.mvc.constant; /** * 错误码的接口 */ public interface ResponseCodeInterface { int getCode(); void setCode(int code); String getMessage(); void setMessage(String message); }
javapackage 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; } @Override public int getCode() { return code; } @Override public void setCode(int code) { this.code = code; } @Override public String getMessage() { return message; } @Override public void setMessage(String message) { this.message = message; } }
异常信息类:
javapackage com.qf.shop.mvc.exception; import lombok.AllArgsConstructor; import lombok.Data; @AllArgsConstructor @Data public class FrameWorkException extends RuntimeException{ private int code; private String message; }
容器类:
javapackage 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循环)的属性,然后进行封装,再调用静态容器整合。
javapackage 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 { @Override public 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层的所有文件:fileList List<File> fileList= new ArrayList<>(); getFileList(filePath,fileList); //将fileList转换为classList List<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();//父级URI Method[] 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();//获取子级URI List<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;//拼接uri HashMap<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.class String fileAbsolutePath = file.getAbsolutePath(); //\com\qf\shop\web\controller\A.class fileAbsolutePath = fileAbsolutePath.replace(realPath, ""); //com\qf\shop\web\controller\A fileAbsolutePath = fileAbsolutePath.substring(1,fileAbsolutePath.lastIndexOf(".")); //com.qf.shop.web.controller.A String 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\classes String realPath = servletContext.getRealPath("WEB-INF\\classes");//发布路径中的编译后的.class文件路径 //com\qf\shop\web\controller controllerPosition = controllerPosition.replaceAll("\\.", "\\\\"); //F:\apache-tomcat-8.0.49\webapps\ROOT\WEB-INF\classes/com\qf\shop\web\controller String 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中进行配置
xml<listener> <listener-class>com.qf.shop.mvc.listerner.ApplicationListener</listener-class> </listener>
可能遇到的问题
1.发布路径报错
解决方案:将发布路径改为Tomcat的webApps下的ROOT根路径
2.监听器没有工作
解决方案:在web项目的pom.xml中添加java框架项目的依赖