SpringMVC 基本使用 && 手动实现机制

admin 2024年7月23日16:23:52评论5 views字数 63785阅读212分37秒阅读模式

SpringMVC 基本使用 && 手动实现机制

SpringMVC 是基于 Spring 的 WEB 应用

  1. SpringMVC 从易用性,效率上 比曾经流行的 Struts2更好
  2. SpringMVC 是 WEB 层框架
  3. SpringMVC 通过注解,让 POJO 成为控制器,不需要继承类或者实现接口
  4. SpringMVC 采用低耦合的组件设计方式,具有更好扩展和灵活性
  5. 支持 REST 格式的 URL 请求
  6. SpringMVC 是基于 Spring 的, 也就是 SpringMVC 是在 Spring 基础上的。SpringMVC 的核心包 spring-webmvc-xx.jar 和 spring-web-xx.jar
SpringMVC 基本使用 && 手动实现机制

环境搭建 && 使用方法

基本使用方法

下面我们可以创建一个Tomcat项目, 如图:SpringMVC 基本使用 && 手动实现机制当然了, 我们下面在WEB-INF/lib目录下放置一系列jar包, 如图:SpringMVC 基本使用 && 手动实现机制其中这些.jar文件放置到lib目录下.

接下来我们创建applicationContext-mvc.xml文件, 代码如下:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">    <context:component-scan base-package="com.heihu577"/> <!-- 自动扫描包 -->    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">        <!-- 配置视图解析器 -->        <property name="prefix" value="/WEB-INF/pages/"/> <!-- 到该目录下去寻找 -->        <property name="suffix" value=".jsp"/> <!-- 后缀为 .jsp -->    </bean></beans>

随后我们配置Tomcat - WEB-INF - 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>springDispatcherServlet</servlet-name>        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>        <init-param>            <param-name>contextConfigLocation</param-name> <!-- 配置该属性, 自动操作配置文件 -->            <param-value>classpath:applicationContext-mvc.xml</param-value>        </init-param>        <load-on-startup>1</load-on-startup> <!-- WEB 项目启动时, 自动加载 DispatcherServlet -->    </servlet>    <servlet-mapping>        <servlet-name>springDispatcherServlet</servlet-name>        <url-pattern>/</url-pattern> <!-- 用户所有的请求都经过 DispatcherServlet 处理 -->    </servlet-mapping></web-app>

定义一个Spring MVC容器:

