SpringMVC 基本使用 && 手动实现机制
SpringMVC 是基于 Spring 的 WEB 应用
SpringMVC 从易用性,效率上 比曾经流行的 Struts2更好 SpringMVC 是 WEB 层框架 SpringMVC 通过注解,让 POJO 成为控制器,不需要继承类或者实现接口 SpringMVC 采用低耦合的组件设计方式,具有更好扩展和灵活性 支持 REST 格式的 URL 请求 SpringMVC 是基于 Spring 的, 也就是 SpringMVC 是在 Spring 基础上的。SpringMVC 的核心包 spring-webmvc-xx.jar 和 spring-web-xx.jar
环境搭建 && 使用方法
基本使用方法
下面我们可以创建一个Tomcat
项目, 如图:当然了, 我们下面在
WEB-INF/lib
目录下放置一系列jar
包, 如图:其中这些
.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 开发简化很多
* */
public class UserServlet {
/*
* @RequestMapping("/login") 类似于原生的 Tomcat 中的 url-pattern
* 访问: http://localhost/web工程路径/login 即可访问到 login 方法
* */
"/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配置项
, 如图:那么会自动寻找
/WEB-INF/springDispatcherServlet-servlet.xml
文件, 例如:
Spring MVC 工作流程图
RequestMapping
RequestMapping 修饰到类上定义路由 && 指定访问方法
@RequestMapping("/data")
@Controller
public 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")
@Controller
public 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
请求.我们在
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")
@Controller
public 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 页面
}
}
这样看可能有点笼统, 可以参考下图:
Spring MVC 映射请求数据
在我们前面给的案例中, 接收参数可以直接在方法的参数中指明, 如:
@RequestMapping("/test")
public String myTester(String name) // 使用 ?name=xxx 进行传递
但是如果我们想要浏览器传递的参数
与方法接收的参数名称并不相同, 则需要使用@RequestParam
注解进行指明, 例如:
@RequestMapping("/vote")
@Component
public 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
域中, 结果如图:
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";
}
@ModelAttribute
public 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
文件.
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
若找不到/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 的信息, 是请求转发
}
请求转发流程分析
最终还是进入到了
fd.forward
方法.
302 跳转流程分析
手动实现 SpringMVC 底层机制
首先我们创建Maven项目, 如图:创建完
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
:
定义必须的注解 && 定义 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
, 如下:
@Controller
public 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
方法, 例如:
@Override
public 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
实现类, 如下:定义完毕之后, 我们修改
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
方法中被使用, 如下:
@Override
public 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();
@Override
public 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);
}
}
}
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("---------HeihuDispatcherServlet---------doGet---------");
executeDispatch(req, resp);
}
运行结果:
@Controller
public 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
获取配置文件信息是写死的, 现在我们动态获取, 修改为如下即可:
@Override
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);
}
scanRequestMapping();
}
完成自定义 @Service 注解
定义Monster
Bean如下:
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();
}
@Service
public 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
为如下代码:
@Controller
public 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
路由的方法为:
@Controller
public 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
即可对应到对应的页面, 准备如下控制器:
@Controller
public 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")
@ResponseBody
public 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 的错误信息
, 如下:我们想要在表单后给出信息, 如:
特殊类型和字符串转换
在这里我们演示, 日期类型 && 数字类型格式限制, 我们只需要在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>
定义完毕之后, 发送如下请求, 得到结果:
数据验证
当然了, 上面我们只是进行增加了限制, 下面我们来接收到错误信息, 修改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>
数据验证细节说明
-
在需要验证的 JavaBean
的字段上加上相应的验证注解. -
在目标方法上, 在 JavaBean
类型的参数前, 添加@Valid
注解, 告知SpringMVC
该Bean
是需要验证的 -
在 @Valid
注解之后, 添加一个Errors
类型的参数, 可以获取到验证的错误信息 -
需要使用 <form:errors path="字段名"></form:errors>
标签来显示错误消息, 写在<form:form>
标签内生效 -
错误消息的国际化文件 i18n.properties
中文需要使用unicode
编码
@Range 只决定了该字段的范围, 但是却不阻止该字段是否为 null, 我们可以通过如下方式组合使用, 例如:
@NotNull(message = "age 不能为空")
@Range(min = 1, max = 100) // 年龄必须在 [1, 100] 区间内
private Integer age;
取消属性绑定
如果不希望服务器接收表单传递进来的某个属性, 那么我们可以使用这项技术. 在MonsterController
类中定义一个void initBinder
, 并使用注解声明, 如下:
@InitBinder
public void initBinder(WebDataBinder webDataBinder) {
webDataBinder.setDisallowedFields("name");
// 声明之后, SpringMVC 会拒绝填充 request 请求过来的 name 属性. setDisallowedFields 支持可变参数, 可以填写多个参数.
}
当然了, 使用setDisallowedFields
指定取消某个属性绑定后, 我们就不要在该字段上增加验证注解
了, 例如:@NotEmpty
等.当然了, 这里name的值是实打实的
null
, 外面有单引号是因为我们Monster Bean
实体的toString
方法的定义, 如下:
@Override
public 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;
}
最终前端控制台输出如下:
如果在某个控制器中, 所有的方法都需要返回 json 数据, 那么我们可以在该控制器类上进行声明 @ResponseBody 注解. 如果希望返回 Json 数组, 例如: [obj1,obj2], 那么我们可以声明方法返回类型为 List<具体Bean> 进行返回. 如果一个类 同时含有 @Controller 以及 @ResponseBody, 那么可以简写为 @RestController
HttpMessageConverter JSON转换器
处理机制如下:当控制器处理方法使用到
@RequestBody | @ResponseBody | HttpEntity<T> | ResponseEntity<T>
时候, Spring 首先根据请求头|响应头
的Accept
属性选择匹配的HttpMessageConverter
, 进而根据参数类型或泛型类型的过滤得到匹配的HttpMessageConverter
, 若找不到可用的HttpMessageConverter
将报错, 我们可以看到实现该接口的类:下面我们跟进
AbstractJackson2HttpMessageConverter
的第268
行, 并下断点跟进, 方法声明如下:
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException
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
.
自定义拦截器
基本使用
我们可以定义如下拦截器:
@Component
public 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拦截器:具体访问规则如下: 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";
}
}
定义拦截器:
@Component
public 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 异常处理
如果不处理异常, 那么页面的报错信息将及其不友好, 如下:
局部异常
局部异常, 是在控制器中, 增加一个由@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 基本使用 && 手动实现机制
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论