Java:111-SpringMVC的底层原理(中篇)
这里续写上一章博客(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中的特定事件,⽐如HttpSession,ServletRequest的创建和销毁,变量的创建、 销毁和修改等,可以在某些动作前后增加处理,实现监控,⽐如统计在线⼈数,利⽤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
第五步:处理器适配器执⾏Handler(controller的方法,生成对象了,这里相当于调用前面的handle01方法,他负责调用)
第六步:Handler执行完成给处理器适配器返回ModelAndView,即处理器适配器得到返回的ModelAndView,这也是为什么前面我们操作方法时,是可以直接操作他并返回的,而返回给的人就是处理器适配器,就算你不返回,那么处理器适配器或者在之前,即他们两个中间,可能会进行其他的处理,来设置ModelAndView,并给处理器适配器
第七步:处理器适配器向前端控制器返回 ModelAndView(因为适配或者返回数据,所以是适配),ModelAndView 是SpringMVC 框架的一个 底层对 象,包括 Model 和 View
第⼋步:前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图(加上前后的补充,即前面的配置视图解析器)
第九步:视图解析器向前端控制器返回View
第⼗步:前端控制器进行视图渲染,就是将模型数据(在 ModelAndView 对象中)填充到 request 域,改变了servlet,最终操作servlet来进行返回
第⼗一步:前端控制器向用户响应结果(jsp的)
即可以理解:请求找路径并返回(1,2,3),给路径让其判断路径并返回且获得对应对象(4,5,6,7),变成参数解析(如拼接) 进行转发(8,9),然后到jsp(10),最后渲染(11)
所以说:
第一次拦截:在1和5中间处理
第二次拦截:在6到8中间处理
第三次拦截:在9到11中间处理
其实通过图片,我们应该知道,第一次拦截应该是在3和4中在前端控制器旁边处理,而7和8就是第二次拦截,10到11则是第三次拦截(这里也可能是9到10)
所以可以知道,其实mvc自带的有一些拦截,这也是对应注解,比如@RequestBody或者@ResponseBody可以操作的原因
当然,我们也可以进行添加拦截,这在后面会说明的
为了更加的知道拦截器的处理,我们直接来进行实战:
注意:mvc的拦截器是里面的,也就是说,servlet原本的过滤器必然先处理或者后处理
实际上我们学习源码很大程度是必须要有实战的,因为一个框架的源码我们基本是不可能全部读完的,这取决于一个框架是由很长时间的迭代,以及很多人一起开发完成的,当然,如果你的框架够小,那么可以是单独完成,在这种情况下,学习框架中,用阅读源码来学习,我们只能知道他的一点实现方式,所以学习框架通常需要直接的实战来进行学习,来直接的确定他的作用,而不是单独看源码来确定作用(你怎么知道他有没有其他关联,并且要知道这个关联需要看更多的源码),也就是说,实际上源码的解析大多数是让你知道他的实现方式,而不是具体细节(比如,他为什么这样定义变量等等),当然,除了实现方式有时候也需要学习设计模式,这个在以后会单独给一个博客来进行处理的,先了解一些框架的设计模式再说
那么,既然要实战,我们首先需要操作一个项目,项目如下:
对应的依赖,在前面我们已经给过多次了,这里我们继续给出吧
<packaging>war</packaging><dependencies><dependency><!--mvc需要的依赖,即有前端控制器DispatcherServlet--><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.1.5.RELEASE</version></dependency><!--servlet坐标,若不使用对应的类,如HttpServletRequest的话,可以不加--><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.8</version><!--这个必须要,后面的可以不写(后面两个),但最好写上,防止其他情况--></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.9.8</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.9.0</version></dependency></dependencies>
如果需要补充,自行补充吧
对应的web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><servlet><servlet-name>dispatcherServlet</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:springmvc.xml</param-value></init-param></servlet><servlet-mapping><servlet-name>dispatcherServlet</servlet-name><url-pattern>/</url-pattern></servlet-mapping></web-app>
index.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<button id="btn">ajax提交</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;charset=utf-8',success: function (data) {console.log(data);alert(data)}})})
</script>
</body>
</html>
springmvc.xml:
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://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.xsd"><context:component-scan base-package="com.controller"/><mvc:annotation-driven></mvc:annotation-driven>
</beans>
entity里面的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 List<User> ajax(@RequestBody List<User> 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:interceptors><mvc:interceptor><mvc:mapping path="/**"/><!--项目路径下的对应路径,一个*代表一个路径下,但不代表路径下的路径下,两个基本就是所有--><!--对所有的controller类里面的所有方法都进行拦截(因为是/**)--><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>
实际上mvc:interceptors代表可以配置多个,在配置多个时,一般需要操作路径,防止都进行拦截的处理(这个时候不写可能报错,具体可以测试),而不是指定的拦截(因为默认是/**),所以存在mvc:interceptors中存在mvc:interceptor,所以如果你只有一个并且需要是/ * *,那么可以这样写:
<mvc:interceptors><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptors>
<!--
其中<mvc:mapping path="/**"/>也可以进行去掉,因为默认是/**
直接省略<mvc:interceptor>,代表只有你这一个的意思,但是由于只有一个,通常情况下,默认是/**的,而不会给你进行路径的设置,所以如果你只有一个并且需要是/**,那么可以这样写
-->
一般我们建议使用这样的方式来进行编写:
<!--配置拦截器--><mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><!--项目路径下的对应路径,一个*代表一个路径下,但不代表路径下的路径下,两个基本就是所有--><!--对所有的controller类里面的所有方法都进行拦截(因为是/**)--><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>
<!--一般来说,/**的/代表从端口开始,这里了解即可,所以大多数访问也会进行拦截处理(但是也要知道,一个端口基本只有一个服务器占用,所以并不会影响其他服务器的,所以不需要考虑其他服务器的影响问题(除非是业务逻辑上的影响,比如文件的操作))
虽然大多数访问都会进行拦截处理,但是要知道,他是在需要执行对应的方法执行进行的拦截,也就是说,如果你并没有需要执行对应的方法,那么就不会执行,而不会也自然不会出现true,这里我们需要注意的是:只有当前面的拦截的返回值或者说,第一个拦截的返回值为true时,后面的两个拦截才会进行处理,这也是为什么如果你访问index.jsp,由于第一个拦截没有执行(他并不会操作对应的方法,自然也就不会经过这个拦截方法了,一般情况下,没有经过这个方法时,对应进行操作的变量是默认为false,但是只要你操作了这个拦截,或者就算你不进行重写,他默认放行,因为这个时候他是默认的方法是true的,前面的HandlerInterceptor接口可以看到返回值的),那么其他的拦截,比如第三个拦截没有起作用的原因,默认情况下,如果你不执行第一个拦截,那么就是false的这里的true是否看起来相当于过滤器中的放行的意思呢,之所以需要这样的规定,是保证其他的,如index.jsp不会操作拦截的原因(因为是直接的访问,并不需要操作拦截,而减低操作的可能,从而使得服务器可以接收更多请求,或者减少内存损耗)
-->
当然其实还有功能,这是mvc设计出来的功能,他也可以进行去掉,比如他可以选择性的不拦截一个路径的请求:
<mvc:interceptors><mvc:interceptor><mvc:mapping path="/**"/><mvc:exclude-mapping path="/demo/**"/> <!--选择不拦截demo对应路径下面的请求--><bean class="com.Interceptor.MyInterceptor"></bean></mvc:interceptor></mvc:interceptors>
这样就能不拦截demo开头的处理了,一般情况下,servlet或者mvc的普通拦截都是在项目路径下的直接拦截,比如前面的测试的路径test/in,就是放在项目路径后面,而不是端口路径
编写好后,我们执行前面的代码,看看后端的结果:
打印如下:
/*
public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
Handler业务逻辑执行之前拦截一次,我是第一次[User{id='1', username='张三'}, User{id='2', username='李四'}]public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
null
在Handler逻辑执行完毕但未跳转页面之前拦截一次,我是第二次public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
null
在跳转页面之后拦截一次,我是第三次
*/
这个时候,你可以选择将返回值变成false,那么我们看看这个打印结果:
/*
public java.util.List<com.entity.User> com.controller.test.ajax(java.util.List<com.entity.User>)
Handler业务逻辑执行之前拦截一次,我是第一次
*/
我们可以发现,他甚至连对应的方法都不执行了,而方法都不执行,默认情况下,是没有响应体信息的,那么在前端显示的就是空白(这个时候,甚至都不会操作视图,也就是单纯的返回空响应),也就是说,这个true也决定了对应的controller的方法的执行,这也是为什么默认情况下,对应的HandlerInterceptor方法的返回值是true的一个原因
现在我们添加一个前端jsp,test.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<%System.out.println(1);
%>
1
</script>
</body>
</html>
上面的这个地方加上代码:
<%System.out.println(1);
%>
这个是jsp的语法(在51章博客有说明),在转译和编译的情况下,会进行处理的,最终操作拦截,那么我们在springmvc.xml中加上如下的配置:
<bean id="viewResolver"class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/"></property><property name="suffix" 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 [view="test"; 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 [view="test/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;charset=utf-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;charset=utf-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:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor></mvc:interceptors><bean id="viewResolver"class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="prefix" value="/"></property><property name="suffix" 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:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.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:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor3"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.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:interceptors><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor1"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor2"></bean></mvc:interceptor><mvc:interceptor><mvc:mapping path="/**"/><bean class="com.Interceptor.MyInterceptor3"></bean></mvc:interceptor></mvc:interceptors>
执行看看结果:
/*
preHandle1....
preHandle2....
preHandle3....
afterCompletion2....
afterCompletion1....
*/
可以发现,之前的结论是正确的(后面先打印afterCompletion2…,是反序的原因)
那么还有个问题,为什么mvc的拦截器中,第一次拦截是顺序的,其他两个是反序的或者说倒序的:
解释如下:
这个默认规则的原因是为了在执行拦截器时提供更多的灵活性和可能性,考虑以下情况:
第一个拦截器通常用于做一些准备工作,如日志记录、身份验证等,按顺序执行有助于确保这些准备工作在控制器方法之前完成
控制器方法执行后,倒序执行其他拦截器可以用于清理工作、日志记录和一些其他操作,这确保了在请求处理完毕后执行这些操作,因为这些工作一般先创建的后清理
但是实际上顺序的问题,大多数我们并不需要注意,并且,也存在可以修改顺序的处理,只是可能需要某些版本(好像现在基本都不行了)
处理multipart形式的数据:
前面虽然我们使用mvc处理过问题,但是还是有些操作我们没有说明,比如方便我们操作的api,现在来补充一下:
首先加上依赖:
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version></dependency>
配置上传文件解析器:
<bean id="multipartResolver"class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="1000000000"/></bean>
<!--
MultipartResolver的组件在前面我们说过是操作上传的,而:
public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware {
即CommonsMultipartResolver是MultipartResolver的子类
-->
对应的前端(index.jsp):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="demo/upload"><input type="file" name="uploadFile"/><input type="submit" 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),因为不同的操作系统对一个目录里面的文件数量的多少是有限的,一般情况下存在如下的限制(网上搜索到的):/*FAT32:FAT32 文件系统(常见于旧的 Windows 版本)对每个目录有文件数目的限制,通常约为 65534 个文件NTFS:NTFS 文件系统(常见于现代 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 contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</title>
</head>
<body>
1
</body>
</html>
执行后,我们上传一个文件或者说给一个文件,然后看看当前项目是否出现对应的文件或者目录,然而并没有,为什么
这里就需要考虑maven的操作与传统的web的区别了,首先,我们可以选择实现一个传统的web处理(这里可以选择看看第50章博客):
为此这里我也不得不给出三种创建web的方式,空项目,传统web,maven项目
首先我们从空项目来进行处理,由于空项目并没有使用到maven或者并没有进行指定使用maven,所以一般空项目只能到传统的web(即空项目到web一般与传统的web项目是一样的),然而这是一般的说法,基本上任何形式都可以从空项目变化而来,因为他就是在idea中,所以这里应该有五种方式:空项目(操作java程序),空项目到web,空项目到maven,(传统)web,maven等等
创建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 version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><servlet><servlet-name>ConfigServlet</servlet-name><servlet-class>servlet</servlet-class></servlet><servlet-mapping><servlet-name>ConfigServlet</servlet-name><url-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 version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>testma</artifactId> <!--这里建议写上与项目名称一样的,也就是maven--><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties></project>
但是他并没有进行处理,我们需要如下的配置(maven本身也可以看成一个框架,而idea需要被他支持,就如空项目到web时需要指定jar包一样,当然,也可以添加框架(实际上也就是对应的jar包),但是由于maven的jar比较多,所以这里我们就选择添加框架了):
一般来说,这个文件添加好后,右下角就会出现这个,当然,一般都会出现了,否则可能你需要重新的删除在创建(因为基本没有其他办法来构建maven,因为功能也并非都会提供了)
之后的选择,基本选择第一个即可,当然,还是需要看具体情况,最好翻译一下
然后你可以选择在其内容加上:
<packaging>war</packaging>
一般这个时候,由于刷新了,那么他基本就出现了对应的显示(M)
某种程度上,点击右下角后,把这里出现的这个勾勾去掉也行:
而maven的创建操作,我们就不处理了,因为我们操作过很多次了
至此,对应的五个方式(空项目(操作java程序),空项目到web,空项目到maven,(传统)web,maven)我们操作完毕,其中
maven操作web我们就不处理了,因为我们可以只设置打包方式即可,比较简单
但是为了看看前面的问题,所以我们还是需要进行处理的,首先创建webapp文件:
如下:
如果前面再pom.xml中没有写上maven,而是testma,那么项目名称后面一般会有[testma]
依赖如下:
<dependencies><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency></dependencies>
即对应的配置与前面的一样,我们看看maven与传统web的区别是什么:
我们经过测试:
当操作方式为如下时会出现不同的结果(配置tomcat时,会出现的):
/*
操作方式是:
maven:war exploded:F:\maven\target\maven-1.0-SNAPSHOT\
maven:war:F:\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" docBase="F:\maven\target\maven-1.0-SNAPSHOT.war" />
F:\Program Files\apache-tomcat-8.5.50\webapps\maven\ //项目下面(maven一般是项目名称的)
进入maven:war exploded,看看其结果:
<Context path="/maven" docBase="F:\maven\target\maven-1.0-SNAPSHOT" />
F:\maven\target\maven-1.0-SNAPSHOT\
进入之前测试的xianmu:Web exploded,看看其结果:
<Context path="/xianmu" docBase="F:\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),因为不同的操作系统对一个目录里面的文件数量的多少是有限的,一般情况下存在如下的限制(网上搜索到的):/*FAT32:FAT32 文件系统(常见于旧的 Windows 版本)对每个目录有文件数目的限制,通常约为 65534 个文件NTFS:NTFS 文件系统(常见于现代 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 contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head><title>Title</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来决定,越小那么就越优先(上面没有测试数字,实际上数字也是的,由于1<a,那么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并不好处理,具体可以百度,一般mybatis,spring,springmvc基本都是需要xml和注解一起的,反正那个方便使用那个):
通常来说,注解的性能会比xml慢点(通常指启动的时候,有时候运行时也会,但是方便许多,特别的是考虑到注解的扫描(当包非常多时,有些不需要的可能也会扫描到))
在手写之前,我们创建一个项目:
web.xml中是:
<!--
<?xml version="1.0" encoding="UTF-8"?>
-->
<!--
大多数情况下:<?xml version="1.0" encoding="UTF-8"?>的操作是可以选择不写的,但是,大多数关于xml解析的代码,或多或少都会处理这个,虽然大多数默认都是这样的处理,也就是<?xml version="1.0" encoding="UTF-8"?>,只是我们还是建议写上,那么这里操作了xml,自然最终也会操作xml解析(如果可以你也能自己写一个),并且由于是服务器的处理,其中自然有内部的关于web.xml的读取方式,现在先这样写上,后面会补充
-->
<web-app></web-app>
现在开始编写,首先,我们创建com.mvc.framework包,然后再该包下创建DispatcherServlet类:
上面是需要服务器的包的,自然需要引入,也就是:
<dependencies><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</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 version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><display-name>create</display-name><servlet><servlet-name>mvc</servlet-name><servlet-class>com.mvc.framework.DispatcherServlet</servlet-class></servlet><servlet-mapping><servlet-name>mvc</servlet-name><url-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会操作两个对象,假设是a,b,ioc容器我们假设为ioc,mvc也存在对象,我们认为是c//所以手写spring将a,b加入到ioc中,那么ioc有a,b了,这个时候,像一些注入,会到ioc中的信息//这个时候并没有c,所以spring不能访问c,处理了这些操作后,mvc才会将c给ioc,这个时候mvc在处理这些时,可以得到a,b,因为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操作get,post操作post(但是是同一个方法),与HttpServlet相关,具体是操作了service,只是在中间可能会操作注解(前提是设置了),判断是否是对应的请求方式而进行报错的,比如进入到了get,那么在里面判断注解是否是get相关,可以选择再次的得到请求,而进行补充判断):
@Overridepublic void init(ServletConfig config) throws ServletException {//加载配置文件//扫描相关的类//初始化bean对象(ioc容器)//实现依赖注入//构造一个HandlerMapping处理器映射器,来完成映射关系//映射关系创建好,那么根据请求查看映射,来决定调用谁}
在资源文件夹下补充mvc的配置文件mvc.xml:
<beans>
<component-scan base-package="com.mvc.framework"/>
</beans>
在web.xml中加上如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><display-name>create</display-name><servlet><servlet-name>mvc</servlet-name><servlet-class>com.mvc.framework.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><!--编写方式受我们源码的处理,由于是服务器的处理,那么这里我们就这样写
当然,由于只是值,这里也可以写成任何只要你可以得到路径的地址即可,比如甚至可以写绝对路径,这都看你如何处理了,到时候如何处理和使用什么方式来操作都需要自行处理的,当然,大多数我们并不会记住关于读取文件或者说读取项目或者找当前项目路径的相关api,这个时候可以选择网上查找,这并不难以找到
--><param-value>mvc.xml</param-value><!--直接这样写可能会报错,这是可能idea判断的问题(认为你要加上classpath:,即classpath:mvc.xml),实际上这里可以随便写数值,所以忽略即可,idea的提示也并不是万能的--></init-param></servlet><servlet-mapping><servlet-name>mvc</servlet-name><url-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信息:
那么我们加上如下的依赖:
<dependency><groupId>dom4j</groupId><artifactId>dom4j</artifactId><version>1.6.1</version></dependency><dependency><groupId>jaxen</groupId><artifactId>jaxen</artifactId><version>1.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 List<String> classNames = new ArrayList<>();//需要一个bean的存放(ioc容器)private static Map<String, Object> map = new HashMap<>();//缓存已经进行过依赖注入的信息private static List<String> fieldsAlreayProcessed = new ArrayList<>();//url和method的映射关系private static Map<String, 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.Entry<String, 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.Entry<String, 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(value+declaredField.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 {ju=1;}//放入map,因为id(beanName)有了,对应的实例也有了,自然放入//id作为类名称首字母小写或者指定的类名称id//然而指定名称并不友好,因为可能存在多个不同的类是相同的名称,所以这个名称需要与全限定名进行拼接(也就是上面的beanName += aClass.getName();,这里操作两个if并不友好,因为需要前面的数据),但是这个全限定名可能也是对应的接口而不是类(到这里你是否理解了,为什么spring在如果容器中存在多个相同的实例时,会报错了吧,因为判断这个的话,非常麻烦,所以spring只能存在一个实例(当然,也可以存在多个,只是他需要对应一些信息,比如也操作这样的名称,或者变量名称等等),且他是利用遍历来赋值的,这样就非常简单了,当然,如果你考虑了所有情况,那么在获取时,自然比spring快,只是存在没有必要的空间,所以互有好处,spring,节省空间,但是获取时需要性能,而这里需要空间,但是获取时性能更快)if(ju==1){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是97,A是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(value+declaredField.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(ju==1){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 {Class<a> 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 Map<String, Integer> paramIndexMapping; //参数顺序,key是参数名,value是代表第几个参数//这个具体如何使用和操作获取,看后面的处理//对应于query(HttpServletRequest request, HttpServletResponse response, String name)//那么map中保存的key=name,value=2(这里我们规定下标从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 Map<String, Integer> getParamIndexMapping() {return paramIndexMapping;}public void setParamIndexMapping(Map<String, Integer> paramIndexMapping) {this.paramIndexMapping = paramIndexMapping;}
}
现在我们修改initHandlerMapping方法的内容(我这里将注释去掉了,自己对应一下):
前提,我们将之前创建的private static Map<String, Method> handlerMapping = new HashMap<>();注释掉,因为不使用这个了,而是使用这个:
private List<Handler> 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, Comparable<String>, 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)//获取该方法的参数列表信息,自然包括名称或者类型或者位置(从左到右,起始下标为0)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{//其他的类型,保存其名称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 {Class<a> 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];//给数组参数值,并操作顺序,先拿取参数集合(这里我们只是模拟,就不考虑文件的处理了,所以这里一般我们认为考虑键值对的参数传递)Map<String, String[]> parameterMap = req.getParameterMap(); //一个键是可以对应多个值的Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();//遍历所以从请求中拿取的参数for (Map.Entry<String, String[]> param : entries) {String value = "";//如果是多个的话,那么合并吧,比如name=1&name=2,那么这里的结果就是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?name=1234,其中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) {Class<a> 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());}}//打印的结果给出部分:/*main:arg0fb:arg0.arg1fa:arg0.arg1获取的方法是反过来获取的,并且参数也是固定的模式当然,参数列表是从左到右的*/}
}
为什么会这样,这一般需要看版本,一般在jdk8开始就这样处理了,当然,这样的还有私有的,也就是设置可以访问private变量的变量值,在jdk8之前可能不用设置,但是之后(包括jdk8)不能直接的访问私有属性了(可能随着时间的推移,也会改变),因为需要进行设置这个,所以不能直接访问私有属性了
为什么要这样处理:这是为了提高字节码的紧凑性和安全性,参数名称在编译后被抹去,主要有以下原因:
隐私和安全性:在字节码中包含参数名称可能泄漏程序的细节信息,可能会被滥用,这对于某些应用来说是不可接受的,因为它可以暴露敏感信息
字节码紧凑性:字节码文件通常被用于分发和执行,如果包含了大量参数名称,将会增加文件大小,在资源受限的环境中,较小的字节码文件可以提高性能和减小存储需求
字节码的紧凑型还比较合理,但是为什么字节码会影响安全,举例:
反编译和逆向工程:如果参数名称包含在字节码中,恶意用户或攻击者可以更容易地反编译和逆向工程你的应用程序,以获取关于应用程序的内部结构和逻辑的信息,这可以帮助他们发现潜在的漏洞或弱点,甚至滥用应用程序
敏感信息泄露:在某些情况下,方法的参数名称可能包含敏感信息,如果这些名称泄露到字节码中,可能会暴露敏感数据、业务逻辑或应用程序内部细节,从而引发安全风险
安全性问题披露:如果方法参数名称包含有关应用程序内部的信息,攻击者可能会更容易发现应用程序的潜在漏洞,从而加大应用程序受攻击的风险
当然,spring是解决的或者说mvc是解决的(这里我们称为spring,因为最终是使用spring的主要内容的),但是他是通过其他方式,所以并不会暴露,然而,对方也可以通过spring来获取该参数,所以也只是避免了方便的获取,那么在不使用spring时,他既然会出现arg0这样的名称,那么有没有操作让他不这样做,其实是有的,只需要加上这个操作:
-parameters,编译选项
一般情况他是操作编译的,那么应该是如此的:javac -parameters YourClass.java,然而我们是操作项目,不可能这样处理,应该需要配置来使得项目默认这样处理,这里我们就以maven为例:
在maven中的pom.xml中操作如下:
<!--与dependencies同级别
-->
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version> <!-- 版本号根据实际情况调整 --><configuration><source>11</source> <!-- Java 版本,根据你的项目调整 --><target>11</target> <!-- Java 版本,根据你的项目调整 --><compilerArguments><parameters /> <!-- 这里是主要的操作 --></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的方式,这里我们选择拿取第三方的代码(这里我们使用ASM,spring也是使用类似的或者相同的),先在framework包下创建包config,然后在该包下我们创建几个类(在spring中有类似的这样的处理,这里我们手动写一个),这几个类在后面会给出
在手写之前,我们使用上面的配置,启动运行一下,然后操作mvc看看结果,如果出现了打印的值不是null,而是1234,那么我们可以说初步完成(这个时候,你可以选择给@Service操作value,然后@Autowired对应(在spring中,我们并没有这样的处理,这里我们是这样处理的),经过测试,也打印了1234,也可以测试将String name变成int name,如果打印为0,说明也操作成功(我们的方式,而不是Spring的mvc的报错),然后我们将路径中,比如:/demo的开头的/去掉,访问,可以发现,也可以,那么说明我们的操作代码编写完毕,当然,可能有些地方有问题,但是暂时没有,且功能都正确,如果出现了,请自行修改一下吧),然后我们使用spring对应的方式来解决:
首先我们需要引入依赖:
<dependency><groupId>org.ow2.asm</groupId><artifactId>asm</artifactId><version>9.2</version> <!-- 根据需要的版本号调整 --></dependency>
然后去掉这个依赖配置:
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version> <!-- 版本号根据实际情况调整 --><configuration><source>1.8</source> <!-- Java 版本,根据你的项目调整 --><target>1.8</target> <!-- Java 版本,根据你的项目调整 --><compilerArguments><parameters /></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) {Class<a> 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相匹配,如果匹配,则返回一个新的//有些的参数://access:1,name:<init>(构造)或者fa,descriptor:()V,signature:null,exceptions:null//一般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调用后,会最终调用到这里//提取参数名,如果上面的对应了,那么有多少参数,那么这里执行多次,当然,当前的引用也会执行一次//有些的参数://name:this(当前对象引用,不用怕,基本上这是必须需要上面的return new MethodParameterVisitor();执行后才可)或者a,descriptor:Lcom/mvc/framework/servlet/a;,signature:null,start:对象,end:对象,index:0//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)//获取该方法的参数列表信息,自然包括名称或者类型或者位置(从左到右,起始下标为0)Map<String,String> prmap = null;try {//对应得到的基本都是String,所以上面的可以这样处理(Map<String,String>)prmap = 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,所以上面的可以这样处理(Map<String,String>)prmap = 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章博客)去看
相关文章:
Java:111-SpringMVC的底层原理(中篇)
这里续写上一章博客(110章博客): 现在我们来学习一下高级的技术,前面的mvc知识,我们基本可以在67章博客及其后面相关的博客可以学习到,现在开始学习精髓: Spring MVC 高级技术: …...
Vue3新特性指南:探索新增指令、内置组件和改进
Vue.js是一款流行的JavaScript框架,用于构建现代Web应用。Vue3是Vue.js的最新版本,引入了许多新特性和改进。本文将介绍Vue3新增的指令、内置组件以及其他值得关注的改进,并提供使用组合式API的用法示例。 一、新增指令 v-is指令: v-is指令用于动态组件,可以根据表达式的值来…...
Qt项目天气预报(2) - 重写事件函数
鼠标右键实现退出界面 知识点QMenu: QMenu 弹出对话框 --> 相对QMessageBox 更加轻量点 QMenu是Qt库中用于创建弹出式菜单的类,它通常出现在应用程序的顶部菜单栏、按钮的右键菜单或自定义上下文菜单中。以下是关于QMenu的详细介绍: 1. 类的基本特…...
uni-app前端,社区团购系统搭建部署
目录 前言: 一、社区团购模式: 二、社区团购系统功能: 三、总结: 前言: 区团购系统是一种利用互联网技术和组织力量,通过线上线下结合的方式,为社区居民提供方便快捷的商品和服务采购平台。…...
基于iBeacon蓝牙定位技术的反向寻车系统
随着城市化进程的加速和汽车保有量的不断增加,大型停车场成为了人们日常生活中不可或缺的一部分。然而,在繁忙的停车场中快速找到自己的车辆,成为了许多车主的难题。为了解决这一问题,维小帮基于iBeacon蓝牙技术打造的反向寻车系统…...
CCAA质量管理【学习笔记】 备考知识点笔记(五)质量设计方法与工具
第五节 质量设计方法与工具 1 任 务 分 解 法 1.1 概念 任务分解法,又称工作分解结构 (Work Breakdown Structure, 简 称 WBS) 。WBS 指以可交付成果为 导向,对项目团队为实现项目目标并完成规定的可交付成果而执行的工作所进行的层次分解。W…...
RIP路由协议汇总(华为)
#交换设备 RIP路由协议汇总 一、原理概述 当网络中路由器的路由条目非常多时,可以通过路由汇总(又称路由汇聚或路由聚合)来减少路由条目数,加快路由收敛时间和增强网络稳定性。路由汇总的原理是,同一个自然网段内的不…...
服务部署:.NET项目使用Docker构建镜像与部署
前提条件 安装Docker:确保你的Linux系统上已经安装了Docker。如果没有,请参考官方文档进行安装。 步骤一:准备项目文件 将你的.NET项目从Windows系统复制到Linux系统。你可以使用Git、SCP等工具来完成这个操作。如何是使用virtualbox虚拟电…...
力扣1170.比较字符串最小字母出现频次
力扣1170.比较字符串最小字母出现频次 f()是找最小字母的次数 即cnt中第一个出现过的字母 将word中所有字符串都处理存入nums nums排序 再做二分 class Solution {public:int f(string s){int cnt[26] {0};for(char c:s)cnt[c - a] ;for(int i0;i<26;i) cout<<cn…...
boost asio异步服务器(3)增加发送队列实现全双工通信
增加发送节点 构造发送节点,管理发送数据。发送节点的类如下。 这个发送节点用于保证发送和接收数据的有效性。 增加发送队列 前边实现的是一个简单的echo服务器,也就是服务器将收到的内容发送给对应的客户端。但是在实际的服务器设计中,服务…...
49.Chome浏览器有三种清缓存方式
49.Chome浏览器有三种清缓存方式:正常重新加载、硬件重新加载、清空缓存并硬性重新加载 1、【正常重新加载】 触发方式:①F5 ②CtrlR ③在地址栏上回车 ④点击链接 如果缓存不过期会使用缓存。这样浏览器可以避免重新下载JavaScript文件、图像、…...
Python爬取与可视化-豆瓣电影数据
引言 在数据科学的学习过程中,数据获取与数据可视化是两项重要的技能。本文将展示如何通过Python爬取豆瓣电影Top250的电影数据,并将这些数据存储到数据库中,随后进行数据分析和可视化展示。这个项目涵盖了从数据抓取、存储到数据可视化的整个…...
【背包题】oj题库
目录 1282 - 简单背包问题 1780 - 采灵芝 1888 - 多重背包(1)编辑 1891 - 开心的金明 2073 - 码头的集装箱 1905 - 混合背包 1282 - 简单背包问题 #include <bits/stdc.h> using namespace std; //二维数组:dp[i][j]max(dp[i-1][j],v[i]dp[…...
Web前端弱势因素:深入探讨与挑战解析
Web前端弱势因素:深入探讨与挑战解析 在快速发展的Web前端领域,尽管技术日新月异,但仍存在一些固有的弱势因素。这些因素不仅影响了开发效率和用户体验,也带来了诸多挑战。本文将深入探讨Web前端的弱势因素,并从四个方…...
元素在超出当前界面的下拉列表中如何定位
有时我们会遇到一种情况是,当我们找一个视频列表中的视频,在页面的最底层,此时selenium 无法定位到这个元素,因为 selenium只能定位页面上显示出来内容的元素,需要通过下拉框把界面拉到该元素所在的位置,再…...
Vscode中使用make命令
前言 需要注意,如下操作需要进行网络代理,否则会出现安装失败的情况 安装 第一步 — 安装MingGW (1)进入官网下载 (2)下载完成之后,双击exe文件 (3)点击Install &#x…...
配置完eslint没有用?
当你使用 npx eslint --init 生成配置文件后 你也配置好了.prettierrc 当你在代码写一点小问题的时候 发现eslint没有进行检查 原因是你生成的 .eslintrc.js中没有加上这个配置 extends: [.....plugin:prettier/recommended],加上以后重启vscode你会发现...
[Nacos]No spring.config.import property has been defined
在学习 Spring Cloud Alibaba ,Nacos组件,创建一个cloudalibaba-config-nacos-client,加载多配置集时遇到问题 配置了 bootstrap.yml 后启动项目报错: 是因为在springcloud 2020.0.2版本中把bootstrap的相关依赖从spring-cloud-s…...
【操作与配置】Pytorch环境搭建
安装显卡驱动 显卡驱动是一种软件程序,用于控制显卡硬件与操作系统之间的通信和交互。显卡驱动负责向操作系统提供有关显卡硬件的信息,以及使操作系统能够正确地控制和管理显卡的各种功能和性能。显卡驱动还包含了针对不同应用程序和游戏的优化ÿ…...
判断QT程序是否重复运行
打开exe,再次打开进行提示。 main.cpp添加: #include "QtFilePreview.h" #include <QtWidgets/QApplication> #include <windows.h> #include <qmessagebox.h> #pragma execution_character_set("utf-8")bool Ch…...
利用Axios封装及泛型实现定制化HTTP请求处理
本案例旨在教授如何使用Axios库结合TypeScript泛型进行HTTP请求的高级封装,以提升代码的可复用性和类型安全性。我们将通过一个具体的示例,学习如何创建一个通用的请求函数,它能够适应不同类型的API响应,并在请求前后加入自定义逻…...
RN6752V1 高性能AHD转MIPIDVPBT656BT601芯片方案,目前适用于车载方案居多
RN6752V1描述: RN6752V1是一种模拟高清晰度(模拟高清)视频解码器IC,专为汽车应用而设计。它集成了所有必要的功能块: AFE,PLL,解码逻辑,MIPI和I2C接口等,在一个小的5mm …...
Rust 基金会的商标政策更新引发社区争议
Rust 基金会最近更新了其商标政策,引发了社区内的一些争议。 Rust 是一种高性能系统编程语言,拥有庞大的开发者社区。Rust 基金会成立于 2020 年,旨在支持和推动 Rust 语言的发展。该基金会负责管理 Rust 的商标,并制定了商标使用…...
Java Opencv识别图片上的虫子
最近有个需求,希望识别图片上的虫子,对于java来说,图像识别不是很好做。在网上也搜索了很多,很多的代码都是不完整,或者下载下载报错,有的写的很长看不懂。所以自己试着用java的opencv写了一段代码。发现识…...
微型操作系统内核源码详解系列五(1):arm cortex m3架构
系列一:微型操作系统内核源码详解系列一:rtos内核源码概论篇(以freertos为例)-CSDN博客 系列二:微型操作系统内核源码详解系列二:数据结构和对象篇(以freertos为例)-CSDN博客 系列…...
值传递和址传递
值传递 上面的代码是想要交换x,y的值,把x,y传递给swap函数之后,执行下面的操作: 在swap中a和b交换了,但是和x,y没有关系,所以x,y在main中不会变。 址传递 下面再看把x…...
【three.js】自定义物体形状BufferGeometry
目录 一、认识缓冲类型几何体BufferGeometry 二、将各个顶点连线 一、认识缓冲类型几何体BufferGeometry threejs的长方体BoxGeometry、球体SphereGeometry等几何体都是基于BoxGeometry类构建的,BufferGeometry是一个没有任何形状的空几何体,你可以通过BufferGeometry自定…...
Mac 使用 Homebrew 安装 Python3
在macOS系统中,使用Homebrew安装Python3并进行环境配置的步骤如下: 打开终端。 运行以下命令安装Python3: brew install python3 安装完成后,可以通过以下命令检查Python3的版本: python3 --version 为了确保终端…...
汽车行驶中是怎么保障轴瓦安全的?
汽车轴瓦是一种用于减少摩擦和支撑转动部件的关键零部件,通常用于发动机的曲轴、凸轮轴等转动部件上。主要作用是减少转动部件之间的摩擦,支撑和保护曲轴、凸轮轴等旋转部件,确保它们在高速旋转时的稳定性和耐用性。 在汽车轴瓦加工过程中&am…...
洗地机哪款好?洗地机十大名牌排行榜
随着科技的发展,各种家居清洁工具层出不穷,为我们的生活带来了诸多便利。在众多清洁工具中,洗地机的清洁效果更受大家喜爱,它能够完美解决了扫地机无法做到的干湿垃圾“一遍清洁”效果,而且几乎能解决日常生活中所有的…...
色弱做网站/seo指的是搜索引擎营销
Solution 考虑分开统计朝向每一个方向的所有狐狸对答案的贡献. 比如说以向右为例, 我们用箭标表示每一只狐狸的方向, 用\(\)表示当前一步移动之前的每一只狐狸的位置.\[ \begin{aligned} \sum_{d_i \rightarrow} x_iy_i & \left( \sum_{d_i \rightarrow} S \times (x_i …...
wordpress禁止外链/国外电商平台有哪些
| 好看请赞,养成习惯你有一个思想,我有一个思想,我们交换后,一个人就有两个思想If you can NOT explain it simply, you do NOT understand it well enough现陆续将Demo代码和技术文章整理在一起 Github实践精选 ,方便…...
制作wordpress插件/深圳网络营销和推广渠道
1.安装编译环境 2.上传源码包并解压 3.编译并安装 4.复制配置文件到 /etc目录下 5.开启Redis,默认在前端运行,效果如图 命令:redis-server /etc/redis.conf6.修改配置文件 /etc/redis.conf 以守护进程运行 将daemonize参数修改为yes 将…...
中国建设银行重庆网站首页/上海百度提升优化
简介 这里我想说的是,程序本身不需要关心表的结构,只需要按照视图定义来取数据或更新数据。 什么是视图 说起视图呢,就是说它相当于一个虚拟的表,你看不到它,但是你可以根据它来更新和操作表,视图中的wi…...
网上做问卷报酬不错的网站是/网络广告人社区
最近做了一个webservice应用,发现传输过程中,只要有特殊这符,就会导致出错 于是上网搜了一些方法,先把源WebService输出的内容进行特殊字符转换十六进制,接到收后再进行返转换 ///<summary>///StringExtenstion 的摘要说明///</summary>public static class String…...
h5网站开发软件下载/站长之家官网登录入口
问题 Push failed: fatal: Authentication failed for httpspycharm invalid authentication data couldn’t kickstart handshaking 解决...