SpringMVC学习总结 + 【手写SpringMVC底层机制核心】
SpringMVC笔记
SpringMVC介绍
基本介绍
- SpringMVC 是WEB 层框架, 接管了Web 层组件, 支持MVC 的开发模式/开发架构
- SpringMVC 通过注解,让POJO 成为控制器,不需要继承类或者实现接口
- SpringMVC 采用低耦合的组件设计方式,具有更好扩展和灵活性.
- 支持REST 格式的URL 请求.
Spring MVC和Spring之间的关系
- Spring MVC 只是Spring 处理WEB 层请求的一个模块/组件, Spring MVC 的基石是Servlet 【Java WEB】
- Spring Boot 是为了简化开发者的使用, 推出的封神框架(约定优于配置,简化了Spring的配置流程), SpringBoot 包含很多组件/框架,Spring 就是最核心的内容之一,也包含SpringMVC
- Spring Boot > Spring > Spring MVC
@RequestMapping
基本介绍
@RequestMapping 注解可以指定控制器/处理器的某个方法的请求的url
@RequestMapping(value = "/buy")
public String buy(){
System.out.println("购买商品");
return "success";
}
其它使用方式
-
@RequestMapping 可以修饰方法和类
- @RequestMapping 注解可以修饰方法,还可以修饰类当同时修饰类和方法时,
请求的url 就是组合/类请求值/方法请求值
@RequestMapping(value = "/user" ) @Controller public class UserHandler { @RequestMapping(value = "/buy") public String buy(){ System.out.println("购买商品"); return "success"; } }
此时url = /user/buy
- @RequestMapping 注解可以修饰方法,还可以修饰类当同时修饰类和方法时,
-
@RequestMapping 可以指定请求方式
- @RequestMapping 还可以指定请求的方式(post/get/put/delete..), 请求的方式需
要和指定的一样,否则报错
- @RequestMapping 还可以指定请求的方式(post/get/put/delete..), 请求的方式需
@RequestMapping(value = "/find", method = RequestMethod.GET)
public String search(String bookId) {
System.out.println("查询书籍bookId= " + bookId);
return "success";
}
- SpringMVC 控制器默认支持GET 和POST 两种方式, 也就是你不指定method , 可以接收
GET 和POST 请求
@RequestMapping(value = "/buy")//默认支持GET 和POST 两种方式
public String buy(){
System.out.println("购买商品");
return "success";
}
3.@RequestMapping 可指定params 和headers 支持简单表达式
-
param1: 表示请求必须包含名为param1 的请求参数
-
!=param1: 表示请求不能包含名为param1 的请求参数
-
param1 != value1: 表示请求包含名为param1 的请求参数,但其值不能为value1
-
{"param1=value1", "param2"}: 请求必须包含名为param1 和param2 的两个请求参数,
且param1 参数的值必须为value1
@RequestMapping(value = "/find", params = "bookId", method = RequestMethod.GET)
public String search(String bookId) {
System.out.println("查询书籍bookId= " + bookId);
return "success";
}
4.@RequestMapping 支持Ant 风格资源地址
- ?:匹配文件名中的一个字符
- *:匹配文件名中的任意字符
- **:匹配多层路径
- Ant 风格的url 地址举例
/user/*/createUser: 匹配/user/aaa/createUser、/user/bbb/createUser 等URL
/user/**/createUser: 匹配/user/createUser、/user/aaa/bbb/createUser 等URL
/user/createUser??: 匹配/user/createUseraa、/user/createUserbb 等URL
- Ant 风格的url 地址举例
5.@RequestMapping 可配合@PathVariable 映射URL 绑定的占位符
- 不需要在url 地址上带参数名了,更加的简洁明了
// 我们希望目标方法获取到username 和userid, value="/xx/{username}" -@PathVariable("username")
@RequestMapping(value = "/reg/{username}/{userId}")
public String register(@PathVariable("username") String username,
@PathVariable("userId") String userId){
System.out.println("接收到参数--" + "username= " + username + "--" + "usreid= " + userId);
return "success";
}
<a href="user/reg/kristina/300">占位符的演示</a>
6.请求的简写形式
@GetMapping @PostMapping @PutMapping @DeleteMapping
Rest请求风格
基本介绍
- REST:即Representational State Transfer。(资源)表现层状态转化。是目前流行的请求方式。它结构清晰, 很多网站采用。
- HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:
- GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。
传统的请求方法
在url中:
getBook?id=1 GET
delete?id=1 GET
update POST
add POST
说明: 传统的url 是通过参数来说明crud 的类型,rest 是通过get/post/put/delete 来说明crud 的类型
REST 的核心过滤器
- 当前的浏览器只支持post/get 请求,因此为了得到put/delete 的请求方式需要使用Spring
提供的HiddenHttpMethodFilter 过滤器进行转换. - HiddenHttpMethodFilter:浏览器form 表单只支持GET 与POST 请求,而DELETE、PUT
等method 并不支持,Spring 添加了一个过滤器,可以将这些请求转换为标准的http 方
法,使得支持GET、POST、PUT 与DELETE 请求 - HiddenHttpMethodFilter 能对post 请求方式进行转换
- 过滤器需要在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>
当我们需要 用delete/put 的方式时,
name="_method" 名字需要写成_method 因为后台的HiddenHttpMethodFilter
就是按这个名字来获取hidden 域的值,从而进行请求转换的.
<form action="user/book/100" method="post">
<input type="hidden" name="_method" value="PUT">
<input type="submit" value="修改书籍~">
</form>
public String delBook(@PathVariable("id") String id) {
System.out.println("删除书籍id= " + id);
//return "success"; [如果这样返回会报错JSPs only permit GET POST orHEAD]
return "redirect:/user/success"; //重定向到一个没有指定method 的Handler 方法
}
注意事项
- HiddenHttpMethodFilter, 在将post 转成delete / put 请求时,是按_method 参数名来读取的
- 如果web 项目是运行在Tomcat 8 及以上,会发现被过滤成DELETE 和PUT 请求,到达控制器时能顺利执行,但是返回时(forward)会报HTTP 405 的错误提示:消息JSP 只允许GET、POST 或HEAD。
- 因此,将请求转发(forward)改为请求重定向(redirect):重定向到一个Handler,由Handler 转发到页面
SpringMVC映射请求数据
获取参数值
/**
* @RequestParam(value="name", required=false)
* 1.@RequestParam : 表示说明一个接受到的参数
* 2.value="name" : 接收的参数名是name
* 3.required=false : 表示该参数可以有,也可以没有,如果required=true,表示必须传递该参数.
* 默认是required=true
*/
@RequestMapping("vote01")
public String test01(@RequestParam(value = "name",required = false) String username){
System.out.println("得到的username= "+username);
return "success";
}
- @RequestParam 表示会接收提交的参数
- value = "name" 表示提交的参数名是name
- required = false 表示该参数可以没有,默认是true 表示必须有这个参数
- 当我们使用@RequestParam(value = "name",required = false) 就表示 请求的参数名和目标方法的形参名 可以不一致
获取Http请求消息头
@RequestMapping(value = "/vote02")
public String test02(@RequestHeader(value = "Accept-Encoding")String ae,
@RequestHeader(value = "Host")String host) {
System.out.println("Accept-Encoding =" + ae);
System.out.println("Host =" + host);
//返回到一个结果
return "success";
}
获取javabean 形式的数据
@RequestMapping(value = "/vote03")
public String test03(Master master) {
System.out.println("主人信息= " + master);
//返回结果
return "success";
}
-
方法的形参 (Master master) 用对应的对象类型指定即可,SpringMVC会自动的进行封装
-
如果自动的完成封装,要求提交的数据参数名和对象的字段名保持一致
-
支持级联数据获取: 如果对象的属性任然是一个对象,就通过 字段名.字段名 来提交
比如Master[Pet] ,可以通过 pet.id ,pet.name 来指定
-
如果参数名和字段名不匹配,则对象的属性就是null
ServletAPI
- 开发中原生的ServletAPI仍然可以使用
@RequestMapping(value = "/vote04")
public String test04(HttpServletRequest request,HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("username= "+username+" ,password= "+password);
//返回结果
return "success";
}
- 除了HttpServletRequest, HttpServletResponse 还可以其它对象也可以这样的形式获取
- HttpSession、java.security.Principal,InputStream,OutputStream,Reader,Writer
模型数据
数据放入Request域
通过HttpServletRequest放入Request域
- 通过HttpServletRequest放入Request域是默认机制
@RequestMapping(value = "/vote05")
public String test05(Master master) {
return "vote_ok";
}
- 当我们提交的数据和对象名保持一致,那么SpringMVC回自动封装成一个对象【在前面 获取javabean 形式的数据 讲过】
- SpringMVC还会有一个机制,会自动将封装的这个对象【model模型】放入Request域
- 也可以手动将对象放入Request域
- 会以 k-v 的形式放入Request域,k 是类名首字母小写,v 是对象
-
如果我们需要向Request域中添加新的属性/对象
request.setAttribute("address","beijing");
-
如果我们要修改默认机制自动放入的对象的属性
master.setName("pp");
通过请求的方法参数Map<String,Object>放入Request域
@RequestMapping(value = "/vote06")
public String test06(Master master, Map<String,Object> map) {
map.put("address","tianjing");
map.put("master",null);
//返回到一个结果
return "vote_ok";
}
- SpringMVC会遍历Map,然会将map中的 k-v 存放到Request域
- 如果 map.put("master",null); 会覆盖默认机制的master,为null
通过返回ModelAndView对象实现Request域数据
@RequestMapping(value = "/vote07")
public ModelAndView test07(Master master) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("address","shanghai");
modelAndView.addObject("master",null);
//指定跳转的视图名称
modelAndView.setViewName("vote_ok");
return modelAndView;
}
- SpringMVC会遍历ModelAndView,然会将ModelAndView中的 k-v 存放到Request域
- 如果 modelAndView.addObject("master",null); 会覆盖默认机制的master,为null
注意事项:
- 从本质上看,请求响应的方法 return "xx",是返回了一个字符串,其实本质是返回一个ModelAndView对象,只是默认被封装起来了
- ModelAndView对象既可以包含model数据,也可以包含视图信息
- ModelAndView对象的addObject("","");方法 可以添加key -value数据,默认在Request域中
- ModelAndView对象setView方法是指定 视图名称
数据放入Session域
@RequestMapping(value = "/vote08")
public String test08(Master master,HttpSession session){
session.setAttribute("master",master);
session.setAttribute("address","guangzhou");
return "vote_ok";
}
@ModelAttribute
@ModelAttribute
public void prepareModel(){
System.out.println("prepareModel()-----完成准备工作-----");
}
- 在某个方法上加上 @ModelAttribute 注解,那么在调用该Handler的任何方法都会调用这个方法
- 类似Aop的前置通知
视图和视图解析器
基本介绍
- 在springMVC 中的目标方法最终返回都是一个视图(有各种视图).
- 返回的视图都会由一个视图解析器来处理(视图解析器有很多种)
自定义视图
- 在默认情况下,我们都是返回默认的视图, 然后这个返回的视图交由SpringMVC 的 InternalResourceViewResolver 视图处理器来处理的
前缀 value="/WEB-INF/pages/" 和 后缀 value=".jsp" 之后会拼接 返回给视图解析器的返回值
<!-- 配置默认视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!-- 配置属性 prefix 前缀 和 suffix 后缀-->
<!-- 这里的前缀 value="/WEB-INF/pages/" 和 后缀 value=".jsp" 之后会拼接 返回给视图解析器的返回值
例如 UserSerlet return "login ok";
就会拼接成 /WEB-INF/pages/login_ok.jsp 从而进行跳转-->
<property name="prefix" value="/WEB-INF/pages/"/>
<property name="suffix" value=".jsp"/>
</bean>
2. 在实际开发中,我们有时需要自定义视图,这样可以满足更多更复杂的需求.
需要在spring配置文件 , 增加自定义视图解析器
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="99"></property>
</bean>
- name="order" :表示给这个解析器设置优先级,
- 我们自己的视图解析优先级高,Order 值越小,优先级越高
编写自己的视图
- 继承 AbstractView 就可以作为一个视图使用
- @Component(value = "myView")会作为id= myView 的一个组件 注入到容器中
@Component(value = "zyView")
public class MyView extends AbstractView {
@Override
protected void renderMergedOutputModel(Map<String, Object> map,
HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
//该方法完成视图渲染
//并且可以确定我们要跳转的页面 /WEB-INF/pages/my_view.jsp
System.out.println("进入到自己的视图..");
//请求转发到 /WEB-INF/pages/my_view.jsp
//第一个斜杠会解析成 工程路径-> springmvc/
httpServletRequest.getRequestDispatcher("/WEB-INF/pages/my_view.jsp")
.forward(httpServletRequest,httpServletResponse);
}
}
进行跳转
@RequestMapping("/goods")
@Controller
public class GoodsHandler {
@RequestMapping(value = "/buy")
public String buy() {
System.out.println("--buy()被调用--");
return "zyView";
}
}
自定义视图小结
-
自定义视图: 创建一个View 的bean, 该bean 需要继承自AbstractView, 并实现 renderMergedOutputModel 方法.
-
并把自定义View 加入到IOC 容器中
-
自定义视图的视图处理器,使用BeanNameViewResolver, 这个视图处理器也需要配置到ioc 容器
-
BeanNameViewResolver 的调用优先级需要设置一下,设置order 比Integer.MAX_VAL 小的值. 以确保其在InternalResourceViewResolver 之前被调用
自定义视图-工作流程
-
SpringMVC 调用目标方法, 返回自定义View 在IOC 容器中的id
-
SpringMVC 调用BeanNameViewResolver 解析视图: 从IOC 容器中获取返回id 值对应的bean, 即自定义的View 的对象
-
SpringMVC 调用自定义视图的renderMergedOutputModel 方法渲染视图
-
如果在SpringMVC 调用目标方法, 返回自定义View 在IOC 容器中的id不存在, 则仍然按照默认的视图处理器机制处理
自定义解析器的执行流程-源码
/**
* 自定义解析器的执行流程
* 1.
* @RequestMapping(value = "/buy")
* public String buy() {
* System.out.println("--buy()被调用--");
* return "zyView"; -->
* }
* 2.
* ApplicationContext context = obtainApplicationContext();
* if (!context.containsBean(viewName)) {//判断viewName是否在容器中
* // Allow for ViewResolver chaining...
* return null;
* }
* if (!context.isTypeMatch(viewName, View.class)) {//判断是否继承了 AbstractView 实际上是判断是否实现了View接口 因为AbstractView实现了View接口
* if (logger.isDebugEnabled()) {
* logger.debug("Found bean named '" + viewName + "' but it does not implement View");
* }
* // Since we're looking into the general ApplicationContext here,
* // let's accept this as a non-match and allow for chaining as well...
* return null;
* }
* return context.getBean(viewName, View.class); -->
* }
* 3.
* protected void renderMergedOutputModel(Map<String, Object> map,
* HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
*
* //该方法完成视图渲染
* //并且可以确定我们要跳转的页面 /WEB-INF/pages/my_view.jsp
* System.out.println("进入到自己的视图..");
*
* //请求转发到 /WEB-INF/pages/my_view.jsp
* //第一个斜杠会解析成 工程路径-> springmvc/
* httpServletRequest.getRequestDispatcher("/WEB-INF/pages/my_view.jsp")
* .forward(httpServletRequest,httpServletResponse); -->
* }
* }
* 4.
* <h1>进入到my_view页面</h1>
* <p>是从自定义视图来的...</p>
*
*
*/
默认解析器的执行流程-源码
/**
* 默认解析器的执行流程
* 1.
* public String buy() {
* System.out.println("--buy()被调用--");
* return "zyView";->
* }
* 2.
* public InternalResourceViewResolver(String prefix, String suffix) {
* this();
* setPrefix(prefix);//用于拼接
* setSuffix(suffix);//用于拼接
* }
* 3.
* protected AbstractUrlBasedView buildView(String viewName) throws Exception {
* InternalResourceView view = (InternalResourceView) super.buildView(viewName);//调用super.buildView(viewName)
* if (this.alwaysInclude != null) {
* view.setAlwaysInclude(this.alwaysInclude);
* }
* view.setPreventDispatchLoop(true);
* return view;
* }
* 4.
* 找不到 报错404
*/
找不到自定义解析器 会调用默认解析器
/**
* 找不到自定义解析器 会调用默认解析器
* 1.
* public String buy() {
* System.out.println("--buy()被调用--");
* return "zyView"; -->
* }
* 2.
* ApplicationContext context = obtainApplicationContext();
* if (!context.containsBean(viewName)) {//找不到返回null
* // Allow for ViewResolver chaining...
* return null;-->
* }
* 3.
* if (this.viewResolvers != null) {
* for (ViewResolver viewResolver : this.viewResolvers) { //遍历解析器
* View view = viewResolver.resolveViewName(viewName, locale);//此时以及走默认解析器那一套了
* if (view != null) {
* return view;//进行拼接前缀和后缀 但找不到
* }
* }
* 4.
* 拼接后找不到 报错 404
*/
默认解析器一旦解析 不会去自定义解析器
- 因为默认解析器会拼接 view != null 就return了
* if (this.viewResolvers != null) {
* for (ViewResolver viewResolver : this.viewResolvers) {
* View view = viewResolver.resolveViewName(viewName, locale);
* if (view != null) {
* return view;
* }
* }
目标方法直接指定转发或重定向
- 默认返回的方式是请求转发,然后用视图处理器进行处理
@RequestMapping(value = "/buy")
public String buy(){
return "success";
}
- 也可以在目标方法直接指定重定向或转发的url 地址
请求转发:return "forword:路径"
return "forword:/WEB-INF/pages/my_view.jsp"
重定向:return "redirect:路径"
return "redirect:login.jsp"
注意事项:
-
对于重定向,不能重定向到WEB-INF目录下
重定向在响应头返回的URL中是 /工程路径/login.jsp【此时,浏览器会将第一个 / 处理成 IP:端口/工程路径/login.jsp】
-
对于请求转发,是在服务器内部进行,第一个 / 解析成 工程路径,进行转发
数据格式化
基本介绍
引出:在我们提交数据(比如表单时)SpringMVC 怎样对提交的数据进行转换和处理的??
- 基本数据类型可以和字符串之间自动完成转换
- Spring MVC 上下文中内建了很多转换器,可完成大多数Java 类型的转换工作
基本数据类型和字符串自动转换
<form:form action="save" method="post" modelAttribute="monster">
妖怪名字: <form:input path="name"/> <form:errors path="name"/> <br><br>
妖怪年龄~: <form:input path="age"/> <form:errors path="age"/><br><br>
电子邮件: <form:input path="email"/> <form:errors path="email"/><br><br>
妖怪生日: <form:input path="birthday"/> <form:errors path="birthday"/>要求以"9999-11-11"的形式<br><br>
妖怪薪水: <form:input path="salary"/> <form:errors path="salary"/>要求以"123,890.12"的形式<br><br>
<input type="submit" value="添加妖怪"/>
</form:form>
- SpringMVC 表单标签在显示之前必须在request 中有一个bean, 该bean 的属性和表单标签的字段要对应!
- request 中的key 为: form 标签的modelAttribute 属性值, 比如这里的monsters
- SpringMVC 的form:form 标签的action 属性值中的/ 不代表WEB 应用的根目录.
@RequestMapping(value = "/addMonsterUI")
public String addMonsterUI(Map<String, Object> map) {
//这里需要给request 增加一个monster , 因为jsp 页面的modelAttribute="monster"需要
//是springMVC 的内部的检测机制即使是一个空的也需要,否则报错
map.put("monster", new Monster());
return "datavalid/monster_addUI";
}
说明:
- 当我们在浏览器发送 age=10 时,会把10转换成String类型,到达后端后,又会把String转成 int/Integer
- 而发送 age=aaa 时,会把aaa转成 string类型,到达后端后,把String类型 的 aaa转换成 int/Integer,此时会报错
特殊数据类型和字符串间的转换
-
特殊数据类型和字符串之间的转换使用注解(比如日期,规定格式的小数比如货币形式等)
-
对于日期和货币可以使用@DateTimeFormat 和@NumberFormat 注解. 把这两个注解标记在字段上即可.(JavaBean上)
@DateTimeFormat(pattern = "yyy-MM-dd") private Date birthday;
验证国际化
- 对输入的数据(比如表单数据),进行必要的验证,并给出相应的提示信息
- 对于验证表单数据,springMVC 提供了很多实用的注解, 这些注解由JSR 303 验证框架提供.
JSR 303 验证框架
-
JSR 303 是Java 为Bean 数据合法性校验提供的标准框架,它已经包含在JavaEE 中
-
JSR 303 通过在Bean 属性上标注类似于@NotNull、@Max 等标准的注解指定校验规则,
并通过标准的验证接口对Bean 进行验证 -
JSR 303 提供的基本验证注解有:
Hibernate Validator 扩展注解
-
Hibernate Validator 和Hibernate 没有关系,只是JSR 303 实现的一个扩展.
-
Hibernate Validator 是JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支
持以下的扩展注解: -
扩展注解有如下:
@NotNull(message = "年龄必须1-100")
@Range(min = 1,max = 100)
private Integer age;
@NotNull
private String name ;
@NotNull(message = "生日必须符合格式")
@DateTimeFormat(pattern = "yyy-MM-dd")
private Date birthday;
@NotNull(message = "salary必须符合格式")
@NumberFormat(pattern = "###,###.##")
private Float salary;
@RequestMapping(value = "/save")
public String save(@Valid Monster monster, Errors errors, Map<String,Object> map) {}
- @Valid Monster monster:表示对monster 接收的数据进行校验
- Errors errors: 表示如果校验出现错误,将校验的错误信息保存到 errors 中
- Map<String,Object> map:如果校验出现错误,会将校验的错误信息保存到map,并且同时保存 monster对象
- 在前端使用<form:errors path="name"/> 回显错误信息
妖怪名字: <form:input path="name"/> <form:errors path="name"/> <br><br>
妖怪年龄~: <form:input path="age"/> <form:errors path="age"/><br><br>
电子邮件: <form:input path="email"/> <form:errors path="email"/><br><br>
妖怪生日: <form:input path="birthday"/> <form:errors path="birthday"/>要求以"9999-11-11"的形式<br><br>
妖怪薪水: <form:input path="salary"/> <form:errors path="salary"/>要求以"123,890.12"的形式<br><br>
自定义验证错误信息
1.需要在Spring配置文件中配置相关bean
<!-- 配置国际化错误信息的资源处理bean -->
<bean id="messageSource" class=
"org.springframework.context.support.ResourceBundleMessageSource">
<!-- 配置国际化文件名字
如果你这样配的话,表示messageSource 回到src/i18nXXX.properties 去读取错误信息
-->
<property name="basename" value="i18n"></property>
</bean>
2.需要在src目录下创建 i18nxxx.properties去读取错误信息
NotEmpty.monster.name=\u7528\u6237\u540d\u4e0d\u80fd\u4e3a\u7a7a
typeMismatch.monster.age=\u5e74\u9f84\u8981\u6c42\u5728\u0031\u002d\u0031\u0035\u0030\u4e4b\u95f4
typeMismatch.monster.birthday=\u751f\u65e5\u683c\u5f0f\u4e0d\u6b63\u786e
typeMismatch.monster.salary=\u85aa\u6c34\u683c\u5f0f\u4e0d\u6b63\u786e
3.用工具将希望自定义的错误信息转换成Unicode编码填入i18nxxx.properties
注意事项
-
在需要验证的Javabean/POJO 的字段上加上相应的验证注解.
-
目标方法上,在JavaBean/POJO 类型的参数前, 添加@Valid 注解. 告知SpringMVC该bean 是需要验证的
@RequestMapping(value = "/save")
public String save(@Valid Monster monster, Errors errors, Map<String,Object> map) {}
- 在@Valid 注解之后, 添加一个Errors 或BindingResult 类型的参数, 可以获取到验证的错误信息
- 需要使用<form:errors path="email"></form:errors> 标签来显示错误消息, 这个标签,需要写在form:form 标签内生效.
<form:form action="save" method="post" modelAttribute="monster">
妖怪名字: <form:input path="name"/> <form:errors path="name"/> <br><br>
</form:form>
-
错误消息的国际化文件i18n.properties , 中文需要是Unicode 编码,使用工具转码.
格式: 验证规则.表单modelAttribute 值.属性名=消息信息NotEmpty.monster.name=\u540D\u5B57\u4E0D\u80FD\u4E3A\u7A7A
typeMismatch.monster.age=\u7C7B\u578B\u4E0D\u5339\u914D -
注解@NotNull 和@NotEmpty 的区别说明
-
@NotEmpty 是判断 null 的 并且可以接收 任何类型
-
@NotNull 是判断null 和 empty的,接收String,collection,map和array
-
如果是字符串验证空, 建议使用@NotEmpty
- SpingMVC 验证时,会根据不同的验证错误, 返回对应的信息
注解组合使用
- 使用@NotNull + @Range 组合使用
@NotNull(message = "年龄必须1-100")
@Range(min = 1,max = 100)
private Integer age;
取消属性绑定 @InitBinder 注解
- 不希望接收到某个表单对应的属性的值,则可以通过@InitBinder 注解取消绑定.
- 使用@InitBinder 标识的该方法,可以对WebDataBinder 对象进行初始化。
- WebDataBinder 是DataBinder 的子类,用于完成由表单字段到JavaBean 属性的绑定
- @InitBinder 方法不能有返回值,它必须声明为void。
- @InitBinder 方法的参数通常是是WebDataBinder
@InitBinder
public void initBinder(WebDataBinder webDataBinder){
webDataBinder.setDisallowedFields("name");//表示取消属性name的绑定; 这里可以填写多个字段
}
- 取消属性的绑定,那么在JavaBean中的校验注解也应该去掉
中文乱码问题处理
- Spring提供的过滤器处理中文
<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>
处理JSON格式
@ResponseBody
- 服务器 -----JSON数据-----> 客户端/浏览器
@RequestMapping(value = "/json/dog")
@ResponseBody
public Dog getJson(){
Dog dog = new Dog("大黄", "贝加尔");
return dog;
}
@RequestBody
客户端/浏览器-----JSON数据-----> 服务器
@RequestMapping(value = "/save2")
@ResponseBody
public User save2(@RequestBody User user){
System.out.println("user= "+user);
return user;
}
- 将客户端/浏览器发送的json字符串数据封装成 JavaBean对象
- 再把这个 JavaBean对象 以 json对象形式返回
- (@RequestBody User user)在形参上指定
- SpringMVC就会将提交的json字符串数据填充给指定JavaBean
注意事项
- 目标方法正常返回json需要的数据,可以是对象也可以是集合
- @ResponseBody可以写在类上,这样对该类所有的方法生效
- @ResponseBody + @Controller 可以直接写成@RestController
HttpMessageConverter
- SpringMVC 处理JSON-底层实现是依靠HttpMessageConverter
来进行转换的
工作机制简图
-
使用HttpMessageConverter
将请求信息转化并绑定到处理方法的入参中, 或将响应结果转为对应类型的响应信息,Spring 提供了两种途径: -
使用@RequestBody / @ResponseBody 对目标方法进行标注
-
使用HttpEntity
/ ResponseEntity 作为目标方法的入参或返回值
-
-
当控制器处理方法使用到@RequestBody/@ResponseBody 或HttpEntity
/ResponseEntity 时, Spring 首先根据请求头或响应头的Accept 属性选择匹配的HttpMessageConverter, 进而根据参数类型或泛型类型的过滤得到匹配的HttpMessageConverter, 若找不到可用的HttpMessageConverter 将报错
SpringMVC文件上传
- 在SpringMVC 中,通过返回ResponseEntity
的类型,可以实现文件下载的功能 - 需要构建 ResponseEntity 对象,需要1.得到http响应头 2.http响应状态 3.下载文件的数据
@RequestMapping(value="/downFile")
public ResponseEntity<byte[]> downFile(HttpSession session) throws Exception{
//1.先获取到要下载 的 InputStream
InputStream resourceAsStream =
session.getServletContext().getResourceAsStream("/img/1.jpg");
//2.开辟存放文件的byte数组 -> 支持二进制数据
//resourceAsStream.available() 返回资源文件的大小
byte[] bytes = new byte[resourceAsStream.available() ];
//3. 将要下载的文件数据读入到byte数组
resourceAsStream.read(bytes);
/*
ResponseEntity 的构造器:
public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, HttpStatus status) {
this(body, headers, (Object) status);
}
*/
//准备构造ResponseEntity 对象
//4.构建httpStatus
HttpStatus httpStatus = HttpStatus.OK;
//5.构建http响应头
HttpHeaders headers = new HttpHeaders();
//指定浏览器以附件的形式处理回送数据
headers.add("Content-Disposition","attachment;filename=1.jpg");
ResponseEntity<byte[]> responseEntity =
new ResponseEntity<>(bytes, headers, httpStatus);
return responseEntity;
}
-
文件下载响应头的设置
content-type 指示响应内容的格式
content-disposition 指示如何处理响应内容,一般有两种方式:1. inline:直接在页面显示 2.attchment:以附件形式下载
SpringMVC文件上传
基本介绍
- Spring MVC 为文件上传提供了直接的支持,
- 这种支持是通过即插即用的MultipartResolver 实现的。
- Spring 用Jakarta Commons FileUpload 技术实现了一个MultipartResolver 实现类:CommonsMultipartResovler
- Spring MVC 上下文中默认没有装配MultipartResovler,因此默认情况下不能处理文件的上传工作,
- 如果想使用Spring 的文件上传功能,需现在上下文中配置MultipartResolver
<!--文件上传-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>
- 需要在form表单添加属性 enctype="multipart/form-data"
<form action="<%=request.getContextPath()%>/fileUpload" method="post" enctype="multipart/form-data">
文件介绍:<input type="text" name="introduce"><br>
选择文件:<input type="file" name="file"><br>
<input type="submit" value="上传文件">
</form>
文件上传
//编写方法 处理文件上传的请求
@RequestMapping(value = "/fileUpload")
public String fileUpload(@RequestParam(value = "file") MultipartFile file,
HttpServletRequest request, String introduce) throws IOException {
//接收到提交的文件名
String originalFilename = file.getOriginalFilename();
System.out.println("提交的文件名= " + originalFilename);
System.out.println("文件介绍= " + introduce);
//获取到文件保存的目标位置/路径
String filePath =
request.getServletContext().getRealPath("/img/" + originalFilename);
//创建文件
File saveToFile = new File(filePath);
//将上传的文件转存到 saveToFile
file.transferTo(saveToFile);
return "success";
}
拦截器
基本介绍
- Spring MVC 也可以使用拦截器对请求进行拦截处理,
- 用户可以自定义拦截器来实现特定的功能.
- 自定义的拦截器必须实现HandlerInterceptor 接口
自定义拦截器的三个方法
-
preHandle():这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求request 进行处理。
-
postHandle():这个方法在目标方法处理完请求后执行
-
afterCompletion():这个方法在完全处理完请求后被调用,可以在该方法中进行一些资源
清理的操作。
在Spring配置文件中配置拦截器
默认配置是都所有的目标方法都进行拦截, 也可以指定拦截目标方法, 比如只是拦截hi
<mvc:interceptors>
<ref bean="myInterceptor01"/>//直接引用对应拦截器
<mvc:interceptor>
<mvc:mapping path="/hi"/>
<ref bean="myInterceptor01"/>
</mvc:interceptor>
</mvc:interceptor>
mvc:mapping 支持通配符, 同时指定不对哪些目标方法进行拦截
<mvc:interceptor>
<mvc:mapping path="/h*"/>
<mvc:exclude-mapping path="/hello"/>
<ref bean="myInterceptor01"/>
</mvc:interceptor>
- 这样配置会拦截h打头的url指定的方法
<mvc:interceptor>
<mvc:mapping path="/h*"/>
<mvc:exclude-mapping path="/hello"/>
<ref bean="myInterceptor02"/>
</mvc:interceptor>
- 这样配置会拦截h打头的url指定的方法并且排除 url是hello的方法
自定义拦截器执行流程分析图
● 自定义拦截器执行流程说明
- 如果preHandle 方法返回false, 则不再执行目标方法, 可以在此指定返回页面
- postHandle 在目标方法被执行后执行. 可以在方法中访问到目标方法返回的 ModelAndView 对象
- 若preHandle 返回true, 则afterCompletion 方法在渲染视图之后被执行.
- 若preHandle 返回false, 则afterCompletion 方法不会被调用
- 在配置拦截器时,可以指定该拦截器对哪些请求生效,哪些请求不生效
注意事项
- 拦截器需要配置才生效,不配置是不生效的.
- 如果preHandler() 方法返回了false, 就不会执行目标方法(前提是你的目标方法被拦截了), 程序员可以在这里根据业务需要指定跳转页面.
多个拦截器
注意事项
-
如果第1 个拦截器的preHandle() 返回false , 后面都不在执行
-
如果第2 个拦截器的preHandle() 返回false , 就直接执行第1 个拦截器的
afterCompletion()方法, 如果拦截器更多,规则类似 -
前面说的规则,都是目标方法被拦截的前提
异常处理
基本介绍
-
Spring MVC 通过HandlerExceptionResolver 处理程序的异常,包括Handler 映射、数据绑定以及目标方法执行时发生的异常。
-
主要处理Handler 中用@ExceptionHandler 注解定义的方法。
-
ExceptionHandlerMethodResolver 内部若找不到@ExceptionHandler 注解的话, 会找@ControllerAdvice 类的@ExceptionHandler 注解方法, 这样就相当于一个全局异常处理器
-
如果不去处理异常,tomcat会默认机制处理,用户看到的页面非常不友好
异常处理的优先级
局部异常 > 全局异常 > SimpleMappingExceptionResolver > tomcat默认机制
局部异常
//局部异常就是直接在这个Handler 中编写即可
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String localException(Exception ex,HttpServletRequest request){
System.out.println("异常信息是~" + ex.getMessage());
//如何将异常的信息带到下一个页面.
request.setAttribute("reason", ex.getMessage());
return "exception_mes";
}
全局异常
@ControllerAdvice//@ControllerAdvice 表示了该注解,就是全局异常
public class MyGlobalException {
@ExceptionHandler({NumberFormatException.class, ClassCastException.class})
public String globalException(Exception ex, HttpServletRequest request){
System.out.println("全局异常处理---"+ex.getMessage());
request.setAttribute("reason",ex.getMessage());
return "exception_mes";
}
- @ControllerAdvice 表示了该注解,就是全局异常
自定义异常
- 通过@ResponseStatus 注解, 可以自定义异常
- 格式:@ResponseStatus(reason = "异常原因",value = httpStatus状态 )
@ResponseStatus(reason = "年龄需要在1-120之间",value = HttpStatus.BAD_REQUEST )
public class AgeException extends RuntimeException{//需要继承RuntimeException/Exception
}
- 自定义异常类需要继承RuntimeException/Exception
- httpStatus会有很多状态
-
如果想在其他页面看到reason信息,加上带String构造器即可
public AgeException(String message) { super(message); }
统一处理异常信息
基本介绍
-
如果希望对所有异常进行统一处理,可以使用SimpleMappingExceptionResolver
-
它将异常类名映射为视图名,即发生异常时使用对应的视图报告异常
-
需要在ioc 容器中配置
<!-- 统一处理异常-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.Exception">allEx</prop>
</props>
</property>
</bean>
- 在这个标签内 就可以配置出现异常需要跳转的页面
allEx - key="java.lang.Exception" 是异常的范围,这样设置可以对未知异常进行统一处理,也就是所有异常都处理
SpringMVC执行流程以及源码剖析
SpringMVC执行流程示意图
执行流程-源码剖析
-
发出请求url
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { logRequest(request);
-
调用处理器映射器
//getWebApplicationContext() 就是Spring容器 request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); //走完这一步前端控制器就有了Spring容器 doDispatch(request, response);//进入分发 2.1 HandlerExecutionChain mappedHandler = null;//有属性 处理器链 2.2 mappedHandler = getHandler(processedRequest);// mappedHandler中已经有 目标Handler 和 拦截器链
-
调用处理器适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());//得到处理器适配器 String method = request.getMethod();得到请求方式
-
调用Handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//调用Handler mav = invokeHandlerMethod(request, response, handlerMethod); invocableMethod.invokeAndHandle(webRequest, mavContainer);//调用Handler中的目标方法了 ModelAndView modelAndView = new ModelAndView();//到达目标方法 return invoke0(this.method, var1, var2);//目标方法执行完毕返回 return getModelAndView(mavContainer, modelFactory, webRequest); return mav;//返回给前端处理器 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//回到起点,返回了一个 ModelAndView
-
调用视图解析器
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); render(mv, request, response);//进行渲染 //进入render 方法 String viewName = mv.getViewName();//拿到视图名称 view = resolveViewName(viewName, mv.getModelInternal(), locale, request);//进行视图解析 //调用视图解析器 if (this.viewResolvers != null) { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale);//解析完毕返回视图 if (view != null) { return view;//返回视图给前端控制器 } } }
-
视图渲染
view.render(mv.getModelInternal(), request, response); renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);//渲染合并 输出模型 //进入方法 RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);//获得RequestDispatcher
-
返回响应
rd.forward(request, response);//返回响应
手写SpringMVC底层机制!
-
前景提要:实现的是SpringMVC核心机制
-
对一些细枝末节的代码做了简化,比如字符串的处理...
-
完成哪些机制
- 机制一: 通过@RequestMapping ,可以标记一个方法,编写路径url,浏览器就能通过url完成调用
- 机制二: 进行依赖注入,使之不需要传统的new 一个对象,而是直接从IOC容器中获得
- 机制三:通过@RequestParam,如果浏览器传递的参数名和目标方法的形参不一致,可以通过value设置进行匹配
- 机制四:在目标方法完成后,跳转到相关页面 请求转发/重定向
- 机制五:在目标方法完成后,通过@Response注解,向浏览器发送JSON格式数据
手写添加配置
思路
- 需要配置pom.xml的依赖
- 需要写一个Servlet 作为前端控制器
- 需要配置Web.xml 中的前端控制器 1).url 2)配置spring容器配置文件的classpath: 3)跟随Tomcat 自启动
- 需要配置spring容器配置文件
- 需要配置spring容器配置文件 扫描的路径 <component-scan ...>
实现
- 需要配置pom.xml的依赖
<!-- 配置原生Servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
<!-- <scope> 表示引入的jar的作用范围
provided 表示该项目在打包 放到生产环境时,不需要带上 servlet-api.jar
因为tomcat本身有 servlet-api.jar,到时直接使用tomcat本身的 servlet-api.jar-->
</dependency>
<!-- 配置dom4j-->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.1</version>
</dependency>
<!-- 配置常用的工具类-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
- 需要写一个Servlet 作为前端控制器
public class ZyDispatcherServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("ZyDispatcherServlet-doPost--");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("ZyDispatcherServlet-doGet--");
}
}
-
需要配置Web.xml 中的前端控制器 1).url 2)配置spring容器配置文件的classpath: 3)跟随Tomcat 自启动
-
需要配置spring容器配置文件 扫描的路径 <component-scan ...>
<servlet>
<servlet-name>ZyDispatcherServlet</servlet-name>
<servlet-class>com.code_study.zyspringmvc.servlet.ZyDispatcherServlet</servlet-class>
<!--配置参数,指定要操作的spring配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:zyspringmvc.xml</param-value>
</init-param>
<!--跟随tomcat自启动-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ZyDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
- 需要配置spring容器配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<beans>
<!-- 要扫描的包-->
<component-scan base-package="com.code_study.controller,com.code_study.service"></component-scan>
</beans>
完成浏览器可以请求控制层
思路
- 创建@Controller和自己的Controller
- 编写工具类XMLParser,解析spring容器配置文件
- 开发自己的 Spring容器,得到扫描类的全路径列表
- 开发自己的 前端控制器,实例化对象到容器中
- 完成请求的URL和控制器方法的映射关系
- 完成前端控制器分发请求到对应控制器
- 自定义注解@Controller
- 创建Controller
- 需要写一个工具类XMLParser来解析在spring容器配置文件 扫描的路径 <component-scan ...> 的包 返回所有的路径
- 这个所有的路径 一个split(",")分隔,都进行扫描
- 需要写自己的 前端控制器
- 需要写自己的 Spring容器
- 在前端控制器中 需要添加方法 scanPackage() 扫描 XMLParser 解析出来的路径
- 在Spring容器中 需要添加一个属性 classFullPathList 来保存扫描出来的类的全路径
- 需要添加一个属性 ioc 来存放反射生成的bean对象 也就是过滤classFullPathList 中没有@Controller注解的一些路径 并实例化
- 需要添加类Handler 这个类要保存 一个url 对应的 一个控制器的方法的 映射 ,也就是说,根据这个url,可以找到对应控制器的对应方法
- 需要添加一个属性 HandlerList 用于 保存Handler 【url 和 控制器的映射】
- 需要添加三个方法 一个是initHandlerMapping(),完成 url 对应的 一个控制器的方法的 映射,即 将ioc 中bean 中的 方法进行反射,获取url,将 url,method,bean 封装成Handler 放入HandlerList 保存
- 添加第二个方法 getHandler(),需要将浏览器发送的request请求中的 uri拿出来,遍历HandlerList 进行配对,如果有 就返回对应的Handler
- 添加第三个方法 executeDispatch(),进行分发处理,需要 调用getHandler() 获取浏览器发送的request请求 对应的 Handler ,获取Handler 中的method 进行反射调用,method .invoke() 实现分发请求。
实现
- 自定义注解@Controller
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
- 创建Controller
@Controller
public class MonsterController {
}
- 需要写一个工具类XMLParser来解析在spring容器配置文件 扫描的路径 <component-scan ...> 的包 返回所有的路径
public class XMLParser {
public static String getBasePackage(String xmlFile){
SAXReader saxReader = new SAXReader();
ClassLoader classLoader = XMLParser.class.getClassLoader();
InputStream resourceAsStream = classLoader.getResourceAsStream(xmlFile);
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
Element element = rootElement.element("component-scan");
String basePackage = element.attribute("base-package").getText();
return basePackage;
} catch (DocumentException e) {
throw new RuntimeException(e);
}
}
}
- 这个所有的路径 一个split(",")分隔,都进行扫描
- 需要写自己的 前端控制器
- 需要写自己的 Spring容器
- 在前端控制器中 需要添加方法 scanPackage() 扫描 XMLParser 解析出来的路径
public void scanPackage(String pack) {
//获得包所在的工作路径 [绝对路径]
URL url =
this.getClass().getClassLoader().//获取类的加载器
//得到指定包对应的工作路径 [绝对路径]
getResource("/" + pack.replaceAll("\\.", "/"));
// System.out.println("url= "+url);
//根据得到的路径,对其进行扫描,把类的全路径 保存到 classFullPathList
String path = url.getFile();
//在io中 把目录也是为一个文件
File file = new File(path);
//遍历file 【遍历出文件和子目录】
for (File f : file.listFiles()) {
if (f.isDirectory()) {//如果是目录
//需要递归扫描 找子目录
scanPackage(pack + "." + f.getName());
} else {
//的确是个文件
//扫描到的文件可能是 .class 文件 也可能是其他文件
//就算是.class 文件 也需要判断是否需要注入容器 有无加 @Controller注解
//目前无法拿到注解 因为没法反射 所以先把文件的全路径都保存到 classFullPathList 之后在注入对象到容器时再处理
String classFullPath =
//类的全路径不需要.class 去掉.class
pack + "." + f.getName().replaceAll(".class", "");
//保存到 classFullPathList
classFullPathList.add(classFullPath);
}
}
}
- 在Spring容器中 需要添加一个属性 classFullPathList 来保存扫描出来的类的全路径
//保存扫描的包/子包类的全路径
private List<String> classFullPathList =
new ArrayList<>();
- 需要添加一个属性 ioc 来存放反射生成的bean对象 也就是过滤classFullPathList 中没有@Controller注解的一些路径 并实例化
//定义属性 ioc -> 存放反射生成的bean对象 比如Controller / Service /Dao
public ConcurrentHashMap<String, Object> ioc =
new ConcurrentHashMap<>();
- 编写方法,将扫描到的类,在满足情况下 反射到ioc容器
//编写方法,将扫描到的类,在满足情况下 反射到ioc容器
public void executeInstance() {
if (classFullPathList.size() == 0) {
//说明没有扫描到类
return;
}
//遍历classFullList
for (String classFullPath : classFullPathList) {
try {
Class<?> clazz = Class.forName(classFullPath);
if (clazz.isAnnotationPresent(Controller.class)) {//处理@Controller
String className = clazz.getSimpleName();
Object instance = clazz.newInstance();
String value = clazz.getAnnotation(Controller.class).value();
if (!"".equals(value)) {
className = value;
} else {
className = StringUtils.uncapitalize(className);
}
ioc.put(className, instance);
}
else if (clazz.isAnnotationPresent(Service.class)) {//处理@Service
String className = clazz.getSimpleName();//类名
Service serviceAnnotation = clazz.getAnnotation(Service.class);
String value = serviceAnnotation.value();
if (!"".equals(value)) {
className = value;
Object instance = clazz.newInstance();
ioc.put(className, instance);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
- 需要添加类Handler 这个类要保存 一个url 对应的 一个控制器的方法的 映射 ,也就是说,根据这个url,可以找到对应控制器的对应方法
ZyHandler {
private String url;
private Method method;
private Object controller;
public ZyHandler() {
}
public ZyHandler(String url, Method method, Object controller) {
this.url = url;
this.method = method;
this.controller = controller;
}
//需要提供getter和setter方法...
- 需要添加一个属性 HandlerList 用于 保存Handler 【url 和 控制器的映射】
//定义属性 HandlerList -> 保存ZyHandler 【url 和 控制器的映射】
private List<ZyHandler> HandlerList =
new ArrayList<>();
- 需要添加三个方法 一个是initHandlerMapping(),完成 url 对应的 一个控制器的方法的 映射,即 将ioc 中bean 中的 方法进行反射,获取url,将 url,method,bean 封装成Handler 放入HandlerList 保存
private void initHandlerMapping(){
//遍历 ioc
for (Map.Entry<String,Object> entry: zyWebApplicationContext.ioc.entrySet()) {
if (zyWebApplicationContext.ioc.isEmpty()){
return;
}
Object bean = entry.getValue();
Class<?> clazz = bean.getClass();
if (clazz.isAnnotationPresent(Controller.class)){
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
if (declaredMethod.isAnnotationPresent(RequestMapping.class)){
String url = declaredMethod.getAnnotation(RequestMapping.class).value();
ZyHandler zyHandler = new ZyHandler(url, declaredMethod, bean);
HandlerList.add(zyHandler);
}
}
}
}
}
- 添加第二个方法 getHandler(),需要将浏览器发送的request请求中的 uri拿出来,遍历HandlerList 进行配对,如果有 就返回对应的Handler
//编写方法,通过request对象 返回ZyHandler对象 ,如果没有返回null
private ZyHandler getZyHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
//遍历HandlerList
for (ZyHandler zyHandler : HandlerList) {
if (requestURI.equals(zyHandler.getUrl())){
return zyHandler;
}
}
return null;
}
- 添加第三个方法 executeDispatch(),进行分发处理,需要 调用getHandler() 获取浏览器发送的request请求 对应的 Handler ,获取Handler 中的method 进行反射调用,method .invoke() 实现分发请求。
public void executeDispatch(HttpServletRequest request,HttpServletResponse response){
ZyHandler zyHandler = getZyHandler(request);
try {
if (null == zyHandler){
response.getWriter().write("<h1>404 NOT FOUND</h1>");
}
Method method = zyHandler.getMethod();
method.invoke(zyHandler.getController(),request,response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
从web.xml文件中动态获取spring配置文件
思路
- 首先,我们自己写的前端控制器是一个Servlet,它有 servletConfig,可以servletConfig.getInitParameter("xxx") 来获取之前在web.xml配置的classpath:
- 工具类XMLParser 是在spring容器中解析的 ,web.xml配置的classpath: 是在前端控制器中获取的,因此需要spring容器提供有参构造器,在前端控制器添加参数 spring容器,将classpath 传到spring容器中进行解析。
实现
- 首先,我们自己写的前端控制器是一个Servlet,它有 servletConfig,可以servletConfig.getInitParameter("xxx") 来获取之前在web.xml配置的classpath:
- 工具类XMLParser 是在spring容器中解析的 ,web.xml配置的classpath: 是在前端控制器中获取的,因此需要spring容器提供有参构造器,在前端控制器添加参数 spring容器,将classpath 传到spring容器中进行解析。
@Override
public void init() throws ServletException {
String configLocation = getServletConfig().getInitParameter("contextConfigLocation");
System.out.println("ZyDispatcherServlet 初始化---");
zyWebApplicationContext = new ZyWebApplicationContext(configLocation);
zyWebApplicationContext.init();
initHandlerMapping();
System.out.println("HandlerList= "+HandlerList);
}
private String configLocation;
public ZyWebApplicationContext(String configLocation) {
this.configLocation = configLocation;
}
public void init(){
System.out.println("ZyWebApplicationContext 初始化---");
String basePackage = XMLParser.getBasePackage(configLocation.split(":")[1]);
String[] basePackages = basePackage.split(",");
if (basePackages.length >0) {
for (String pack : basePackages) {
scanPackage(pack);
}
}
}
@Service注解
思路
- @Service注解是写在类上的 即@Target(ElementType.TYPE)
- 这个注解标识的类就是一个Service,那么同样是在executeInstance()方法中判断是否有注解@Service,有的话就保存到ioc容器中
- 由于是Service,那么保存进ioc 的 k-v 中的 k 就有三种。第一种,就是默认值,用接口的类型的名字首字母小写;第二种,在@Service注解种设置了value属性,那么 k = value;第三章,需要用类名首字母小写也可以获取bean
实现
- @Service注解是写在类上的 即@Target(ElementType.TYPE)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
- 这个注解标识的类就是一个Service,那么同样是在executeInstance()方法中判断是否有注解@Service,有的话就保存到ioc容器中
- 由于是Service,那么保存进ioc 的 k-v 中的 k 就有三种。第一种,就是默认值,用接口的类型的名字首字母小写;第二种,在@Service注解种设置了value属性,那么 k = value;第三章,需要用类名首字母小写也可以获取bean
public void executeInstance() {
if (classFullPathList.size() == 0){
return;
}
//遍历 classFullPathList
for (String classFullPath : classFullPathList) {
try {
Class<?> clazz = Class.forName(classFullPath);
if (clazz.isAnnotationPresent(Controller.class)){
Controller controller = clazz.getAnnotation(Controller.class);
String value = controller.value();
String className = clazz.getSimpleName();
Object instance = clazz.newInstance();
if ("".equals(value)){
className = StringUtils.uncapitalize(className);
}else {
className = value;
}
ioc.put(className,instance);
} else if (clazz.isAnnotationPresent(Service.class)) {
Service serviceAnnotation = clazz.getAnnotation(Service.class);
String annoattionValue = serviceAnnotation.value();
Object instance = clazz.newInstance();
if ("".equals(annoattionValue)){
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> anInterface : interfaces) {
String simpleName = anInterface.getSimpleName();
simpleName = StringUtils.uncapitalize(simpleName);
ioc.put(simpleName,instance);
}
//可以通过类名首字母小写
String simpleName = clazz.getSimpleName();
simpleName = StringUtils.uncapitalize(simpleName);
ioc.put(simpleName,instance);
}else {
ioc.put(annoattionValue,instance);
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Autowried 依赖注入
思路
- @Autowried注解是用在字段上,通过ioc容器自动装配,因此 @Target(ElementType.FIELD)
- 需要遍历ioc中所有的 bean 中的所有字段,来进行判断是否需要自动装配
- 通过反射获取字段,判断该字段是否带有@Autowried 注解,有的话就判断value值
- value为"",说明按默认规则 用字段类型首字母小写 去ioc容器进行查找,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");
- 如果有值,去ioc容器进行查找该值对应的bean,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");如果有该bean,就用该字段的set()方法将本身的bean 和 查找ioc获取的bean进行装配
- 即 declaredField.set(bean, beanInIOC);
- 由于字段是私有属性private,需要暴力破解declaredField.setAccessible(true);
实现
- @Autowried注解是用在字段上,通过ioc容器自动装配,因此 @Target(ElementType.FIELD)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}
-
需要遍历ioc中所有的 bean 中的所有字段,来进行判断是否需要自动装配
-
通过反射获取字段,判断该字段是否带有@Autowried 注解,有的话就判断value值
-
value为"",说明按默认规则 用字段类型首字母小写 去ioc容器进行查找,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");
-
如果有值,去ioc容器进行查找该值对应的bean,如果没有抛出空指针异常throw new NullPointerException("ioc 没有该bean");如果有该bean,就用该字段的set()方法将本身的bean 和 查找ioc获取的bean进行装配
-
即 declaredField.set(bean, beanInIOC);
-
由于字段是私有属性private,需要暴力破解declaredField.setAccessible(true);
public void executeAutoWired(){
//遍历ioc
if (ioc.isEmpty()){
return;
}
//获取容器里的所有bean 以及 bean对应的字段
for (Map.Entry<String, Object> entry : ioc.entrySet()) {
Object bean = entry.getValue();
Class<?> clazz = bean.getClass();
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
//通过反射获取字段,判断该字段是否带有@Autowried 注解,有的话就判断value值
if (declaredField.isAnnotationPresent(Autowired.class)){
Autowired annotation =
declaredField.getAnnotation(Autowired.class);
String beanName = annotation.value();
try {
//value为"",说明按默认规则 用字段类型首字母小写 去ioc容器进行查找,
if ("".equals(beanName)){
beanName = declaredField.getType().getSimpleName();
beanName = StringUtils.uncapitalize(beanName);
}
Object iocBean = ioc.get(beanName);
//如果没有抛出空指针异常
if (null == iocBean){
throw new NullPointerException("ioc 没有该Bean");
}
declaredField.setAccessible(true);//暴力破解
declaredField.set(bean,iocBean);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
}
}
@RequestParam
思路
- @RequestParam 是写在PARAMETER上的 也就是说 @Target(ElementType.PARAMETER)
- 这个注解实现的功能 大方向是 分发请求 也就是说 写在 executeDispatch()方法中的
- 要实现 @RequestParam 需要将形参列表封装到一个数组中 , 因为反射 invoke 可以传入一个数组作为可变参数解析
- 那么就需要两个数组 一个数组是新创建的数组(大小应与,目标数组的形参个数一致) 将实参放入进去 ;另一个是 反射拿到的目标方法的形参的数组
- 这两个数组的内容需要一一对应,因为反射需要顺序一致
- 需要获取到request 中请求的参数Map 获取参数名和参数值
- 可以将功能具体化到 完成这个新数组的实参填写 【完成新数组之后放入invoke方法进行反射就行】
- 将步骤拆解成 1)完成HttpServletRequest 和 HttpServletResponse 的填写 ;2)完成带有@RequestParam注解的形参的填写;3)完成普通的方法参数没有@RequestParam注解的形参的填写
- 完成HttpServletRequest 和 HttpServletResponse 的填写:就需要先拿到目标方法的所有参数。根据类型的名称 和 形参列表进行匹配 填写
- 完成带有@RequestParam注解的形参的填写:需要添加方法,获取目标方法中 带有@RequestParam注解的形参是属于第几个参数的,返回int 代表第几个参数,如果有@RequestParam注解,就会返回相应的索引,如果没有就会返回-1 进行普通方法参数的填写 处理
- 普通方法参数的 填写:需要添加方法,将目标方法的所有的形参的名称 反射保存到 List 返回,再通过遍历判断 请求的参数名 和 List中一致的 填写到 数组中
- 普通方法参数的 填写 需要插件,使用java8的特性 解决在默认情况下 parameter.getName() 获取的名字不是形参真正的名字,而是[arg0,arg1,arg2...]的问题
实现
- @RequestParam 是写在PARAMETER上的 也就是说 @Target(ElementType.PARAMETER)
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value() default "";
}
- 这个注解实现的功能 大方向是 分发请求 也就是说 写在 executeDispatch()方法中的
- 要实现 @RequestParam 需要将形参列表封装到一个数组中 , 因为反射 invoke 可以传入一个数组作为可变参数解析
Method method = zyHandler.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes();//形参数组
Object[] params = new Object[parameterTypes.length];
- 那么就需要两个数组 一个数组是新创建的数组(大小应与,目标数组的形参个数一致) 将实参放入进去 ;另一个是 反射拿到的目标方法的形参的数组
- 这两个数组的内容需要一一对应,因为反射需要顺序一致
- 需要获取到request 中请求的参数Map 获取参数名和参数值
request.setCharacterEncoding("utf-8");
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String name = entry.getKey();//参数 键
String value = entry.getValue()[0];//参数 值
-
可以将功能具体化到 完成这个新数组的实参填写 【完成新数组之后放入invoke方法进行反射就行】
-
将步骤拆解成 1)完成HttpServletRequest 和 HttpServletResponse 的填写 ;2)完成带有@RequestParam注解的形参的填写;3)完成普通的方法参数没有@RequestParam注解的形参的填写
-
完成HttpServletRequest 和 HttpServletResponse 的填写:就需要先拿到目标方法的所有参数。根据类型的名称 和 形参列表进行匹配 填写
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
params[i] = request;
} else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
params[i] = response;
}
}
- 完成带有@RequestParam注解的形参的填写:需要添加方法,获取目标方法中 带有@RequestParam注解的形参是属于第几个参数的,返回int 代表第几个参数,如果有@RequestParam注解,就会返回相应的索引,如果没有就会返回-1 进行普通方法参数的填写 处理
//获取目标方法中 带有@RequestParam注解的形参是属于第几个参数的,返
private int getRequestParamterIndex(Method method,String name){
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
if (parameter.isAnnotationPresent(RequestParam.class)) {
String value = parameter.getAnnotation(RequestParam.class).value();
if (name.equals(value)) {
return i;//返回int 代表第几个参数
}
}
}
return -1;
}
int requestParamterIndex = getRequestParamterIndex(zyHandler.getMethod(), name);
if (requestParamterIndex != -1) {
params[requestParamterIndex] = value;
}
- 普通方法参数的 填写:需要添加方法,将目标方法的所有的形参的名称 反射保存到 List 返回,再通过遍历判断 请求的参数名 和 List中一致的 填写到 数组中
//将目标方法的所有的形参的名称 反射保存到 List 返回
private List<String> getParameterNames(Method method){
List<String> parameterList = new ArrayList<>();
//获取到所有的参数名称
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
//在默认情况下 parameter.getName() 获取的名字不是形参真正的名字
//而是[arg0,arg1,arg2...]
//需要插件,使用java8的特性 解决
String name = parameter.getName();
parameterList.add(name);
}
System.out.println("目标方法的形参列表=" + parameterList);
return parameterList;
}
else {
//没找到@RequestParam 对应参数--使用默认机制
//1. 得到目标方法的所有形参名
//2. 对得到目标方法的所有形参名进行遍历,
//如果匹配就把当前请求的参数值放入params
List<String> parameterNames = getParameterNames(zyHandler.getMethod());
for (int i = 0; i < parameterNames.size(); i++) {
if (name.equals(parameterNames.get(i))) {
params[i] = value;
break;
}
}
}
- 普通方法参数的 填写 需要插件,使用java8的特性 解决在默认情况下 parameter.getName() 获取的名字不是形参真正的名字,而是[arg0,arg1,arg2...]的问题
<!--可以进行json操作-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.0</version>
</dependency>
- 完整代码
public void executeDispatch(HttpServletRequest request, HttpServletResponse response) {
ZyHandler zyHandler = getZyHandler(request);
try {
if (null == zyHandler) {
response.getWriter().write("<h1>404 NOT FOUND!</h1>");
} else {
Method method = zyHandler.getMethod();
Class<?>[] parameterTypes = method.getParameterTypes();//形参数组
Object[] params = new Object[parameterTypes.length];
//遍历 parameterTypes
//获取 HttpServletRequest , HttpServletResponse 在形参数组中的位置
//将request 和 response 保存到 params相应的位置上
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
if ("HttpServletRequest".equals(parameterType.getSimpleName())) {
params[i] = request;
} else if ("HttpServletResponse".equals(parameterType.getSimpleName())) {
params[i] = response;
}
}
//获取request中的
request.setCharacterEncoding("utf-8");
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String name = entry.getKey();//参数 键
String value = entry.getValue()[0];//参数 值
//获取形参数组中 带有@RequestParam 的形参 的位置
//将带有@RequestParam 保存到 params相应的位置上
int requestParamterIndex = getRequestParamterIndex(zyHandler.getMethod(), name);
if (requestParamterIndex != -1) {
params[requestParamterIndex] = value;
} else {
//没找到@RequestParam 对应参数--使用默认机制
//1. 得到目标方法的所有形参名
//2. 对得到目标方法的所有形参名进行遍历,
//如果匹配就把当前请求的参数值放入params
List<String> parameterNames = getParameterNames(zyHandler.getMethod());
for (int i = 0; i < parameterNames.size(); i++) {
if (name.equals(parameterNames.get(i))) {
params[i] = value;
break;
}
}
}
}
视图解析
思路
- 首先 视图解析 它是在分发请求后 在目标方法中进行视图的跳转 forward 或者 redirect,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
- 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
- 当返回的是String类型后 我们就可以根据 splic(":")进行分隔
- splic(":")[0] 就是进行跳转的方式 forward 或者 redirect
- splic(":")[1] 就是进行跳转的页面
- 如果没有":" ,就说明是默认情况,forward 处理即可
实现
-
首先 视图解析 它是在分发请求后 在目标方法中进行视图的跳转 forward 或者 redirect,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
-
在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
Object result = zyHandler.getMethod().invoke(zyHandler.getController(), params);
-
当返回的是String类型后 我们就可以根据 splic(":")进行分隔
-
splic(":")[0] 就是进行跳转的方式 forward 或者 redirect
-
splic(":")[1] 就是进行跳转的页面
-
如果没有":" ,就说明是默认情况,forward 处理即可
if (result instanceof String){
String viewName = (String) result;
if (viewName.contains(":")) {
String viewType = viewName.split(":")[0];
String viewPage = viewName.split(":")[1];
if ("forward".equals(viewType)){
request.getRequestDispatcher(viewPage).forward(request,response);
}else if (("redirect".equals(viewType))){
response.sendRedirect(viewPage);
}
}else {
request.getRequestDispatcher(viewName).forward(request,response);
}
}
@ResponseBody 返回JSON数据
实现
- 首先@ResponseBody 是写在方法上的 因此 @Target(ElementType.METHOD)
- 没有默认值 @ResponseBody仅仅作为一个标识
- @ResponseBody它是在分发请求后 在目标方法中标识该方法返回JSON格式的数据给浏览器,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
- 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
- 当返回的是一个集合类型,并且判断该方法有注解@ResponseBody ,就说明是需要向浏览器返回JSON格式数据
- 用jackson 包下的 objectWriter对象的 objectMapper.writeValueAsString()方法 ,可以很轻松的将集合转化为JSON进行返回
- 不要忘了设置response格式,防止乱码,response.setContentType("text/html;charset=utf-8");
- 直接用response.getWriter.writer()将转化后的结果返回给浏览器即可
思路
- 首先@ResponseBody 是写在方法上的 因此 @Target(ElementType.METHOD)
- 没有默认值 @ResponseBody仅仅作为一个标识
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}
- @ResponseBody它是在分发请求后 在目标方法中标识该方法返回JSON格式的数据给浏览器,所以这部分的代码应该在 前端处理器的executeDispatch 中 对目标方法进行反射后调用
- 在对目标方法进行反射 method.invoke() 就会有返回值,根据返回值的类型进行相应业务处理
- 当返回的是一个集合类型,并且判断该方法有注解@ResponseBody ,就说明是需要向浏览器返回JSON格式数据
- 用jackson 包下的 objectWriter对象的 objectMapper.writeValueAsString()方法 ,可以很轻松的将集合转化为JSON进行返回
else if (result instanceof ArrayList) {
if (zyHandler.getMethod().isAnnotationPresent(ResponseBody.class)) {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(json);
writer.flush();
writer.close();
}
}
- 不要忘了设置response格式,防止乱码,response.setContentType("text/html;charset=utf-8");
- 直接用response.getWriter.writer()将转化后的结果返回给浏览器即可
本文学习内容来自韩顺平老师的课程
仅供个人参考学习