做号网站,国外云服务器推荐,网站换服务器对排名有影响吗,网页设计资料下载网站这里续写上一章博客#xff08;110章博客#xff09;#xff1a;
现在我们来学习一下高级的技术#xff0c;前面的mvc知识#xff0c;我们基本可以在67章博客及其后面相关的博客可以学习到#xff0c;现在开始学习精髓#xff1a;
Spring MVC 高级技术#xff1a; …这里续写上一章博客110章博客
现在我们来学习一下高级的技术前面的mvc知识我们基本可以在67章博客及其后面相关的博客可以学习到现在开始学习精髓
Spring MVC 高级技术
拦截器Inteceptor使用
监听器、过滤器和拦截器对⽐前面两个在53章博客可以学习到后面只是名称上的解释如果可以那么前面两个也可以说成是拦截器所以存在很多框架的拦截器
Servlet处理Request请求和Response响应
过滤器Filter对Request请求起到过滤的作用作用在Servlet之前如果配置为/*可以对所 有的资源访问servlet、js/css静态资源等进行过滤处理
监听器Listener实现了javax.servlet.ServletContextListener 接⼝的服务器端组件它随Web应用的启动⽽启动只初始化一次然后会一直运行监视随Web应用的停⽌⽽销毁
监听器的作用与过滤器虽然都有拦截的意思但是偏重不同
监听器可以选择对一些数据或者说数据变化进行监听以及拦截而过滤则是对过来的请求直接拦截而不是更加里面的数据拦截所以一般情况下过滤器通常是在监听器之前进行拦截的
那么说监听器一般有如下的作用
作用一做一些初始化工作web应用中spring容器启动ContextLoaderListener
作用⼆监听web中的特定事件⽐如HttpSessionServletRequest的创建和销毁变量的创建、 销毁和修改等可以在某些动作前后增加处理实现监控⽐如统计在线⼈数利⽤HttpSessionLisener等
拦截器Interceptor是SpringMVC、Struts等表现层框架自己的不会拦截jsp/html/css/image的访问等只会拦截访问的控制器方法Handler一般来说这个拦截在一定程度上使用了过滤器以及监听器因为需要确定拦截的数据通常需要先获得所以mvc的拦截器的实现方式通常在于过滤器或者说监听器注意只是因为他需要对应的数据所以他才会在于其他的器如过滤和监听否则拦截器一般只是拦截指定数据的处理而不是在于什么
根据上面的说明其实从配置的⻆度也能够总结发现serlvet、filter、listener是配置在web.xml中的⽽interceptor是 配置在表现层框架自己的配置⽂件中的所以Interceptor一般是框架自己的
根据前面的说明可以知道如下
拦截器会在如下的情况可能会发生拦截
Handler业务逻辑执行之前拦截一次操作url
在Handler逻辑执行完毕但未跳转⻚⾯之前拦截一次操作转发如果没有那么一般操作响应数据如果也没有那么虽然拦截但并未做什么
在跳转⻚⾯之后拦截一次比如对应的json的处理或者并未处理 前面我们知道了这个图 以及他的说明
流程说明
第一步用户发送请求⾄前端控制器DispatcherServlet
第⼆步DispatcherServlet收到请求调⽤HandlerMapping处理器映射器一般是map保存的
第三步处理器映射器根据请求Url找到具体的Handler后端控制器可以根据xml配置、注解进行查找因为查找所以是映射⽣成处理器对象及处理器拦截器如果有则⽣成一并返回DispatcherServlet他负责创建
第四步DispatcherServlet调⽤HandlerAdapter处理器适配器去调⽤Handler
第五步处理器适配器执⾏Handlercontroller的方法生成对象了这里相当于调用前面的handle01方法他负责调用
第六步Handler执行完成给处理器适配器返回ModelAndView即处理器适配器得到返回的ModelAndView这也是为什么前面我们操作方法时是可以直接操作他并返回的而返回给的人就是处理器适配器就算你不返回那么处理器适配器或者在之前即他们两个中间可能会进行其他的处理来设置ModelAndView并给处理器适配器
第七步处理器适配器向前端控制器返回 ModelAndView因为适配或者返回数据所以是适配ModelAndView 是SpringMVC 框架的一个 底层对 象包括 Model 和 View
第⼋步前端控制器请求视图解析器去进行视图解析根据逻辑视图名来解析真正的视图加上前后的补充即前面的配置视图解析器
第九步视图解析器向前端控制器返回View
第⼗步前端控制器进行视图渲染就是将模型数据在 ModelAndView 对象中填充到 request 域改变了servlet最终操作servlet来进行返回
第⼗一步前端控制器向用户响应结果jsp的
即可以理解请求找路径并返回123给路径让其判断路径并返回且获得对应对象4567变成参数解析如拼接 进行转发89然后到jsp10最后渲染11
所以说
第一次拦截在1和5中间处理
第二次拦截在6到8中间处理
第三次拦截在9到11中间处理
其实通过图片我们应该知道第一次拦截应该是在3和4中在前端控制器旁边处理而7和8就是第二次拦截10到11则是第三次拦截这里也可能是9到10
所以可以知道其实mvc自带的有一些拦截这也是对应注解比如RequestBody或者ResponseBody可以操作的原因
当然我们也可以进行添加拦截这在后面会说明的
为了更加的知道拦截器的处理我们直接来进行实战
注意mvc的拦截器是里面的也就是说servlet原本的过滤器必然先处理或者后处理
实际上我们学习源码很大程度是必须要有实战的因为一个框架的源码我们基本是不可能全部读完的这取决于一个框架是由很长时间的迭代以及很多人一起开发完成的当然如果你的框架够小那么可以是单独完成在这种情况下学习框架中用阅读源码来学习我们只能知道他的一点实现方式所以学习框架通常需要直接的实战来进行学习来直接的确定他的作用而不是单独看源码来确定作用你怎么知道他有没有其他关联并且要知道这个关联需要看更多的源码也就是说实际上源码的解析大多数是让你知道他的实现方式而不是具体细节比如他为什么这样定义变量等等当然除了实现方式有时候也需要学习设计模式这个在以后会单独给一个博客来进行处理的先了解一些框架的设计模式再说
那么既然要实战我们首先需要操作一个项目项目如下 对应的依赖在前面我们已经给过多次了这里我们继续给出吧 packagingwar/packagingdependenciesdependency!--mvc需要的依赖即有前端控制器DispatcherServlet--groupIdorg.springframework/groupIdartifactIdspring-webmvc/artifactIdversion5.1.5.RELEASE/version/dependency!--servlet坐标若不使用对应的类如HttpServletRequest的话可以不加--dependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion3.1.0/version/dependencydependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactIdversion2.9.8/version!--这个必须要后面的可以不写后面两个但最好写上防止其他情况--/dependencydependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-core/artifactIdversion2.9.8/version/dependencydependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-annotations/artifactIdversion2.9.0/version/dependency/dependencies如果需要补充自行补充吧
对应的web.xml
?xml version1.0 encodingUTF-8?
web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaeexmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsdversion4.0servletservlet-namedispatcherServlet/servlet-nameservlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-classinit-paramparam-namecontextConfigLocation/param-nameparam-valueclasspath:springmvc.xml/param-value/init-param/servletservlet-mappingservlet-namedispatcherServlet/servlet-nameurl-pattern//url-pattern/servlet-mapping/web-appindex.jsp
% page contentTypetext/html;charsetUTF-8 languagejava %
html
headtitleTitle/title
/head
body
script srchttps://code.jquery.com/jquery-3.6.0.min.js/script
button idbtnajax提交/button
script$(#btn).click(function () {let url test/in;let data [{id:1,username:张三},{id:2,username:李四}];$.ajax({type: POST,//大小写可以忽略url: url,data: data,contentType: application/json;charsetutf-8,success: function (data) {console.log(data);alert(data)}})})
/script
/body
/html
springmvc.xml
beans xmlnshttp://www.springframework.org/schema/beansxmlns:mvchttp://www.springframework.org/schema/mvcxmlns:contexthttp://www.springframework.org/schema/contextxmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/mvchttp://www.springframework.org/schema/mvc/spring-mvc.xsdcontext:component-scan base-packagecom.controller/mvc:annotation-driven/mvc:annotation-driven
/beansentity里面的User类
package com.entity;public class User {String id;String username;public String getId() {return id;}public void setId(String id) {this.id id;}public String getUsername() {return username;}public void setUsername(String username) {this.username username;}Overridepublic String toString() {return User{ id id \ , username username \ };}
}
test类
package com.controller;import com.entity.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import java.util.List;Controller
RequestMapping(/test)
public class test {RequestMapping(in)ResponseBodypublic ListUser ajax(RequestBody ListUser list) {System.out.println(list);return list;}
}
自行配置tomcat启动运行看看结果那么我们基础操作搭建完毕现在我们来操作一下拦截
在com包下创建Interceptor包然后创建MyInterceptor类
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;public class MyInterceptor implements HandlerInterceptor {
}
其中HandlerInterceptor接口如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.web.servlet;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;public interface HandlerInterceptor {//默认的不要求强制被实现default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {return true; //默认是true的}default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable ModelAndView modelAndView) throws Exception {}default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable Exception ex) throws Exception {}
}
这三个代表了三个地方前面的三个拦截比如
preHandle是第一个拦截postHandle是第二个拦截afterCompletion是第三个拦截实际上一个框架的编写除了给出一些功能外还需要存在扩展功能有的话最好而拦截的处理基本就是框架基本的扩展了所以在spring中也存在多个拦截的处理包括mybatis当然他们可能并没有特别的说明是拦截但是你也或多或少可以知道可以操作一些类来实现在中间进行处理的方式这其实也算是一种拦截因为是我们自行处理的所以拦截器在某些方面可以是这样的认为框架自身给出可以扩展的方式都可以称为拦截器
实际上通过前面我们也明白前端控制器底层基本上也是操作get和post而servlet也是但是mvc是建立在servlet上的所以前端控制器通常也是生成了servlet在前面我们学习了前端控制器只是生成一个servlet一般也可以是他自己其中只是操作了拦截进行的处理这个拦截或多或少使用了过滤或者监听所以说具体的是否监听或者过滤的处理可能也是对应的配置导致进行的某些配置再处理因为你删除了配置前端控制器的拦截也就不复存在了那么在这种情况下我们的三次拦截也只是其中多个拦截的扩展那么如果这个时候你操作了传统的拦截那么就需要看配置的先后顺序
我们继续修改MyInterceptor类的内容
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor implements HandlerInterceptor {//第一次个/*Handler业务逻辑执行之前拦截一次*/Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//程序先执⾏preHandle()方法如果该方法的返回值为true则程序会继续向下执行处理器中的方法否则将不再向下执行//也就是说这个拦截器拥有着结束执行的能力并不是拦截器只能操作扩展实际上过滤或者监听也可以称为拦截器的System.out.println(handler);System.out.println(Handler业务逻辑执行之前拦截一次我是第一次);return true;//我们可以看到handler这个变量实际上他就是准备执行对应的handler方法的但是需要看看是否返回true//默认情况下HandlerInterceptor里面是返回true的自己可以看一看就知道了//由于他是在执行具体方法之前的拦截所以一般来说我们会使用他来完成一些权限的处理//实际上你也可以使用过滤器来完成但是需要配置所以使用mvc封装的比较方便}//第二次个/*在Handler逻辑执行完毕但未跳转⻚⾯之前拦截一次看参数就知道除了当前的方法外还有对应的视图和数据modelAndView因为是之后的吗所以在没有进行渲染时你可以选择针对数据进行某些修改这个在spring中也存在这样的处理在一个bean进行生成时你也可以进行拦截进行某些处理或者修改*/Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println(handler);System.out.println(modelAndView);System.out.println(在Handler逻辑执行完毕但未跳转页面之前拦截一次我是第二次);}//第三次个/*在跳转⻚⾯之后拦截一次由于具体的数据和视图都操作完毕那么这里只能获取对应的方法以及你需要的异常信息了一般来说跳转页面就是将内容响应给前端所以一般渲染视图中10到11一般是这里了渲染视图一般是需要根据文件如jsp进行替换数据的过程其中对数据的替换使用这样才能进行响应即jsp是一个视图那么通过转译编译就是对应的第10步的处理了10前面是根据视图对象找到该文件10后面是转译编译中间考虑拦截因为中间的处理都在前端控制器所以或多或少他们的数据在某个情况下是共享的所以可以替换的处理一般情况下是在jsp替换后准备响应时进行的拦截也就是说11步就是最后的响应比如如果你在jsp中操作了输出语句那么这个值输出后这个方法才会进行处理其实在68章博客中一般是最后也说明了这三个拦截方法可以选择去看一下*/Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println(handler);System.out.println(ex);System.out.println(在跳转页面之后拦截一次我是第三次);}
}
我们写好了拦截器自然需要进行使用在mvc中使用是必须需要配置的并且他是mvc的所以是需要在mvc的对应的配置文件中进行配置
对应的springmvc.xml的补充 !--配置拦截器--mvc:interceptorsmvc:interceptormvc:mapping path/**/!--项目路径下的对应路径一个*代表一个路径下但不代表路径下的路径下两个基本就是所有--!--对所有的controller类里面的所有方法都进行拦截因为是/**--bean classcom.Interceptor.MyInterceptor/bean/mvc:interceptor/mvc:interceptors
实际上mvc:interceptors代表可以配置多个在配置多个时一般需要操作路径防止都进行拦截的处理这个时候不写可能报错具体可以测试而不是指定的拦截因为默认是/**所以存在mvc:interceptors中存在mvc:interceptor所以如果你只有一个并且需要是/ * *那么可以这样写 mvc:interceptorsbean classcom.Interceptor.MyInterceptor/bean/mvc:interceptors
!--
其中mvc:mapping path/**/也可以进行去掉因为默认是/**
直接省略mvc:interceptor代表只有你这一个的意思但是由于只有一个通常情况下默认是/**的而不会给你进行路径的设置所以如果你只有一个并且需要是/**那么可以这样写
--一般我们建议使用这样的方式来进行编写 !--配置拦截器--mvc:interceptorsmvc:interceptormvc:mapping path/**/!--项目路径下的对应路径一个*代表一个路径下但不代表路径下的路径下两个基本就是所有--!--对所有的controller类里面的所有方法都进行拦截因为是/**--bean classcom.Interceptor.MyInterceptor/bean/mvc:interceptor/mvc:interceptors
!--一般来说/**的/代表从端口开始这里了解即可所以大多数访问也会进行拦截处理但是也要知道一个端口基本只有一个服务器占用所以并不会影响其他服务器的所以不需要考虑其他服务器的影响问题除非是业务逻辑上的影响比如文件的操作
虽然大多数访问都会进行拦截处理但是要知道他是在需要执行对应的方法执行进行的拦截也就是说如果你并没有需要执行对应的方法那么就不会执行而不会也自然不会出现true这里我们需要注意的是只有当前面的拦截的返回值或者说第一个拦截的返回值为true时后面的两个拦截才会进行处理这也是为什么如果你访问index.jsp由于第一个拦截没有执行他并不会操作对应的方法自然也就不会经过这个拦截方法了一般情况下没有经过这个方法时对应进行操作的变量是默认为false但是只要你操作了这个拦截或者就算你不进行重写他默认放行因为这个时候他是默认的方法是true的前面的HandlerInterceptor接口可以看到返回值的那么其他的拦截比如第三个拦截没有起作用的原因默认情况下如果你不执行第一个拦截那么就是false的这里的true是否看起来相当于过滤器中的放行的意思呢之所以需要这样的规定是保证其他的如index.jsp不会操作拦截的原因因为是直接的访问并不需要操作拦截而减低操作的可能从而使得服务器可以接收更多请求或者减少内存损耗
--当然其实还有功能这是mvc设计出来的功能他也可以进行去掉比如他可以选择性的不拦截一个路径的请求 mvc:interceptorsmvc:interceptormvc:mapping path/**/mvc:exclude-mapping path/demo/**/ !--选择不拦截demo对应路径下面的请求--bean classcom.Interceptor.MyInterceptor/bean/mvc:interceptor/mvc:interceptors这样就能不拦截demo开头的处理了一般情况下servlet或者mvc的普通拦截都是在项目路径下的直接拦截比如前面的测试的路径test/in就是放在项目路径后面而不是端口路径
编写好后我们执行前面的代码看看后端的结果
打印如下
/*
public java.util.Listcom.entity.User com.controller.test.ajax(java.util.Listcom.entity.User)
Handler业务逻辑执行之前拦截一次我是第一次[User{id1, username张三}, User{id2, username李四}]public java.util.Listcom.entity.User com.controller.test.ajax(java.util.Listcom.entity.User)
null
在Handler逻辑执行完毕但未跳转页面之前拦截一次我是第二次public java.util.Listcom.entity.User com.controller.test.ajax(java.util.Listcom.entity.User)
null
在跳转页面之后拦截一次我是第三次
*/这个时候你可以选择将返回值变成false那么我们看看这个打印结果
/*
public java.util.Listcom.entity.User com.controller.test.ajax(java.util.Listcom.entity.User)
Handler业务逻辑执行之前拦截一次我是第一次
*/我们可以发现他甚至连对应的方法都不执行了而方法都不执行默认情况下是没有响应体信息的那么在前端显示的就是空白这个时候甚至都不会操作视图也就是单纯的返回空响应也就是说这个true也决定了对应的controller的方法的执行这也是为什么默认情况下对应的HandlerInterceptor方法的返回值是true的一个原因
现在我们添加一个前端jsptest.jsp
% page contentTypetext/html;charsetUTF-8 languagejava %
html
headtitleTitle/title
/head
body
%System.out.println(1);
%
1
/script
/body
/html
上面的这个地方加上代码
%System.out.println(1);
%这个是jsp的语法在51章博客有说明在转译和编译的情况下会进行处理的最终操作拦截那么我们在springmvc.xml中加上如下的配置
bean idviewResolverclassorg.springframework.web.servlet.view.InternalResourceViewResolverproperty nameprefix value//propertyproperty namesuffix value.jsp/property/bean然后再test类中加上这个代码 RequestMapping(ix)public String ix() {return test;}首先还是false的返回值然后直接的在url中后面加上test/ix访问即可这个时候我们查看后端的打印
/*
public java.lang.String com.controller.test.ix()
Handler业务逻辑执行之前拦截一次我是第一次
*/因为false不会经过方法所以在前端是显示空白的
经过这两次的测试可以发现handler的打印信息包含了访问权限返回值对有包路径的方法及其参数列表等等我们给ix方法加上两个参数列表即
public String ix(String a,Integer b) {对应的打印信息如下
/*
public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
Handler业务逻辑执行之前拦截一次我是第一次
*/即也的确如此现在我们给false变成true的返回值然后看看打印结果
/*
public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
Handler业务逻辑执行之前拦截一次我是第一次public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
ModelAndView [viewtest; model{}]
在Handler逻辑执行完毕但未跳转页面之前拦截一次我是第二次1public java.lang.String com.controller.test.ix(java.lang.String,java.lang.Integer)
null
在跳转页面之后拦截一次我是第三次*/也验证了前面注释中的如果你在jsp中操作了输出语句那么这个值输出后这个方法才会进行处理
但是还有一个问题如果controller对应的方法没有返回值呢因为没有返回值说明他不会经过视图而不经过视图那么第三个拦截是否不会进行了所以我们修改ix方法 RequestMapping(ix)public void ix(String a,Integer b) {}因为当我们没有给出视图名时会将请求参数进行拼接一般是RequestMapping的整个拼接
我们还是true因为false修改与不修改是一样的反正都不执行那么他的修改没有意义我们看看执行后的打印结果
/*
public void com.controller.test.ix(java.lang.String,java.lang.Integer) throws java.io.IOException
Handler业务逻辑执行之前拦截一次我是第一次public void com.controller.test.ix(java.lang.String,java.lang.Integer) throws java.io.IOException
ModelAndView [viewtest/ix; model{}]
在Handler逻辑执行完毕但未跳转页面之前拦截一次我是第二次public void com.controller.test.ix(java.lang.String,java.lang.Integer) throws java.io.IOException
null
在跳转页面之后拦截一次我是第三次
*/但是这里还存在一些细节这是前面并没有说明的我们看如下
我们继续修改 RequestMapping(ix)public String ix(String a, Integer b) throws IOException {return null;}执行之后结果与上面的一样也就证明了其实对应的视图默认是null是默认虽然会根据组件得到路径的视图所以当你返回类型为void时他的视图结果与返回null值是一样的然而虽然根据路径得到了视图但是其实在没有视图时也会得到结果为什么我们看这个代码
RequestMapping(ix)public void ix(String a, Integer b, HttpServletResponse mm) throws IOException {mm.setContentType(text/html;charsetutf-8);PrintWriter writer mm.getWriter();writer.println(哈哈哈);}后端打印
/*
public void com.controller.test.ix(java.lang.String,java.lang.Integer,javax.servlet.http.HttpServletResponse) throws java.io.IOException
Handler业务逻辑执行之前拦截一次我是第一次public void com.controller.test.ix(java.lang.String,java.lang.Integer,javax.servlet.http.HttpServletResponse) throws java.io.IOException
null
在Handler逻辑执行完毕但未跳转页面之前拦截一次我是第二次public void com.controller.test.ix(java.lang.String,java.lang.Integer,javax.servlet.http.HttpServletResponse) throws java.io.IOException
null
在跳转页面之后拦截一次我是第三次
*/可以发现没有视图了没有视图相当于直接的返回的操作直接操作响应体信息了但是为什么加上这些就会没有呢这是因为如果你要自己加上响应信息的话那么必然会与原来的如jsp而发生冲突一般来说io并不能覆盖只是增加导致对应的jsp信息出现问题所以他们是需要分开的所以只要你存在要自行操作响应体信息的那么视图就会为null通过测试只要你在参数列表中加上HttpServletResponse mm比如 RequestMapping(ix)public void ix(String a, Integer b, HttpServletResponse mm) throws IOException {
// mm.setContentType(text/html;charsetutf-8);
// PrintWriter writer mm.getWriter();
// writer.println(哈哈哈);}那么视图就为null即执行的操作响应体信息如果你没有设置自然前端显示空白
而打印信息中第二次和第三次无论是否操作了HttpServletResponse或者无论是否报错都会出现也就是说后面两个并没有互相联系的不能让对方放行的处理所以他们基本必然都会打印了但是报错的话视图是找不到了那么就不会出现对应的转译编译才对为什么第三次拦截也会出现呢在前面我们说过了10到11则是第三次拦截这里也可能是9到10虽然在第8次报错但是第9次的操作需要返回报错信息这个时候是会经过第三次拦截的简单来说有返回就算是空的他通常也会操作
至此我们的细节基本说明完毕
拦截器的执行流程
在运行程序时拦截器的执行是有一定顺序的该顺序与配置⽂件中所定义的拦截器的顺序相关单个 拦截器在程序中的执行流程如下图所示 1程序先执⾏preHandle()方法如果该方法的返回值为true则程序会继续向下执行处理器中的方 法否则将不再向下执行
2在业务处理器即控制器Controller类处理完请求后会执⾏postHandle()方法然后会通过DispatcherServlet向客户端返回响应
3在DispatcherServlet处理完请求后才会执⾏afterCompletion()方法
再结合这个图吧 根据前面的说明他的流程也可以说是对的
总结一下 上面是单个拦截器的流程那么多个拦截器的执行流程呢
多个拦截器假设有两个拦截器Interceptor1和Interceptor2并且在配置⽂件中 Interceptor1拦截 器配置在前在程序中的执行流程如下图所示 从图可以看出当有多个拦截器同时工作时它们的preHandle()方法会按照配置⽂件中拦截器的配置 顺序执行⽽它们的postHandle()方法和afterCompletion()方法则会按照配置顺序的反序执行
我们来看例子
首先创建两个类在Interceptor包中
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor1 implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objecthandler) throws Exception {System.out.println(preHandle1....);return true;}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println(postHandle1....);}Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Objecthandler, Exception ex) throws Exception {System.out.println(afterCompletion1....);}
}
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor2 implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objecthandler) throws Exception {System.out.println(preHandle2....);return true;}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println(postHandle2....);}Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Objecthandler, Exception ex) throws Exception {System.out.println(afterCompletion2....);}
}我们进行配置去掉原来的
mvc:interceptorsmvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor1/bean/mvc:interceptormvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor2/bean/mvc:interceptor/mvc:interceptorsbean idviewResolverclassorg.springframework.web.servlet.view.InternalResourceViewResolverproperty nameprefix value//propertyproperty namesuffix value.jsp/property/bean后端之前处理的
RequestMapping(ix)public String ix(String a, Integer b) throws IOException {return test;}现在我们执行操作上面的test.jsp看看打印信息
/*
preHandle1....
preHandle2....
postHandle2....
postHandle1....
1
afterCompletion2....
afterCompletion1....
*/也的确是第一个是顺序后配置的在后面你修改配置中拦截的顺序即可后面两个是反序现在我们来测试让其中一个
配置放在前面 mvc:interceptorsmvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor2/bean/mvc:interceptormvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor1/bean/mvc:interceptor/mvc:interceptors打印结果
/*
preHandle2....
preHandle1....
postHandle1....
postHandle2....
1
afterCompletion1....
afterCompletion2....
*/也的确如此那么如果其中一个不放行呢我们将preHandle2…的对应方法的返回值设置为false看看上面的配置不变执行后看看打印结果
/*
preHandle2....
*/只有这一个那么我们将配置顺序改变回来执行看看结果
/*
preHandle1....
preHandle2....
afterCompletion1....
*/感觉到了如果你的返回值是false那么你后面的拦截都不能进行处理包括后面的第一次拦截但是如果存在第一次拦截执行完毕那么允许他执行第三次拦截虽然是这样说但是也只是一个结论我们选择继续添加一个类来进行处理
package com.Interceptor;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class MyInterceptor3 implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objecthandler) throws Exception {System.out.println(preHandle3....);return false;}Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,ModelAndView modelAndView) throws Exception {System.out.println(postHandle3....);}Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Objecthandler, Exception ex) throws Exception {System.out.println(afterCompletion3....);}
}将preHandle2…的对应方法的返回值设置为true修改回来然后配置如下 mvc:interceptorsmvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor1/bean/mvc:interceptormvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor3/bean/mvc:interceptormvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor2/bean/mvc:interceptor/mvc:interceptors执行看看结果
/*
preHandle1....
preHandle3....
afterCompletion1....
*/可以发现结论正确但是为什么会这样按道理说postHandle1…应该也会执行啊为什么没有呢实际上如果没有false那么结果应该是
/*
preHandle1....
preHandle3....
preHandle2....
postHandle2....
postHandle3....
postHandle1....
1
afterCompletion2....
afterCompletion3....
afterCompletion1....
*/其中中间的处理基本都是方法的处理方法没有进行处理自然没有视图没有视图前端自然返回空白信息响应体没有信息那么这个false会导致后面的不进行处理但是afterCompletion1…对应的方法的拦截是在响应哪里而这个由于preHandle1…是返回true的他的方法是你造成的不处理而不是我自己所以说afterCompletion1…会进行打印那么这个拦截的关系就有意思了其中preHandle1…以后就这样说明了一般这样说是说他对应的方法注意即可的返回值的确影响后面的放行但是对后面两个方法的放行的影响是不同的postHandle1…是根据true造成的放行来决定执行的也就是说只要你放行了true影响放行的参数那么我就会执行但是如果你没有放行那么我自然也执行不了而afterCompletion1…只看你的返回值而不看你是否放行所以在单独的拦截器的时候true和false的结果都是他们两个执行或者不执行但是存在其他的拦截时那么放行这个处理和判断true的处理的区别就出现了这也是为什么afterCompletion1…会执行但是postHandle1…不会执行
为了验证这样的结果我们修改配置文件 mvc:interceptorsmvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor1/bean/mvc:interceptormvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor2/bean/mvc:interceptormvc:interceptormvc:mapping path/**/bean classcom.Interceptor.MyInterceptor3/bean/mvc:interceptor/mvc:interceptors执行看看结果
/*
preHandle1....
preHandle2....
preHandle3....
afterCompletion2....
afterCompletion1....
*/可以发现之前的结论是正确的后面先打印afterCompletion2…是反序的原因
那么还有个问题为什么mvc的拦截器中第一次拦截是顺序的其他两个是反序的或者说倒序的
解释如下
这个默认规则的原因是为了在执行拦截器时提供更多的灵活性和可能性考虑以下情况
第一个拦截器通常用于做一些准备工作如日志记录、身份验证等按顺序执行有助于确保这些准备工作在控制器方法之前完成
控制器方法执行后倒序执行其他拦截器可以用于清理工作、日志记录和一些其他操作这确保了在请求处理完毕后执行这些操作因为这些工作一般先创建的后清理
但是实际上顺序的问题大多数我们并不需要注意并且也存在可以修改顺序的处理只是可能需要某些版本好像现在基本都不行了
处理multipart形式的数据
前面虽然我们使用mvc处理过问题但是还是有些操作我们没有说明比如方便我们操作的api现在来补充一下
首先加上依赖 dependencygroupIdcommons-fileupload/groupIdartifactIdcommons-fileupload/artifactIdversion1.3.1/version/dependency配置上传文件解析器
bean idmultipartResolverclassorg.springframework.web.multipart.commons.CommonsMultipartResolverproperty namemaxUploadSize value1000000000//bean
!--
MultipartResolver的组件在前面我们说过是操作上传的而
public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware {
即CommonsMultipartResolver是MultipartResolver的子类
--对应的前端index.jsp
% page contentTypetext/html;charsetUTF-8 languagejava %
html
headtitleTitle/title
/head
body
form methodpost enctypemultipart/form-data actiondemo/uploadinput typefile nameuploadFile/input typesubmit value上传/
/form
/body
/html
对应的后端我们创建FileController类
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;Controller
RequestMapping(/demo)
public class FileController {RequestMapping(upload)public String upload(MultipartFile uploadFile, HttpServletRequest request) throws IOException {// 文件原名如xxx.jpgString originalFilename uploadFile.getOriginalFilename();// 获取文件的扩展名如jpgString extendName originalFilename.substring(originalFilename.lastIndexOf(.) 1, originalFilename.length());String uuid UUID.randomUUID().toString();// 新的文件名字大多数服务器基本都是如此这是保证文件不重复String newName uuid . extendName;//String getRealPath(String path)返回包含给定虚拟路径的实际路径的字符串//这里的参数是/那么代表就是当前项目的地址假如你的项目名称是test那么就算test的绝对路径String realPath request.getSession().getServletContext().getRealPath(/);System.out.println(realPath);// 后面解决文件夹存放文件数量限制所以按日期存放文件名称主要是UUID因为不同的操作系统对一个目录里面的文件数量的多少是有限的一般情况下存在如下的限制网上搜索到的/*FAT32FAT32 文件系统常见于旧的 Windows 版本对每个目录有文件数目的限制通常约为 65534 个文件NTFSNTFS 文件系统常见于现代 Windows 版本支持更多的文件数目通常数以百万计ext4在类Unix/Linux系统上ext4 文件系统支持大量的文件通常在数百万到数十亿文件之间具体取决于文件系统的配置HFS苹果的HFS文件系统支持大量文件数以百万计APFS苹果的APFS文件系统支持更多文件也在数百万到数十亿之间上面的这些数字代表一个目录中可以存放的文件数量而正是由于有这些限制所以一般情况下我们的文件会放在不同目录中而按照日期的存放就非常好的即存放一天中产生的文件数量*/String datePath new SimpleDateFormat(yyyy-MM-dd).format(new Date());//对应的项目路径加上日期的总体名称那么就是一个在项目里面的文件夹了File floder new File(realPath / datePath);if (!floder.exists()) { //判断是否存在如果存在那么就不用创建该目录了floder.mkdirs();}//使用他的api来操作用一个新的File接收对应的File组成一个路径然后加上对应的基本不重复的文件名称最终目录和文件都存在了//并且将uploadFile对应的文件信息他存在字节的前面我们处理过了比如byte[] bytes file.getBytes();给这个新的文件或者说完成复制//一个保存文件信息的基本必然是保存了字节所以其他的我们要保存文件进行封装时一般也与这里一样保存字节的uploadFile.transferTo(new File(floder, newName));return success;}
}
然后如果你按照流程来的话对应的视图解析器应该是配置的所以我们在index.jsp同级别创建一个success.jsp
% page contentTypetext/html;charsetUTF-8 languagejava %
html
headtitleTitle/title
/head
body
1
/body
/html
执行后我们上传一个文件或者说给一个文件然后看看当前项目是否出现对应的文件或者目录然而并没有为什么
这里就需要考虑maven的操作与传统的web的区别了首先我们可以选择实现一个传统的web处理这里可以选择看看第50章博客
为此这里我也不得不给出三种创建web的方式空项目传统webmaven项目
首先我们从空项目来进行处理由于空项目并没有使用到maven或者并没有进行指定使用maven所以一般空项目只能到传统的web即空项目到web一般与传统的web项目是一样的然而这是一般的说法基本上任何形式都可以从空项目变化而来因为他就是在idea中所以这里应该有五种方式空项目操作java程序空项目到web空项目到maven传统webmaven等等
创建ser1空项目 然后进入如下 空项目是什么都没有有时也会包括模块一般情况下一个项目是对应一个模块的如果没有模块我们可以创建模块 这里了解即可创建模块一般是如下 创建后选择关闭idea打开我们创建好模块的这个目录即可然后可以这样 从上面我们可以看到这个目录后面有个sources root这个再后面说明他代表这个目录下面是专门写java的所以这个时候创建的目录是包的意思否则不是
直接执行看看结果即可一般sources root的出现除了配置外执行java代码也会出现在项目中他虽然也是文件夹但是他也由于是项目所以可以直接创建java文件虽然不能再其里面的文件夹里面创建java文件右键可以看到与其里面的目录有是不同的选项的但是一般情况下我们需要src这个目录但是要明白src这个目录其实也是认为创造的在maven或者web中使用而已而单纯的java并不一定使用他但是为了统一所以我们可以选择创建src这个包括来操作也就是 至此可以说空项目操作java程序操作完毕现在我们来完成空项目到web
空项目到web需要这样的处理一般情况下是需要很多的东西当然我们也可以选择创建一个web项目来观察一下然后再从空项目到web归根揭底web和空项目的区别就是对应目录赋予一些操作属性让他作为资源文件以及相关web中的对应文件也赋予属性由于是赋予的所以对应的文件名称并非需要固定比如web对应的资源文件可以变成webb只是有些插件或者说idea的某些自动处理比如maven的依赖自动判断文件名称来自动配置会处理这些文件名称所以我们大多数都会配置对应的固定文件比如web中配置war包那么对应的文件若是web或者webapp通常会自动配置这里也要注意随着时间的推移这些名称可能会变所以注意即可一般好像只有webapp了web已经没有了如果在以前博客中有说明那么大概是以前的某个版本或者是以前的操作以前的版本也是可能会发生改变的因为官方也是维护的除非是固定的版本这些操作再老版本的idea中可能需要手动的处理但是新版的一般并不存在这样的处理了或者忽略一些处理所以这里我们来操作一些赋予这个操作
其他的删除回到这里 然后这样 这样src就可以操作java代码而再xianmu的直接目录里面就不行了很明显这个选项是赋予这个目录存放java或者被编译器编译的地方更加可以说编译器只会去这个属性中操作java代码自然导致操作发生改变如这个时候右键xianmu这个目录时出现的选项发生变化了这些操作之所以会这样其实是idea软件自身处理的或者idea也处理了jdk的某些配置以及或者说idea他手动的帮我们选择编译以及执行然后将结果放在控制台中这个的显示自然也可以认为是一个文件中或者直接的显示而不是文件中这些操作都可以被我们处理因为二进制就是可以这样二进制出现显示还是复杂的就算在操作系统中显示的处理一般也并没有具体说明因为是需要通过硬件完成对规定排列的映射的这里了解即可
好了既然src的情况我们说明完毕但是空项目操作到web还不知道如何操作现在我们来操作一下 然后操作如下 这样对应的前面就会出现如下 这样你就可以操作启动服务器了具体自行配置可以看50章博客如果启动后出现对应的数据那么操作成功这就是空项目到web的处理那么传统web是如何处理实际上也就是一次性到这里
其中idea或者说web项目通常会有隐藏的设置即自动读取在WEB-INF/lib/下面的jar包这里了解即可当然其实我们也可以手动的指定但是一般情况下新版的可能没有这样的设置的所以建议指定
一般情况下传统项目的处理基本只会在老版本的idea中新版的没有具体在50章博客中可以看到所以这里我们就不多说了
现在我们先操作一下这个前面的这里就需要考虑maven的操作与传统的web的区别了 先加上这个具体的下载地址是如下
链接https://pan.baidu.com/s/1Yyd662YY99X7wEGIZ4FN7w
提取码alsk
然后配置如下 点击这个在目录中选择lib文件或者直接选择jar包都可选择文件说明是操作里面的所有jar包的
然后总体操作是如下 在web.xml中配置如下
?xml version1.0 encodingUTF-8?
web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaeexmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsdversion4.0servletservlet-nameConfigServlet/servlet-nameservlet-classservlet/servlet-class/servletservlet-mappingservlet-nameConfigServlet/servlet-nameurl-pattern/config/url-pattern/servlet-mapping
/web-app补充对应的servlet的部分内容 Overridepublic void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {String realPath servletRequest.getServletContext().getRealPath(/);System.out.println(realPath);}启动访问对应的config记得加上看看打印结果这里就是我们需要查看的区别
F:\xianmu\out\artifacts\xianmu_Web_exploded\即他操作的是编译后最终的结果其实也可以看出来他的操作在很大程度上是对应的这个结果
我们可以在启动时查看下方启动日志即可查看临时的tomcat简称为临时处理或者说tomcat副本比如我的就是
C:\Users\33988\AppData\Local\JetBrains\IntelliJIdea2021.3\tomcat\062b022d-1657-4d8b-b4e9-da3eeae3d227\conf\Catalina\localhost里面的配置文件就是这个F:\xianmu\out\artifacts\xianmu_Web_exploded即他还是操作临时的tomcat
那么现在就剩下两个了即空项目到maven以及直接的maven空项目到maven其实也并不是很难
我们创建如下的空项目 然后我们操作如此 配置这些 那么怎么变成maven项目呢我们可以思考要变成对应的这个项目必然是需要使用到maven这就需要我们配置使用了具体如何配置看如下
先创建pom.xml与src同级别
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdorg.example/groupIdartifactIdtestma/artifactId !--这里建议写上与项目名称一样的也就是maven--version1.0-SNAPSHOT/versionpropertiesmaven.compiler.source11/maven.compiler.sourcemaven.compiler.target11/maven.compiler.target/properties/project但是他并没有进行处理我们需要如下的配置maven本身也可以看成一个框架而idea需要被他支持就如空项目到web时需要指定jar包一样当然也可以添加框架实际上也就是对应的jar包但是由于maven的jar比较多所以这里我们就选择添加框架了 一般来说这个文件添加好后右下角就会出现这个当然一般都会出现了否则可能你需要重新的删除在创建因为基本没有其他办法来构建maven因为功能也并非都会提供了
之后的选择基本选择第一个即可当然还是需要看具体情况最好翻译一下
然后你可以选择在其内容加上
packagingwar/packaging一般这个时候由于刷新了那么他基本就出现了对应的显示M
某种程度上点击右下角后把这里出现的这个勾勾去掉也行 而maven的创建操作我们就不处理了因为我们操作过很多次了
至此对应的五个方式空项目操作java程序空项目到web空项目到maven传统webmaven我们操作完毕其中
maven操作web我们就不处理了因为我们可以只设置打包方式即可比较简单
但是为了看看前面的问题所以我们还是需要进行处理的首先创建webapp文件
如下
如果前面再pom.xml中没有写上maven而是testma那么项目名称后面一般会有[testma] 依赖如下 dependenciesdependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion3.1.0/versionscopeprovided/scope/dependency/dependencies即对应的配置与前面的一样我们看看maven与传统web的区别是什么
我们经过测试
当操作方式为如下时会出现不同的结果配置tomcat时会出现的
/*
操作方式是
maven:war explodedF:\maven\target\maven-1.0-SNAPSHOT\
maven:warF:\Program Files\apache-tomcat-8.5.50\webapps\maven\
xianmu:Web exploded之前的xianmu这个项目F:\xianmu\out\artifacts\xianmu_Web_exploded\
对应与下面如图以后看到这个不要以为是后面没有数据而是在下一行了
*/如图 至于他们里面的配置你可以选择的再引入或者添加时点击来自谁即可也可以手动处理一般exploded的基本相同可能部分不同但是好像也并不影响注意即可在选项中点击号一般就会知道了
所以maven:war exploded和xianmu:Web exploded由于对应的类型的对象基本是一样的是临时的我们可以测试看看
首先进入maven:war时对应的临时处理看看其配置文件的结果
Context path/maven docBaseF:\maven\target\maven-1.0-SNAPSHOT.war /F:\Program Files\apache-tomcat-8.5.50\webapps\maven\ //项目下面maven一般是项目名称的进入maven:war exploded看看其结果
Context path/maven docBaseF:\maven\target\maven-1.0-SNAPSHOT /F:\maven\target\maven-1.0-SNAPSHOT\进入之前测试的xianmu:Web exploded看看其结果
Context path/xianmu docBaseF:\xianmu\out\artifacts\xianmu_Web_exploded /F:\xianmu\out\artifacts\xianmu_Web_exploded\很明显带有exploded的结果与配置文件一致而没有的则是操作本来的tomcat并且放在里面为什么这就需要一些隐藏的配置处理了在明显的指定war时那么他的操作就会自动在本来的tomcat中处理而不是临时的处理目录这也可以说是默认的处理具体情况还是看tomcat的源码了可能是因为需要这样的情况才会弄出来吧
如果没有值呢是空值呢如对应的代码
String realPath servletRequest.getServletContext().getRealPath(/);上面中的/不加而是变成
String realPath servletRequest.getServletContext().getRealPath();结果如何
经过测试默认的结果还是加上/“的结果一样所以默认加上”/“并且经过测试所以如果是getRealPath(“a”);或者getRealPath(”/a);他们的结果都是/a这种情况得到的结果就是在路径后面加上a所以这个a我们最好写成一下比较好的目录比如文件文件相关的目录比如uploads所以这里参数的意思也就是加上参数得到的整体路径的意思了所以大多数为了得到完整的路径一般都会显示的操作/即可
当然为了在后面解决很多的疑问或者说以前的疑问我决定统一的将web的mvc相关对于路径的说法以及传统web关于路径的说法进行处理
一般在web中关于路径的说明存在多种一般主要是四种比如转发重定向xml的路径注解的路径等等
然而这些的说明在以前也说明了这里就不多说比如可以到67章博客看看若有遗漏也可以选择到50章博客补充如果还有那么可以自己进行测试一般加上50章博客的话是没有遗漏的
所以关于getRealPath路径的区别我们说明完毕回到之前的操作
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;Controller
RequestMapping(/demo)
public class FileController {RequestMapping(upload)public String upload(MultipartFile uploadFile, HttpServletRequest request) throws IOException {// 文件原名如xxx.jpgString originalFilename uploadFile.getOriginalFilename();// 获取文件的扩展名如jpgString extendName originalFilename.substring(originalFilename.lastIndexOf(.) 1, originalFilename.length());String uuid UUID.randomUUID().toString();// 新的文件名字大多数服务器基本都是如此这是保证文件不重复String newName uuid . extendName;//String getRealPath(String path)返回包含给定虚拟路径的实际路径的字符串//这里的参数是/那么代表就是当前项目的地址假如你的项目名称是test那么就算test的绝对路径String realPath request.getSession().getServletContext().getRealPath(/);System.out.println(realPath);// 后面解决文件夹存放文件数量限制所以按日期存放文件名称主要是UUID因为不同的操作系统对一个目录里面的文件数量的多少是有限的一般情况下存在如下的限制网上搜索到的/*FAT32FAT32 文件系统常见于旧的 Windows 版本对每个目录有文件数目的限制通常约为 65534 个文件NTFSNTFS 文件系统常见于现代 Windows 版本支持更多的文件数目通常数以百万计ext4在类Unix/Linux系统上ext4 文件系统支持大量的文件通常在数百万到数十亿文件之间具体取决于文件系统的配置HFS苹果的HFS文件系统支持大量文件数以百万计APFS苹果的APFS文件系统支持更多文件也在数百万到数十亿之间上面的这些数字代表一个目录中可以存放的文件数量而正是由于有这些限制所以一般情况下我们的文件会放在不同目录中而按照日期的存放就非常好的即存放一天中产生的文件数量*/String datePath new SimpleDateFormat(yyyy-MM-dd).format(new Date());//对应的项目路径加上日期的总体名称那么就是一个在项目里面的文件夹了File floder new File(realPath / datePath);if (!floder.exists()) { //判断是否存在如果存在那么就不用创建该目录了floder.mkdirs();}//使用他的api来操作用一个新的File接收对应的File组成一个路径然后加上对应的基本不重复的文件名称最终目录和文件都存在了//并且将uploadFile对应的文件信息他存在字节的前面我们处理过了比如byte[] bytes file.getBytes();给这个新的文件或者说完成复制//一个保存文件信息的基本必然是保存了字节所以其他的我们要保存文件进行封装时一般也与这里一样保存字节的uploadFile.transferTo(new File(floder, newName));return success;}
}
执行后我们上传一个文件或者说给一个文件然后看看当前项目是否出现对应的文件或者目录然而并没有为什么就是因为由于其操作的是不带有exploded的所以到原本的tomcat中里面生成了然而一般在idea中可能并不会显示因为他可能是只会显示在起始的目录如target里面也可能还要里面或者其他的隐藏F:\Program Files\apache-tomcat-8.5.50\webapps\maven\这里了解即可
但是上面的操作中由于没有到当前项目的路径那么他是不是有问题的实际上一般情况下这只是不同系统中的tomcat的处理而已而当我们发版也就是部署到服务器提供给用户使用时时一般在linux中而这个系统下一般都是当前项目所在即指向的是当前项目里面类似于前面的F:\Program Files\apache-tomcat-8.5.50\webapps\maven\所以我们这个代码是没有问题的只是环境不同而已这个时候如果是没有exploded的就会到当前项目中实际上发版的也就是这个所以一般的我们并不考虑在开发中的路径处理因为最后一定是没有exploded的即路径在生产中是基本对的开发代表在idea中操作写代码生产代表已经发版当然idea中也可以考虑只是需要一些设置而已如路径的处理以及tomcat选择的处理这些可以百度查看这里就不说明了
至此对处理multipart形式的数据的一些补充我们补充完毕
实际上对于UUID来说也是可能出现重复因为他存在如下的情况
/*
UUID通用唯一标识符通常由以下元素组成
时间戳UUID 的一部分通常包含与其生成时间相关的信息这有助于确保 UUID 在大多数情况下是唯一的不同的 UUID 版本可以包含不同的时间戳信息
时钟序列UUID 可能包含一个时钟序列以防止在同一时刻生成多个相同的 UUID
节点标识符在某些情况下UUID 可能包含节点标识符用于标识生成 UUID 的计算机或设备
版本号UUID 包含一个版本号指示生成 UUID 的算法或标准
变体号UUID 包含一个变体号表示 UUID 结构的变体
随机或伪随机位UUID 包含一些随机或伪随机位以提高唯一性
不同的 UUID 版本具有不同的组成方式但它们都旨在生成全局唯一标识符最常见的 UUID 版本是基于时间的版本例如UUIDv1 和UUIDv2和随机版本例如UUIDv4不同的应用程序和系统可以根据需求选择适当的 UUID 版本UUID的常见表示形式是一个由32个十六进制字符组成的字符串例如550e8400-e29b-41d4-a716-446655440000
总之UUID由多个组成部分组成这些部分的结构和含义取决于UUID的版本和标准不过UUID的主要目标是在全球范围内确保唯一性
UUID通用唯一标识符的设计目的是确保在理论上生成的UUID在全球范围内是唯一的UUID标准规定了生成UUID的方法通常结合时间戳、节点标识符、时钟序列和随机数等元素来创建UUID这些因素的组合使得生成相同UUID的机会非常低以至于可以被认为是可以忽略不计的
然而要理解的是由于UUID的唯一性是基于生成UUID的算法和环境的所以在极少数情况下可能会出现重复的UUID这种情况通常出现在以下情况下
UUID生成算法不按标准实现某些自定义或不符合标准规范的UUID生成算法可能会导致UUID的重复
生成UUID的设备或系统出现问题如果设备或系统在生成UUID时发生错误或故障也可能导致UUID的重复
非标准UUID版本某些应用程序或系统可能会实现自定义的UUID版本这些版本可能不遵循标准规范从而导致重复的UUID
特别的如果是专门依靠操作时间的UUID那么修改系统时间容易重复就算不是也会提高重复的可能性所以我们会认为修改系统时间会使得UUID重复的说法并不是错误的
尽管存在极少数情况下可能出现UUID重复的可能性但在大多数情况下UUID仍然是可靠的全局唯一标识符适用于各种应用程序和系统特别是需要唯一性标识的情况如数据库记录、分布式系统中的节点标识等如果需要更高级别的唯一性保证可以考虑结合其他标识方法如数据库自增主键或全局唯一的命名空间标识符Namespace Identifier
*/在控制器中处理异常
在前面我们知道有HandlerExceptionResolver这样的组件是来处理异常的我们自然也会围绕这个来处理一般来说对应注解的识别就是由他来处理的
在controller包下创建GlobalExceptionResolver类
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;// 可以让我们优雅的捕获所有Controller对象handler⽅法抛出的异常
//ControllerAdvice
Controller
public class GlobalExceptionResolver {//mvc的异常处理机制异常处理器//当报错后会自动找到这个注解的方法然后将错误信息给exception这个参数变量//这个里面的值一般代表了我们需要创建什么异常对象将错误信息给这个对象然后赋值给下面的参数变量//这里就可能有疑问了为什么不自动识别类型来创建对象呢因为怕他是接口保证多态的ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView new ModelAndView();modelAndView.addObject(msg, exception.getMessage());modelAndView.setViewName(error);return modelAndView;}RequestMapping(err)public String ix(String a, Integer b) throws IOException {System.out.println(1);int i 1 / 0;return err;}
}
对应的jsp
% page contentTypetext/html;charsetUTF-8 languagejava %
html
headtitleTitle/title
/head
body
错误是${msg}
/body
/html
一般情况下我们访问err具体访问自己应该知道了一般来说异常不会经过第二个拦截这是因为出现异常自然不会考虑数据或者视图的问题所以一般就会规定不会经过第二次拦截只会依靠固有的数据或者视图进行处理异常即
modelAndView.addObject(msg, exception.getMessage());
modelAndView.setViewName(error);所以注释这个
// ExceptionHandler(ArithmeticException.class)自然第二个拦截没有且第三个拦截打印出对应的信息我们写上的所以他的作用只是在出现异常时进一步的处理视图操作否则按照默认的异常视图进行处理你可以在浏览器看看注释后的界面就知道了
一般情况下写在当前controller中的对应的异常处理只会对自身进行生效并且如果存在多个比如 ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView new ModelAndView();modelAndView.addObject(msg, exception.getMessage());modelAndView.setViewName(error);return modelAndView;}ExceptionHandler(ArithmeticException.class)public ModelAndView handleException1(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView new ModelAndView();modelAndView.addObject(msg, exception.getMessage()22);modelAndView.setViewName(error);return modelAndView;}那么会报错由上所述我们应该需要统一的异常处理所以我们需要一个全局的处理那么我们再次的创建一个类
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletResponse;Controller
public class Global {ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView new ModelAndView();modelAndView.addObject(msg, exception.getMessage());modelAndView.setViewName(error);return modelAndView;}
}
然后把GlobalExceptionResolver类中的异常处理都进行删除然后启动看看是否在这个类里面处理了异常发现并没有说明的确对应的异常只能操作自身的controller的那么怎么将这个异常处理变成全局呢我们操作如下
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletResponse;ControllerAdvice
Controller
public class Global {ExceptionHandler(ArithmeticException.class)public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {ModelAndView modelAndView new ModelAndView();modelAndView.addObject(msg, exception.getMessage());modelAndView.setViewName(error);return modelAndView;}
}
现在我们继续执行会发现他处理异常了也就是说ControllerAdvice可以使得这个类里面或者说这个controller里面的异常的那个处理变成全局的处理当然并不必须需要他是controller写上也没有关系首先controller的主要作用是定位如果不作为全局异常的话两个都可以单独写上进行处理而ControllerAdvice也是算一个定位只不过他是异常的定位而已当然你写上controller也没有关系可以在不处理异常时作为controller使用也行而在处理异常时ControllerAdvice可以作为controller使用的虽然他并不是所以这个时候可以不加对应的controller照样的可以进行操作因为ExceptionHandler只有一个也只能有一个否则报错
那么如果存在多个全局呢谁先使用答看下图 经过大量的测试发现由于windows中A和a是一样的所以可以得到当字母的数量或者说文件名长度相等时按照Ascii来决定越小那么就越优先上面没有测试数字实际上数字也是的由于1a那么Gloaa1优先于Gloaaa否则的话长度越长越优先
当然上面的说明并不重要因为全局的一个就够了
基于Flash属性的跨重定向请求数据传递
在前面我们知道一个组件FlashMapManager一般就用于这里
重定向时请求参数会丢失我们往往需要重新携带请求参数我们可以进行⼿动参数拼接如下
我们创建一个类
package com.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;Controller
public class Redirect {RequestMapping(/re)public String re() {return redirect:ree?name 1;
// return redirect://ree?name 1; 并不到http这是一个区别虽然传统重定向会但是这里是经过项目或者说框架的处理的
// return redirect:/ree?name 1; 当前和到项目的区别并不到端口一个区别}RequestMapping(/ree)public String ree(String name) {System.out.println(name);System.out.println(1);return ree;}
}
先访问re看看结果吧
上述拼接参数的方法属于get请求携带参数⻓度有限制参数安全性也不⾼因为在url上此时我们可以使⽤SpringMVC提供的flash属性机制向上下⽂中添加flash属性框架会在session中记录该属性值当 跳转到⻚⾯之后框架会⾃动删除flash属性不需要我们⼿动删除通过这种方式进行重定向参数传递 参数⻓度和安全性都得到了保障如下
继续在上面的类中加上方法 RequestMapping(/reee)public String reee(RedirectAttributes redirectAttributes,HttpSession session) {// addFlashAttribute⽅法设置了一个flash类型属性该属性会被暂存到session中注意他不是session的存储的说明只是在这个会话的其他区域而已所以后面的name为null// 但它不会永久保留这个属性的存储时间很短暂通常只会在两次请求之间有效// 所以也可以说在跳转到⻚⾯之后该属性销毁虽然是两次请求之间并且他的销毁在于重定向参数赋值或者说处理后进行的//也就是说下面的reeee方法的name2的结果就是nullredirectAttributes.addFlashAttribute(name, 22);Object name session.getAttribute(name);System.out.println(name); //null只是在session中的对应区域而不是session的专门区域虽然都在session中但是是不同的区域的这里需要注意实际上session只是一个范围而flash通常在这个范围里面所以并不能说明flash就一定在session中只是基本在里面而已因为你可以修改源码而进行处理虽然大多数并不会这样做且没有意义return redirect:reeee?name 1;}RequestMapping(/reeee)//ModelAttribute可以获取存在model数据的存在类似于jsp的${varName}方式具体可以在第52章博客中知道
//但是如果你并没有处理这些数据那么他自然按照原始的处理也就是String na的处理相当于不加这个注解其实他默认处理了但是由于这个注解的存在导致操作覆盖了public String reeee(String name, HttpSession session, ModelAttribute(name) String na, ModelAndView modelAndView) {System.out.println(modelAndView);System.out.println(na); //22得到了String name2 (String) session.getAttribute(name);System.out.println(name2); //nullSystem.out.println(44444444);System.out.println(name);Object name1 session.getAttribute(name);System.out.println(name1);return ree;}执行后测试一下吧但是为什么flash英文是显示的意思类型属性只会在两次请求之间有效解释如下
/*Flash类型属性通常是在Web开发中用于在一次HTTP请求中传递数据到下一次HTTP请求的一种机制这种属性只在两次请求之间有效的原因涉及到Web应用程序的工作原理和HTTP协议的无状态性
HTTP协议是一种无状态协议每个HTTP请求都是独立的服务器不会在不同请求之间保存客户端的状态信息这意味着每个HTTP请求都是相互独立的服务器不会自动记住之前请求的信息Flash属性的作用就是在这种无状态协议下将数据从一个请求传递到下一个请求实现一种有状态的体验
Flash属性的工作原理通常如下
1在第一次HTTP请求中服务器设置Flash属性将数据存储在它里面如前面的redirectAttributes.addFlashAttribute(name, 22);这个时候数据还是在第一次请求中的这是存放的位置是专门用来进行后面操作的位置或者对象的
2服务器将Flash属性专门的数据中的数据发送给客户端作为响应
3客户端收到响应后可以从Flash属性中提取数据并在下一次HTTP请求中发送回服务器也可以认为是专门给下一个请求的也放在一个专门的区域当给下一个请求后或者赋值后自动清空这个区域的对应数据并且也并不是地址的操作只是数据而已
4在第二次HTTP请求中服务器可以读取Flash属性中的数据前端给的在http协议中是可以选择的进行处理一般来说前端给后端是放在请求信息中的而后端给前端是放在响应信息的完成数据的传递这个时候在特定时候前面说明了进行清空对应在服务端的Flash属性中的数据我们需要注意的是虽然他们存在响应信息或者请求信息中但是一般浏览器并不会让我们查看到对应的请求信息或者响应信息除了一些其key-value或者字符串外基本上一些二进制数据不会显示这里可以参照前面的文件上传的那个请求体当然响应体一般可以看到但是中间是否省略了什么就不确定了
反正不管怎么样前端和后端在按照合理的方式不会让我们用户看到Flash属性的数据并且满足二次请求销毁的成果
当然如果技术够高可以选择破解浏览器来获取这个数据所以尽管可能有安全措施来保护你比如浏览器自身但是网络中的绝对安全性是不存在的但采取适当的安全措施可以显著提高您的个人信息和数据的安全性这也强调了个人隐私和安全的重要性以及采取适当的措施来保护自己的信息
*/至此解释完毕现在我们来⼿写 MVC 框架上面虽然说明了很多知识但是基本只有手写出来我们才能知道更加深层次的东西现在开始手写MVC框架
回顾SpringMVC执行的⼤致原理后续根据这个模仿⼿写自己的mvc框架 spring的维护是必须的因为他最终保存的对象数据内容是用来判断映射的难道是凭空保存或者直接保存吗总需要一个总地方吧spring的地方
具体的说明就是前端控制器进行初始化的配置而扫描交给spring来处理springmvc有spring的然后根据前端控制器初始化的信息在一个servlet中是可以操作同一个请求头和响应头的或者说请求信息和响应信息来决定调用谁也就是映射后面进行一系列的处理如jsp的响应最终得到我们的结果具体说明看后面就知道了我们手写一个类似的即可
手写MVC框架之注解开发也可以存在对应的xml开发只是对与mvc来说通常是需要注解的单纯的xml并不好处理具体可以百度一般mybatisspringspringmvc基本都是需要xml和注解一起的反正那个方便使用那个
通常来说注解的性能会比xml慢点通常指启动的时候有时候运行时也会但是方便许多特别的是考虑到注解的扫描当包非常多时有些不需要的可能也会扫描到
在手写之前我们创建一个项目 web.xml中是
!--
?xml version1.0 encodingUTF-8?
--
!--
大多数情况下?xml version1.0 encodingUTF-8?的操作是可以选择不写的但是大多数关于xml解析的代码或多或少都会处理这个虽然大多数默认都是这样的处理也就是?xml version1.0 encodingUTF-8?只是我们还是建议写上那么这里操作了xml自然最终也会操作xml解析如果可以你也能自己写一个并且由于是服务器的处理其中自然有内部的关于web.xml的读取方式现在先这样写上后面会补充
--
web-app/web-app现在开始编写首先我们创建com.mvc.framework包然后再该包下创建DispatcherServlet类
上面是需要服务器的包的自然需要引入也就是 dependenciesdependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion3.1.0/versionscopeprovided/scope !--开发时操作可以验证前面的tomcat有且自动给出对应的依赖--/dependency/dependencies然后我们创建这个类包名自行创建可以看下面的这个package com.mvc.framework;来创建
package com.mvc.framework;import javax.servlet.http.HttpServlet;//唯一的一个servlet
public class DispatcherServlet extends HttpServlet {
}
然后修改web.xml补充服务器的读取方式这是固定的除非你修改服务器也就是tomcat的处理servlet当然由于框架是建立在这个上面的所以自然是保留的
?xml version1.0 encodingUTF-8?
web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaeexmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsdversion4.0display-namecreate/display-nameservletservlet-namemvc/servlet-nameservlet-classcom.mvc.framework.DispatcherServlet/servlet-class/servletservlet-mappingservlet-namemvc/servlet-nameurl-pattern/*/url-pattern/servlet-mapping
/web-app
!--写上后这个/*就会操作这个DispatcherServlet的servlet的处理--这个超级熟悉了吧但是我们需要明白这里是操作原生的servlet的我们mvc是建立在他之上的具体到50章博客学习我们在操作映射之前首先需要得到对应的对象来操作方法从而操作映射所以我们需要定义一些注解或者说完成Spring相关的操作
现在我们在framework包下创建servlet包将DispatcherServlet移动到这个包里面然后再在framework包下创建annotations包然后在该包下创建几个注解
package com.mvc.framework.annotations;import java.lang.annotation.*;Documented
Target(ElementType.TYPE)
Retention(RetentionPolicy.RUNTIME)
public interface Controller {String value() default ;
}
package com.mvc.framework.annotations;import java.lang.annotation.*;Documented
Target(ElementType.TYPE)
Retention(RetentionPolicy.RUNTIME)
public interface Service {String value() default ;
}
package com.mvc.framework.annotations;import java.lang.annotation.*;Documented
Target({ElementType.TYPE,ElementType.METHOD})
Retention(RetentionPolicy.RUNTIME)
public interface RequestMapping {String value() default ;
}
package com.mvc.framework.annotations;import java.lang.annotation.*;Documented
Target(ElementType.FIELD )
Retention(RetentionPolicy.RUNTIME)
public interface Autowired {String value() default ;
}
很明显我们需要操作Controller到Service中进行处理的注解我们定义好了现在我们开始进行开发
为了操作到Service的注解的处理我们需要在framework包下创建service包再创建如下的两个类或者接口
package com.mvc.framework.service;public interface DemoService {String get(String name);
}
package com.mvc.framework.service.impl;import com.mvc.framework.service.DemoService;public class DemoServiceImpl implements DemoService {Overridepublic String get(String name) {System.out.println(打印name);return name;}
}
回到DispatcherServlet在这里自然是需要进行包的扫描的并且这个扫描需要知道往那里进行扫描这里考虑在mvc相关xml中进行处理然后Spring管理扫描后的结果最后进行映射的处理现在首先是处理扫描
补充DispatcherServlet
package com.mvc.framework.servlet;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;//唯一的一个servlet
public class DispatcherServlet extends HttpServlet {//可以选择加载配置文件而得到一些相关的信息来给后面的内容进行数据的判断比如注解中只能是post等等而判断是否报错Overridepublic void init(ServletConfig config) throws ServletException {//加载配置文件//扫描相关的类有时候mvc的保存实例的对象通常与单纯的spring的实例对象这里是说明的ioc容器对象而不是单独的一个里面的对象因为有时候mvc的保存实例的对象中有保存二字是不同一样虽然他们可能存在上下级在67章博客可能有具体的说明//但是寻常来说这种上下的级别一般也是代码层面的如果可以自行编写时可以选择忽略这种情况只是比较麻烦这是由于mvc在处理时//一般来说无论这些解释说的在怎么的花里胡哨底层的原因只是是否可以访问到对方而已一般来说mvc比较慢并且他可能是一个新的实例//所以mvc在操作spring时可能会拿到他已经操作好的实例对象就如在一个方法中后面的变量得到访问变量的值但是前面的变量不能访问后面的而可以访问自然可以完成赋值即mvc可以注入spring的对象反过来就不行但是我们也可以手动的进行给spring进行注册//这就会涉及到底层代码之间的修改因为他们的问题是顺序和访问权限不能直接访问对象只能得到并且设置的也是在对应中才能操作受访问权限影响的问题受框架自身的影响所以只能是底层代码之间的修改了//但是也要明白由于容器是同一个所以如果不考虑过程中的赋值的话其实他们并没有上下级别只是既然使用了spring的话那么我们自然会在容器处理后直接的操作这是为什么spring和mvc虽然是同一个容器但是存在上下级的原因//比如说现在spring会操作两个对象假设是abioc容器我们假设为iocmvc也存在对象我们认为是c//所以手写spring将ab加入到ioc中那么ioc有ab了这个时候像一些注入会到ioc中的信息//这个时候并没有c所以spring不能访问c处理了这些操作后mvc才会将c给ioc这个时候mvc在处理这些时可以得到ab因为ioc有这个所以上下级关系只是顺序问题但是这个上下级别的关系也只是体现在初始化的处理中最终他们都是在ioc中如果可以的话当你拿取ioc容器那么就可以得到这些信息这个在某种情况下由于顺序问题的我们会称为父子容器并在这种考虑下我们会认为他们是不同容器虽然本质是同一个但是单纯以数据来说的话他们是不同的所以如果一些博客说明是不同的那么大多数是建立在数据不同而不是容器本身}//处理请求的地方一般情况下mvc中get和post是操作同一个方法而该方法是统一进行处理的为了方便这样的处理我们就在get中调用post即可虽然之前根据请求方式来决定调用谁这样处理就会使得无论是何种方式都是同一个方法由于参数互通自然可以选择得到请求方式来判断与注解的是否一致而不是看方法名使得他的请求方式就会变这自然是不合常理的//原来mvc中是public class DispatcherServlet extends FrameworkServlet {/*他里面存在protected final void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.processRequest(request, response); //都是这个方法}protected final void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.processRequest(request, response);}*///重写一下HttpServlet对应的两个方法Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req,resp);}Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}
由于这个是主要的操作所以相关的代码就应该写在对应的doGet或者doPost的具体细节在前面可能有些许的说明但是还是需要看这里怎么处理
我们继续补充或者修改可以将get也到post因为post可以包含get的处理一般来说mvc也是如此但是他一般是get操作getpost操作post但是是同一个方法与HttpServlet相关具体是操作了service只是在中间可能会操作注解前提是设置了判断是否是对应的请求方式而进行报错的比如进入到了get那么在里面判断注解是否是get相关可以选择再次的得到请求而进行补充判断 Overridepublic void init(ServletConfig config) throws ServletException {//加载配置文件//扫描相关的类//初始化bean对象ioc容器//实现依赖注入//构造一个HandlerMapping处理器映射器来完成映射关系//映射关系创建好那么根据请求查看映射来决定调用谁}在资源文件夹下补充mvc的配置文件mvc.xml
beans
component-scan base-packagecom.mvc.framework/
/beans在web.xml中加上如下
?xml version1.0 encodingUTF-8?
web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaeexmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsdversion4.0display-namecreate/display-nameservletservlet-namemvc/servlet-nameservlet-classcom.mvc.framework.servlet.DispatcherServlet/servlet-classinit-paramparam-namecontextConfigLocation/param-name!--编写方式受我们源码的处理由于是服务器的处理那么这里我们就这样写
当然由于只是值这里也可以写成任何只要你可以得到路径的地址即可比如甚至可以写绝对路径这都看你如何处理了到时候如何处理和使用什么方式来操作都需要自行处理的当然大多数我们并不会记住关于读取文件或者说读取项目或者找当前项目路径的相关api这个时候可以选择网上查找这并不难以找到
--param-valuemvc.xml/param-value!--直接这样写可能会报错这是可能idea判断的问题认为你要加上classpath:即classpath:mvc.xml实际上这里可以随便写数值所以忽略即可idea的提示也并不是万能的--/init-param/servletservlet-mappingservlet-namemvc/servlet-nameurl-pattern/*/url-pattern/servlet-mapping
/web-app回到后端初始化在这里写上如下
package com.mvc.framework.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;//唯一的一个servlet
public class DispatcherServlet extends HttpServlet {Overridepublic void init(ServletConfig config) throws ServletException {//加载配置文件//手写获取对应的指定的值如果说mvc中这个名称不可改变那么说明这里是写死的//这个是读取web.xml的虽然web.xml是固定的但是里面的数据是mvc进行处理的比如这里//因为我们需要时刻记住mvc是建立在servlet之上的而不是单独的框架所以需要servlet否则mvc是自然处理不了的String contextConfigLocation config.getInitParameter(contextConfigLocation);//得到之后需要根据这个来确定路径使得加载配置文件String s doLoadconfig(contextConfigLocation);//扫描相关的类doScan(s);//初始化bean对象ioc容器也就是根据扫描得到的全限定名创建对象并保存doInstance();//实现依赖注入//维护和处理操作依赖注入关系doAutoWired();//上面三个在上一章博客中或者说108章博客中就有操作当然这里可以选择操作一样的也可以不一样只需要实现即可虽然基本存在多种方式但是一般是108章那一种还有spring的那一种三级缓存//这里我们还是选择考虑108章博客的处理//构造一个HandlerMapping处理器映射器来完成映射关系initHandlerMapping();//当然在上一章博客时对应的map是一个全局的所以都是同一个容器的也说明了是同一个只是顺序问题而已System.out.println(初始化完成...等待请求与映射匹配了);//映射关系创建好那么根据请求查看映射来决定调用谁}//构造一个HandlerMapping处理器映射器private void initHandlerMapping() {}//实现依赖注入private void doAutoWired() {}//初始化类private void doInstance() {}//扫描类或者扫描到注解参数是在那里扫描private void doScan(String path) {}//加载配置文件得到信息这里比较简单只需要得到地址即可private String doLoadconfig(String contextConfigLocation) {return ;}Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
}
实际上在上面使用静态块也可以操作只是没有对应的请求数据所以我们使用初始化一般情况下我们需要xml相关依赖来读取对应的xml信息
那么我们加上如下的依赖 dependencygroupIddom4j/groupIdartifactIddom4j/artifactIdversion1.6.1/version/dependencydependencygroupIdjaxen/groupIdartifactIdjaxen/artifactIdversion1.1.6/version/dependency在真正编写之前我们需要补充一些代码在framework包下创建controller包然后在里面创建如下的类
package com.mvc.framework.controller;import com.mvc.framework.annotations.Autowired;
import com.mvc.framework.annotations.Controller;
import com.mvc.framework.annotations.RequestMapping;
import com.mvc.framework.service.DemoService;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;Controller
RequestMapping(/demo)
public class DemoController {Autowiredprivate DemoService demoService;RequestMapping(/query)public String query(HttpServletRequest request, HttpServletResponse response, String name) {String s demoService.get(name);return s;}}
然后在DispatcherServlet中添加如下的代码
package com.mvc.framework.servlet;import com.mvc.framework.annotations.Autowired;
import com.mvc.framework.annotations.Controller;
import com.mvc.framework.annotations.RequestMapping;
import com.mvc.framework.annotations.Service;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;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.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;//唯一的一个servlet
//WebServlet(name 1, urlPatterns /a)
public class DispatcherServlet extends HttpServlet {//这里也可以选择不加静态具体看你自己//存放每个类对应的全限定名这里其实可以不用静态的因为他只是在这里处理而已而几乎不会给其他对象或者说类来使用最后统一操作他来创建对象如果是同一个容器那么这个应该不是同一个他只是记录当前扫描的也就是说对应的容器应该是更加上层的当然//这里是以mvc为住所以对应的容器或者说map就放在这里了private static ListString classNames new ArrayList();//需要一个bean的存放ioc容器private static MapString, Object map new HashMap();//缓存已经进行过依赖注入的信息private static ListString fieldsAlreayProcessed new ArrayList();//url和method的映射关系private static MapString, Method handlerMapping new HashMap();Overridepublic void init(ServletConfig config) {//加载配置文件//手写获取对应的指定的值如果说mvc中这个名称不可改变那么说明这里是写死的//这个是读取web.xml的虽然web.xml是固定的但是里面的数据是mvc进行处理的比如这里//因为我们需要时刻记住mvc是建立在servlet之上的而不是单独的框架所以需要servlet否则mvc是自然处理不了的String contextConfigLocation config.getInitParameter(contextConfigLocation);//得到之后需要根据这个来确定路径使得加载配置文件String s doLoadconfig(contextConfigLocation);//扫描相关的类doScan(s);//初始化bean对象ioc容器也就是根据扫描得到的全限定名创建对象并保存doInstance();//实现依赖注入//维护和处理操作依赖注入关系doAutoWired();//上面三个在上一章博客中或者说108章博客中就有操作当然这里可以选择操作一样的也可以不一样只需要实现即可虽然基本存在多种方式但是一般是108章那一种还有spring的那一种三级缓存//构造一个HandlerMapping处理器映射器来完成映射关系initHandlerMapping();//当然在上一章博客时对应的map是一个全局的所以都是同一个容器的也说明了是同一个只是顺序问题而已System.out.println(初始化完成...等待请求与映射匹配了);//映射关系创建好那么根据请求查看映射来决定调用谁}//方法的位置也建议从上到下这里却反过来了当然这只是模拟mvc框架的操作并不会有很大影响以后注意即可//构造一个HandlerMapping处理器映射器private void initHandlerMapping() {//这里基本上是比spring的依赖操作更加的有难度在前一章博客中我们几乎学习过spring的源码所以后面的依赖操作我们几乎可以明白//但是这里还是第一次所以需要多次的理解虽然对只看前一章来说也是第一次但是对比这里的第一次难度还是较小的这里的难道比较大//因为依赖的处理主要是死循环的解决在这里的我们判断当前类全限定名加上变量的总名称判断是否赋值过即可赋值过那么不赋值直接退出然后根据递归自然都会处理完毕//递归在中途也会顺便的解决过程中的对象这样也会使得后续更加的方便当然那基本也是类似三级缓存的处理都是找到对应的类型赋值后结束//其中spring在三级缓存拿取结束而这里也是在对应的map拿取结束只是spring的是当时创建然后赋值后删除的所以并不需要考虑是否可以赋值的问题赋值就结束了也不会到里面去的因为当时创建他们必然只需要赋值即可而是直接的结束//而spring没有所以需要考虑是否可以赋值即判断名称来结束//上面的看看就行这是说明为什么spring是较小难道的原因因为只是一个循环依赖的问题的处理其实也正是他所以是较小难度否则是没有难度的//现在我们开始操作//最关键的环节//将url和method建立关联//这里首先需要大致吃透前面的mvc的知识的细节并且我们也需要观看27章博客的最后一部分这样才基本可以明白甚至更加的可以看懂//当然了我也会写上注释的让你更加的看得懂//首先我们需要思路也就是我们现在已经有了对于的对象实例了我们自然可以通过该实例得到里面的方法上面的相应注解//然后根据当前类实例的上面的注解进行拼接路径得到需要的url的路径拦截然后我们可以创建map保存拦截路径和方法的对应的映射关系//这样在真正操作请求时判断请求路径与路径拦截而进行执行哪个方法具体思路是这样的但是实现需要看后面的处理//当然这些是初始化操作所以前面说明组件时第一步是调用处理器映射器而不是创建因为已经初始化了if (map.isEmpty()) { //如果map为空自然直接的退出return;}for (Map.EntryString, Object entry : map.entrySet()) {//获取这个实例的ClassClass? aClass entry.getValue().getClass();//如果包含这个注解那么操作他否则不会只有这个注解才操作路径当然我们一般建议先操作不存在时直接的结束当前循环//这样其实更加的好维护防止后续的代码执行不对这是一个写法当然只要合理都可只是建议而已//所以我们以后需要这样的规范即所有需要结束的判断写在前面正确的处理写在后面尽量将可以操作结束的判断比如这里的不存在提取出来写在前面//因为这样就可以很明显的知道一个方法里面需要满足什么条件而不是需要去在后面翻找正常情况下对应后面代码也是较好处理的所以不考虑代码无效定义的说明只考虑找到的问题这里由于比较少的代码所以我们并不是特别在意//但是在以后开发大代码时建议这样哦在任何的作用域都希望如此if (aClass.isAnnotationPresent(Controller.class)) {String baseUrl ;//看看有没有对应的路径的注解if (aClass.isAnnotationPresent(RequestMapping.class)) {//拿取对应的值String value aClass.getAnnotation(RequestMapping.class).value();//按照对应已经写好的DemoController那么这里的value相当于拿到了RequestMapping(/demo)中的/demo这个字符串值baseUrl value; //拿取前缀}//当然如果上面的没有拿取前缀自然还是//现在我们拿取方法//输入itar一般会出现下面的相当于快捷键吧具体可能是自定义的且根据的上面的第一个数组来处理的Method[] methods aClass.getMethods();for (int j 0; j methods.length; j) {Method method methods[j];//大多数的Class操作的基本都可以操作isAnnotationPresent以及getAnnotationif (method.isAnnotationPresent(RequestMapping.class)) {RequestMapping annotation method.getAnnotation(RequestMapping.class);String value annotation.value(); //按照之前写好的这里相当于拿到了/query//定义临时url因为前面的baseUrl可能存在值而他需要为所有的方法进行拼接的所以不能给他进行赋值String url baseUrl;url value;//操作url和method的映射关系关系使用map保存起来handlerMapping.put(url, method);//你会发现并能没有难度难在哪里实际上难在后面最终到的doPost请求方法的处理//我们到后面去看看吧}}}}}//实现依赖注入private void doAutoWired() {if (map.isEmpty()) { //如果map为空自然直接的退出return;}// 遍历map中所有对象查看对象中的字段是否有Autowired注解如果有需要操作依赖注入关系for (Map.EntryString, Object entry : map.entrySet()) {try {//将对象作为参数传递doObjectDependancy(entry.getValue());} catch (Exception e) {e.printStackTrace();}}}//开始操作依赖注入关系传递实例对象private static void doObjectDependancy(Object object) {//Field[] getDeclaredFields()用于获取此Class对象所表示类中所有成员变量信息Field[] declaredFields object.getClass().getDeclaredFields();//没有成员那么退出if (declaredFields null || declaredFields.length 0) {return;}for (int i 0; i declaredFields.length; i) {//拿取第一个成员变量的信息如成员变量是public int i 0;那么得到public int com.she.factory.bb.i//其中值不会操作在结构中我们并不能操作其值最多只能通过这个结构去设置创建的对象的值Field declaredField declaredFields[i];//判断是否存在对应的注解如果不存在那么结束当前循环而不是结束循环看看下一个成员变量if (!declaredField.isAnnotationPresent(Autowired.class)) {continue;}//判断当前字段是否处理过如果已经处理过则continue避免嵌套处理死循环这里我们的实现是与spring是不同的//在spring中我们创建一个类的实例时会顺便在注入时判断对方是否存在而创建对方的实例是否可以创建实例而不是直接创建我们基本都处理了判断的如配置或者注解从而考虑循环依赖//但是这里我们首先统一创建实例了然后在得到对应的实例后继续看看该实例里面是否也存在对应的注解以此类推直到都进行设置//但是如果存在对应的实例中操作时是自身或者已经操作的实例这个自身代表的是自己而不是其他类的自己类的类型那么就会出现死循环如果我中有你你中有我且你已经操作的我不退出这不是死循环是什么所以就会直接的退出退出当前循环即continue;//这里与Spring是极为不同的Spring是报错而这里是不会赋值即退出使得不会出现死循环虽然我们也可以使得报错即也就不会报错了所以这里不会出现循环依赖的问题因为我们对应的类就已经创建好了就与你的三级缓存一样虽然三级缓存是在需要的时候也保存了对应的实例使得赋值只是我首先统一操作的那么我这里的缓存与三级缓存的本质还是一样的都是保存实例只是这里是保存id因为实例都是创建好的就不需要三级缓存将实例移动了//但是由于我们是统一处理的而不是与spring一样所以在一定程度上需要更多的空间如果项目非常的大那么这可能是一个不好的情况要不然为什么spring是使用时创建呢而由于这里是测试所以我们使用这种方式就不用考虑三级缓存的处理了//boolean contains(Object o)判断是否包含指定对象//判断当前类的全限定名加上该变量名称组成的字符串是否存在//如果存在说明已经注入了if (fieldsAlreayProcessed.contains(object.getClass().getName() . declaredField.getName())) {continue;}//这里也会操作技巧一般情况下我们都会判断是否可行才会操作真正的代码而不是先操作真正的代码然后处理是否可行所以上面的结束循环先处理//当然我们基本都会意识到这样的问题的这里只是提醒一下Object dependObject null;Autowired annotation declaredField.getAnnotation(Autowired.class);String value annotation.value();if (.equals(value.trim())) { //清空两边空格防止你加上空格来混淆//先按照声明的是接口去获取如果获取不到再按照首字母小写//拿取对应变量类型的全限定名若是基本类型那么就是本身如int就是int//然而int基本操作的全限定名中不可能作为类使用所以在int中处理该注解是没有意义的在spring中基本也是如此除非是以后的版本可能会有dependObject map.get(declaredField.getType().getName());//如果没有获取那么根据当前变量类型的首字母小写去获取上面是默认处理接口的//然而大多数按照规范的话基本都有接口但是是按照规范防止没有规范的所以使用下面的处理if (dependObject null) {//getType是得到了Field的ClassdependObject map.get(lowerFirst(declaredField.getType().getSimpleName()));}} else {//如果是指定了名称那么我们选择操作拼接全限定名来处理dependObject map.get(valuedeclaredField.getType().getName());}//正好如果不匹配的话为null那么防止递归出现问题所以操作跳过//在spring中是操作报错的而导致不能运行程序这里我们跳过顺便设置null吧虽然在成员变量中默认的对象基本都是null//上面正好对应之前操作的接口和类的操作当然在Spring中是查询全部//来找到对应类型的实例getBean的这里我们是自定义的自然不会相同//一般来说Class的getName是全限定名而其他的就是对应的类名称//而Class的getSimpleName则是类名称其他的并没有getSimpleName方法// 记录下给哪个对象的哪个属性设置过避免死循环递归的死循环fieldsAlreayProcessed.add(object.getClass().getName() . declaredField.getName());//递归if (dependObject ! null) {doObjectDependancy(dependObject);}//全部设置好后我们进行设置//设置可以访问private变量的变量值在jdk8之前可能不用设置//但是之后包括jdk8不能直接的访问私有属性了可能随着时间的推移也会改变因为需要进行设置这个所以不能直接访问私有属性了declaredField.setAccessible(true);//给对应的对象的该成员变量设置这个值try {declaredField.set(object, dependObject);} catch (Exception e) {e.printStackTrace();}}}//初始化类private void doInstance() {//如果没有全限定名说明对应的指定扫描的地方不存在任何的文件处理if (classNames.size() 0) return;if (classNames.size() 0) return; //其实就算一个程序中其可能不存在负数但是加个意外的条件还是比较好的所以上面的代码可以注释掉虽然他也没有错因为也基本不会出现负数try {for (int i 0; i classNames.size(); i) {//拿取对应的全限定名称String className classNames.get(i);// 通过对应的全限定名称拿取其Class对象Class? aClass Class.forName(className);//接下来判断Controller和Service的注解的区别一般情况下mvc的这里通常只会判断Controller以及依赖注入的情况而不会处理Service//其实这也是顺序出现的底层原因但是由于这里是统一处理的所以这里的ioc容器是mvc和spring共有的这里需要注意一下//判断对应的类上是否存在对应的注解if (aClass.isAnnotationPresent(Controller.class)) {//既然存在对应的注解那么进入//Controller一般并不操作value所以不考虑//获取类名称String simpleName aClass.getSimpleName();String s lowerFirst(simpleName);//创建实例实际上这个实例由于是根据对应的Class他也有具体的类来得到或者全限定名所以创建的实例相当于我们在main中操作创建实际上在main创建也是需要导入或者当前目录下完成//所以就是合理的Object o aClass.newInstance();map.put(s, o);}//这里就有一个想法好像使用if-else也是可行的的确他就是可行的但是这样的好处是在中间可以继续处理并且也可以自定义结束方案//而不是利用if-else中最后的处理才进行如果比灵活性那么多个if就是好的如果比稳定那么if-else是好的但是在代码逻辑非常正确的情况下那么多个if就是好的//也就是多个if上限高下限低当然多个if由于下限低所以在某些情况下可能并不好处理比如存在两个判断但是后面都需要他们的数据而两个if一般基本只能重复了比如这里的依赖注入的方法即指定名称并不友好这里if (aClass.isAnnotationPresent(Service.class)) {String beanName aClass.getAnnotation(Service.class).value();//创建实例Object o aClass.newInstance();int ju 0;if (.equals(beanName.trim())) {//如进入这里那么说明对应注解我们没有进行设置value那么默认是我们注解设置的默认的操作首字母小写//Class的getSimpleName方法是获取类名称beanName lowerFirst(aClass.getSimpleName());}else {ju1;}//放入map因为idbeanName有了对应的实例也有了自然放入//id作为类名称首字母小写或者指定的类名称id//然而指定名称并不友好因为可能存在多个不同的类是相同的名称所以这个名称需要与全限定名进行拼接也就是上面的beanName aClass.getName();这里操作两个if并不友好因为需要前面的数据但是这个全限定名可能也是对应的接口而不是类到这里你是否理解了为什么spring在如果容器中存在多个相同的实例时会报错了吧因为判断这个的话非常麻烦所以spring只能存在一个实例当然也可以存在多个只是他需要对应一些信息比如也操作这样的名称或者变量名称等等且他是利用遍历来赋值的这样就非常简单了当然如果你考虑了所有情况那么在获取时自然比spring快只是存在没有必要的空间所以互有好处spring节省空间但是获取时需要性能而这里需要空间但是获取时性能更快if(ju1){UtilGetClassInterfaces.getkeyClass(beanName,aClass,map,o);}else{map.put(beanName, o);}//当然你也可以这样由于我们的类通常有接口在controller中通常没有接口所以不考虑他所以在一定程度上是可以给接口id的虽然spring并没有这样的操作但并不意味着我们不能//操作如下//Class?[] getInterfaces()获取实现的所有接口Class?[] interfaces aClass.getInterfaces();if (interfaces ! null interfaces.length 0) {for (int j 0; j interfaces.length; j) {//如果你实现的是java.io.Serializable接口那么打印这个anInterface时结果是interface java.io.Serializable//如果接口是java.io.Serializable接口那么对应的getName就是java.io.SerializableClass? anInterface interfaces[j];// 以接口的全限定类名作为id放入如果想要名称那么可以通过其他api具体可以百度一般getSimpleName好像可以这里我们继续创建一个吧在map中我们通常是不会指向同一个的map.put(anInterface.getName(), aClass.newInstance());//为什么全限定名是对应的整个路径呢解释如下//全限定名这个术语的名称来自于它的作用它提供了完整的、唯一的类名标识以避免命名冲突全表示完整性限定表示唯一性因此全限定名是一个完整且唯一的标识//也的确对应的全限定名比如com.mvc.framework.servlet.DispatcherServlet在项目中也的确是完整且唯一的要不然也不会作为生成Class对象的参数}}}}} catch (Exception e) {e.printStackTrace();}}//将str首字母进行小写private static String lowerFirst(String str) {char[] chars str.toCharArray();if (A chars[0] chars[0] Z) {chars[0] 32; //在ASCII中a是97A是65相差32}return String.valueOf(chars); //将字符变成String}//扫描类或者扫描到注解参数是在那里扫描private void doScan(String scanPackage) {try {//获取对应包所在的绝对路径String scanPackagePath Thread.currentThread().getContextClassLoader().getResource().getPath() scanPackage.replaceAll(\\., /);scanPackagePath URLDecoder.decode(scanPackagePath, StandardCharsets.UTF_8.toString());File pack new File(scanPackagePath); //参数可以是以/开头的File[] files pack.listFiles();for (File file : files) {//如果是一个目录那么为了保证是下一个目录所以我们以当前路径加上这个目录名称if (file.isDirectory()) {//这样可以使得继续处理知道找到里面的文件doScan(scanPackage . file.getName());//对应的目录处理完了那么应该要下一个文件或者目录了continue;}//找到是一个文件且是class那么进行处理if (file.getName().endsWith(.class)) {//当前的包名加上文件名就是全限定名String className scanPackage . file.getName().replaceAll(.class, );classNames.add(className); //保存好}}} catch (Exception e) {e.printStackTrace();}}//加载配置文件得到信息这里比较简单只需要得到地址即可private String doLoadconfig(String contextConfigLocation) {//加载xmlInputStream resourceAsStream DispatcherServlet.class.getClassLoader().getResourceAsStream(contextConfigLocation);//使用读取xml的依赖来进行处理//获取XML解析对象SAXReader saxReader new SAXReader();try {//解析XML获取文档对象documentDocument document saxReader.read(resourceAsStream);//getRootElement()获得根元素Element rootElement document.getRootElement();//直接找到这个标签得到他的信息对象但是其父类只能操作标签自身信息所以这里需要强转Element element (Element) rootElement.selectSingleNode(//component-scan);//获得对应属性的值String attribute element.attributeValue(base-package);return attribute;} catch (Exception e) {e.printStackTrace();}return ;}Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req, resp);}Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//我们几乎保存了method和url的映射所以开始进行处理//获取url//String getRequestURI()返回此请求的资源路径信息//如果请求是http://localhost:8080/springmvc_xie/demo/querySystem.out.println(请求资源的路径为 req.getRequestURI()); //请求资源的路径为/springmvc_xie/demo/query//StringBuffer getRequestURL()返回此请求的完整路径信息System.out.println(请求资源的完整路径为 req.getRequestURL()); //请求资源的完整路径为http://localhost:8080/springmvc_xie/demo/query//一般来说完整的路径的前面的信息对访问资源来说通常并不需要所以我们获取这个路径即可String requestURI req.getRequestURI();//获取一个反射的方法Method method handlerMapping.get(requestURI);//不对劲我们发现他存在项目名称因为总路径是/springmvc_xie/demo/query//这个在发版到服务器时基本也是如此虽然在本地我们也可以去掉但是这是名称是可能或者发版时必然存在的//这个我们就需要进行解决}
}
上面的代码在保存实例时保存了与spring完全不一样的模式是保存相关的任何处理
在给出之前先看看如下
package com.mvc.framework.servlet;public class a extends b implements f {public static void main(String[] args) {getkeyClass(a.class);}private static void getkeyClass(Class a) {Class superclass a.getSuperclass();if (java.lang.Object.equals(superclass.getName())) {System.out.println(a.getName());} else {System.out.println(a.getName());getkeyClass(superclass);}Class[] interfaces a.getInterfaces();for (Class anInterface : interfaces) {getkeyInterface(anInterface);}}private static void getkeyInterface(Class a) {Class[] interfaces1 a.getInterfaces();if (interfaces1.length 0) {System.out.println(a.getName());return;}System.out.println(a.getName());Class aClass interfaces1[0];getkeyInterface(aClass);}
}
package com.mvc.framework.servlet;public class b implements c{
}
package com.mvc.framework.servlet;public interface c extends d{
}
package com.mvc.framework.servlet;public interface d {
}
package com.mvc.framework.servlet;public interface f extends g{
}
package com.mvc.framework.servlet;public interface g {
}
对应执行的结果是拿取其所有父类情况的全限定名这个时候我们看这里
//如果是指定了名称那么我们选择操作拼接全限定名来处理
dependObject map.get(valuedeclaredField.getType().getName());
他是根据对应的类型来的而由于多态那么可能存在非常多的类型这也是我们这里的处理方式而不是spring的遍历或者指定名称注意在spring中指定多个相同实例名称时也会报错在id哪里你也会或多或少的知道什么
那么在保存时应该是在这里
在这之前我们首先在framework包下创建util包然后在里面创UtilGetClassInterfaces类加上如下
package com.mvc.framework.util;import java.util.Map;public class UtilGetClassInterfaces {public static void getkeyClass(String beanName, Class a, Map map, Object o) {Class superclass a.getSuperclass();if (java.lang.Object.equals(superclass.getName())) {map.put(beanName a.getName(), o);} else {map.put(beanName a.getName(), o);getkeyClass(beanName, superclass, map, o);}Class[] interfaces a.getInterfaces();for (Class anInterface : interfaces) {getkeyInterface(beanName, anInterface, map, o);}}private static void getkeyInterface(String beanName, Class a, Map map, Object o) {Class[] interfaces1 a.getInterfaces();if (interfaces1.length 0) {map.put(beanName a.getName(), o);return;}map.put(beanName a.getName(), o);Class aClass interfaces1[0];getkeyInterface(beanName, aClass, map, o);}}
在对应的代码中可以看到 if(ju1){UtilGetClassInterfaces.getkeyClass(beanName,aClass,map,o);}else{map.put(beanName, o);}这里就是解决的办法但是如果这种办法是好的为什么spring不使用呢其实我们也可以看到他们是各有利弊的但是spring并没有隐患或者隐患很小而这种有隐患取决于对应保存的相同的对象非常多那么出现操作相同对象时也会出现共享的问题当然这些问题并不大因为实例通常只是提供方法操作而已所以这也算是解决的办法
上面的代码需要仔细看看当然如果出现问题后面也会给出的通过上面的说明可以发现在doPost方法中出现问题了其中就是路径的问题一般我们可以这样的解决
//一般来说完整的路径的前面的信息对访问资源来说通常并不需要所以我们获取这个路径即可String requestURI req.getRequestURI();String contextPath req.getContextPath();/*/springmvc_xie*/System.out.println(项目名称 contextPath); // /springmvc_xieString substring requestURI.substring(contextPath.length(), requestURI.length());System.out.println(拿取的路径 substring); // /demo/query//获取一个反射的方法Method method handlerMapping.get(substring);当然一般来说对应的/demo/query中可能存在就算你不写/demo只是写demo的情况只需要判断开头的情况因为其他情况会默认看成路径的而在mvc中通常是默认加上的所以我们可以修改initHandlerMapping方法
在这之前我们需要考虑一件事你可能也从来没有考虑过也就是说不在方法上加上RequestMapping注解只在类上加上并且单纯的访问类上的路径那么他会访问到这个方法吗答并不会一般会报错在mvc中就是如此而这里我们可以看到如果类上的注解有那么继续往下走只有存在对应的注解方法上的才会进行保存映射然而这里我们还没有进行处理是否报错的问题因为我们还没有写上这里我们后面考虑先考虑/的情况 String value aClass.getAnnotation(RequestMapping.class).value();//判断是否存在不存在加上否则不做处理if (/.equals(value.substring(0, 1)) false) {//默认加上/value / value;}String value annotation.value(); //按照之前写好的这里相当于拿到了/query//同样的也会操作默认加上/if (/.equals(value.substring(0, 1)) false) {//默认加上/value / value;}这两个地方写上来完成默认加上/的处理
针对上面的考虑的一件事的问题其实最终的得到的值是null因为只有存在对应的注解方法上的才会进行保存映射而map在没有对应的key时返回值的value自然就是null考虑null是否报错即可后面会给出的
通过上面我们得到了一个Method我们先看一个案例
package com.mvc.framework.servlet;import java.lang.reflect.Method;public class a {public void fa(String a, Integer b) {System.out.println(1 a b);}public void fb() {System.out.println(1);}public static void main(String[] args) {try {Classa aClass a.class;a a aClass.newInstance();Method[] methods aClass.getMethods();for (int i 0; i methods.length; i) {System.out.println(methods[i].getName());//里面有很多方法遍历所有而我们写的mvc是指定保存的所以可以不用获取Method里面的参数也是得到的Method一定对应的因为是直接获取而不是我们选择的获取即直接后面的处理}Method fa aClass.getMethod(fb);fa.invoke(a);Method fb aClass.getMethod(fa, String.class, Integer.class);fb.invoke(a, 2, 1);} catch (Exception e) {e.printStackTrace();}}
}//其实到这里我们可以知道反射就是可以在运行时拿取其结构信息的只要你想几乎都可以拿到并且可以根据结构信息来实现方法或者赋值前提是需要指定存在的对象而不能单纯的操作但是如果我有这些对象为什么不直接处理方法呢但是也要明白如果这些方法没有呢并且如果他是私有的不能直接改变的呢这个时候就需要反射了我们可以发现要执行invoke的处理来调用类方法我们需要一个类对象也就是当前的对应类对象对应与mvc中的DemoController类对象并且有时候也需要参数所以我们需要思考这里由于map中几乎只能保存一个信息那么这里很明显我们需要创建一个实体类
我们在framework包下创建pojo包然后创建Handler类
package com.mvc.framework.pojo;import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;public class Handler {//保存的对应的类对象private Object controller;//保存对应的Method的对象用来调用private Method method;//操作正则表达式的其存在方法Pattern.matches(regex, this);左边是正则表达式右边this是调用者看后面的main即可private Pattern pattern; //这个具体的作用和赋值在后面会知道了private MapString, Integer paramIndexMapping; //参数顺序key是参数名value是代表第几个参数//这个具体如何使用和操作获取看后面的处理//对应于query(HttpServletRequest request, HttpServletResponse response, String name)//那么map中保存的keynamevalue2这里我们规定下标从0开始public Handler(Object controller, Method method, Pattern pattern) {this.controller controller;this.method method;this.pattern pattern;//这个不需要手动的处理所以参数三个即可this.paramIndexMapping new HashMap();}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;}public Pattern getPattern() {return pattern;}public void setPattern(Pattern pattern) {this.pattern pattern;}public MapString, Integer getParamIndexMapping() {return paramIndexMapping;}public void setParamIndexMapping(MapString, Integer paramIndexMapping) {this.paramIndexMapping paramIndexMapping;}
}
现在我们修改initHandlerMapping方法的内容我这里将注释去掉了自己对应一下
前提我们将之前创建的private static MapString, Method handlerMapping new HashMap();注释掉因为不使用这个了而是使用这个
private ListHandler handlerMapping new ArrayList();
//很明显这里是mvc最开始说明的处理器映射器而调用的处理器适配器则是准备调用对应方法的操作我们由于是模拟所以可处理或者不处理在后面处理时我会进行对比前面的流程的还有一个前提我们需要知道这个
package com.mvc.framework.servlet;import java.util.regex.Matcher;
import java.util.regex.Pattern;public class a {public static void main(String[] args) {String a [0-9]{3};String b 122;/*public final class Stringimplements java.io.Serializable, ComparableString, CharSequence {*/CharSequence aa b;System.out.println(b.matches(a));/*public boolean matches(String regex) {return Pattern.matches(regex, this);}//return Pattern.matches(regex, this);public static boolean matches(String regex, CharSequence input) {Pattern p Pattern.compile(regex);Matcher m p.matcher(input);return m.matches();}//在这个包下package java.util.regex;public final class Patternimplements java.io.Serializable
{
}*///通过上面的处理我们可以分开成这样Pattern pattern Pattern.compile([0-9]{3});//import java.util.regex.Matcher;Matcher m pattern.matcher(122);boolean matches m.matches();System.out.println(matches);//通过源码说明Pattern.compile([0-9]{3});只是一个赋值作用并没有进行识别//而识别的处理在pattern.matcher(122);中看看该参数是否符合赋值后的正则表达式System.out.println(pattern); //[0-9]{3}}
}//正则表达式其实本质是的对照了如果其正则独有的表达并没有是一个没有进行特殊的操作那么
/*
Pattern pattern Pattern.compile(/demo/query);System.out.println(pattern);Matcher m pattern.matcher(/demo/query);boolean matches m.matches();System.out.println(matches);返回true也就是匹配/demo/query也的确匹配/demo/query也就是说除了他的规则处理外其他的基本就是直接的对比
*/现在开始修改
if (method.isAnnotationPresent(RequestMapping.class)) {RequestMapping annotation method.getAnnotation(RequestMapping.class);String value annotation.value(); //按照之前写好的这里相当于拿到了/query//同样的也会操作默认加上/if (/.equals(value.substring(0, 1)) false) {//默认加上/value / value;}//定义临时url因为前面的baseUrl可能存在值而他需要为所有的方法进行拼接的所以不能给他进行赋值String url baseUrl;url value;//开始封装路径和方法信息参数Pattern.compile(url)直接保存对应的正则表达式赋值后的对象Handler handler new Handler(entry.getValue(),method, Pattern.compile(url));//处理参数位置信息//比如query(HttpServletRequest request, HttpServletResponse response, String name)//获取该方法的参数列表信息自然包括名称或者类型或者位置从左到右起始下标为0Parameter[] parameters method.getParameters();for (int i 0; i parameters.length; i) {Parameter parameter parameters[i];//parameter.getType()得到参数类型if(parameter.getType()HttpServletRequest.class||parameter.getType()HttpServletResponse.class){//如果是这两个建议名称就是他们这样就能保证赋值是对应的而不会被其他参数名称所影响具体保存名称干什么在后面就会知道的handler.getParamIndexMapping().put(parameter.getType().getSimpleName(),i);}else{//其他的类型保存其名称handler.getParamIndexMapping().put(parameter.getName(),i);}}//保存映射关系handlerMapping.add(handler);//你会发现并能没有难度难在哪里实际上难在后面最终到的doPost请求方法的处理//我们到后面去看看吧}自己对应一下吧然后我们修改doPost方法
到这里我们可以知道框架的作用了他是定义编写代码方式以及方便代码编写而中间件是在编写好的代码基础上的一个补充或者增强
由于单纯的代码都写在doPost方法里面比较麻烦所以我们定义一个方法然而我也只是定义了一个小的方法看如下就知道了
在之前我们需要知道这个
package com.mvc.framework.servlet;import java.lang.reflect.Method;public class a {public void fa(String a, Integer b, int j) {}public static void main(String[] args) throws Exception {Classa aClass a.class;Method fa aClass.getMethod(fa, String.class, Integer.class, int.class);Class?[] parameterTypes fa.getParameterTypes();for (int i 0; i parameterTypes.length; i) {Class? parameterType parameterTypes[i];System.out.println(parameterType.getSimpleName());}/*StringIntegerint*/}
}
Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {Handler handler getHandler(req);if (handler null) {//如果没有匹配上考虑返回404resp.getWriter().write(404 not found);return;}//参数绑定//先获取所有的参数类型因为对应的map只能保存其固定的两个值//如名称和位置所以我们这里给出类型吧就不额外创建类了Class?[] parameterTypes handler.getMethod().getParameterTypes(); //之前操作名称的下标就是为了这里也顺便为了map本身所以大多数情况下如果需要保存位置建议从0开始//因为大多数的集合或者数组操作都是从0开始的除非是手写的当然系统提供的基本都是从0开始以后不确定从而进行对应//创建一个数组用来保存参数Object[] objects new Object[parameterTypes.length];//保存已经操作的位置int[] ii new int[parameterTypes.length];//给数组参数值并操作顺序先拿取参数集合这里我们只是模拟就不考虑文件的处理了所以这里一般我们认为考虑键值对的参数传递MapString, String[] parameterMap req.getParameterMap(); //一个键是可以对应多个值的SetMap.EntryString, String[] entries parameterMap.entrySet();//遍历所以从请求中拿取的参数for (Map.EntryString, String[] param : entries) {String value ;//如果是多个的话那么合并吧比如name1name2那么这里的结果就是1,2这里考虑在mvc中如果存在多个相同的那么会处理合并for (int i 0; i param.getValue().length; i) {if (i param.getValue().length - 1) {value param.getValue()[i];continue;}value param.getValue()[i] ,;}//上面的就考虑了value处理合并当然如果没有自然不会处理因为value param.getValue()[i];//现在我们拿取了传递的参数我们考虑将参数进行存放//但是考虑到类型是否正确以及名称是否对应所以需要判断存放在objects中//这两个考虑我们都有存放信息也就是位置名称类型//这里就要利用到我们之前保存的handler.getParamIndexMapping()了//getKey是键值对的键自然对应与参数列表中的名称所以这里的判断即可//当然有些是不看名称的但是我们只用名称定义了位置所以每次设置完值建议用数组保存对应的已经操作的位置从而来判断其他位置因为类型在外面定义if (!handler.getParamIndexMapping().containsKey(param.getKey())) {//如果不包含那么结束当前循环那么考虑其他的参数了那么对应的数组中的值自然就是null了//在67章博客中我们说明了类型的变化其实就在这里可以思考一下就行continue;}//拿取位置前提是对应的名称对应才能拿取Integer integer handler.getParamIndexMapping().get(param.getKey());//如果对应了那么看看对应的类型是否可行并且由于我们保存了位置且大多数获取列表的操作都是从0开始且下标为0那么正好对应与类型//进行赋值在赋值之前需要考虑一个问题我们需要看看他的类型是什么来进行其他的处理if (String.equals(parameterTypes[integer].getSimpleName())) {objects[integer] value;}//默认情况下前端传给后端的键值对基本都是字符串if (Integer.equals(parameterTypes[integer].getSimpleName()) || int.equals(parameterTypes[integer].getSimpleName())) {//在mvc中Integer在考虑多个时只会拿取第一个而int与Integer可以互相转换所以也可以是同一个代码value value.split(,)[0];Integer i null;try {i Integer.parseInt(value);} catch (Exception e) {e.printStackTrace();throw new RuntimeException(String转换Integet报错参数名称是 param.getKey());}objects[integer] i;}//其他的类型我们就不判断了因为只是模拟而已//保存位置ii[integer] 1;}//当然有些是不看名称的但是我们只用名称定义了位置所以我们需要所以我们需要考虑如下//用来先考虑默认赋值的而不是先处理请求参数默认赋值的一般不会考虑名称是否一致看前面的规定的名称就知道了他一般只是保证不会影响其他名称自然的我们也不可能直接写出我们只需要判断类型即可//这里我们就需要前面的保存的位置来排除了//当然由于有些名称是固定的所以并不需要对应的数组直接给出得到位置即可否则的话需要循环找位置Integer integer handler.getParamIndexMapping().get(HttpServletRequest.class.getSimpleName());objects[integer] req;ii[integer] 1;integer handler.getParamIndexMapping().get(HttpServletResponse.class.getSimpleName());objects[integer] resp;ii[integer] 1;//解决其他的默认赋值的并且没有保存名称的也就是没有保存对应的位置的//当然也可以不这样做只是有些默认的赋值null会报错这也是mvc的处理方式那么删除这个for循环和不删除就算处理与不处理的区别了后面可以选择测试一下for (int i 0; i ii.length; i) {if (ii[i] 0) {if (int.equals(parameterTypes[i].getSimpleName())) {//用来解决int在mvc中会报错的情况objects[i] 0;}//后面还可以定义double等等其他的默认值反正可以在这里处理默认值赋值为null会报错的情况}}//调用对应的方法invoke后面的参数是可变长参数所以可以操作数组//如果没有赋值那么对应的下标的值自然是null自然也就操作了默认的赋值但是如果对应的是int那么自然会报错这在mvc中是如此//但是这里我们处理了即/*if(int.equals(parameterTypes[i].getSimpleName())){//用来解决int在mvc中会报错的情况objects[i] 0;}*/try {handler.getMethod().invoke(handler.getController(), objects); //一般这里由处理器适配器来完成的得到的结果通常用于视图解析器这里我们没有写这个所以忽略了这里只是调用一下所以称为处理器适配器也行吧} catch (Exception e) {e.printStackTrace();}//至此我们总算执行了对应的方法了当然在mvc中是存在非常多的自带的组件这里我们只是模拟一部分而已//当然这里的两个请求HttpServletRequest req, HttpServletResponse resp是在这里得到的所以对应的mvc得到的也是这个//我们也在前面说明了这个得到了如果没有看这里即可}private Handler getHandler(HttpServletRequest req) {if (handlerMapping.isEmpty()) {return null;}String requestURI req.getRequestURI();String contextPath req.getContextPath();String substring requestURI.substring(contextPath.length(), requestURI.length());for (Handler handler : handlerMapping) {//在这里你可能会有以为好像我们保存url也行为什么非要操作Pattern呢//这是因为正则存在更多的操作而不只是路径所以加上可以进行更好的扩展//具体扩展可以考虑到在RequestMapping注解中加上正则表达式//从而匹配请求路径Matcher matcher handler.getPattern().matcher(substring);//如果不匹配那么找下一个是否匹配否则直接返回匹配的信息if (!matcher.matches()) {continue;}return handler;}//如果没有自然返回nullreturn null;}上面解决了mvc中int的默认赋值null的错误但是我们也可以发现他需要循环mvc使用报错来解决也并不是不行节省一点性能当然这点性能也并不大所以可以认为是mvc的一个疏忽然而这些疏忽并不是很影响具体开发所以mvc就不操作改变了
至此我们可以选择来测试一下
我们回到这里
package com.mvc.framework.controller;import com.mvc.framework.annotations.Autowired;
import com.mvc.framework.annotations.Controller;
import com.mvc.framework.annotations.RequestMapping;
import com.mvc.framework.service.DemoService;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;Controller
RequestMapping(/demo)
public class DemoController {Autowiredprivate DemoService demoService;RequestMapping(/query)public String query(HttpServletRequest request, HttpServletResponse response, String name) {String s demoService.get(name);return s;}}
修改一下
RequestMapping(/query)public String query(HttpServletRequest request, HttpServletResponse response, String name) {System.out.println(request);System.out.println(response); //就算是反射过来的参数的指向地址还是与寻常的参数传递是一样的所以是共享的String s demoService.get(name);return s;}然后修改这个
package com.mvc.framework.service.impl;import com.mvc.framework.annotations.Service;
import com.mvc.framework.service.DemoService;Service //这里加上
public class DemoServiceImpl implements DemoService {Overridepublic String get(String name) {System.out.println(打印name);return name;}
}
然后启动服务器访问这个http://localhost:8080/springmvc_xie/demo/query?name1234其中springmvc_xie是项目名称
访问后你会发现打印的值是null为什么通过我的调试对应的这个地方得到的名称不是参数名称 if (parameter.getType() HttpServletRequest.class || parameter.getType() HttpServletResponse.class) {//如果是这两个建议名称就是他们这样就能保证赋值是对应的而不会被其他参数名称所影响具体保存名称干什么在后面就会知道的handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), i);} else{//其他的类型保存其名称handler.getParamIndexMapping().put(parameter.getName(), i);//这里得到的不是name而是arg2我们看如下}经过上面的问题我们来创建一个类
package com.mvc.framework.servlet;import java.lang.reflect.Method;
import java.lang.reflect.Parameter;public class a {public void fa(String nam2, String name1) {}public void fb(String nam2, String name1) {}public static void main(String[] args) {Classa aClass a.class;Method[] fa aClass.getMethods();for (int i 0; i fa.length; i) {Method method fa[i];System.out.println(method.getName());Parameter[] parameters method.getParameters();for (int ii 0; ii parameters.length; ii) {Parameter parameter parameters[ii];System.out.println(parameter.getName());}}//打印的结果给出部分/*mainarg0fbarg0.arg1faarg0.arg1获取的方法是反过来获取的并且参数也是固定的模式当然参数列表是从左到右的*/}
}
为什么会这样这一般需要看版本一般在jdk8开始就这样处理了当然这样的还有私有的也就是设置可以访问private变量的变量值在jdk8之前可能不用设置但是之后包括jdk8不能直接的访问私有属性了可能随着时间的推移也会改变因为需要进行设置这个所以不能直接访问私有属性了
为什么要这样处理这是为了提高字节码的紧凑性和安全性参数名称在编译后被抹去主要有以下原因
隐私和安全性在字节码中包含参数名称可能泄漏程序的细节信息可能会被滥用这对于某些应用来说是不可接受的因为它可以暴露敏感信息
字节码紧凑性字节码文件通常被用于分发和执行如果包含了大量参数名称将会增加文件大小在资源受限的环境中较小的字节码文件可以提高性能和减小存储需求
字节码的紧凑型还比较合理但是为什么字节码会影响安全举例
反编译和逆向工程如果参数名称包含在字节码中恶意用户或攻击者可以更容易地反编译和逆向工程你的应用程序以获取关于应用程序的内部结构和逻辑的信息这可以帮助他们发现潜在的漏洞或弱点甚至滥用应用程序
敏感信息泄露在某些情况下方法的参数名称可能包含敏感信息如果这些名称泄露到字节码中可能会暴露敏感数据、业务逻辑或应用程序内部细节从而引发安全风险
安全性问题披露如果方法参数名称包含有关应用程序内部的信息攻击者可能会更容易发现应用程序的潜在漏洞从而加大应用程序受攻击的风险
当然spring是解决的或者说mvc是解决的这里我们称为spring因为最终是使用spring的主要内容的但是他是通过其他方式所以并不会暴露然而对方也可以通过spring来获取该参数所以也只是避免了方便的获取那么在不使用spring时他既然会出现arg0这样的名称那么有没有操作让他不这样做其实是有的只需要加上这个操作
-parameters编译选项一般情况他是操作编译的那么应该是如此的javac -parameters YourClass.java然而我们是操作项目不可能这样处理应该需要配置来使得项目默认这样处理这里我们就以maven为例
在maven中的pom.xml中操作如下
!--与dependencies同级别
--
buildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.8.0/version !-- 版本号根据实际情况调整 --configurationsource11/source !-- Java 版本根据你的项目调整 --target11/target !-- Java 版本根据你的项目调整 --compilerArgumentsparameters / !-- 这里是主要的操作 --/compilerArguments/configuration/plugin/plugins/build执行后可以发现他获得了对应的参数名称我们试一下修改版本将上面的11修改成1.8执行后可以发现也获得了参数名称了当然也并不是说任何版本都可以这样可能有些版本这样的操作是不行的现在还不确定看以后并且随着jdk的变化并不是说对方不会对1.8或者11继续修改可能1.8或者11也会不行但是我们也可以发现在之前操作mvc中我们并没有处理这个也就是说明了spring并不是使用这个方式那么他是使用什么方式呢
spring是使用字节码分析库如 ASM来动态分析类文件而不依赖于编译器生成的字节码这种技术使 Spring 能够获取参数名称因为他是直接分析文件的也就可以说他内部的代码是相当于跳过了操作了-parameters选项而访问并处理字节码文件实际上再怎么处理字节码文件中都会保留原始参数名称否则的话也不可能使用该名称的变量了他只是不让你直接获取即可那么如果说选项中他-parameters是解决某一个开关导致的不让获取那么spring是直接从文件找也自然就跳过了这个开关
那么就明白了也就是直接的分析字节码文件好吧这对现在的我们来说几乎不可能手动写好因为需要学习太多知识并且细节非常多基本不是我们个人能够完成的所以我们拿取spring提供给我们的这个操作代码但是引入spring一般我们并不能找到对应的类所以我们选择使用第三方的代码思考为什么main的参数需要是对应的args数组一般他代表命令行参数这取决于对应的main是程序的入口一般我们可以在命令行中执行java时传递比如我们有一个java文件
public class ceshi {public static void main(String[] args){for (String arg : args) {System.out.println(arg);}}
}
在命令行中在目录下执行javac ceshi.java然后执行java ceshi发现什么都没有但是如果是这样的执行java ceshi 1 2 3那么会打印1和2和3具体自行测试也就是说他存在这样的作用
命令行参数的传递命令行参数是用户在终端或命令提示符中输入的它们以字符串的形式传递给程序因此main函数需要一个字符串数组来接收这些参数以便程序能够解析和使用它们
灵活性通过使用命令行参数程序可以根据用户提供的输入进行不同的操作参数的个数和内容可以根据需要进行调整从而增加程序的灵活性
标准化采用参数数组的方式使得命令行参数的处理在不同的编程语言和操作系统中更加一致和标准化这有助于开发人员编写跨平台的代码
现在我们修改1.8回到11看你自己可以选择修改开始使用spring的方式这里我们选择拿取第三方的代码这里我们使用ASMspring也是使用类似的或者相同的先在framework包下创建包config然后在该包下我们创建几个类在spring中有类似的这样的处理这里我们手动写一个这几个类在后面会给出
在手写之前我们使用上面的配置启动运行一下然后操作mvc看看结果如果出现了打印的值不是null而是1234那么我们可以说初步完成这个时候你可以选择给Service操作value然后Autowired对应在spring中我们并没有这样的处理这里我们是这样处理的经过测试也打印了1234也可以测试将String name变成int name如果打印为0说明也操作成功我们的方式而不是Spring的mvc的报错然后我们将路径中比如/demo的开头的/去掉访问可以发现也可以那么说明我们的操作代码编写完毕当然可能有些地方有问题但是暂时没有且功能都正确如果出现了请自行修改一下吧然后我们使用spring对应的方式来解决
首先我们需要引入依赖
dependencygroupIdorg.ow2.asm/groupIdartifactIdasm/artifactIdversion9.2/version !-- 根据需要的版本号调整 --/dependency然后去掉这个依赖配置
buildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.8.0/version !-- 版本号根据实际情况调整 --configurationsource1.8/source !-- Java 版本根据你的项目调整 --target1.8/target !-- Java 版本根据你的项目调整 --compilerArgumentsparameters //compilerArguments/configuration/plugin/plugins/build执行服务器先看看是否得到结果很明显没有得到并且加上这个类来测试时也是没有得到参数名称这个类是之前的
package com.mvc.framework.servlet;import java.lang.reflect.Method;
import java.lang.reflect.Parameter;public class a {public void fa(String nam2, String name1) {}public void fb(String nam2, String name1) {}public static void main(String[] args) {Classa aClass a.class;Method[] fa aClass.getMethods();for (int i 0; i fa.length; i) {Method method fa[i];System.out.println(method.getName());Parameter[] parameters method.getParameters();for (int ii 0; ii parameters.length; ii) {Parameter parameter parameters[ii];System.out.println(parameter.getName());}}}
}
这个时候我们使用上面的依赖来解决不加这个配置的问题
首先我们在framework包下找到前面我们创建的config包没有现在创建也行然后创建下面两个前面的几个类
package com.mvc.framework.config;import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;import java.util.HashMap;
import java.util.Map;public class ParameterNameVisitor extends ClassVisitor {//方法名称private String methodName;//参数名集合private Map map;//给出方法名称public ParameterNameVisitor(String methodName) {super(Opcodes.ASM7);this.methodName methodName;}//在对应的reader.accept(visitor, 0);中最终会调用这个方法//它检查方法的名称是否与methodName相匹配如果匹配则返回一个新的//有些的参数//access1nameinit构造或者fadescriptor()Vsignaturenullexceptionsnull//一般access代表第几个从上到下从1开始name方法名称descriptor参数列表信息()V是构造Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {if (name.equals(methodName)) {return new MethodParameterVisitor();}return null;}public Map getParameterNames() {return map;}class MethodParameterVisitor extends MethodVisitor {public MethodParameterVisitor() {super(Opcodes.ASM7);}//上面的visitMethod调用后会最终调用到这里//提取参数名如果上面的对应了那么有多少参数那么这里执行多次当然当前的引用也会执行一次//有些的参数//namethis当前对象引用不用怕基本上这是必须需要上面的return new MethodParameterVisitor();执行后才可或者adescriptorLcom/mvc/framework/servlet/a;signaturenullstart对象end对象index0//name方法名称descriptor参数列表信息构造的话那么就是类的全限定名index是位置从1开始Overridepublic void visitLocalVariable(String name, String descriptor, String signature, Label start, Label end, int index) {if (index 0) {if (map null) {map new HashMap();}map.put(index - 1, name); //由于引用是退出的所以这里的index需要减一来保证第一个有数据而不是null}}}
}
package com.mvc.framework.config;import org.objectweb.asm.*;import java.util.Map;public class ParameterNameExtractor {//获取指定方法的参数名地址public static Map getParameterNames(String className, String methodName) throws Exception {//ClassReader对象用于读取类的字节码信息这里传递字节文件的全限定名ClassReader reader new ClassReader(className);//ParameterNameVisitor 对象是一个自定义的访问者类用于在字节码中查找指定方法的参数名所以传递方法名称ParameterNameVisitor visitor new ParameterNameVisitor(methodName);//通过 visitor 访问指定类的字节码第二个参数 0 是一个标志//通常用于启用或禁用不同类型的字节码访问这里使用0表示默认设置这个操作一般是根据对应的类版本来处理的//所以了解即可也就是访问字节码的方式一般对应的依赖可能存在多种的这里使用默认的一种reader.accept(visitor, 0);//获取到的方法的参数名集合return visitor.getParameterNames();}
}
然后我们改造这个a类
package com.mvc.framework.servlet;import com.mvc.framework.config.ParameterNameExtractor;
import java.util.Map;public class a {String name;public a(String name) {this.name name;}public a(String name, String j) {this.name name;}public void fa(String nam2, String name1) {}public void fb(String nam2, String name1) {}public static void main(String[] args) throws Exception {String className com.mvc.framework.servlet.a;String methodName fa;Map map ParameterNameExtractor.getParameterNames(className, methodName);for (int i 0; i map.size(); i) {System.out.println(参数列表的位置 i 名称是: map.get(i));}methodName fb;map ParameterNameExtractor.getParameterNames(className, methodName);for (int i 0; i map.size(); i) {System.out.println(参数列表的位置 i 名称是: map.get(i));}}
}
执行后可以发现得到了参数列表那么我们修改mvc的对应的方法也就是这里 //处理参数位置信息//比如query(HttpServletRequest request, HttpServletResponse response, String name)//获取该方法的参数列表信息自然包括名称或者类型或者位置从左到右起始下标为0MapString,String prmap null;try {//对应得到的基本都是String所以上面的可以这样处理MapString,Stringprmap ParameterNameExtractor.getParameterNames(aClass.getName(), method.getName());}catch (Exception e){e.printStackTrace();}Parameter[] parameters method.getParameters();for (int i 0; i parameters.length; i) {Parameter parameter parameters[i];//parameter.getType()得到参数类型if (parameter.getType() HttpServletRequest.class || parameter.getType() HttpServletResponse.class) {//如果是这两个建议名称就是他们这样就能保证赋值是对应的而不会被其他参数名称所影响具体保存名称干什么在后面就会知道的handler.getParamIndexMapping().put(parameter.getType().getSimpleName(), i);} else {//其他的类型保存其名称但是这里是使用prmap这个循环是确认位置的以及之所以使用Parameter也是为了得到一下类型或者其他的比如parameter.getType().getSimpleName()//他们的参数都是从0开始所以可以这样做且对应handler.getParamIndexMapping().put(prmap.get(i), i);}}//保存映射关系handlerMapping.add(handler);执行服务器访问后可以发现打印值是null即没有出来为什么通过调试我们可以发现对应的值是完全可以的但是报错了为了找到问题这里可以给出测试首先在a类里面操作或者修改如下
package com.mvc.framework.servlet;import com.mvc.framework.config.ParameterNameExtractor;import java.util.Map;public class a {public static void main(String[] args) throws Exception {String className com.mvc.framework.controller.DemoController;String methodName query;Map map ParameterNameExtractor.getParameterNames(className, methodName);for (int i 0; i map.size(); i) {System.out.println(参数列表的位置 i 名称是: map.get(i));}}
}
执行后可以得到对应的参数列表然后再mvc我们的模拟中的init中加上如下注意先把他里面的调用都注释 Overridepublic void init(ServletConfig config) {String className com.mvc.framework.controller.DemoController;String methodName query;Map map ParameterNameExtractor.getParameterNames(className, methodName);for (int i 0; i map.size(); i) {System.out.println(参数列表的位置 i 名称是: map.get(i));}}然后执行发现就是对应我们启动服务器的处理那么肯定的是对应的服务器的关系并且一般这个错误是Class not found且错误发生再ClassReader reader new ClassReader(className);
我们看看他的源码 public ClassReader(String className) throws IOException {this(readStream(ClassLoader.getSystemResourceAsStream(className.replace(., /) .class), true));}private static byte[] readStream(InputStream inputStream, boolean close) throws IOException {if (inputStream null) {throw new IOException(Class not found);} else {...可以发现这样的
/*
ClassLoader.getSystemResourceAsStream(className.replace(., /) .class)
后面的className.replace(., /) .class得到的是com/mvc/framework/servlet/a.class这里替换了.同理在服务器中也是如此那么唯一的区别就是ClassLoader.getSystemResourceAsStream了他代表什么
他代表从系统类路径找对应的文件或者说当前项目中找在前面我们说明了这里就需要考虑maven的操作与传统的web的区别了对应的区别是操作web的区别那个时候本质上只是部署的地方的区别虽然我们是不同的构建maven或者传统的web但是对应的最终都是操作服务器来操作对应的路径说明的同样的前面的这个地方也的确是启动服务器来得到不同的路径结果但是最终都是操作服务器而这里我们操作a类时并不是操作服务器的所以a类特殊那么由于他并不是操作服务器所以他一般情况下默认从当前的项目或者说当前的工作路径来找对应的文件并且对应的class可以认为与java路径一直所以com/mvc/framework/servlet/a.class在a类中执行时可以找到因为当前的项目下也加上这个路径正好是对应的文件但是如果在服务器中处理那么一般需要从部署层面来进行处理所以不会默认从当前项目找一般需要完整的路径所以如果这个时候我们还单纯的操作这个路径自然是找不到的那么我们应该这样的处理是获取当前项目部署情况的路径或者直接给出完整的路径一般来说单纯的根据部署情况一般是得不到的所以我们需要完整的路径比如部署方式在前面我们也都知道怎么获取也就是String realPath servletRequest.getServletContext().getRealPath(/);但是一般这样并不能进行操作因为在web中对应的ClassLoader.getSystemResourceAsStream只能拿取当前项目的资源文件或者java了因为class不在当前项目中的因为部署的原因那么有没有其他的方法可以拿取部署的class或者说io呢答一般没有
那么我们就需要ClassReader的另外一个构造方法了也就是
public ClassReader(InputStream inputStream) throws IOException {this(readStream(inputStream, false));}我们可以直接的得到io流赋值给他即可所以我们应该需要这样的操作来得到io
即String realPath servletRequest.getServletContext().getRealPath(/)WEB-INF\classes\全限定名;因为替换了.所以可以这样即通过原始io操作得到了当然服务器部署时默认的WEB-INF\classes是固定的所以写死当然他可能随着某些配置会改变所以还是不建议这样除非他提供了可以得到这个配置的方法那么可以处理虽然前面说一般没有方法拿取部署的class但是是否需要考虑通过其他方法或者是否存在方法直接拿取部署时的class或者说io呢而不是通过完整路径呢在web中是可以通过方法得到当前项目对应文件的io流的然而一般是当前项目直接处理的所以a类可以因为他没有部署路径就是当前的项目而不像web一般只会考虑资源文件且web中只能通过这样的io流处理拿取对应的资源文件也就是说他的处理其实是在编译之前处理的之后的部署的文件还是得不到甚至是任何相关操作的流基本只能操作资源文件java一般也包括所以就算你提供了完整路径也是没有用的因为他只操作当前项目也就是说之前的ClassReader构造方法是几乎不能获取对应的class文件的因为对应使用的是ClassLoader.getSystemResourceAsStream也就是io操作也就是只能在当前项目处理其他相关io操作的几乎都是当前项目的资源处理所以考虑通过方法也几乎不行了所以我们这里也不能通过自带的操作的路径或者对应的方法找当前项目路径来找编译后的class文件当然他操作java文件一般也可以的所以我们还是只能操作使用原始io操作String realPath servletRequest.getServletContext().getRealPath(/)WEB-INF\classes\全限定名;那么现在存在获取WEB-INF\classes方法吗答一般没有了当然了操作mvc必然通常处理服务器所以spring写死基本也不会出现问题的而spring也基本是写死的所以我们只能操作如下了也就是单纯的处理io因为没有方法操作web项目路径找部署的就算有他们也只能操作当前项目处理那么只能通过原始io处理io流了当然现在没有方法并不代表以后没有所以可以选择百度看看实际上上面说了这么多其实就总结如下方法是得到当前项目的io那么就存在如下没有部署时默认class和java是同一路径因为在当前项目那么自然可以通过自带的方法来得到对应的class的io流以及资源文件或者java文件部署时class不在同一路径了不在当前项目了那么不能通过方法得到对应的class的io流了只能通过完整路径了但是资源文件和java文件还是可以获得只是class不能获得了所以需要考虑完整路径了或者存在方法拿取部署io这个存放方法可以百度一般没有
*/经过上面的说明我们还需要修改对应的方法修改如下 /*
//构造一个HandlerMapping处理器映射器来完成映射关系initHandlerMapping();找到上面的修改成这个initHandlerMapping(config);
*///对应的这里private void initHandlerMapping(ServletConfig config) {//对应的这个地方String ba aClass.getName().replace(., /) .class;//对应得到的基本都是String所以上面的可以这样处理MapString,Stringprmap ParameterNameExtractor.getParameterNames(config.getServletContext().getRealPath(/)WEB-INF\\classes\\ba, method.getName());//都是得到getServletContext来处理的所以config也可以//然后修改ParameterNameExtractor类的getParameterNames方法里面的这个InputStream inputStream new FileInputStream(className);//ClassReader对象用于读取类的字节码信息这里传递io流ClassReader reader new ClassReader(inputStream);修改后我们执行访问一下看看结果发现打印对应的值出来了说明我们操作成功
至此我们可以说mvc的模拟框架编写完成到这里你会发现但凡与结构相关的基本上反射都可以做到也就是说注解他是一个结构那么可以绝对的说注解就是由反射来完成操作关联的其实被注解操作的方法大多数只是定义如参数列表具体可能由反射来调用或者说操作并且但凡需要与结构信息相关联的反射也都可以做到所以以后如果出现与结构进行关联操作的那么使用反射吧
到这里应该很明显知道比spring要困难多了吧spring只需要考虑循环依赖而这里除了考虑spring还需要考虑很多的细节特别的是字节码文件的解析问题以及反射的细节使用还有保存对应的关联
当然上面的编写只是mvc的一部分特别的关于视图方面的以及拦截方面的如我们好像并没有判断处理/*的操作我们也没有给出所以说才只是一部分
由于博客字数限制其他内容请到下一篇博客112章博客去看