/* * 如果使用了 SpringMVC, 在一个类上标识 @Controller * 标识将该类视为一个控制器, 注入到 IOC 容器 * 比原生 Servlet 开发简化很多 * */@Controllerpublic class UserServlet {    /*    * @RequestMapping("/login") 类似于原生的 Tomcat 中的 url-pattern    * 访问: http://localhost/web工程路径/login 即可访问到 login 方法    * */    @RequestMapping("/login")    public String login() {        System.out.println("Login OK ...");        return "login_ok"; // 返回结果给试图解析器, InternalResourceViewResolver, 视图解析器会根据配置, 来决定跳转到哪个页面        /*        根据:            <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">                <!-- 配置视图解析器 -->                <property name="prefix" value="/WEB-INF/pages/"/> <!-- 到该目录下去寻找 -->                <property name="suffix" value=".jsp"/> <!-- 后缀为 .jsp -->            </bean>         得出, 我们最终返回的界面是: ${prefix}返回值${.suffix} -> /WEB-INF/pages/login_ok.jsp        */    }}

随后我们创建静态网页, 如下:

<html><head>    <title>登录</title></head><body><h3>登录</h3><form action="login">    u: <input name="username" type="text"><br>    p: <input name="password" type="password"><br>    <input type="submit" value="登录"></form></body></html>

web.xml 未配置 contextConfigLocation 配置项

如果在web.xml中, 没有配置springDispatcher中的spring配置项, 如图:SpringMVC 基本使用 && 手动实现机制那么会自动寻找/WEB-INF/springDispatcherServlet-servlet.xml文件, 例如:SpringMVC 基本使用 && 手动实现机制

Spring MVC 工作流程图
SpringMVC 基本使用 && 手动实现机制

RequestMapping

RequestMapping 修饰到类上定义路由 && 指定访问方法
@RequestMapping("/data")@Controllerpublic class DataHandler {    @RequestMapping(value = "/t1", method = {RequestMethod.GET, RequestMethod.POST}) // 如果不指定 method, 那么两种方法都可以使用.    public String t1() {        System.out.println("t1...");        return "login_ok";    }}

此时我们访问: http://localhost:8080/WEB工程路径/data/t1, 使用GET|POST, 即可访问到我们的t1方法.

RequestMapping 指明传递进来的参数
@RequestMapping("/data")@Controllerpublic class DataHandler {    // 也可以使用 {"id != 1"}    @RequestMapping(value = "/search", params = {"id=1", "bookId"}, method = RequestMethod.GET)    public String search(String bookId) {        System.out.println("BookId = " + bookId);        return "success";    }}

若指明了params, 那么必须传递bookId参数, 必须传递id=1, 否则程序将不能运行.

RequestMapping 通配符使用

?: 匹配单个字符 *: 匹配文件名中的任意字符 **: 匹配多层路由

例如:

@RequestMapping(value = "/message/**")public String message() {    System.out.println("Message OK!");    return "success";}

就可以通过访问http://localhost:8080/WEB工程路径/message/任意进行匹配.

RequestMapping 提取路径变量
@RequestMapping(value = "/reg/{username}/{id}")public String register(@PathVariable("username") String username, @PathVariable("id") String id) {    System.out.println("Register OK!" + username + " | " + id);    return "success";}

那么我们可以通过http://localhost:8080/WEB工程路径/reg/zhangsan/12, 即可得到Register OK!zhangsan | 12返回结果.

注意事项 && 细节

映射的URL, 不能重复, 例如:

@RequestMapping(value = "/hi")public String hi() {    System.out.println("hi");    return "success";}@RequestMapping(value = "/hi")public String hi2() {    System.out.println("hi");    return "success";}

@RequestMapping(method=RequestMethod.GET)我们可以使用简写形式, 例如:@GetMapping|@PostMapping|@PutMapping|@DeleteMapping.

@Component@RequestMapping("/hacker")public class HackerHandler {    @GetMapping("/get")    public String getMappingTest() { // 只可以通过 GET 方法访问        System.out.println("Get");        return "success";    }    @PostMapping("/post")    public String postMappingTest() { // 只可以通过 POST 方法访问        System.out.println("Post");        return "success";    }}

接收参数, 如代码:

@Component@RequestMapping("/hacker")public class HackerHandler {    @GetMapping("/get")    public String getMappingTest(String username) { // 传递 /WEB工程路径/hacker/get?username=xxx        System.out.println("Get Username = " + username); // 即可接收到 xxx        return "success";    }}

REST 请求风格

简单说明: 因为前端中 form 标签没有提供 put|delete 操作, 只提供了get|post操作, 那么我们就可以通过Spring提供的HiddenHttpMethodFilter过滤器将post请求转换为put请求.SpringMVC 基本使用 && 手动实现机制我们在web.xml文件中加入如下配置:

<filter>    <filter-name>hiddenHttpMethodFilter</filter-name>    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class></filter><filter-mapping>    <filter-name>hiddenHttpMethodFilter</filter-name>    <url-pattern>/*</url-pattern></filter-mapping>

随后在springDispatcherServlet-servlet.xml增加如下标记:

<mvc:annotation-driven/> <!-- 能支持 Spring MVC 高级功能, JSR 303 校验, 映射动态请求 --><mvc:default-servlet-handler/> <!-- 将 Spring MVC 不能处理的请求, 交给 Tomcat 处理, 例如 css js -->

准备如下.jsp文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>rest </title>    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>    <script type="text/javascript">        $(function () {            $("#deleteBook").click(function () {                alert("ok");                var href = this.href;                $("#hiddenForm").attr("action", href);                $(":hidden").val("DELETE");                $("#hiddenForm").submit();//这里就是提交删除请求了//这里必须返回 false,否则会提交两次                return false;            });        })    </script></head><body><h3>Rest 风格的 crud 操作案例</h3><br><hr><h3>rest 风格的 url 查询书籍[get]</h3><a href="user/book/100">点击查询书籍</a><br><hr><h3>rest 风格的 url 添加书籍[post]</h3><form action="user/book" method="post">    name:<input name="bookName" type="text"><br>    <input type="submit" value="添加书籍"></form><br><hr><h3>rest 风格的 url, 删除一本书</h3><!-- 1. 这里我们需要将删除方式(get)转成 delete 的方式,需要使用过滤器和 jquery 来完成2. name="_method" 名字需要写成 _method 因为后台的 HiddenHttpMethodFilter就是按这个名字来获取 hidden 域的值,从而进行请求转换的. --><a href="user/book/100" id="deleteBook">删除指定 id 的书</a><form action="" method="POST" id="hiddenForm">    <input type="hidden" name="_method"/></form><br><hr><h3>rest 风格的 url 修改书籍[put]~</h3><form action="user/book/100" method="post">    <input type="hidden" name="_method" value="PUT">    <input type="submit" value="修改书籍~"></form></body></html>

准备如下Spring MVC容器:

@RequestMapping("/user")@Controllerpublic class UserServlet {    @RequestMapping(value = "/book/{id}", method = RequestMethod.GET)    public String getBook(@PathVariable("id") String id) {        System.out.println("查询书籍 ID: " + id);        return "success";    }    @RequestMapping(value = "/book", method = RequestMethod.POST)    public String addBook(String bookName) {        System.out.println("添加书籍, 书籍名称: " + bookName);        return "success";    }    @DeleteMapping("/book/{id}")    public String deleteBook(@PathVariable("id") String id) {        System.out.println("删除数据, ID: " + id);        // return "success"; // 不可以使用return "success";了, 会返回 JSP 只允许 GET、POST 或 HEAD。Jasper 还允许 OPTIONS        return "redirect:/user/success"; // 跳转到下面的 success 方法中, 解析为 /工程路径/user/success    }    @PutMapping("/book/${id}")    public String updateBook(@PathVariable("id") String id) {        System.out.println("修改数据 ID: " + id);        return "redirect:/user/success"; // 跳转到下面的 success 方法中, 解析为 /工程路径/user/success    }    @RequestMapping("/success")    public String success() {        return "success"; // 转发到 success 页面    }}

这样看可能有点笼统, 可以参考下图:SpringMVC 基本使用 && 手动实现机制

Spring MVC 映射请求数据

在我们前面给的案例中, 接收参数可以直接在方法的参数中指明, 如:

@RequestMapping("/test")public String myTester(String name) // 使用 ?name=xxx 进行传递

但是如果我们想要浏览器传递的参数与方法接收的参数名称并不相同, 则需要使用@RequestParam注解进行指明, 例如:

@RequestMapping("/vote")@Componentpublic class VoteHandler {    @RequestMapping("/vote01")    public String vote01(@RequestParam(value = "name", required = false) String username) {        // 浏览器传入 ?name=xxx 即可接收到        System.out.println("UserName = " + username);        return "success";    }}

获取 HTTP 请求消息头

@RequestMapping("/vote02")public String vote02(@RequestHeader(value = "Accept-Encoding") String header) {    System.out.println(header);    return "success";}

访问: http://localhost/MySpringMVC_Web_exploded/vote/vote02 , 即可得到HTTP请求HEADER: Accept-Encoding项.

JavaBean 获取

我们创建两个JavaBean, 并且存在依赖关系, 如下:

public class Pet {    private Integer id;    private String name;    // getter && setter && toString && 有参构造 && 无参构造}

以及

public class Master {    private Integer id;    private String name;    private Pet pet;    // getter && setter && toString && 有参构造 && 无参构造}

建立起依赖关系后, 我们可以定义Spring MVC如下方式来进行接收参数来生成对象:

@RequestMapping("/getBean")public String vote03(Master master) {    System.out.println(master);    /*        访问: getBean?id=1&name=zhangsan&pet.id=1&pet.name=as 结果: Master{id=1, name='zhangsan', pet=Pet{id=1, name='as'}}        访问: http://localhost/MySpringMVC_Web_exploded/vote/getBean?id=1 结果: Master{id=1, name='null', pet=null}    */    return "success";}

获取 Servlet-api

Tomcat目录下的Servlet-api.jar文件, 引入到我们的WEB项目中. 准备如下JAVA代码:

@RequestMapping("/servlet-api")public String servletApi(HttpServletRequest request, HttpServletResponse response) {    String name = request.getParameter("name"); // 访问 servlet-api?name=hello 即可输出    System.out.println("name => " + name); // name => hello    return "success";}

当然了, 也可以这样获取SESSION:

@RequestMapping("/servlet-api")public String servletApi(HttpServletRequest request, HttpServletResponse response, HttpSession sess) {    String name = request.getParameter("name");    HttpSession session = request.getSession();    System.out.println("name => " + name);    System.out.println("SESSION 01 => " + sess);    System.out.println("SESSION 02 => " + session); // 这两个是同一个对象    return "success";}

模型数据

默认机制存放数据

在我们使用参数接收JavaBean对象时, Spring 会将接收进来的JavaBean自动放入request域中, 如:

@RequestMapping("/master")public String MasterAndPet(Master master) {    // request.setAttribute("master", master) 被 SpringMVC 底层执行了    return "showMaster";}

准备向该方法提交的表单:

<form action="vote/master" method="post">    ID: <input type="text" name="id"><br>    NAME: <input type="text" name="name"><br>    PET-ID: <input type="text" name="pet.id"><br>    PET-NAME: <input type="text" name="pet.name"><br>    <input type="submit"></form>

准备接收数据的表单:

<body>    主人ID: ${requestScope.master.id}<br>    主人NAME: ${requestScope.master.name}<br>    宠物ID: ${requestScope.master.pet.id}<br>    宠物NAME: ${requestScope.master.pet.name}<br>    <!-- 外部数据会展示进来. --></body>
通过 HttpServletRequest 放入 request 域
@RequestMapping("/master-servlet-api")public String masterServletApi(Master master100, HttpServletRequest request) {    master100.setName("zhangsan"); // 修改 master 对象属性的名字, 随后放入到 request    // 存放到 request 域中规则是: 按照类名首字母小写存放    return "success";}
通过请求方法参数 Map<String, Object> 放入 request 域
@RequestMapping("/vote06")public String vote(Master master, Map<String,Object> myMap) {    myMap.put("address", "地址");    return "showMaster";}

会自动解析Map, 扫描Map的所有属性, 加入到requestScope域中, 结果如图:SpringMVC 基本使用 && 手动实现机制

ModelAndView
// modelAndViewTester?id=1&name=zhangsan&pet.id=2&pet.name=lisi@RequestMapping("/modelAndViewTester")public ModelAndView modelAndView(Master master) { // master 会默认放入在 requestScope 中    ModelAndView modelAndView = new ModelAndView();    modelAndView.addObject("address", "hebei"); // 在 return modelAndView 前, modelAndView 中的所有的 key - value 会合并放入 requestScope 中    modelAndView.setViewName("showMaster");    return modelAndView;}

随后使用该jsp进行接收:

<html><head>    <title>Title</title>    <meta charset="utf-8"></head><body>    主人ID: ${requestScope.master.id}<br>    主人NAME: ${requestScope.master.name}<br>    宠物ID: ${requestScope.master.pet.id}<br>    宠物NAME: ${requestScope.master.pet.name}<br>    ${requestScope.address}    <!--         展示效果:             主人ID: 1            主人NAME: zhangsan            宠物ID: 2            宠物NAME: lisi            hebei     --></body></html>
将数据存入 SESSION
@RequestMapping("/testSession")public String test08(Master master, HttpSession httpSession) { // testSession?id=1&name=zhangsan&pet.id=2&pet.name=lisi    httpSession.setAttribute("master", master); // 放入 master, 其实这里 requestScope 中也存在该对象    httpSession.setAttribute("address", "guangzhou"); // 放入 address    return "showMaster";}

随后用如下jsp取出:

<html><head>    <title>Title</title>    <meta charset="utf-8"></head><body>    主人ID: ${sessionScope.master.id}<br>    主人NAME: ${sessionScope.master.name}<br>    宠物ID: ${sessionScope.master.pet.id}<br>    宠物NAME: ${sessionScope.master.pet.name}<br>    地址信息: ${sessionScope.address}    <!--      主人ID: 1        主人NAME: zhangsan        宠物ID: 2        宠物NAME: lisi        地址信息: guangzhou     --></body></html>
@ModelAttribute

类似于AOP中的前置通知, 例如:

@RequestMapping("/testSession")public String test08(Master master, HttpSession httpSession) {    System.out.println("---test08---");    httpSession.setAttribute("master", master);    httpSession.setAttribute("address", "guangzhou");    return "showMaster";}@ModelAttributepublic void prepareFunc() {    System.out.println("---prepareFunc---");}

访问/testSession后, 运行结果如下:

---prepareFunc------test08---

Spring 视图解析器

在我们之前, 使用的都是配置好的InternalResourceViewResolver, 下面我们看一下如何自定义视图解析器.

自定义 Spring 视图解析器

定义自定义视图组件:

@Component(value = "heihuView") // 注入到容器中, 这里 @Component(value = "视图名称")public class MyView extends AbstractView { // 定义好自己的视图解析器    @Override    protected void renderMergedOutputModel(Map<String, Object> map, HttpServletRequest httpServletRequest,                                           HttpServletResponse httpServletResponse) throws Exception {        httpServletRequest.getRequestDispatcher("/WEB-INF/pages/my_view.jsp").forward(httpServletRequest,                httpServletResponse);    }}

随后定义web.xml文件, 配置如下:

<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">    <property name="order" value="99"/> <!-- 配置自定义视图解析器, order 值越小, 优先级越高 -->    <!-- 如果 BeanNameViewResolver 成功解析到视图, 那么就直接返回, 如果解析失败, 就会 解析 InternalResourceViewResolver --></bean>

定义并且配置好之后, 我们可以定义如下控制器:

@Component@RequestMapping("/goods")public class goodsHandler {    @RequestMapping("/buy")    public String buy() {        return "heihuView"; // 会匹配到 heihuView 自定义视图解析器    }}

而我们的heihuView自定义视图解析器最终会请求转发到/WEB-INF/pages/my_view.jsp文件, 所以在这里我们定义该文件:

<html><head>    <title>Title</title></head><body>我的VIEW</body></html>

随后我们就可以访问: /goods/buy即可访问到我们的my_view.jsp文件.SpringMVC 基本使用 && 手动实现机制

InternalResourceViewResolver 默认视图解析器设置优先级

还是按照上面的例子, 定义如下.xml:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">    <!-- 配置视图解析器 -->    <property name="prefix" value="/WEB-INF/pages/"/>    <property name="suffix" value=".jsp"/>    <property name="order" value="1"/> <!-- 配置优先级比 BeanNameViewResolver 高 --></bean><bean class="org.springframework.web.servlet.view.BeanNameViewResolver">    <property name="order" value="99"/> <!-- 配置自定义视图解析器, order 值越小, 优先级越高 -->    <!-- 如果 BeanNameViewResolver 成功解析到视图, 那么就直接返回, 如果解析失败, 就会 解析 InternalResourceViewResolver --></bean>

那么, 接着访问/goods/buy将会爆错:

文件[/WEB-INF/pages/heihuView.jsp] 未找到
多视图解析器 DEBUG 过程

自定义视图处理器优先级大于默认视图处理器时, 如果自定义视图处理器匹配失败, 那么就会调用默认视图处理器, 准备如下代码:

@Component@RequestMapping("/goods")public class goodsHandler {    @RequestMapping("/buy")    public String buy() {        return "????";    }}

当然了, 我们并没有????自定义处理器, 我们看一下解析过程:SpringMVC 基本使用 && 手动实现机制第一个视图解析器报错了, 那么就会进入到第二个视图解析器进行处理.

当我们默认视图处理器>自定义视图处理器时, SpringMVC若找不到/WEB-INF/...的界面, 则会直接报错, 不会再返回头来执行我们自定义视图处理器.

请求转发 && 302 跳转
@RequestMapping("/show")public String show() {    // return "redirect:/WEB-INF/springDispatcherServlet-servlet.xml"; // 302 跳转, 访问不到 /WEB-INF 下的内容    return "forward:/WEB-INF/springDispatcherServlet-servlet.xml"; // 可以直接显示出 xml 的信息, 是请求转发}
请求转发流程分析

SpringMVC 基本使用 && 手动实现机制最终还是进入到了fd.forward方法.

302 跳转流程分析
SpringMVC 基本使用 && 手动实现机制

手动实现 SpringMVC 底层机制

首先我们创建Maven项目, 如图:SpringMVC 基本使用 && 手动实现机制创建完Maven项目之后, 我们对pom.xml增加如下依赖:

<dependencies>  <dependency>    <groupId>junit</groupId>    <artifactId>junit</artifactId>    <version>4.11</version>    <scope>test</scope>  </dependency>  <!--引入原生servlet依赖 的jar-->  <dependency>    <groupId>javax.servlet</groupId>    <artifactId>javax.servlet-api</artifactId>    <version>3.1.0</version>    <!--老韩解读    1. scope标签表示引入的jar的作用范围    2. provided:表示该项目在打包,放到生产环境的时候,不需要带上servlet-api.jar    3. 因为tomcat本身是有servlet的jar, 到时直接使用tomcat本身的servlet-api.jar,防止版本冲突    -->    <scope>provided</scope>  </dependency>  <!--引入dom4j,解析xml文件-->  <dependency>    <groupId>dom4j</groupId>    <artifactId>dom4j</artifactId>    <version>1.6.1</version>  </dependency>  <!--引入常用工具类的jar 该jar含有很多常用的类-->  <dependency>    <groupId>org.apache.commons</groupId>    <artifactId>commons-lang3</artifactId>    <version>3.5</version>  </dependency>  <!--引入jackson 使用他的工具类可以进行json操作 -->  <dependency>    <groupId>com.fasterxml.jackson.core</groupId>    <artifactId>jackson-databind</artifactId>    <version>2.12.4</version>  </dependency></dependencies>

编写 HeihuDispatcherServlet 充当核心控制器

编写 DispatcherServlet 架子

编写该DispatcherServlet, 充当Spring中的核心控制器.

// 充当原生的 DispatcherServlet, 本质是一个 Servlet, 充当 HttpServlet.public class HeihuDispatcherServlet extends HttpServlet {    @Override    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        super.doGet(req, resp);    }    @Override    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {        super.doPost(req, resp);    }}

配置 Tomcat 基本环境

编写之后, 我们进行配置web.xml文件, 配置DispatcherServlet, 如:

<web-app>  <display-name>Archetype Created Web Application</display-name>  <servlet>    <servlet-name>heihuDispatcherServlet</servlet-name>    <servlet-class>com.heihu577.Servlet.HeihuDispatcherServlet</servlet-class>    <init-param>      <param-name>contextLocationConfig</param-name>      <param-value>classpath:HeihuSpringMVC.xml</param-value> <!-- 放入到我们 MAVEN 项目中 resource 目录 -->    </init-param>    <load-on-startup>1</load-on-startup> <!-- 直接启动 -->  </servlet>  <servlet-mapping>    <servlet-name>heihuDispatcherServlet</servlet-name>    <url-pattern>/</url-pattern> <!-- 因为 heihuDispatcherServlet 作为前端控制器, 需要拦截所有 Servlet 请求 -->  </servlet-mapping></web-app>

当然了, 接下来进行配置Tomcat:SpringMVC 基本使用 && 手动实现机制

定义必须的注解 && 定义 Controller

当然了, 因为是手写项目, 所以@Controller, @RequestMapping需要我们自己定义. 如下:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface Controller {    String value() default "";}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD}) // 作用在类上, 并且作用在方法上public @interface RequestMapping {    String value() default "";}

定义完注解之后, 我们定义一个Controller, 如下:

@Controllerpublic class MonsterController {    // 为了看到底层机制, 这里接收两个外部参数    @RequestMapping("/list/monster")    public void listMonsters(HttpServletRequest request, HttpServletResponse response) {        response.setContentType("text/html;charset=utf-8");        try {            PrintWriter writer = response.getWriter();            writer.println("<h3>妖怪列表信息...</h3>");        } catch (IOException e) {            throw new RuntimeException(e);        }    }}

定义 XML 文件 && 读取 XML 文件

随后我们定义resources/HeihuSpringMVC.xml文件, 用来定义扫描包的路径:

<?xml version="1.0" encoding="utf-8"?><beans>    <component-scan base="com.heihu577.Controller"/></beans>

并且定义一个工具类, 用来读取XML配置信息:

public class XMLParser {    public static String getBasePackage(String xmlFile) {        SAXReader saxReader = new SAXReader();        try {            InputStream is = XMLParser.class.getClassLoader().getResourceAsStream(xmlFile);            Document document = saxReader.read(is); // 得到整个文档            Element rootElement = document.getRootElement(); // 得到 beans            Element componentScanElement = rootElement.element("component-scan"); // 得到 component-scan            Attribute attribute = componentScanElement.attribute("base");            String basePackage = attribute.getText();            return basePackage;        } catch (DocumentException e) {            throw new RuntimeException(e);        }    }}

测试方法如下:

public class T1 {    @Test    public void readXml() {        String basePackage = XMLParser.getBasePackage("HeihuSpringMVC.xml");        System.out.println(basePackage); // com.heihu577.Controller    }}

得到扫描类的全路径列表

创建HeihuWebApplicationContext, 内容如下:

public class HeihuWebApplicationContext {    private List<String> classFullPath = new ArrayList<>();    public void scanPackage(String pack) { // com.heihu577 -> com/heihu577        // 得到包所在的工作路径/绝对路径        URL url = this.getClass().getClassLoader().getResource("/" + pack.replace('.', '/'));        System.out.println(url); // 这里测试不要使用 Junit, 否则返回 null, 使用 ApplicationContext 进行测试    }}

当然了, 若我们想要测试的话, 可以在HeihuDispatcherServlet中增加init方法, 例如:

@Overridepublic void init() throws ServletException {    HeihuWebApplicationContext heihuWebApplicationContext = new HeihuWebApplicationContext();    heihuWebApplicationContext.scanPackage(XMLParser.getBasePackage("HeihuSpringMVC.xml"));    // XMLParser.getBasePackage("HeihuSpringMVC.xml") 可以从 HeihuSpringMVC.xml 中得到 component-scan标签中的base值}

运行结果如下:

file:/D:/SoftWare/Java8/other/apache-tomcat-8.5.59/webapps/MySpringMVC04_war/WEB-INF/classes/com/heihu577/Controller/

通过上面的代码, 我们可以正常的得到应该扫描的包的信息了, 下面我们将扫描到的Java类, 放入到容器中:

public class HeihuWebApplicationContext {    private List<String> classFullPath = new ArrayList<>();    public void scanPackage(String pack) { // com.heihu577 -> com/heihu577        // 得到包所在的工作路径/绝对路径        URL url = this.getClass().getClassLoader().getResource("/" + pack.replace('.', '/'));        File file = new File(url.getFile());        if (file.isDirectory()) { // 是一个 [目录|包]            File[] files = file.listFiles();            for (File f : files) {                // 进行遍历该文件                if (f.isDirectory()) { // 是一个目录                    scanPackage(pack + "." + f.getName()); // 上级包名 + . + 下级包名, 进行遍历                } else {                    // 包名.类名                    String classPath = pack + "." + f.getName().replace(".class", "");                    classFullPath.add(classPath);                }            }        }        System.out.println(classFullPath);        /*            [com.heihu577.Controller.A.BBController, com.heihu577.Controller.MonsterController]            其中 com.heihu577.Controller.A.BBController 是新建的一个类        */    }}
多路径扫描

配置HeihuSpringMVC.xml文件如下:

<beans>    <component-scan base="com.heihu577.Controller,com.heihu577.Service"/> <!-- 增加了 Service 包 --></beans>

并且在com.heihu577.Service下进行定义MonsterService接口以及MonsterServiceImpl实现类, 如下:SpringMVC 基本使用 && 手动实现机制定义完毕之后, 我们修改HeihuDispatcherServlet::init方法, 修改为如下:

public void init() throws ServletException {    HeihuWebApplicationContext heihuWebApplicationContext = new HeihuWebApplicationContext();    // - heihuWebApplicationContext.scanPackage(XMLParser.getBasePackage("HeihuSpringMVC.xml"));    String basePackage = XMLParser.getBasePackage("HeihuSpringMVC.xml");    String[] bPackage = basePackage.split(",");    for (String pack : bPackage) {        heihuWebApplicationContext.scanPackage(pack);    }    // +}

运行完毕结果如下:

[com.heihu577.Controller.A.BBController, com.heihu577.Controller.MonsterController, com.heihu577.Service.impl.MonsterServiceImpl, com.heihu577.Service.MonsterService]

将携带 @Controller 的放入到 IOC 容器

在这里可以对扫描到的class进行反射判断了, 并且放入到我们自己的IOC容器中, 定义IOC容器如下:

public Map<String, Object> ioc = new ConcurrentHashMap<>();// ... public void scanPackage(String pack) {    // 扫描包的代码...    executeInstance();}// ...public void executeInstance() {    if (classFullPath.size() == 0) {        return; // 没有 class, 直接返回    }    try {        for (String className : classFullPath) {            Class<?> clazz = Class.forName(className);            if (clazz.isAnnotationPresent(Controller.class)) { // 被 @Controller 修饰                String beanName =                        clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1); // 类名首字母小写                ioc.put(beanName, clazz.newInstance());            }        }    } catch (Exception e) {        throw new RuntimeException(e);    }}

解析 @RequestMapping

我们可以定义一个JavaBean, 用于保存@RequestMapping所声明的控制器, @RequestMapping所修饰的方法, 以及@RequestMapping所定义的URL.

public class HeihuHandler {    private String url; // 保存 url 信息    private Object controller; // 保存对应的 controller    private Method method; // 保存方法信息    // getter && setter && toString ...}

随后在HeihuDispatcherServlet中定义一个List, 用于保存HeihuHandler:

public class HeihuDispatcherServlet extends HttpServlet {    private List<HeihuHandler> heihuHandlerList = new Vector<>();    // ... 其他方法}

定义完毕后, 我们可以遍历HeihuWebApplicationContext::ioc, 将其所有被@RequestMapping修饰的生成为一个HeihuHandler对象, 而HeihuWebApplicationContext::ioc在我们HeihuDispatcherServlet::init方法中被使用, 如下:

@Overridepublic void init() throws ServletException {    HeihuWebApplicationContext heihuWebApplicationContext = new HeihuWebApplicationContext();    String basePackage = XMLParser.getBasePackage("HeihuSpringMVC.xml");    String[] bPackage = basePackage.split(",");    for (String pack : bPackage) {        heihuWebApplicationContext.scanPackage(pack);    }}

接下来将HeihuWebApplicationContext对象定义为成员属性:

private HeihuWebApplicationContext heihuWebApplicationContext = new HeihuWebApplicationContext();@Overridepublic void init() throws ServletException {    String basePackage = XMLParser.getBasePackage("HeihuSpringMVC.xml");    String[] bPackage = basePackage.split(",");    for (String pack : bPackage) {        heihuWebApplicationContext.scanPackage(pack);    }}

随后定义扫描方法:

public void scanRequestMapping() {    Set<Map.Entry<String, Object>> entries = heihuWebApplicationContext.ioc.entrySet();    for (Map.Entry<String, Object> entry : entries) {        Object instance = entry.getValue(); // ioc 容器中的对象        Class<?> clazz = instance.getClass(); // 得到该 Class 类        Method[] declaredMethods = clazz.getDeclaredMethods();        for (Method declaredMethod : declaredMethods) {            if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {                HeihuHandler heihuHandler = new HeihuHandler();                RequestMapping requestMapping = declaredMethod.getAnnotation(RequestMapping.class);                String url = requestMapping.value(); // 得到 url 信息                heihuHandler.setUrl(url);                heihuHandler.setMethod(declaredMethod);                heihuHandler.setController(instance);                heihuHandlerList.add(heihuHandler); // 将扫描成功的 HeihuHandler 存入集合中            }        }    }}

随后在init方法最后一行进行调用即可.

分发请求到目标方法

那么接下来, 我们就可以在doGet方法中, 通过得到外部传递进来的request.getRequestURI与我们扫描到的@RequestMapping -> HeihuHandler进行匹配, 如果匹配上的话, 我们通过反射直接调用即可, 例如:

private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {    String requestURI = request.getRequestURI().replace(request.getContextPath(), "");    // 得到访问来的 URI (/WEB工程路径/访问的路径), 然后将工程路径去除即可    for (HeihuHandler heihuHandler : heihuHandlerList) {        if (heihuHandler.getUrl().equals(requestURI)) {            Object controller = heihuHandler.getController();            Method method = heihuHandler.getMethod();            try {                method.invoke(controller, request, response);            } catch (IllegalAccessException e) {                throw new RuntimeException(e);            } catch (InvocationTargetException e) {                throw new RuntimeException(e);            }        }    }}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {    System.out.println("---------HeihuDispatcherServlet---------doGet---------");    executeDispatch(req, resp);}

运行结果:

@Controllerpublic class MonsterController {    // 为了看到底层机制, 这里接收两个外部参数    @RequestMapping("/list/monster")    public void listMonsters(HttpServletRequest request, HttpServletResponse response) {        response.setContentType("text/html;charset=utf-8");        try {            PrintWriter writer = response.getWriter();            writer.println("<h3>妖怪列表信息...</h3>");        } catch (IOException e) {            throw new RuntimeException(e);        }    }}// 访问 http://localhost/工程路径/list/monster 即可得到 <h3>妖怪列表信息...</h3>

动态获取 spring 配置文件

之前我们的HeihuDispatcherServlet::init获取配置文件信息是写死的, 现在我们动态获取, 修改为如下即可:

@Overridepublic void init(ServletConfig servletConfig) throws ServletException {    String locationConfig = servletConfig.getInitParameter("contextLocationConfig"); // 得到 配置文件中的信息    String basePackage = XMLParser.getBasePackage(locationConfig.split(":")[1]);    String[] bPackage = basePackage.split(",");    for (String pack : bPackage) {        heihuWebApplicationContext.scanPackage(pack);    }    scanRequestMapping();}

完成自定义 @Service 注解

定义MonsterBean如下:

public class Monster {    private Integer id;    private String name;    private String skill;    private Integer age;    // 有参构造 && 无参构造 && setter && getter && toString}

MonsterService接口以及MonsterServiceImpl在之前已写过, 只是没有增加@Service注解, 在这里我们定义@Service注解如下:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface Service {    String value() default "";}

并且修改MonsterService && MonsterServiceImpl为如下:

public interface MonsterService {    public List<Monster> listMonsters();}
@Servicepublic class MonsterServiceImpl implements MonsterService {    @Override    public List<Monster> listMonsters() {        ArrayList<Monster> monsters = new ArrayList<>();        monsters.add(new Monster(1, "张三", "吃饭", 100));        monsters.add(new Monster(2, "李四", "打架", 200));        return monsters;    }}

增加一个方法之后, 我们在扫描Controller注解并放入到IOC容器中的代码块, 进行扫描Service, 并且修改HeihuSpringMVC.xml文件内容如下:

<beans>    <component-scan base="com.heihu577.Controller,com.heihu577.Service"/> <!-- 又扫描 Controller 也扫描 Service --></beans>

HeihuWebApplicationContext::executeInstance中定义如下代码:

if (clazz.isAnnotationPresent(Controller.class)) { // 被 @Controller 修饰    String beanName =            clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1); // 类名首字母小写    ioc.put(beanName, clazz.newInstance());} else if (clazz.isAnnotationPresent(Service.class)) {    Service serviceAnnotation = clazz.getAnnotation(Service.class);    String beanName = serviceAnnotation.value();    Object instance = clazz.newInstance();    if ("".equals(beanName)) { // 当没有配置 Service 注解的 value 值时, 默认得到所有接口名称, 并且接口首字母小写        Class<?>[] interfaces = clazz.getInterfaces();        for (Class<?> anInterface : interfaces) {            beanName =                    anInterface.getSimpleName().substring(0, 1).toLowerCase() + anInterface.getSimpleName().substring(1);            ioc.put(beanName, instance); // 放入到 IOC 容器中        }    }    ioc.put(beanName, instance);}

最终运行结果:

是 BEAN 的类: {monsterService=com.heihu577.Service.impl.MonsterServiceImpl@7972fad2, monsterController=com.heihu577.Controller.MonsterController@477536e8}

完成 @Autowired 注解

增加@Autowired注解如下:

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface Autowired {    String value() default "";}

并且定义@Autowired方法如下:

public void executeAutowired() {    if (ioc.isEmpty()) { // 如果 ioc 是空的, 直接返回        return;    }    Set<Map.Entry<String, Object>> entries = ioc.entrySet();    for (Map.Entry<String, Object> entry : entries) { // 遍历当前 ioc 容器        String key = entry.getKey();        Object bean = entry.getValue(); // bean        Field[] declaredFields = bean.getClass().getDeclaredFields();        for (Field declaredField : declaredFields) {            if (declaredField.isAnnotationPresent(Autowired.class)) { // 被 Autowired 所修饰                Autowired annotation = declaredField.getAnnotation(Autowired.class);                String annotationValue = annotation.value();                String beanName;                if ("".equals(annotationValue)) {                    // @Autowired                    Class<?> typeName = declaredField.getType(); // 类名首字母小写                    beanName =                            typeName.getSimpleName().substring(0, 1).toLowerCase() + typeName.getSimpleName().substring(1);                } else {                    // @Autowired("bean名称")                    beanName = annotationValue;                }                declaredField.setAccessible(true);                try {                    Object o = ioc.get(beanName);                    if (o == null) {                        throw new RuntimeException("bean 不存在!");                    }                    declaredField.set(bean, o);                } catch (IllegalAccessException e) {                    throw new RuntimeException(e);                }            }        }    }}

定义完毕之后, 我们应该在扫描RequestMapping方法前, 进行自动装配Autowired, 所以, 在这里我们可以在HeihuDispatcherServlet::init方法中进行定义如下代码:

public void init(ServletConfig servletConfig) throws ServletException {    String locationConfig = servletConfig.getInitParameter("contextLocationConfig"); // 得到 配置文件中的信息    String basePackage = XMLParser.getBasePackage(locationConfig.split(":")[1]);    String[] bPackage = basePackage.split(",");    for (String pack : bPackage) {        heihuWebApplicationContext.scanPackage(pack);    }    heihuWebApplicationContext.executeAutowired(); // 进行自动装配    scanRequestMapping();}

下面可以修改MonsterController为如下代码:

@Controllerpublic class MonsterController {    @Autowired    private MonsterService monsterService; // 增加 Service 属性    // 为了看到底层机制, 这里接收两个外部参数    @RequestMapping("/list/monster")    public void listMonsters(HttpServletRequest request, HttpServletResponse response) {        response.setContentType("text/html;charset=utf-8");        List<Monster> monsters = monsterService.listMonsters(); // 调用 Service 的 listMonsters 方法        try {            PrintWriter writer = response.getWriter();            writer.println("<h3>妖怪列表信息...</h3>" + monsters); // 将结果打印到界面            /*                运行结果: <h3>妖怪列表信息...</h3>[Monster{id=1, name='张三', skill='吃饭', age=100}, Monster{id=2, name='李四', skill='打架', age=200}]            */        } catch (IOException e) {            throw new RuntimeException(e);        }    }}

完成 @RequestParam 注解 && 接收外部参数

在我们之前HeihuDispatcherServlet::executeDispatch中有如下代码:

private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {    String requestURI = request.getRequestURI().replace(request.getContextPath(), "");    // 得到访问来的 URI (/WEB工程路径/访问的路径), 然后将工程路径去除即可    for (HeihuHandler heihuHandler : heihuHandlerList) {        if (heihuHandler.getUrl().equals(requestURI)) {            Object controller = heihuHandler.getController();            Method method = heihuHandler.getMethod();            try {                method.invoke(controller, request, response); // 注意这里的定义            } catch (IllegalAccessException e) {                throw new RuntimeException(e);            } catch (InvocationTargetException e) {                throw new RuntimeException(e);            }        }    }}

那是因为我们的测试案例是public void listMonsters(HttpServletRequest request, HttpServletResponse response), 但是当我们别的案例不这样接收时, 就会出现问题. 这里解决方法其实也简单, 我们可以看到method.invoke方法中接收的参数:

public Object invoke(Object obj, Object... args)
HttpServletRequest && HttpServletResponse 解决

args接收可变数组, 所以在这里我们就可以进行定义一个数组, 按照顺序保存到一个数组中, 然后传递进去, 最终代码如下:

private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {    String requestURI = request.getRequestURI().replace(request.getContextPath(), "");    // 得到访问来的 URI (/WEB工程路径/访问的路径), 然后将工程路径去除即可    for (HeihuHandler heihuHandler : heihuHandlerList) {        if (heihuHandler.getUrl().equals(requestURI)) {            Object controller = heihuHandler.getController();            Method method = heihuHandler.getMethod();            Class<?>[] parameterTypes = method.getParameterTypes(); // 得到所有参数类型            Object[] myParams = new Object[parameterTypes.length]; // 定义参数数组            for (int i = 0; i < myParams.length; i++) {                // 开始遍历 parameterTypes, 判断类型, 一个一个塞入 myParams 数组                Class<?> paramClazz = parameterTypes[i];                if ("HttpServletRequest".equals(paramClazz.getSimpleName())) {                    // 类型名称为 HttpServletRequest                    myParams[i] = request;                } else if ("HttpServletResponse".equals(paramClazz.getSimpleName())) {                    myParams[i] = response;                }            }            try {                method.invoke(controller, myParams); // 注意这里, 传入可变数组            } catch (IllegalAccessException e) {                throw new RuntimeException(e);            } catch (InvocationTargetException e) {                throw new RuntimeException(e);            }        }    }}

这样解决后, 我们的MonsterController是可以正常运行的.

定义 @RequestParam 注解并接收参数

定义RequestParam注解如下:

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.PARAMETER})public @interface RequestParam {    String value() default "";}

随后我们可以定义/monster/list路由的方法为:

@Controllerpublic class MonsterController {    @Autowired    private MonsterService monsterService;    // 为了看到底层机制, 这里接收两个外部参数    @RequestMapping("/list/monster")    public void listMonsters(HttpServletRequest request, HttpServletResponse response,                             @RequestParam("name") String name) { // 在这里多定义一个 @RequestParam 进行接收        System.out.println("name => " + name);        response.setContentType("text/html;charset=utf-8");        List<Monster> monsters = monsterService.listMonsters();        try {            PrintWriter writer = response.getWriter();            writer.println("<h3>妖怪列表信息...</h3>" + monsters);        } catch (IOException e) {            throw new RuntimeException(e);        }    }}

定义完毕之后, 我们可以在HeihuDispatcherServlet::executeDispatch方法中, 加入如下逻辑:

private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {    String requestURI = request.getRequestURI().replace(request.getContextPath(), "");    // 得到访问来的 URI (/WEB工程路径/访问的路径), 然后将工程路径去除即可    for (HeihuHandler heihuHandler : heihuHandlerList) {        if (heihuHandler.getUrl().equals(requestURI)) {            Object controller = heihuHandler.getController();            Method method = heihuHandler.getMethod();            Class<?>[] parameterTypes = method.getParameterTypes(); // 得到所有参数类型            Object[] myParams = new Object[parameterTypes.length]; // 定义参数数组            for (int i = 0; i < myParams.length; i++) {                // 开始遍历 parameterTypes, 判断类型, 一个一个塞入 myParams 数组                Class<?> paramClazz = parameterTypes[i];                if ("HttpServletRequest".equals(paramClazz.getSimpleName())) {                    // 类型名称为 HttpServletRequest                    myParams[i] = request;                } else if ("HttpServletResponse".equals(paramClazz.getSimpleName())) {                    myParams[i] = response;                }            }            // 新增部分...            for (Map.Entry<String, String[]> requestParam : request.getParameterMap().entrySet()) {                // 对 Request 发送进来的所有值进行遍历                String requestParamKey = requestParam.getKey(); // name=zhangsan 中的 name                String requestParamValue = requestParam.getValue()[0];// name=zhangsan 中的 zhangsan                int indexForRequestParam = getIndexForRequestParam(method, requestParamKey); // 判断是否在方法参数中存在                if (indexForRequestParam != -1) { // 如果在方法中存在的话                    myParams[indexForRequestParam] = requestParamValue; // 那么将值传入进来                }            }            try {                method.invoke(controller, myParams);            } catch (IllegalAccessException e) {                throw new RuntimeException(e);            } catch (InvocationTargetException e) {                throw new RuntimeException(e);            }        }    }}

其中getIndexForRequestParam方法的定义如下:

private int getIndexForRequestParam(Method method, String paramName) {    Parameter[] parameters = method.getParameters(); // 注意这里使用 getParameters, 而不是 getParameterTypes 方法, 返回 Parameter    for (int i = 0; i < parameters.length; i++) {        Parameter parameter = parameters[i];        if (parameter.isAnnotationPresent(RequestParam.class)) {            // 如果被 RequestParam 注解所修饰            RequestParam requestParamAnnotation = parameter.getAnnotation(RequestParam.class);            String dstParamValue = requestParamAnnotation.value();            if (paramName.equals(dstParamValue)) {                return i;            }        }    }    return -1; // 没有找到对应的索引}

其本质上, 遍历request发送来的请求一遍, 再遍历方法中的参数一遍, 遍历完毕之后, 通过比较request的Key && 方法中 @RequestParam 中的value值, 找到对应的索引, 随后进行将其赋值到我们的myParams数组中. 最终运行结果:

http://localhost/MySpringMVC04_war/list/monster?name=zhangsan控制台输出:    ---------HeihuDispatcherServlet---------doGet---------    name => zhangsan
没有定义 @RequestParam 接收参数

通常应用于如下方法声明:

@RequestMapping("/list/add")public void addMonster(String monsterName) {    System.out.println("MonsterName => " + monsterName);}

这种在SpringMVC中, 可以通过传入?monsterName=xxx进行传递, 下面我们看一下目前我们应该如何处理.

当然了, 想要解析String monsterName, 我们必须要拿到monsterName, 那么我们如何拿到monsterName值呢? 在HeihuDispatcherServlet类中定义如下方法:

private List<String> getParamName(Method method) {    List<String> params = new Vector<>();    Parameter[] parameters = method.getParameters();    for (Parameter parameter : parameters) {        params.add(parameter.getName()); // [arg0, arg1, arg2] 与 定义的参数名称无法匹配上    }    System.out.println(params);    return params;}

但最终只能拿到[arg0, arg1, arg2], 并那么不到monsterName, 那么解决方法则是, 我们增加一个maven扩展即可, 如下:

<plugin> <!-- 放在 build - plugins 标签中, 配置完之后 clean 项目, 重启服务器 -->    <groupId>org.apache.maven.plugins</groupId>    <artifactId>maven-compiler-plugin</artifactId>    <version>3.7.0</version>    <configuration>        <source>1.8</source>        <target>1.8</target>        <compilerArgs>            <arg>-parameters</arg>        </compilerArgs>        <encoding>utf-8</encoding>    </configuration></plugin>

按照步骤完成之后, 我们就可以看到正常的结果:

private List<String> getParamName(Method method) {    List<String> params = new Vector<>();    Parameter[] parameters = method.getParameters();    for (Parameter parameter : parameters) {        params.add(parameter.getName()); // [request, response, name]    }    System.out.println(params);    return params;}

那么, 我们在HeihuDispatcherServlet::executeDispatch中, 关键逻辑为如下:

private void executeDispatch(HttpServletRequest request, HttpServletResponse response) {    String requestURI = request.getRequestURI().replace(request.getContextPath(), "");    // 得到访问来的 URI (/WEB工程路径/访问的路径), 然后将工程路径去除即可    for (HeihuHandler heihuHandler : heihuHandlerList) {        if (heihuHandler.getUrl().equals(requestURI)) {            Object controller = heihuHandler.getController();            Method method = heihuHandler.getMethod();            Class<?>[] parameterTypes = method.getParameterTypes(); // 得到所有参数类型            Object[] myParams = new Object[parameterTypes.length]; // 定义参数数组            for (int i = 0; i < myParams.length; i++) {                // 开始遍历 parameterTypes, 判断类型, 一个一个塞入 myParams 数组                // 处理默认的 HttpServletRequest && HttpServletResponse                Class<?> paramClazz = parameterTypes[i];                if ("HttpServletRequest".equals(paramClazz.getSimpleName())) {                    // 类型名称为 HttpServletRequest                    myParams[i] = request;                } else if ("HttpServletResponse".equals(paramClazz.getSimpleName())) {                    myParams[i] = response;                }            }            for (Map.Entry<String, String[]> requestParam : request.getParameterMap().entrySet()) {                // 对 Request 发送进来的所有值进行遍历                String requestParamKey = requestParam.getKey(); // name=zhangsan 中的 name                String requestParamValue = requestParam.getValue()[0];// name=zhangsan 中的 zhangsan                int indexForRequestParam = getIndexForRequestParam(method, requestParamKey); // 判断是否在方法参数中存在                if (indexForRequestParam != -1) { // 如果在方法中存在的话                    // 处理 @RequestParam                    myParams[indexForRequestParam] = requestParamValue; // 那么将值传入进来                } else {                    // 处理 String...                    List<String> paramName = getParamName(method);                    for (int i = 0; i < paramName.size(); i++) {                        if (requestParamKey.equals(paramName.get(i))) {                            // 外部传递进来的 key, 在方法定义中存在                            myParams[i] = requestParamValue;                        }                    }                }            }            try {                method.invoke(controller, myParams); // 调用方法            } catch (IllegalAccessException e) {                throw new RuntimeException(e);            } catch (InvocationTargetException e) {                throw new RuntimeException(e);            }        }    }}

完成视图解析功能

请求转发 && 重定向

参考我们springMVC中, 方法返回String即可对应到对应的页面, 准备如下控制器:

@Controllerpublic class UserController {    @RequestMapping("/user/login")    public String login(String username) {        if ("admin".equals(username)) {            return "forward:/login_ok.jsp";        } else {            return "forward:/login_fail.jsp";        }    }}

随后准备三个界面, 如下:login.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>登录界面</title></head><body><h3>用户登录</h3><form action="user/login" method="post">    用户名称: <input type="text" name="username"><br>    <input type="submit" value="登录"></form></body></html>

login_ok.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>Title</title></head><body><h3>登录成功!</h3></body></html>

login_fail.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>Title</title></head><body><h3>登录失败!</h3></body></html>

那么接下来我们底层的控制逻辑(HeihuDispatcherServlet::executeDispatch 方法中method.invoke)如下:

try {    Object result = method.invoke(controller, myParams);    if (result instanceof String) {        // 是 String 数据类型, 那么我们可以解析 return 回来的字符串        String viewName = (String) result;        if (viewName.contains(":")) {            String type = viewName.split(":")[0]; // 是 forward | redirect            String url = viewName.split(":")[1]; // 返回到哪个 url 上            if ("forward".equals(type)) {                // 服务器请求转发, 无需加 /WEB工程路径/                request.getRequestDispatcher(url).forward(request, response);            } else if ("redirect".equals(type)) {                // 浏览器解析, 需要增加 /WEB工程路径/                response.sendRedirect(request.getContextPath() + "/" + url);            }        } else {            // 默认情况使用请求转发            request.getRequestDispatcher(viewName).forward(request, response);        }    }} catch (IllegalAccessException e) {    throw new RuntimeException(e);} catch (InvocationTargetException e) {    throw new RuntimeException(e);} catch (ServletException e) {    throw new RuntimeException(e);} catch (IOException e) {    throw new RuntimeException(e);}

最终可以正常运行.

返回 json 格式数据

如果一个方法被@ResponseBody所修饰, 那么我们就将该方法返回为json格式数据, 发送到浏览器. 定义ResponseBody注解:

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface ResponseBody {}

MonsterController中定义一个返回json数据的路由:

@RequestMapping("/list/monster/json")@ResponseBodypublic List<Monster> listMonster() {    List<Monster> monsters = monsterService.listMonsters();    return monsters;}

核心控制逻辑增加一个else分支:

try {Object result = method.invoke(controller, myParams);if (result instanceof String) {    // 是 String 数据类型, 那么我们可以解析 return 回来的字符串    String viewName = (String) result;    if (viewName.contains(":")) {        String type = viewName.split(":")[0]; // 是 forward | redirect        String url = viewName.split(":")[1]; // 返回到哪个 url 上        if ("forward".equals(type)) {            // 服务器请求转发, 无需加 /WEB工程路径/            request.getRequestDispatcher(url).forward(request, response);        } else if ("redirect".equals(type)) {            // 浏览器解析, 需要增加 /WEB工程路径/            response.sendRedirect(request.getContextPath() + "/" + url);        }    } else {        // 默认情况使用请求转发        request.getRequestDispatcher(viewName).forward(request, response);    }} else if (result instanceof ArrayList) { // 这里处理 json    ObjectMapper objectMapper = new ObjectMapper(); // jackson 中的对象    response.setContentType("text/json;charset=utf-8");    response.getWriter().println(objectMapper.writeValueAsString(result)); // 转换为 json}

运行完毕后, 访问目标url, 会显示如下:

[{"id":1,"name":"张三","skill":"吃饭","age":100},{"id":2,"name":"李四","skill":"打架","age":200}]

Spring 数据格式化

引入对应的Maven依赖:

<dependencies>    <dependency>        <groupId>junit</groupId>        <artifactId>junit</artifactId>        <version>3.8.1</version>        <scope>test</scope>    </dependency>    <!-- c3p0 -->    <dependency>        <groupId>com.mchange</groupId>        <artifactId>c3p0</artifactId>        <version>0.9.5.2</version>    </dependency>    <!-- classmate -->    <dependency>        <groupId>com.fasterxml</groupId>        <artifactId>classmate</artifactId>        <version>0.8.0</version>    </dependency>    <!-- CGLIB -->    <dependency>        <groupId>cglib</groupId>        <artifactId>cglib</artifactId>        <version>3.3.0</version>    </dependency>    <!-- AOP Alliance -->    <dependency>        <groupId>aopalliance</groupId>        <artifactId>aopalliance</artifactId>        <version>1.0</version>    </dependency>    <!-- AspectJ Weaver -->    <dependency>        <groupId>org.aspectj</groupId>        <artifactId>aspectjweaver</artifactId>        <version>1.9.6</version>    </dependency>    <!-- Commons FileUpload -->    <dependency>        <groupId>commons-fileupload</groupId>        <artifactId>commons-fileupload</artifactId>        <version>1.3.1</version>    </dependency>    <!-- Commons IO -->    <dependency>        <groupId>commons-io</groupId>        <artifactId>commons-io</artifactId>        <version>2.2</version>    </dependency>    <!-- Commons Logging -->    <dependency>        <groupId>commons-logging</groupId>        <artifactId>commons-logging</artifactId>        <version>1.1.3</version>    </dependency>    <!-- Hibernate Validator -->    <dependency>        <groupId>org.hibernate</groupId>        <artifactId>hibernate-validator</artifactId>        <version>5.0.0.CR2</version>    </dependency>    <!-- Hibernate Validator Annotation Processor -->    <dependency>        <groupId>org.hibernate</groupId>        <artifactId>hibernate-validator-annotation-processor</artifactId>        <version>5.0.0.CR2</version>    </dependency>    <!-- Jackson Annotations -->    <dependency>        <groupId>com.fasterxml.jackson.core</groupId>        <artifactId>jackson-annotations</artifactId>        <version>2.9.8</version>    </dependency>    <!-- Jackson Core -->    <dependency>        <groupId>com.fasterxml.jackson.core</groupId>        <artifactId>jackson-core</artifactId>        <version>2.9.8</version>    </dependency>    <!-- Jackson Databind -->    <dependency>        <groupId>com.fasterxml.jackson.core</groupId>        <artifactId>jackson-databind</artifactId>        <version>2.9.8</version>    </dependency>    <!-- JBOSS Logging -->    <dependency>        <groupId>org.jboss.logging</groupId>        <artifactId>jboss-logging</artifactId>        <version>3.1.1.GA</version>    </dependency>    <!-- MySQL Connector/J -->    <dependency>        <groupId>mysql</groupId>        <artifactId>mysql-connector-java</artifactId>        <version>8.0.25</version>    </dependency>    <!-- Servlet API -->    <dependency>        <groupId>javax.servlet</groupId>        <artifactId>javax.servlet-api</artifactId>        <version>3.1.0</version>        <scope>provided</scope>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-aop</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-aspects</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-beans</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-context</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-core</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-expression</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-jdbc</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-orm</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-tx</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-web</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>org.springframework</groupId>        <artifactId>spring-webmvc</artifactId>        <version>5.3.8</version>    </dependency>    <dependency>        <groupId>javax.validation</groupId>        <artifactId>validation-api</artifactId>        <version>1.1.0.CR1</version>    </dependency></dependencies>

引入完毕之后, 按照惯例进行配置web.xml - DispatcherServlet以及spring.xml - InteralResourceViewResolver, 对应的xml配置如下.web.xml:

<servlet>    <servlet-name>DispatcherServlet</servlet-name>    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- dispatcherServlet 处理器 -->    <init-param>      <param-name>contextConfigLocation</param-name>      <param-value>classpath:mySpring.xml</param-value>    </init-param>    <load-on-startup>1</load-on-startup></servlet><servlet-mapping>    <servlet-name>DispatcherServlet</servlet-name>    <url-pattern>/</url-pattern></servlet-mapping>

mySpring.xml:

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">    <context:component-scan base-package="com.heihu577"/>    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!-- 默认视图解析器 -->        <property name="prefix" value="/WEB-INF/pages/"/>        <property name="suffix" value=".jsp"/>    </bean>    <mvc:annotation-driven/> <!-- 能支持 Spring MVC 高级功能, JSR 303 校验, 映射动态请求 -->    <mvc:default-servlet-handler/> <!-- 将 Spring MVC 不能处理的请求, 交给 Tomcat 处理, 例如 css js --></beans>

基本类型和字符串自动转换

首先定义一个Controller, 如下:

@Controller@RequestMapping("/monster")public class MonsterController {    @RequestMapping("/add")    public String addMonsterUI(HttpServletRequest request) {        request.setAttribute("monster", new Monster()); // 准备一个域对象, 名称与 jsp页面中 modelAttribute 值相同        return "monster_add";    }}

随后定义monster_add.jsp页面, 如下:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %><%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>Title</title>    <base href="<%=request.getContextPath()%>/"></head><body>    <form:form action="monster/save" method="post" modelAttribute="monster">        妖怪名: <form:input path="name"/> <br>        妖怪年龄: <form:input path="age"/> <br>        电子邮件: <form:input path="email"/> <br>        <input type="submit" value="添加妖怪">    </form:form></body></html>

当然了, 我们也应该定义一个/monster/save的方法, 如下:

@RequestMapping("/save")public String saveMonster(Monster monster) {    System.out.println("Monster >> " + monster);    /*        public class Monster {            private Integer id;            private String name;            private Integer age;            private String email;        }        当 request 传入进来的参数无法转换为字段所对应的类型时, 会报错, 报错信息如下        Field error in object 'monster' on field 'age': rejected value [1a]; codes [typeMismatch.monster.age,typeMismatch.age,typeMismatch.java.lang.Integer,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [monster.age,age]; arguments []; default message [age]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Integer' for property 'age'; nested exception is java.lang.NumberFormatException: For input string: "1a"]]    */    return "success";}

那么接下来的问题就是, 我们如何让他不要在页面上抛出Tomcat 的错误信息, 如下:SpringMVC 基本使用 && 手动实现机制我们想要在表单后给出信息, 如:SpringMVC 基本使用 && 手动实现机制

特殊类型和字符串转换

在这里我们演示, 日期类型 && 数字类型格式限制, 我们只需要在Monster类中增加两个字段, 并加上约束即可:

@DateTimeFormat(pattern = "yyyy-MM-dd") // 增加 年-月-日 限制private Date birthday;@NumberFormat(pattern = "###,###.##") // # 表示一个 0 ~ 9 的一个数字 特殊符号表示占位符private Float salary;// 并且给出对应的 setter && getter 方法

并且在添加界面增加如下:

<form:form action="monster/save" method="post" modelAttribute="monster">    妖怪名: <form:input path="name"/> <br>    妖怪年龄: <form:input path="age"/> <br>    电子邮件: <form:input path="email"/> <br>    日期: <form:input path="birthday"/> 格式:年-月-日 <br>    薪水: <form:input path="salary"/> 格式: xxx,xxx.xx <br>    <input type="submit" value="添加妖怪"></form:form>

定义完毕之后, 发送如下请求, 得到结果:SpringMVC 基本使用 && 手动实现机制

数据验证

当然了, 上面我们只是进行增加了限制, 下面我们来接收到错误信息, 修改Monster为如下情况:

public class Monster {    private Integer id;    @NotEmpty // 不允许 name 为空    private String name;    @Range(min = 1, max = 100) // 年龄必须在 [1, 100] 区间内    private Integer age;    private String email;    @DateTimeFormat(pattern = "yyyy-MM-dd") // 增加 年-月-日 限制    private Date birthday;    @NumberFormat(pattern = "###,###.##") // # 表示一个 0 ~ 9 的一个数字 特殊符号表示占位符    private Float salary;    // 其他方法...}

对应控制器修改为如下情况:

@RequestMapping("/save")public String saveMonster(@Valid Monster monster, Errors errors, Map<String, Object> map) { // 增加 @Valid 注解, 表示    // @Range    // @NotNull 等规则校验生效    // 如果校验失败, 那么将错误信息保存到 Errors 中...    // map 中保存了错误信息以及 monster 对象    System.out.println("====== monster ======");    System.out.println(monster);    System.out.println("====== errors ======");    List<ObjectError> allErrors = errors.getAllErrors();    for (ObjectError error : allErrors) {        System.out.println(error);    }    System.out.println("====== map ======");    System.out.println(map);    /*    传入 age = 121 时:        ====== monster ======        Monster{id=null, name='zhangsan', age=121, email='[email protected]', birthday=Tue Mar 26 00:00:00 CST 2024, salary=123123.11}        ====== errors ======        Field error in object 'monster' on field 'age': rejected value [121]; codes [Range.monster.age,Range.age,Range.java.lang.Integer,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [monster.age,age]; arguments []; default message [age],100,1]; default message [需要在1和100之间]        ====== map ======        {monster=Monster{id=null, name='zhangsan', age=121, email='[email protected]', birthday=Tue Mar 26 00:00:00 CST 2024, salary=123123.11}, org.springframework.validation.BindingResult.monster=org.springframework.validation.BeanPropertyBindingResult: 1 errors        Field error in object 'monster' on field 'age': rejected value [121]; codes [Range.monster.age,Range.age,Range.java.lang.Integer,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [monster.age,age]; arguments []; default message [age],100,1]; default message [需要在1和100之间]}    */    return "success";}

如果想要将错误信息展示到前端, 我们应该如下配置, jsp界面如下:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %><%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %><html><head>    <title>Title</title>    <base href="<%=request.getContextPath()%>/"></head><body>    <form:form action="monster/save" method="post" modelAttribute="monster">        妖怪名: <form:input path="name"/> <form:errors path="name"/><br>        妖怪年龄: <form:input path="age"/> <form:errors path="age"/><br>        电子邮件: <form:input path="email"/> <form:errors path="email"/><br>        日期: <form:input path="birthday"/> 格式:年-月-日 <form:errors path="birthday"/><br>        薪水: <form:input path="salary"/> 格式: xxx,xxx.xx <form:errors path="salary"/><br>        <input type="submit" value="添加妖怪">    </form:form></body></html>

随后我们配置控制器:

@RequestMapping("/save")public String saveMonster(@Valid Monster monster, Errors errors, Map<String, Object> map) { // 增加 @Valid 注解, 表示    // @Range    // @NotNull 等规则校验生效    // 如果校验失败, 那么将错误信息保存到 Errors 中...    // map 中保存了错误信息以及 monster 对象    if (errors.hasErrors()) {        map.put("errors", errors);        return "monster_add";    } else {        return "success";    }}

当输入错误信息时页面展示:

<!-- 输入 -->妖怪年龄: <input id="age" name="age" type="text" value="121"/> <span id="age.errors">需要在1和100之间</span><br>

修改报错信息

定义如下bean.

<bean class="org.springframework.context.support.ResourceBundleMessageSource">    <property name="basename" value="i18n"/> <!-- 需要对应 classpath:i18n.properties 文件 --></bean>

然后我们观察上面的报错:

codes [Range.monster.age,Range.age,Range.java.lang.Integer,Range];

那么我们i18n.properties的文件内容定义为如下内容:

Range.monster.age=Age Error!!!

那么错误信息就变为了:

妖怪年龄: <input id="age" name="age" type="text" value="121"/> <span id="age.errors">Age Error!!!</span><br>

数据验证细节说明

  1. 在需要验证的JavaBean的字段上加上相应的验证注解.
  2. 在目标方法上, 在JavaBean类型的参数前, 添加 @Valid 注解, 告知 SpringMVCBean 是需要验证的
  3. @Valid 注解之后, 添加一个 Errors 类型的参数, 可以获取到验证的错误信息
  4. 需要使用 <form:errors path="字段名"></form:errors>标签来显示错误消息, 写在<form:form>标签内生效
  5. 错误消息的国际化文件i18n.properties中文需要使用unicode编码

@Range 只决定了该字段的范围, 但是却不阻止该字段是否为 null, 我们可以通过如下方式组合使用, 例如:

@NotNull(message = "age 不能为空")@Range(min = 1, max = 100) // 年龄必须在 [1, 100] 区间内private Integer age;

取消属性绑定

如果不希望服务器接收表单传递进来的某个属性, 那么我们可以使用这项技术. 在MonsterController类中定义一个void initBinder, 并使用注解声明, 如下:

@InitBinderpublic void initBinder(WebDataBinder webDataBinder) {    webDataBinder.setDisallowedFields("name");    // 声明之后, SpringMVC 会拒绝填充 request 请求过来的 name 属性. setDisallowedFields 支持可变参数, 可以填写多个参数.}

当然了, 使用setDisallowedFields指定取消某个属性绑定后, 我们就不要在该字段上增加验证注解了, 例如:@NotEmpty等.SpringMVC 基本使用 && 手动实现机制当然了, 这里name的值是实打实的null, 外面有单引号是因为我们Monster Bean实体的toString方法的定义, 如下:

@Overridepublic String toString() {    return "Monster{" +            "id=" + id +            ", name='" + name + ''' + // 这里强制加了单引号            ", age=" + age +            ", email='" + email + ''' +            ", birthday=" + birthday +            ", salary=" + salary +            '}';}

中文乱码问题解决

创建自己的过滤器

准备Filter:

public class MyCharacterFilter implements Filter {    private String encoding;    @Override    public void init(FilterConfig filterConfig) throws ServletException {        encoding = filterConfig.getInitParameter("encoding");    }    @Override    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {        if (encoding != null) {            servletRequest.setCharacterEncoding("utf-8");        }        filterChain.doFilter(servletRequest, servletResponse);    }    @Override    public void destroy() {}}

随后在web.xml文件中进行配置:

<filter><filter-name>myCharacterFilter</filter-name><filter-class>com.heihu577.WEB.Filter.MyCharacterFilter</filter-class><init-param>  <param-name>encoding</param-name>  <param-value>utf-8</param-value></init-param></filter><filter-mapping><filter-name>myCharacterFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>

增加filter之后, 即可以正常显示信息.

使用 SpringMVC 自带的过滤器

这里可以直接使用SpringMVC所提供的过滤器, 更加方便, 直接配置:

<filter>    <filter-name>characterEncodingFilter</filter-name>    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>    <init-param>      <param-name>encoding</param-name>      <param-value>UTF-8</param-value>    </init-param></filter><filter-mapping>    <filter-name>characterEncodingFilter</filter-name>    <url-pattern>/*</url-pattern></filter-mapping>

SpringMVC 返回|接收 json 数据

ResponseBody 返回 json

@ResponseBody // 加入该注解@RequestMapping("/json")public Monster MonsterAPI() { // 加入返回的对象    Monster monster = new Monster(1, "张三", 1, "[email protected]", new Date(), 12f);    // 访问得到如下界面: {"id":1,"name":"张三","age":1,"email":"[email protected]","birthday":1711618649940,"salary":12.0}    return monster;}

RequestBody 接收 json

定义如下jsp文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>Title</title>    <base href="<%=request.getContextPath()%>/"/>    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>    <script>        $(function () {            $("#go").click(function () {                let userNameVal = $("input[name=username]").val();                let passwordVal = $("input[name=password]").val();                let obj = {username: userNameVal, password: passwordVal}; // key: 对应JavaBean 的字段名称 Value: HTML表单中的value                $.ajax({                    url: $("form").attr("action"), // 请求的 URL                    type: "POST", // 请求方法 (GET, POST, PUT, DELETE 等)                    data: JSON.stringify(obj), // 发送到服务器的数据                    dataType: "json", // 预期服务器返回的数据类型 (xml, html, json, jsonp, script, text)                    contentType: "application/json; charset=utf-8", // 发送信息至服务器时内容编码类型                    headers: { "X-Requested-With": "XMLHttpRequest" }, // 设置请求头                    success: function(data, textStatus, jqXHR) {                        console.log(data);                    }                });                return false;            });        });    </script></head><body><form action="monster/loadJson" method="post">    用户名: <input type="text" name="username"><br>    密码: <input type="password" name="password"><br>    <input id="go" type="submit"></form></body></html>

定义如下MonsterController:

@RequestMapping("/loadJson")@ResponseBody // 这里定义 @ResponseBody 是为了返回 json 数据public User UserAPI(@RequestBody User userJsonObj) { // 这里定义 @RequestBody 是为了接收json数据    System.out.println(userJsonObj); // User{username='12', password='123'}    return userJsonObj;}

最终前端控制台输出如下:SpringMVC 基本使用 && 手动实现机制

  1. 如果在某个控制器中, 所有的方法都需要返回 json 数据, 那么我们可以在该控制器类上进行声明 @ResponseBody 注解.
  2. 如果希望返回 Json 数组, 例如: [obj1,obj2], 那么我们可以声明方法返回类型为 List<具体Bean> 进行返回.
  3. 如果一个类 同时含有 @Controller 以及 @ResponseBody, 那么可以简写为 @RestController

HttpMessageConverter JSON转换器

处理机制如下:SpringMVC 基本使用 && 手动实现机制当控制器处理方法使用到@RequestBody | @ResponseBody | HttpEntity<T> | ResponseEntity<T>时候, Spring 首先根据请求头|响应头Accept属性选择匹配的HttpMessageConverter, 进而根据参数类型或泛型类型的过滤得到匹配的HttpMessageConverter, 若找不到可用的HttpMessageConverter将报错, 我们可以看到实现该接口的类:SpringMVC 基本使用 && 手动实现机制下面我们跟进AbstractJackson2HttpMessageConverter的第268行, 并下断点跟进, 方法声明如下:

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException
SpringMVC 基本使用 && 手动实现机制

SpringMVC 文件上传|下载

文件上传

准备文件上传表单:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>Title</title>    <base href="<%=request.getContextPath()%>/"></head><body>    <form action="file/upload" method="post" enctype="multipart/form-data">        文件介绍: <input type="text" name="introduce"><br>        选择文件: <input type="file" name="file"><br>        <input type="submit">    </form></body></html>

并且定义如下bean:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/><!-- 这里ID不能乱填, 底层只匹配 multipartResolver -->

定义完毕之后, 我们定义一个用于上传文件的控制器:

@Controller@RequestMapping("/file")public class FileUploadController {    @RequestMapping("/upload")    public String upload(@RequestParam("file") MultipartFile multipartFile, HttpServletRequest request,                         String introduce) throws IOException {        String originalFilename = multipartFile.getOriginalFilename(); // 获取文件名        System.out.println("你上传的文件名称: " + originalFilename);        System.out.println("文件介绍: " + introduce);        String dstPath = request.getServletContext().getRealPath("/img/" + originalFilename); // 要上传的最终路径        multipartFile.transferTo(new File(dstPath)); // 文件转存到目标路径        return "success";    }}

文件下载

@RequestMapping("/downFile")public ResponseEntity<byte[]> downFile(HttpSession session) throws IOException {    // 1. 读取到 web 根目录下 /img/img.png 文件, 得到 InputStream 流    InputStream resourceAsStream = session.getServletContext().getResourceAsStream("/img/img.png");    // 2. InputStream::available 可以返回文件大小, 创建相应大小的 byte[] 数组    byte[] byteData = new byte[resourceAsStream.available()];    // 3. InputStream::read(byte[]) 将信息塞入到 byte[] 中    resourceAsStream.read(byteData);    // 4. 创建 HttpStatus 状态码    HttpStatus ok = HttpStatus.OK;    // 5. 创建服务器响应的 header 头    HttpHeaders httpHeaders = new HttpHeaders();    httpHeaders.add("Content-Disposition", "attachment; filename=img.png");    // 6. 创建 ResponseEntity 并返回, 参数分别为: byte[], HTTP Header头, 状态码    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(byteData, httpHeaders, ok);    return responseEntity;}

访问后, 即可下载到img.png文件, 本地路径为: XX盘/WEB工程路径/img/img.png.

自定义拦截器

SpringMVC 基本使用 && 手动实现机制

基本使用

我们可以定义如下拦截器:

@Componentpublic class MyInterceptor implements HandlerInterceptor {    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 方法调用前执行        System.out.println("--MyInterceptor--preHandle()--");        return true; // 当 return false 时, 方法将暂停调用    }    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,                      @Nullable ModelAndView modelAndView) throws Exception { // 方法调用后, 视图显示前执行        System.out.println("--MyInterceptor--postHandle()--");    }    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { // 视图解析后执行        System.out.println("--MyInterceptor--afterCompletion()--");    }}

随后我们可以定义mySpring.xml文件如下:

<mvc:interceptors>    <ref bean="myInterceptor"/> <!-- 配置将其生效 --></mvc:interceptors>

配置成功后, 我们创建如下测试控制器:

@Controller@RequestMapping("/Test")public class TestController {    @RequestMapping("/hi")    public String hi() {        System.out.println("--TestController--hi()--");        /*            访问 /Test/hi 运行结果如下:                --MyInterceptor--preHandle()--                --TestController--hi()--                --MyInterceptor--postHandle()--                --MyInterceptor--afterCompletion()--            运行完毕后, 我们可以看到, 运行流程.        */        return "success";    }}

注意事项 && 细节

默认配置拦截器是所有的目标方法都进行拦截, 也可以指定拦截目标方法, 比如: 只是拦截/Test/hi

<mvc:interceptors>    <mvc:interceptor>        <mvc:mapping path="/Test/hi"/>        <ref bean="myInterceptor"/>    </mvc:interceptor></mvc:interceptors>

那如果我们想拦截/Test下的所有路径, 应该如下操作:

<mvc:interceptors>    <mvc:interceptor>        <mvc:mapping path="/Test/*"/> <!-- 使用 * 号通配符进行匹配 -->        <ref bean="myInterceptor"/>    </mvc:interceptor></mvc:interceptors>

当然了, 如果是通配符, 但是我们又不想拦截/Test/hello, 我们可以这样定义:

<mvc:interceptors>    <mvc:interceptor>        <mvc:mapping path="/Test/*"/>        <mvc:exclude-mapping path="/Test/hello"/>        <ref bean="myInterceptor"/>    </mvc:interceptor></mvc:interceptors>

拦截器链

当笔者配置如下拦截器:

<mvc:interceptors>    <mvc:interceptor>        <mvc:mapping path="/Test/*"/>        <ref bean="myInterceptor"/>    </mvc:interceptor>    <mvc:interceptor>        <mvc:mapping path="/Test/*"/>        <ref bean="my2Interceptor"/>    </mvc:interceptor></mvc:interceptors>

那么将形成拦截器链.

假设有A拦截器以及B拦截器:SpringMVC 基本使用 && 手动实现机制具体访问规则如下: browser -> A::pre -> B::pre -> H::hi -> B::post -> A::post -> render -> B::after -> A::after -> browser

定义完毕之后, 看一下运行结果:

--MyInterceptor--preHandle()----My2Interceptor--preHandle----TestController--hi()----My2Interceptor--postHandle----MyInterceptor--postHandle()----My2Interceptor--afterCompletion----MyInterceptor--afterCompletion()--

实际应用

定义一个/User/Search?username=XXX用户名查询界面, 如果传递进来的username是admin, 那么直接拦截, 并给出信息: 禁止查询管理员用户的信息. 定义Controller:

@Controller@RequestMapping("/user")public class UserController {    @RequestMapping("/search")    public String search(String username) {        return "success";    }}

定义拦截器:

@Componentpublic class UserInterceptor implements HandlerInterceptor {    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        String username = request.getParameter("username");        if ("admin".equals(username)) {            request.getRequestDispatcher("/WEB-INF/pages/error.jsp").forward(request, response);            return false;        }        return true;    }    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,                           @Nullable ModelAndView modelAndView) throws Exception {    }    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,                                @Nullable Exception ex) throws Exception {    }}

配置拦截器:

<mvc:interceptors>    <mvc:interceptor>        <mvc:mapping path="/user/search"/>        <ref bean="userInterceptor"/>    </mvc:interceptor></mvc:interceptors>

SpringMVC 异常处理

如果不处理异常, 那么页面的报错信息将及其不友好, 如下:SpringMVC 基本使用 && 手动实现机制

局部异常

局部异常, 是在控制器中, 增加一个由@ExceptionHandler所声明的方法, 如下:

@Controller@RequestMapping("/Exception")public class MyExceptionController {    @ExceptionHandler({ArithmeticException.class, NullPointerException.class})    // Class<? extends Throwable>[] value() default {}; 可以填写多个异常信息    public String localException(Exception ex, HttpServletRequest request) {        System.out.println("局部异常信息: " + ex.getMessage());        request.setAttribute("reason", ex.getMessage()); // 将异常信息放入到 request 域中        return "error";    }    @RequestMapping("/Test01")    public String test01(Integer number) {        int i = 9 / number; // 当出现异常错误, 将调用 localException 方法, 其核心验证逻辑也是出现的错误类型, 与localException上定义的注解进行匹配.        return "success";    }}

准备error.jsp页面如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %><html><head>    <title>Title</title></head><body><h4>NONONO ErrorMsg: ${requestScope.reason}</h4></body></html>

访问/Exception/Test01?number=0时, 结果如下:

<h4>NONONO ErrorMsg: / by zero</h4>

全局异常

准备如下控制器:

@RequestMapping("/Test02")public String test02(String number) {    int result = Integer.parseInt(number); // 当无法进行转换时, 会抛出 NumberFormatException    return "success";}

随后我们定义全局异常处理类, 如下:

@ControllerAdvice // 标注该注解, 就是全局异常处理类public class MyGlobalException {    @ExceptionHandler({NumberFormatException.class, ClassCastException.class})    public String globalException(Exception ex, HttpServletRequest request) {        System.out.println("全局异常处理 ...");        request.setAttribute("reason", ex.getMessage());        return "error";    }}

进行捕获异常, 传入 Exception/Test02?number=abc后前端error.jsp页面显示如下:

<h4>NONONO ErrorMsg: For input string: "abc"</h4>

当异常发生时, 局部异常的优先级要大于全局异常的优先级.

自定义异常

SpringMVC中, 允许自定义一个异常, 来返回某个返回值 | HTTP状态码的展示.

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "年龄出现错误...")public class AgeException extends RuntimeException {}

其中code参数的参数如下:

public enum HttpStatus {    CONTINUE(100, HttpStatus.Series.INFORMATIONAL, "Continue"),    SWITCHING_PROTOCOLS(101, HttpStatus.Series.INFORMATIONAL, "Switching Protocols"),    PROCESSING(102, HttpStatus.Series.INFORMATIONAL, "Processing"),    CHECKPOINT(103, HttpStatus.Series.INFORMATIONAL, "Checkpoint"),    OK(200, HttpStatus.Series.SUCCESSFUL, "OK"),    CREATED(201, HttpStatus.Series.SUCCESSFUL, "Created"),    ACCEPTED(202, HttpStatus.Series.SUCCESSFUL, "Accepted"),    // ... 更多其它的, 跟进 HttpStatus 源码即可.}

随后我们定义如下控制器:

@RequestMapping("/Test03")public String test03() {    throw new AgeException();}

随后界面将返回400, 并给出信息: 年龄出现错误....

当然了, 我们也可以在局部异常 | 全局异常

统一处理异常信息

定义如下控制器:

@RequestMapping("/Test04")public String test04() {    int[] intArr = new int[]{1, 3, 5};    System.out.println(intArr[90]); // 会抛出 ArrayIndexOutOfBoundsException    return "success";}

那么我们配置统一异常处理器:

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">    <property name="exceptionMappings">        <props>            <prop key="java.lang.ArrayIndexOutOfBoundsException">ArrayIndexOutOfBoundsException</prop>            <!-- 配置好该项目, 最终界面会请求转发到 ArrayIndexOutOfBoundsException 页面, 因为配置了 InternalResourceViewResolver, 会根据 InternalResourceViewResolver 的规则跳转到具体界面 -->        </props>    </property></bean>

定义/WEB-INF/pages/ArrayIndexOutOfBoundsException.jsp页面如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head>    <title>数组越界异常...</title></head><body><h3>你已越界...</h3></body></html>

正常访问即可, 当然也可以进行配置Exception, 因为Exception中有很多子类, 就不进行演示了.

异常优先级: 局部异常 > 全局异常 > 统一处理异常 > Tomcat 默认机制

原文始发于微信公众号(Heihu Share):SpringMVC 基本使用 && 手动实现机制

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年7月23日16:23:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   SpringMVC 基本使用 && 手动实现机制https://cn-sec.com/archives/2989374.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息