在这里说的底层机制的实现主要是指:前端控制器、Controller、Service注入容器、对象自动装配、控制器方法获取参数、视图解析、返回json数据。
前端控制器
前端控制器就是核心控制器。在这里我们可以设计一个Servlet来充当核心控制器:LingDispatcherServlet.java.这个控制器的作用主要是接收响应前端过来的Http请求和Response响应。一开始需要在web.xml中配置好控制器的 请求路径,还要配置好SpringMVC的xml文件: lingspringmvc.xml.
lingspringmvc.xml:
<?xml version="1.0" encoding="UTF-8" ?> <beans> <component-scan base-package="com.linghu.controller"></component-scan> </beans>
有了这个xml文件就好办了,里面写了Controller层的类路径,只需要通过dom4j技术将类路径读出来,就可以轻松的将该文件下的类文件进行读取遍历,再去分析他们是否加了我们设计的注解,如果加了就保留类路径,甚至加入到我们自己设计的ioc容器中。在读取xml文件里的内容的时候,可以单独写一个工具类XMLParser.java:
package com.linghu.springmvc.xml; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import java.io.InputStream; /** * @author linghu * @date 2023/9/11 14:22 */ public class XMLParser { public static String getBasePackage(String xmlFile){ SAXReader saxReader = new SAXReader(); InputStream resourceAsStream = XMLParser.class.getClassLoader().getResourceAsStream(xmlFile); try { Document document = saxReader.read(resourceAsStream); Element rootElement = document.getRootElement(); Element componentScanElement = rootElement.element("component-scan"); Attribute attribute = componentScanElement.attribute("base-package"); String basePackage = attribute.getText(); System.out.println("basePackage="+basePackage); return basePackage; } catch (DocumentException e) { e.printStackTrace(); } return null; } }
Controller注解
在Controller层,我们会在类上标注Controller注解,容器就会将这个类路径扫描加入到我们的容器中。在这里我们将自己设计一个自己的注解Controller注解。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Controller { String value() default ""; }
RequestMapping注解
前端发出请求的时候,请求地址为:IP+端口+URI。RequestMapping注解可以规定我们请求的URI。同样我们需要自己设计一个属于自己的RequestMapping注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestMapping { String value() default ""; }
前端发出请求的时候,如果携带了参数,后端接收的时候就要标注接收的参数字段,会用到注解 RequestMapping注解。
自定义容器LingWebApplicationContext
需要自己设计一个容器,将我们从lingspringmvc.xml中读取到的类路径下的.class文件的路径列表全部存起来。具体来说 就是:
<?xml version="1.0" encoding="UTF-8" ?> <beans> <component-scan base-package="com.linghu.controller"></component-scan> </beans>
需要将上面com.linghu.controller这个路径下的类文件的路径进行存储。以后这个扫描的包不光有controller,还会有service,dao等等。
这个容器里最重要的就是要保存我们扫描包和子包下的全类路径,所以我们定义了一个 classFullPathList
集合用来存储。
private ArrayList<String> classFullPathList=new ArrayList<>();
接下来就是利用工具类 XMLParser去读取springmvc.xml配置文件里的包路径,接着利用文件和IO的知识去扫描这个包路径下的文件和目录,将这个路径下的所有.class文件的后缀.class裁剪掉,将路径名和文件名进行拼接就得到了类文件的类路径了,我们将他们存储到classFullPathList集合中即可。如下:
这个类路径就对应下图:
这里目前只讨论controller注解的类,因为springmvc.xml扫描的包只写了controller这个包。
利用文件和IO的知识去扫描这个包路径下的文件和目录是重点,在扫描以后,我们要将这个路径下的所有.class文件的后缀.class裁剪掉,将路径名和文件名进行拼接。这两步是最重要的。scanPackage()函数可以完成这两个重要的东西。
public void init(){ String basePackage = XMLParser.getBasePackage("lingspringmvc.xml"); String[] packages = basePackage.split(","); if (packages.length>0){ for (String pack :packages) { scanPackage(pack); } } System.out.println("classFullPathList="+classFullPathList); } public void scanPackage(String pack){ URL url = this.getClass(). getClassLoader(). getResource("/" + pack.replaceAll("\\.", "/")); String path = url.getFile();//获取所有文件的目录 File dir = new File(path); for (File f :dir.listFiles()) { if (f.isDirectory()){ scanPackage(pack+"."+f.getName()); }else { String classFullPath=pack+"."+f.getName().replaceAll(".class",""); classFullPathList.add(classFullPath); } } }
将classFullPathList存放的扫描的全类路径文件的类名提出来,将第一个字母小写,作为bean对象的名称,类似于以前xml配置bean对象的时候,声明的bean的id。然后将类路径进行反射创建对象,同时将beanName和反射创建好的对象放到ioc容器中。如下:
public void executeInstance(){ if (classFullPathList.size()==0){ return; } try { for (String classFullPath:classFullPathList) { Class<?> clazz = Class.forName(classFullPath); if (clazz.isAnnotationPresent(Controller.class)){ String beanName=clazz.getSimpleName().substring(0,1).toLowerCase()+ clazz.getSimpleName().substring(1); //同时将beanName和反射创建好的对象放到ioc容器中 ioc.put(beanName,clazz.newInstance()); } } } catch (Exception e) { e.printStackTrace(); } }
设计handlerList
Controller层会写很多接口,规范化请求的地址,用注解RequestMapping进行了标识,RequestMapping的Value值其实就是请求的地址,我们把它取出来单独命名成url,在把RequestMapping标识的方法的方法名取出来命名成method,最后把当前方法所在的类,也就是Controller标识的这个类的对象命名成controller。我们现在有了url,controller,method。其实就已经拿到了url和method的映射关系了,现在将其封装保存到handerList集合中。这样做的好处是,当前端发来请求的时候,我们可以取出请求的url,通过url在handerList中找到对应的调用的方法名,实现调用的映射。
先设计一个hander,用来封装url,controller,method:
package com.linghu.springmvc.handler; import java.lang.reflect.Method; /** * @author linghu * @date 2023/9/12 9:23 */ public class LingHandler { private String url; private Object controller; private Method method; public LingHandler() { } public LingHandler(String url, Object controller, Method method) { this.url = url; this.controller = controller; this.method = method; } @Override public String toString() { return "LingHandler{" + "url='" + url + '\'' + ", controller=" + controller + ", method=" + method + '}'; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public Object getController() { return controller; } public void setController(Object controller) { this.controller = controller; } public Method getMethod() { return method; } public void setMethod(Method method) { this.method = method; } }
private void initHandlerMapping(){ if (lingWebApplicationContext.ioc.isEmpty()){ return; } for (Map.Entry<String,Object> entry :lingWebApplicationContext.ioc.entrySet()) { Class<?> clazz = entry.getValue().getClass(); if (clazz.isAnnotationPresent(Controller.class)){ Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method method :declaredMethods) { if (method.isAnnotationPresent(RequestMapping.class)){ RequestMapping requestMappingAnnotation = method.getAnnotation(RequestMapping.class); String url = requestMappingAnnotation.value(); LingHandler lingHandler = new LingHandler(url,entry.getValue(),method); handlerList.add(lingHandler); } } } } }
initHandlerMapping()会将容器中的对象,也就是之前 我们通过类路径反射创建的对象进行遍历,遍历过程中筛选出被Controller注解标识过的类,将类里的方法再全部进行遍历,遍历过程中将被RequestMapping注解标识的方法选出来,将这些方法的方法名命名成method,注解RequestMapping的value值取出来命名成url,将当前类命名成controller,最后将url,controller,method放到LingHandler对象中,在将LingHandler对象放到handerList集合中。
完成分发请求
前端发送一个请求过来,无论是get,post请求都要经过我的servlet,这个时候可以取出请求request信息里的uri,这样我就得到了前端想要请求的Controller层的具体方法,其实拿到这个方法我们就可以利用反射进行调用了。所以完成分发请求的还是我们的servlet,也就是文章一开篇就说的前端控制器,核心控制器,它就是我们的整个大脑核心,负责接收请求,完成请求分发,分发到具体要执行的Controller层的方法去。
前端过来的请求,我们可以让它统一都走post请求。
LingDispatcherServlet.java:
@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 { System.out.println("LingDispatcherServlet doPost"); executeDispatch(req, resp); }
executeDispatch(req, resp)便是携带着前端request请求的具体执行分发请求的方法。
public void executeDispatch(HttpServletRequest request, HttpServletResponse response){ LingHandler lingHandler = getLingHandler(request); try { if (lingHandler==null){ response.getWriter().print("<h1>404</h1>"); }else { lingHandler.getMethod().invoke(lingHandler.getController(),request,response); } } catch (Exception e) { e.printStackTrace(); } }
lingHandler.getMethod().invoke(lingHandler.getController(),request,response);便是反射调用,是完成分发的核心。在这里lingHandler.getMethod()本身就是Method类型的对象,所以可以进行调用。getLingHandler(request)的具体过程如下:
public LingHandler getLingHandler(HttpServletRequest request){ String requestURI = request.getRequestURI(); for (LingHandler lingHandler :handlerList) { if (requestURI.equals(lingHandler.getUrl())){ return lingHandler; } } return null; }
getLingHandler完成的便是查看handlerList集合中有没有前端请求的方法url,有的话就直接把handler对象返回,handler对象里保存着前端请求url和调用后端方法名的映射关系,通过映射关系我们可以查到具体要调用的方法是谁。
Service注解和AutoWired注解
这两个注解的实现其实和Controller注解差不多的流程三板斧。先通过元注解定义好这两个注解,在扫描全类路径的时候,去判断有没有Service注解,有的话就获取文件下的所有接口名,对接口名字首字母变小写,然后拼接得到新的beanName,最后通过反射创建对象,将beanName和对象加入到ioc中。
} else if (clazz.isAnnotationPresent(Service.class)) { Service serviceAnnotation = clazz.getAnnotation(Service.class); String beanName = serviceAnnotation.value(); if ("".equals(beanName)){ Class<?>[] interfaces = clazz.getInterfaces(); for (Class<?> anInterface :interfaces) { String beanName2=anInterface.getSimpleName().substring(0,1).toLowerCase()+ anInterface.getSimpleName().substring(1); ioc.put(beanName2,clazz.newInstance()); } }else { ioc.put(beanName,clazz.newInstance()); } }
设计Controller注解的时候,我们是直接获取的 类名,在这里设计Service注解的时候我们获取的是接口名字,是因为接口内部装满了所有实现类,而我们的Service注解又是写在这些实现类上面 的,我们通过获取接口,就有机会遍历到这些实现类,如果不通过获取接口,直接获取实现类代价要大点。
AutoWired注解的作用是将dao层和业务层对象注入到ioc中,方便业务层或者控制层调用dao层和业务层。扫描什么的其实不难,就是全文扫描带AutoWired注解的属性,将其加入到容器中。加入的这个过程放在一个函数executeAutoWired()中。
public void executeAutoWired(){ if (ioc.isEmpty()){ throw new RuntimeException("容器中没有可以装配的bean"); } for (Map.Entry<String,Object> entry :ioc.entrySet()) { String key = entry.getKey(); Object bean = entry.getValue(); Field[] declaredFields = bean.getClass().getDeclaredFields(); for (Field declaredField :declaredFields) { if (declaredField.isAnnotationPresent(AutoWired.class)){ AutoWired autoWiredAnnotation = declaredField.getAnnotation(AutoWired.class); String beanName = autoWiredAnnotation.value(); if ("".equals(beanName)){ Class<?> type = declaredField.getType(); beanName= type.getSimpleName().substring(0,1).toLowerCase()+ type.getSimpleName().substring(1); } declaredField.setAccessible(true); try { if (ioc.get(beanName)==null){ throw new RuntimeException("容器中没有注入该bean"); } declaredField.set(bean,ioc.get(beanName)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
RequestParam注解
前端在发送请求的时候,会携带一些参数过来,这个时候后端要接收请求的时候处理好参数字段的对应关系。我们可以用requestparam注解标识前端对应过来的参数字段。
@Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequestParam { String value() default ""; }
public void executeDispatch(HttpServletRequest request, HttpServletResponse response){ LingHandler lingHandler = getLingHandler(request); try { if (lingHandler==null){ response.getWriter().print("<h1>404</h1>"); }else { // lingHandler.getMethod().invoke(lingHandler.getController(),request,response); Class<?>[] parameterTypes = lingHandler.getMethod().getParameterTypes(); Object [] params = new Object[parameterTypes.length]; for (int i=0;i< parameterTypes.length;i++){ Class<?> parameterType=parameterTypes[i]; if ("HttpServletRequest".equals(parameterType.getSimpleName())){ params[i]=request; } else if ("HttpServletResponse".equals(parameterType.getSimpleName())) { params[i]=response; } } Map<String,String[]> parameterMap = request.getParameterMap(); for (Map.Entry<String,String[]> entry :parameterMap.entrySet()) { String name = entry.getKey(); String value = entry.getValue()[0]; System.out.println("请求的参数:"+name+"----"+value); int indexRequestParamIndex= getIndexRequestParamIndex(lingHandler.getMethod(),name); if (indexRequestParamIndex!=-1){ params[indexRequestParamIndex]=value; }else { } } lingHandler.getMethod().invoke(lingHandler.getController(),params); } } catch (Exception e) { e.printStackTrace(); } } public int getIndexRequestParamIndex(Method method,String name){ Parameter[] parameters = method.getParameters(); for (int i=0;i<parameters.length;i++){ Parameter parameter=parameters[i]; boolean annotationPresent = parameter.isAnnotationPresent(RequestParam.class); if (annotationPresent){ RequestParam requestParamAnnotation = parameter.getAnnotation(RequestParam.class); String value = requestParamAnnotation.value(); if (name.equals(value)){ return i; } } } return -1; }