Spring MVC
如何理解MVC架构和三层架构
三层架构的缺点和解决方案
缺点:
- 每个功能都要声明对应的Servlet,Servlet数量太多。即使让一个Servlet包括多个方法,也无法从根本上解决Servlet包括多个方法,也无法从根本上解决Servlet数量多的问题。
- 在Servlet中获取请求数据比较麻烦:需要大量的requestt.getParameter(“name”)。
- 手动的类型转换:比如int age = Integer.parseInt(request.getParameter(“age”))
- 响应的方式的代码其实只想声明对应的响应数据。比如:request.getRequestDispatcher(path).forward(request,response); 中最关键的还是path。
解决方案:就是SpringMVC等MVC框架。
Spring的解决方案?
-
项目只声明一个Servlet,该Servlet作为项目请求的公共入口。并且在该Servlet中根据请求地址调用对应的控制代码处理请求。
如果将控制方法(之前Servlet调用业务层、指出跳转路径等功能)全部声明在Servlet中造成代码的体系结构不清晰。
-
将控制方法单独声明到控制类中(Controller类)。
然后Servlet中根据请求动态的调用对应的控制类中的控制方法处理请求即可。
认识SpringMVC
SpringMVC (全称Spring Web MVC)是Spring提供给Web应用的框架设计,其实是Spring的一部分。它是一种基于Servlet的技术,提供了核心控制器和相关的组件,并制定了松散的结构,以适合各种灵活的需要。
SpringMVC 已经成为目前最主流的 MVC 框架之一。自从Spring 2.5版本发布后,由于支持注解配置,易用性有了大幅度的提高。随着 Spring3.0 的发布,全面超越 Struts2,成为最优秀的 MVC 框架。目前业界普遍选择了 SpringMVC 作为 Java EE 项目表述层开发的首选方案。
-
Spring 家族原生产品,与 IOC 容器等基础设施无缝对接
-
表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
-
代码清新简洁,大幅度提升开发效率
-
性能卓著,尤其适合现代大型、超大型互联网项目要求
-
支持RESTful 编程风格的请求。
-
可适配、非侵入:可以根据不同的应用场景,选择合适的控制器子类(simple型、command型、from型、wizard型、multi-action型或者自定义),而不是一个单一控制器(比如Action/ActionForm)
-
因为模型数据不存放在特定的API里,而是放在一个Model里(Map数据结构实现,因此很容易被其他框架使用)
-
更加简单、强大的异常处理
总之,简单易用,性能优良,同类技术中市场占有率最高,不用犹豫,就是它了。
创建web项目
1.创建Maven Java项目
2.添加Web支持【JBLJavaToWeb插件转化为web项目】
3添加Web资源
4.部署并访问
Html网页:可以访问
图片:也可以访问
Thymeleaf网页:不可以直接访问,必须经过Thymeleaf解析器解析后再跳转到页面,所以在项目中一般把Thymeleaf网页放到Web-inf/templates
Hello SpringMVC的基本实现
1.添加依赖
<dependencies>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
</dependencies>
2.web.xml
<!--配置SpringMVC总控制器,唯一的Servlet -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--指定SpringMVC配置文件的名称和位置,有默认位置 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- 启动服务器时就加载总控制器-->
<load-on-startup>1</load-on-startup>
</servlet>
<!--配置总控制器的访问路径 -->
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<!--其实可以是*.do *.abc,模拟Struct2、Struct,要求所有分控制器方法的访问路径必须以
.action结尾
在这种配置下,html css js png jpg 不经过总控制器
-->
<url-pattern>*.action</url-pattern>
</servlet-mapping>
3.配置文件spring-mvc.xml
springmvc的配置文件就是spring的配置文件,一个即可,结构完全相同。如果项目规模比较大,可以分拆为多个配置文件。
<!-- 配置注解扫描基准路径:@Controller @Service @Repository @Component-->
<context:component-scan base-package="com.dyy.controller"></context:component-scan>
<!-- Thymeleaf视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<!-- 视图前缀 -->
<property name="prefix" value="/WEB-INF/templates/"/>
<!-- 视图后缀 -->
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML5"/>
<property name="characterEncoding" value="UTF-8" />
</bean>
</property>
</bean>
</property>
</bean>
4.开发视图层页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a th:href="@{/hello/login.action}">进入</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>结果页面</h3>
<p th:text="${msg}"> </p>
</body>
</html>
5.开发分控制器
分控制器不需要继承或者实现其他类或者接口。
方法的返回值是String类型,对应转发的页面的逻辑的视图,由总控制器负责添加前缀和后缀构成物理视图,进行解析渲染并返回客户端。
方法上面使用@RequestMapping指明该方法对应的访问路径。因为目前总控制器的访问路径是*.action,所以所有的Controller方法的访问路径必须以action结尾。
@Controller
public class HelloController {//Handler
@RequestMapping("/hello/toIndex2.action")
public String toIndex2(){
System.out.println("------HelloController toIndex2-------");
//return "/WEB/INF/templates/index2.html";
return "index2";
}
@RequestMapping("/hello/login.action")
public String login(Model model){
System.out.println("-------HelloController login--------");
model.addAttribute("msg","login success!!");
return "result";
}
}
6.部署访问
注意:可以将某个分控制器的请求路径设为/,项目启动后自动访问。前提是:
- 去掉欢迎页面,比如index.html
- 将总控制器的
<url-pattern>设置为/,但是设置为/后需要增加一个注解:<mvc:annotation-driven></mvc:anntotation-driven>,可以让对分控制器的访问可以正常进行,但是静态资源无法访问了,这需要同时在增加一个注解:<mvc:default-servlet-handler></mvc:default-servlet-handler>,静态资源也可以正常访问。
Hello SpringMVC的完善
1.添加日志
-
添加依赖
<!-- 日志 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> -
添加属性文件【log-back.xml】
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true"> <!-- 指定日志输出的位置 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!-- 日志输出的格式 --> <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --> <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern> </encoder> </appender> <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR --> <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --> <root level="INFO"> <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender --> <appender-ref ref="STDOUT" /> </root> <!-- 根据特殊需求指定局部日志级别 --> <logger name="org.springframework.web.servlet.DispatcherServlet" level="DEBUG" /> <!-- 指定包的日志级别--> <logger name="com.atguigu.controller" level="DEBUG" /> </configuration> -
给分控制器添加日志
@Controller public class UserController { private Logger logger = LoggerFactory.getLogger(UserController.class); @RequestMapping("/") public String toLogin123(){ //return "/WEB-INF/templates/login.html"; logger.debug("------UserController toLogin123 ----"); //如果需要,调用业务层 return "login"; } @RequestMapping("hello/login.action") public String login(Model model){ //调用业务层,完成登录功能 logger.debug("-----UserController login ---------"); model.addAttribute("msg","登录成功"); return "result"; } } -
使用简单Slf4j日志(lombok的日志注解)@Slf4j
@Controller @Slf4j public class UserController { //private Logger logger = LoggerFactory.getLogger(UserController.class); @RequestMapping("/") public String toLogin123(){ //return "/WEB-INF/templates/login.html"; log.debug("------UserController toLogin123!! ----"); //如果需要,调用业务层 return "login"; } @RequestMapping("hello/login.action") public String login(Model model){ //调用业务层,完成登录功能 log.debug("-----UserController login!! ---------"); model.addAttribute("msg","登录成功"); return "result"; } }
2.总控制器的访问路径
Web项目中的资源,可以分为:
- 静态资料
- JSP资源【现在用的少了】
- 对分控制器的访问
总控制器的访问路径方式:
- *.action对静态资源和JSP资源的访问不经过总控制器,曾经最流行的方式。缺点:不支持Restful风格。
- /*所有的请求都经过,但是JSP和静态资源不应该经过,不要这么写。
- /只有这里可以这么写。对分控制器和静态资源的访问会经过,但是对JSP的访问不会经过。优点是支持Restful风格。这是目前推荐的方式。在该方式,分控制器路径是否写.action后缀已经不重要了,但是Restful风格不建议写后缀。缺点是静态资源也会经过,其实是没有必要的,也会出问题。
如何放过静态资源的请求?
解决方案:
<!--启用注解驱动,后面做了好多事情,标配 --> <mvc:annotation-driven></mvc:annotation-driven> <!--当总控制器设置为/的时候,静态资源也会经过总控制器,在总控制器中,对静态资源进行特殊处理, 而不是按照路径找分控制器--> <mvc:default-servlet-handler></mvc:default-servlet-handler>
RequestMapping
1.作用
从注解的名称上我们可以看到,@RequestMapping注解的作用就是将请求的URL地址和处理请求的方式关联起来,建立映射关系。
2.位置
写在类上 @RequestMapping(“/hello”)
写在方法上 @RquesetMapping(“/login”)
最终方法的请求路径是是类上和方法上连接:/hello/login
3.使用
①精确匹配
<a th:href="@{/hello/login.action}">提交登录</a><br>
<a th:href="@{/hello/to/spring/mvc}">HelloWorld</a><br/>
@RequestMapping("hello/login.action")
public String login(Model model){
//调用业务层,完成登录功能
log.debug("-----UserController login!! ---------");
model.addAttribute("msg","登录成功");
return "result";
}
@RequestMapping("/hello/to/spring/mvc")
public String login2(Model model){
model.addAttribute("msg","/hello/to/spring/mvc!!");
log.debug("-----UserController login2 /hello/to/spring/mvc------");
return "result";
}
②模糊匹配
<a th:href="@{/hello/fruit/apple}">@RequestMapping模糊匹配[apple]</a><br/>
<a th:href="@{/hello/fruit/orange}">@RequestMapping模糊匹配[orange]</a><br/>
<a th:href="@{/hello/fruit/banana}">@RequestMapping模糊匹配[banana]</a><br/>
@RequestMapping("/hello/fruit/*")
public String login3(Model model){
model.addAttribute("msg","/hello/fruit/*");
log.debug("-----UserController login3 /hello/fruit/*------");
return "result";
}
其他写法
@RequestMapping(value = "/to/spring/mvc",method = RequestMethod.GET )
@GetMapping("/to/spring/mvc")
@PostMapping("/to/spring/mvc")
4.视图控制器
如果一个分控制器的方法的作用只是页面的跳转,可以使用<mvc:view-controller>来替代。
以下两种功能是等价的。
@RequestMapping("/")
public String toLogin123(){
//return "/WEB-INF/templates/login.html";
log.debug("------UserController toLogin123??----");
//如果需要,调用业务层
return "login";
}
<!--配置视图控制器 -->
<mvc:view-controller path="/" view-name="login"></mvc:view-controller>
获取请求参数
1.一名一值
- 直接用同名变量接收
- 或者使用@RequestParam声明对应哪个变量,括号内是控件的name
<h3>获取请求参数-一名一值</h3>
<a th:href="@{/hello/login1(username=zhangsan,password=123456)}">登录1</a><br>
<a href="/springmvc/hello/login2?username=zhangsan&password=123456">登录2</a><br>
<a th:href="@{/hello/register(username=zhangsan,age=12,salary=600.0)}">注册</a><br>
<a th:href="@{/hello/register(username=zhangsan,age=12)}">注册2</a><br>
@Controller
@Slf4j
@RequestMapping("/hello")
public class UserController { //UserHandler
@RequestMapping("/login1")
public String login(String username,String password){
log.debug("login:"+username+" "+password);
return "result";
}
@RequestMapping("/login2")
public String login2(@RequestParam("username") String uname, @RequestParam("password") String pwd){
log.debug("login:"+uname+" "+pwd);
return "result";
}
@RequestMapping("/register")
public String register(String username,Integer age,double salary){
log.debug("login:"+username+" "+age+" "+salary);
return "result";
}
@RequestMapping("/register2")
public String register2(String username,Integer age,
@RequestParam(value = "salary",required = false,defaultValue = "800") double salary){
log.debug("login:"+username+" "+age+" "+salary);
return "result";
}
}
2.一名多值
需要用List接收,泛型需要对应的类型。使用@RequestParam声明。
<h3>获取请求参数-一名多值</h3>
<form th:action="@{/hello/getTeams}" method="post">
请选择你最喜欢的球队:
<input type="checkbox" name="team" value="Brazil"/>巴西
<input type="checkbox" name="team" value="German"/>德国
<input type="checkbox" name="team" value="French"/>法国
<input type="checkbox" name="team" value="Holland"/>荷兰
<input type="checkbox" name="team" value="Italian"/>意大利
<input type="checkbox" name="team" value="China"/>中国
<br/>
<input type="submit" value="保存"/>
</form>
@RequestMapping("/getTeams")
public String getTeam(@RequestParam("team") List<String> teamList){
log.debug("teamList:"+teamList);
return "result";
}
3.实体类
直接用实体类接收
<h3>获取请求参数-实体类</h3>
<form th:action="@{/hello/addEmp}" method="post">
姓名:<input type="text" name="empName"/><br/>
年龄:<input type="text" name="empAge"/><br/>
工资:<input type="text" name="empSalary"/><br/>
<input type="submit" value="保存"/>
</form>
@RequestMapping("/addEmp")
public String addEmp(Employee emp123){
log.debug("emp:"+emp123);
return "result";
}
注意1:页面上直接写实体类的属性,比如empName,而不需要写emp123.empName
注意2:分控制器中使用实体类的参数直接接收,和变量名无关。底层调用相应属性的getter和setter方法
注意3:解决POST中文乱码问题
<!---解决Post请求中文乱码的过滤器-->
<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>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
4.实体类含级联属性
直接使用类接收。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private String stuName;
private School school;
private List<Subject> subjectList;//List
//private Subject[] subjectArray;//Array
//private Set<Teacher> teacherSet;//Set ?? 不推荐使用
private Map<String, Double> scores;//Map
}
<h3>获取请求参数-实体类包含级联属性</h3>
<form th:action="@{/hello/addStu}" method="post">
姓名:<input type="text" name="stuName"/><br/>
学校编号:<input type="text" name="school.schoolNo"/><br/>
学校名称:<input type="text" name="school.schoolName"/><br/>
课程1编号:<input type="text" name="subjectList[0].subjectId"/><br/>
课程1名称:<input type="text" name="subjectList[0].subjectName"/><br/>
课程2编号:<input type="text" name="subjectList[1].subjectId"/><br/>
课程2名称:<input type="text" name="subjectList[1].subjectName"/><br/>
MySQL课程成绩:<input type="text" name="scores['mysql']"/><br/>
Java课程成绩:<input type="text" name="scores['java']"/><br/>
<input type="submit" value="保存"/>
</form>
@RequestMapping("/addStu")
public String addStu(Student stu){
log.debug("stu:"+stu);
return "result";
}
⭐@RequestParam注解省略的情况
当请求参数名和形参名一致,可以省略@RequestParam("userName")注解
但是,省略后代码可读性下降而且将来在SpringCloud中不能省略,所以建议还是不要省略
获取请求头
- 使用 @RequestHeader 注解获取请求消息头信息
- name 或 value 属性:指定请求消息头名称
- DefaultValue 属性:设置默认值
@RequestMapping("/getHeader")
public String getHeader(@RequestHeader("User-Agent") String userAgent, @RequestHeader("Referer") String referer){
log.debug("header User-Agent:"+userAgent);
log.debug("header Referer:"+referer);
return "result";
}
获取指定Cookie
@CookieValue(value = "JSESSIONID",defaultValue = "null")
- 使用 @CookieValue 注解获取指定名称的 Cookie 数据
- name 或 value 属性:指定Cookie 名称
- defaultValue 属性:设置默认值
- 形参位置声明 HttpSession 类型的参数即可获取 HttpSession 对象
@RequestMapping("/getCookie")
public String getCookie(@CookieValue(value = "JSESSIONID",defaultValue = "null") String sessionId, HttpSession session){
log.debug("sessionId:"+sessionId);
return "result";
}
发送数据
①额外封装一层
public class EmployeeParam {
private List<Employee> employeeList;
}
②表单
直接发送 List<Employee>:<br/>
<form th:action="@{/param/list/emp}" method="post">
1号员工姓名:<input type="text" name="employeeList[0].empName" /><br/>
1号员工年龄:<input type="text" name="employeeList[0].empAge" /><br/>
1号员工工资:<input type="text" name="employeeList[0].empSalary" /><br/>
2号员工姓名:<input type="text" name="employeeList[1].empName" /><br/>
2号员工年龄:<input type="text" name="employeeList[1].empAge" /><br/>
2号员工工资:<input type="text" name="employeeList[1].empSalary" /><br/>
<button type="submit">保存</button>
</form>
③handler方法
@RequestMapping("/param/list/emp")
public String saveEmpList(
// SpringMVC 访问这里实体类的setEmployeeList()方法注入数据
EmployeeParam employeeParam
) {
List<Employee> employeeList = employeeParam.getEmployeeList();
for (Employee employee : employeeList) {
logger.debug(employee.toString());
}
return "target";
}
页面跳转控制
转发是服务器跳转,客户端不知道。
重定向是客户端跳转,客户端知道。
如果要获取保存在request中的数据,那么必须使用转发。
转发只能转发到同一个项目,最多也就是同一个服务器的其他项目(特殊设置),重定向可以跳转到互联网的任意位置。
转发可以跳转到项目的WEB-INF目录下。
重定向不可以跳转到项目的WEB-INF目录下(重定向是客户端跳转)。
@RequestMapping("/dispatcher1")
public String dispatcher1(){
//return "result";
return "forward:/WEB-INF/templates/result.html";
}
@RequestMapping("/dispatcher2")
public String dispatcher2(){
return "forward:/outer.html";
}
@RequestMapping("/redirect1")
public String redirect1(){
return "redirect:/WEB-INF/templates/result.html"; //不可以
}
@RequestMapping("/redirect2")
public String redirect2(){
return "redirect:/outer.html"; //在Spring中,重定向的/也代表当前项目
}
@RequestMapping("/redirect3")
public String redirect3(){
return "redirect:http://www.baidu.com";
}
获取原生Servlet API对象
@Autowired
private ServletContext servletContext;
@RequestMapping("/getAPI")
public String getAPI(HttpServletRequest request, HttpServletResponse response,HttpSession session){
String requestURL = request.getRequestURL().toString();
//HttpSession session1 = request.getSession();
request.setAttribute("msg","username is error"); //仅限于当前用户的当前请求
session.setAttribute("username","zhangsan");//仅限于当前用户的多个请求 购物车 用户
servletContext.setAttribute("count",10000);//所有用户的所有请求 网站的访问认识
response.addHeader("schoolName","atguigu");
return "result";
}
<span th:text="${msg}"></span><br>
<hr>
<span th:text="${session.username}"></span><br>
<span th:text="${application.count}"></span><br>
注意:如果在Web组件直接传递数据,在session与application域中传递数据,请使用原生API。如果在request域中传递数据,一般不使用原生的API,而是使用下面的四种方式【属性域】。
属性域
1.使用Model类型的形参
@RequestMapping("/getModel")
public String getModel(Model model){
model.addAttribute("msg","username is null(model)");
return "result"; //请求域的数据必须使用转发
}
2.使用Map类型的形参
@RequestMapping("/getMap")
public String getMap(Map map){
map.put("msg","username is null(map)");
return "result"; //请求域的数据必须使用转发
}
3.使用ModelMap类型的形参
@RequestMapping("/getModelMap")
public String getModelMap(ModelMap modelMap){
modelMap.addAttribute("msg","username is null(modelmap)");
return "result"; //请求域的数据必须使用转发
}
4.使用ModelAndView对象
@RequestMapping("/getModelAndView")
public ModelAndView getModelAndView(){
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg","username is null(modelandview)");
modelAndView.setViewName("result");
return modelAndView; //请求域的数据必须使用转发
}
注意:
-
使用Model、Map、ModelMap来传递数据,方法的返回值是String类,最终SpringMVC的底层都会变为使用ModelAndView来存储数据和视图。
-
底层都是将模型存入请求域。最终找到的源码位置,所在类:org.thymeleaf.context.WebEngineContext
;所在方法:setVariable();实际上的生效语句就是this.request.setAttribute(name,value)。
-
SpringMVC 传入的 Model、ModelMap、Map类型的参数其实本质上【底层实现类】都是 BindingAwareModelMap 类型的。
表单数据回显
1.回显简单标签
实体类包装
@Controller
public class TigerController {
@RequestMapping("/tiger/findById")
public String findById(Model model){
//实际中要访问数据库,获取指定id的数据
Tiger tiger = new Tiger();
tiger.setTigerId(5);
tiger.setTigerName("tomCat老虎");
tiger.setTigerSalary(666.66);
//存储信息到请求域
model.addAttribute("tiger1",tiger);
//返回(转发)
return "updateTiger";
}
}
<h3>修改信息</h3>
<form>
ID: <input type="text" name="tigerId" th:value="${tiger1.tigerId}"><br>
名称:<input type="text" name="tigerName" th:value="${tiger1.tigerName}"><br>
薪资:<input type="text" name="tigerSalary" th:value="${tiger1.tigerSalary}"><br>
<input type="submit" value="提交">
</form>
2.回显单显框和下拉框
实体类类型的ArrayList包装
@RequestMapping("/tiger/findById2")
public String findById2(Model model){
//实际中要访问数据库,获取指定id的数据
Tiger tiger = new Tiger();
tiger.setTigerId(5);
tiger.setTigerName("tomCat");
tiger.setTigerSalary(666.66);
tiger.setSeason(new Season("s3","Authmn"));
//准备四个季节
List<Season> seasonList = new ArrayList<>();
seasonList.add(new Season("s1","Spring"));
seasonList.add(new Season("s2","Summer"));
seasonList.add(new Season("s3","Authmn"));
seasonList.add(new Season("s4","Winter"));
//存储信息到请求域
model.addAttribute("tiger1",tiger);
model.addAttribute("seasonList",seasonList);
//返回(转发)
return "updateTiger2";
}
季节:<input type="radio" name="season"
th:each="season:${seasonList}"
th:value="${season.seasonId}"
th:text="${season.seasonName}"
th:checked="${season.seasonId==tiger1.season.seasonId}"><br>
季节:<select name="season" >
<option th:each="season:${seasonList}"
th:value="${season.seasonId}"
th:text="${season.seasonName}"
th:selected="${season.seasonId==tiger1.season.seasonId}"
></option>
</select>
3.回显复选框
ArrayList
@RequestMapping("/tiger/findById3")
public String findById3(Model model){
//实际中要访问数据库,获取指定id的数据
Tiger tiger = new Tiger();
tiger.setTigerId(5);
tiger.setTigerName("tomCat");
tiger.setTigerSalary(666.66);
tiger.getFriendList().add(new Friend(1,"monkey"));
tiger.getFriendList().add(new Friend(3,"turkey"));
//准备四个朋友
List<Friend> friendList = new ArrayList<>();
friendList.add(new Friend(1,"monkey"));
friendList.add(new Friend(2,"donkey"));
friendList.add(new Friend(3,"turkey"));
friendList.add(new Friend(4,"mickey"));
//存储信息到请求域
model.addAttribute("tiger1",tiger);
model.addAttribute("friendList",friendList);
//返回(转发)
return "updateTiger3";
}
<h3>修改信息</h3>
<form>
ID: <input type="text" name="tigerId" th:value="${tiger1.tigerId}"><br>
名称:<input type="text" name="tigerName" th:value="${tiger1.tigerName}"><br>
薪资:<input type="text" name="tigerSalary" th:value="${tiger1.tigerSalary}"><br>
朋友:<input type="checkbox" name="season"
th:each="friend:${friendList}"
th:value="${friend.friendId}"
th:text="${friend.friendName}"
th:checked="${#lists.contains(tiger1.friendList,friend)}"><br>
<input type="submit" value="提交">
</form>
案例:影院系统
此处使用HashMap模拟数据库,不涉及和MyBatis、Spring的整合,重点是SpringMVC的使用:先搭建环境,通过页面、分控制器的操作来说功能。
1.准备SpringMVC环境
1.依赖
Servlet依赖
SpringMVC依赖
Spring和Thymeleaf的整合依赖
Lombok
日志Logback
Spring Test
Junit5
2.web.xml
SpringMVC总控制器
解决Post请求中文乱码问题
3.Spring-mvc.xml
启用MVC注解驱动
解决静态资源访问问题
组件(@Controller @Service)扫描
Thymeleaf解析器
4.日志logback.xml
resources/logback.xml
5.创建目录结构
2.准备实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Movie {
private String movieId;
private String movieName;
private Double moviePrice;
}
3.准备业务层接口和实现类代码(提供模拟数据)
public interface MovieService {
List<Movie> getAll();
Movie getMovieById(String movieId);
void saveMovie(Movie movie);
void updateMovie(Movie movie);
void removeMovieById(String movieId);
}
4.测试业务层代码
@SpringJUnitConfig(locations = "classpath:spring-mvc.xml")
public class TestMovie {
@Autowired
private MovieService movieService;
@Test
public void testSelectAll(){
List<Movie> movieList = this.movieService.getAll();
movieList.forEach(m-> System.out.println(m));
}
@Test
public void testSaveMovie(){
Movie movie = new Movie("m17","唐人街探案2",60.0);
this.movieService.saveMovie(movie);
List<Movie> movieList = this.movieService.getAll();
movieList.forEach(m-> System.out.println(m));
}
}
5.显示首页
配置首页,此处使用的xml配置映射。
<!-- 配置视图控制器-->
<mvc:view-controller path="/" view-name="portal"></mvc:view-controller>
<a th:href="@{/movie/getAll}">进入影院系统</a>
6.显示所有电影
@Controller
@RequestMapping("/movie")
public class MovieController {
@Autowired
private MovieService movieService;
@RequestMapping("/getAll")
public String getAll(Model model){
//调用业务层获取电影列表
List<Movie> movieList = this.movieService.getAll();
//将电影列表数据存入请求域
model.addAttribute("movieList",movieList);
//指定返回的视图
return "movieList";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style type="text/css">
h3{
text-align: center;
}
table#table1{
width: 60%;
margin: 10px auto;
border-collapse: collapse;
}
table#table1 th, td{
text-align: center;
border:1px solid red;
}
</style>
</head>
<body>
<h3>电影列表</h3>
<table id="table1">
<tr>
<th>电影编号</th>
<th>电影名称</th>
<th>观看价格</th>
<th>操作</th>
</tr>
<tbody>
<tr th:each="movie:${movieList}">
<td th:text="${movie.movieId}"></td>
<td th:text="${movie.movieName}"></td>
<td th:text="${movie.moviePrice}"></td>
<td>
修改 删除
</td>
</tr>
</tbody>
</table>
</body>
</html>
完善
- 隔行变色
- 区分是否有电影
<h3>电影列表</h3>
<table id="table1">
<tr>
<th>电影编号</th>
<th>电影名称</th>
<th>观看价格</th>
<th>status.index</th>
<th>status.count</th>
<th>操作</th>
</tr>
<tbody th:if="${movieList==null || movieList.size()==0}">
<tr>
<td colspan="10">一部电影也没有</td>
</tr>
</tbody>
<tbody th:if="${not #lists.isEmpty(movieList)}">
<tr th:each="movie,status:${movieList}" th:class="${status.index%2==0?'white':'beige'}">
<td th:text="${movie.movieId}"></td>
<td th:text="${movie.movieName}"></td>
<td th:text="${movie.moviePrice}"></td>
<td th:text="${status.index}"></td>
<td th:text="${status.count}"></td>
<td>
修改 删除
</td>
</tr>
</tbody>
</table>
7.添加新电影功能
1.跳转到添加页面
<mvc:view-controller path="/movie/toAdd" view-name="movieAdd"></mvc:view-controller>
<h3>添加电影信息</h3>
<form th:action="@{/movie/addMovie}" method="post">
电影编号:<input type="text" name="movieId"><br>
电影名称:<input type="text" name="movieName"><br>
电影价格:<input type="number" step="0.01" name="moviePrice"><br>
<input type="submit">
</form>
2.完成添加功能
@RequestMapping("/addMovie")
public String addMovie(Movie movie){
this.movieService.saveMovie(movie);
//return "forward:/movie/getAll";//??
return "redirect:/movie/getAll";
}
此处只能使用重定向。
转发的地址是跳转前的地址,会导致表单重复提交。
重定向的地址是跳转后的地址,不会导致表单重复提交。
8.修改指定电影信息
1.查询指定电影信息并回显
<a th:href="@{/movie/getMovieById(movieId=${movie.movieId})}">修改</a>
@RequestMapping("/getMovieById")
public String getMovieById(String movieId,Model model){
Movie movie = this.movieService.getMovieById(movieId);
model.addAttribute("movie",movie);
return "movieUpdate";
}
<h3>修改电影信息</h3>
<form th:action="@{/movie/updateMovie}" method="post">
电影编号:<input type="text" name="movieId" th:value="${movie.movieId}" readonly><br>
电影名称:<input type="text" name="movieName" th:value="${movie.movieName}"><br>
电影价格:<input type="number" step="0.01" name="moviePrice" th:value="${movie.moviePrice}"><br>
<input type="submit">
</form>
2.完成修改功能
@RequestMapping("/updateMovie")
public String updateMovie(Movie movie){
this.movieService.updateMovie(movie);
return "redirect:/movie/getAll";
}
9.总结
1.开发主要步骤
搭建项目环境
准备业务层代码
进行功能开发(页面Thymeleaf)和分控制器(@Controller)
2.前端知识点
1.判断集合是否为空
<tbody th:if="${movieList==null || movieList.size()==0}">
<tbody th:if="${not #lists.isEmpty(movieList)}">
2.遍历List
<tr th:each="movie,status:${movieList}">
<td th:text="${movie.movieId}"></td>
<td th:text="${movie.movieName}"></td>
<td th:text="${movie.moviePrice}"></td>
<td th:text="${status.index}"></td>
<td th:text="${status.count}"></td>
</tr>
3.实现隔行变色
准备同一个标签的两个不同颜色的tr标签样式
.white{
background-color: white;
}
.beige{
background-color: beige;
}
在遍历时同时设置一个变量自增,然后对这个变量进行判断。
<tr th:each="movie,status:${movieList}" th:class="${status.index%2==0?'white':'beige'}">
</tr>
4.修改和删除的超链接,传递参数的方式
-
href中:使用(变量名=thymeleaf表达式)赋值
-
事件中:使用[[]]将thymeleaf表达式框起来
<a th:href="@{/movie/getMovieById(movieId=${movie.movieId})}">修改</a>
<a th:href="@{/movie/removeMovieById(movieId=${movie.movieId})}">删除</a>
<a href="javascript:void(0)" th:onclick="confirmRemove([[${movie.movieId}]])">删除2</a>
<a href="javascript:void(0)" th:onclick='confirmRemove([[${movie.movieId}]])'>删除3</a>
注意:如果想要删除先获得确认在进行删除,一定要使href失效,可以使用javascript:void(0)。
5.表单数据回显
<form th:action="@{/movie/updateMovie}" method="post">
电影编号:<input type="text" name="movieId" th:value="${movie.movieId}" readonly><br>
电影名称:<input type="text" name="movieName" th:value="${movie.movieName}"><br>
电影价格:<input type="number" step="0.01" name="moviePrice" th:value="${movie.moviePrice}"><br>
<input type="submit">
</form>
3.分控制器
-
如何接收客户端的参数,传递基本参数和实体类。
public String removeMovieById(String movieId){..} public String addMovie(Movie movie){..} -
如何传递参数给下一个组件,有多种方式,此处使用Model传递,底层将数据保存在request域。
@RequestMapping("/getMovieById") public String getMovieById(String movieId,Model model){ Movie movie = this.movieService.getMovieById(movieId); model.addAttribute("movie",movie); return "movieUpdate"; } -
DML操作后如何跳转?
- 不能直接跳转到页面来显示,应该先查询在显示
- 处理完后建议使用redirect而不是forward进行跳转,避免表单的重复提交。
@RequestMapping("/updateMovie") public String updateMovie(Movie movie){ this.movieService.updateMovie(movie); return "redirect:/movie/getAll"; }
Restful风格
Restful架构,是目前非常流行的一种互联网软件架构。它结构清晰,符合标准、易于理解,扩展方便,正得到越来越多网站的采用。
REST,即Representational State Transfer的缩写,可以翻译为“表示性的状态转移”,“表现层资源的状态转移”。
如果一个架构符合REST原则,就称它为RESTful架构。
①资源
REST的名称“表现层的状态转化”中,省略了主语。“表现层”其实指的是“资源”的“表现层”。
所谓“资源”,就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源。访问它的URI就可以,因此URI就成了每一个资源的地址或者独一无二的识别符。
②表现层(Representation)
资源是一种信息实体,它可以有多种外在表现形式。我们把“资源”具体呈现出来的形式,叫做它的“表现层”。
比如,文本可以使用txt格式表现,也可以用html格式、xml格式、JSON格式表现,甚至可以使用二进制格式;图片可以使用JPG格式表现,也可以使用PNG格式表现。
③状态转移
互联网通信协议HTTP协议是一个无状态协议。这一维着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生“状态转化”。而这种转化是建立在表现层之上的,所以就是“表现层状态转化”。
客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET POST PUT DELETE。它们分别对应四种基本操作:
- GET用来获取资源
- POST用来新建资源
- PUT用来更新资源
- DELETE用来删除资源
| 操作 | 传统风格 | REST 风格 |
|---|---|---|
| 保存 | /CRUD/saveEmp | URI 地址:/CRUD/emp 请求方式:POST |
| 删除 | /CRUD/removeEmp?empId=2 | URI 地址:/CRUD/emp/2 请求方式:DELETE |
| 更新 | /CRUD/updateEmp | URI 地址:/CRUD/emp 请求方式:PUT |
| 查询(表单回显) | /CRUD/findEmpById?empId=2 | URI 地址:/CRUD/emp/2 请求方式:GET |
目前主要使用HTTP协议(就是REST思想提出者Roy Thomas Fielding设计的)来实现REST思想,网络上有人从事件的角度,一句话总结REST:URI定位资源,用HTTP动词(GET,POST,PUT,DELETE)描述操作。
幂等性和安全性
| HTTP操作 | 资源操作 | 幂等性 | 安全性 |
|---|---|---|---|
| GET | SELECT | 是 | 是 |
| POST | INSERT/SAVE | 否 | 否 |
| PUT | UPDATE/EDIT | 是 | 否 |
| DELETE | DELETE/REMOVE | 是 | 否 |
Restful请求方式映射:GET
<h3>Restful请求映射:Get</h3>
<a th:href="@{/emp}">查询所有员工</a><br>
<a th:href="@{/emp/1}">查询指定编号的员工</a><br>
<a th:href="@{/emp/zh/300}">按照姓名和工资查询员工</a><br
@Controller
@Slf4j
public class EmployeeController {
@GetMapping("/emp")
public String findAll(){
log.debug("--------EmployeeController findAll ---------");
return "result";
}
@GetMapping("/emp/{empId}")
public String findById(@PathVariable("empId") Integer empId){
log.debug("-------EmployeeController findById :"+empId+"------");
return "result";
}
@GetMapping("/emp/{empName}/{minSalary}")
public String findEmp(@PathVariable("empName") String ename,@PathVariable("minSalary") Double minSal){
log.debug("-------EmployeeController findEmp :"+ename+","+minSal+"------");
return "result";
}
}
RequestMapping中使用{}表示参数,方法的参数中使用@PathVariable指定对应关系
Restful请求方式映射:POST
<h3>Restful请求映射:POST</h3>
<form th:action="@{/emp}" method="post">
员工姓名:<input type="text" name="empName"><br>
员工薪水:<input type="text" name="empSalary"><br>
<input type="submit" value="提交">
</form>
@PostMapping("/emp")
public String addEmp(Employee emp){
log.debug("-------EmployeeController addEmp:"+emp+" ----------");
return "result";
}
Restful请求方式映射:PUT
<!--将POST请求转换为put delete请求 (要看_method的值),必须写在CharacterEncodingFilter后面 -->
<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>
HiddenHttpMethodFilter必须写在CharacterEncodingFilter后面
<h3>Restful请求映射:PUT</h3>
<form th:action="@{/emp}" method="POST">
<input type="hidden" name="_method" value="put">
员工编号:<input type="text" name="empId" value="7839"><br>
员工姓名:<input type="text" name="empName" value="King"><br>
员工薪水:<input type="text" name="empSalary" value="5000"><br>
<input type="submit" value="提交">
</form>
• 要点1:原请求方式必须是 post • 要点2:新的请求方式名称通过请求参数发送 • 要点3:请求参数名称必须是_method • 要点4:请求参数的值就是要改成的请求方式
@PutMapping("/emp")
public String updateEmp(Employee emp, HttpServletRequest request){
log.debug("-----EmployeeController updateEmp:"+request.getMethod()+"--------------");
log.debug("-------EmployeeController updateEmp:"+emp+" ----------");
return "result";
}
Restful请求方式映射:DELETE
<script type="text/javascript" th:src="@{/js/vue.js}"></script>
<h3>Restful请求映射:DELETE</h3>
<div id="app">
<a th:href="@{/emp/1}" @click="deleteEmp">删除编号为1的员工</a>
<a th:href="@{/emp/2}" @click.prevent="confirmDeleteEmp">删除编号为2的员工</a>
</div>
需要提供一个表单,里面使用hidden,必须指定name="_method" value="delete",是用来转发请求的。
<form id="form1" method="post">
<input type="hidden" name="_method" value="delete">
</form>
script type="text/javascript">
new Vue({
el:"#app",
data:{
} ,
methods:{
deleteEmp:function(event){
//获取通用表单
var formElem = document.getElementById("form1");
//修改通用表单的action为超链接的href
formElem.action = event.target.href;
//提交表单
formElem.submit();
//阻止超链接的默认行为(请求href的资源)
//event.preventDefault();
},
confirmDeleteEmp:function(event){
var flag = window.confirm("您确认要删除该员工信息吗");
if(flag){
//获取通用表单
var formElem = document.getElementById("form1");
//修改通用表单的action为超链接的href
formElem.action = event.target.href;
//提交表单
formElem.submit();
//阻止超链接的默认行为(请求href的资源)
//event.preventDefault();
}
}
}
});
</script>
@DeleteMapping("/emp/{empId}")
public String deleteEmp(@PathVariable("empId") Integer empId,HttpServletRequest request){
log.debug("-----------EmployeeController deleteEmp:"+request.getMethod()+"--------------");
log.debug("-------EmployeeController deleteEmp:"+empId+" ----------");
return "result";
}
总结
使用Restful风格重构项目:
- 修改页面路径
- 修改分控制器的RequestMapping
- 使用@PathVariable指定对应关系
- 页面跳转到分控制器方法是注意路径。
Ajax传递参数
要求:
导入axios和vue的js文件。
基本类型参数
页面
<script type="text/javascript" th:src="@{/js/vue.js}"></script>
<script type="text/javascript" th:src="@{/js/axios.min.js}"></script>
<a href="javascript:void(0)" @click="AjaxDemo1">Ajax请求1</a>
<script type="text/javascript">
new Vue({
el:"#app",
methods:{
AjaxDemo1:function(){
//发送一个Ajax请求
axios({
method:"POST",
//url:"/springmvc/ajax/ajaxDemo1",
url:"[[@{/ajax/ajaxDemo1}]]",
params:{
sid:1,
name:"zhangsan",
score:90.5
}
})
.then(function(result){
//console.info(result);
alert(result.data+" "+result.status);
})
.catch(function(error1){
//console.debug(error)
console.debug(error1)
//console.debug(error1.data)
});
}
}
});
</script>
后台
@Controller
@Slf4j
public class AjaxController {
@RequestMapping("/ajax/ajaxDemo1")
@ResponseBody
public String ajaxDemo1(Integer sid,String name,Double score){
log.debug("ajaxDemo1:"+sid+","+name+","+score);
int n = 10/0;
return "ok"; //WEB-INF/templates/ok.html
}
}
实体类
页面
<h3>Ajax请求2:传递实体类型参数</h3>
<a href="javascript:void(0)" @click="AjaxDemo2">Ajax请求2</a>
new Vue({
el:"#app",
methods:{
AjaxDemo2:function(){
//发送一个Ajax请求
axios({
method:"POST",
//url:"/springmvc/ajax/ajaxDemo1",
url:"[[@{/ajax/ajaxDemo2}]]",
params:{
empId:7839,
empName:"King",
empSalary:5000.0
}
})
.then(function(result){
//console.info(result);
alert(result.data+" "+result.status);
})
.catch(function(error1){
//console.debug(error)
console.debug(error1)
console.debug(error1.response)
console.debug(error1.response.data)
alert(error1.response.data.innerHTML);
//console.debug(error1.data)
});
}
}
});
</script>
后台
@RequestMapping("/ajax/ajaxDemo2")
public String ajaxDemo2(Employee emp){
log.debug("ajaxDemo2:"+emp);
int n = 10/0;
return "ok"; //WEB-INF/templates/ok.html
}
实体类带级联属性-1
<h3>Ajax请求3-1:传递实体类型参数带级联属性</h3>
<a href="javascript:void(0)" @click="AjaxDemo31">Ajax请求3-1</a>
AjaxDemo31:function(){
//发送一个Ajax请求
axios({
method:"POST",
//url:"/springmvc/ajax/ajaxDemo1",
url:"[[@{/ajax/ajaxDemo31}]]",
params:{
empId:7839,
empName:"King",
empSalary:5000.0,
hiredate:"1999-12-23",
"dept.deptNo":10,
"dept.dname":"教学部"
}
})
.then(function(result){
//console.info(result);
alert(result.data+" "+result.status);
})
.catch(function(error1){
//console.debug(error)
console.debug(error1)
console.debug(error1.response)
console.debug(error1.response.data)
});
}
后台
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer empId;
private String empName;
private double empSalary;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date hiredate;//!!
private Dept dept;//!!!
}
@RequestMapping("/ajax/ajaxDemo31")
public String ajaxDemo31(Employee emp){
log.debug("ajaxDemo31:"+emp);
int n = 10/0;
return "ok"; //WEB-INF/templates/ok.html
}
注意:接收日期类型的参数
Java.util.Date 需要使用 @DateTimeFormat(pattern = "yyyy-MM-dd")
Java.sql.Date 格式必须是yyyy-MM-dd,无需 @DateTimeFormat
实体类带级联属性-2
页面
<h3>Ajax请求3-2:传递实体类型参数带级联属性</h3>
<a href="javascript:void(0)" @click="AjaxDemo32">Ajax请求3-2</a>
AjaxDemo32:function(){
//发送一个Ajax请求
axios({
method:"POST",
//url:"/springmvc/ajax/ajaxDemo1",
url:"[[@{/ajax/ajaxDemo32}]]",
data:{
empId:7839,
empName:"King",
empSalary:5000.0,
hiredate:"1999-12-23",
dept:{
deptNo:10,
dname:"教学部"
}
}
})
.then(function(result){
//console.info(result);
alert(result.data+" "+result.status);
})
.catch(function(error1){
//console.debug(error)
console.debug(error1)
console.debug(error1.response)
console.debug(error1.response.data)
});
},
后台
@RequestMapping("/ajax/ajaxDemo32")
public String ajaxDemo32(@RequestBody Employee emp){
log.debug("ajaxDemo32:"+emp);
//int n = 10/0;
return "ok"; //WEB-INF/templates/ok.html
}
依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.1</version>
</dependency>
注意
- 必须添加jackson json组件(幕后英雄)
- 分控制器方法中接收数据必须使用@RequestBody
- 前端页面中使用Ajax传递数据使用data属性而不是params
- 前端页面中传递数据要使用json格式
为什么要加@RequestBody
@RequestBody 接收客户端请求时,从请求体而不是请求头、URL中获取数据
@ResponseBody 给客户端响应时,不是转发和重定向的跳转,而是将数据直接放入响应体
发送Ajax请求时,使用data,将数据放入请求体,服务器的分控制器就从请求体中拿数据
发送Ajax请求时,使用params,将数据写到url的后面,比如 ajax/ajaxDemo31?empId=xx&ename=xx&...,不管是get还是post
实体类对象、集合
前面的四个Ajax请求,请求的内容不同,但是返回的都是简单的字符串,如果返回实体类对象、集合对象,该怎么办?
页面
<h3>Ajax请求4:返回实体类或者集合转换为JSON数据</h3>
<a href="javascript:void(0)" @click="AjaxDemo4">Ajax请求4</a>
AjaxDemo4:function(){
//发送一个Ajax请求
axios({
method:"POST",
//url:"/springmvc/ajax/ajaxDemo1",
url:"[[@{/ajax/ajaxDemo4}]]",
})
.then(function(result){
//console.info(result);
//alert(result.data+" "+result.status);
console.info(result.data)
})
.catch(function(error1){
//console.debug(error)
console.debug(error1)
});
},
后台
@RequestMapping("/ajax/ajaxDemo4")
public Employee ajaxDemo4(){
Employee emp = new Employee();
emp.setEmpId(7839);
emp.setEmpName("King");
emp.setEmpSalary(5000.0);
emp.setHiredate(java.sql.Date.valueOf("1999-12-22"));
Dept dept = new Dept(10,"教学部");
emp.setDept(dept);
return emp;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer empId;
private String empName;
private double empSalary;
//从客户端请求的日期数据的格式
@DateTimeFormat(pattern = "yyyy-MM-dd")
//响应给客户端的日期数据的格式
@JsonFormat(pattern = "yyyy年MM月dd日")
private Date hiredate;//!!
private Dept dept;//!!!
}
注意:
背后离不开Jackson组件的作用(将对象或集合转换为JSON字符串)
拦截器
和过滤器非常的相似,解决的问题类似。
拦截器和过滤器的相似点
- 步骤基本相似:拦住、处理、放行。
- 都可以组成链状结构。早执行玩结束,玩执行早结束。
拦截器和过滤器的不同点
- 过滤器是JavaEE中的概念,在Servlet之前执行。拦截器是springMVC等MVC框架的概念,在总控制器Servlet之后,在分控制器之前执行。
- 拦截的范围不同。
- 对IoC容器的使用不同(拦截器可以直接使用IoC容器)。
创建拦截器
如何创建拦截器:
方法1:实现HandlerInterceptor,推荐使用该方式
方法2:继承HandlerInterceptorAdapter,JDK8之后该方式已经过时,因为接口中已经给了空实现。
public class MyInterceptor1 implements HandlerInterceptor {
/**
* 在请求的目标资源(可以是分控制器,或者静态资源)执行之前执行
* @param request 请求
* @param response 响应
* @param handler 如果访问的是分控制器,就是封装后的请求的分控制器方法,
* 如果访问不是分控器,也会封装成其他类
* 为了可以处理各种情况,此处类型为Object
* @return 返回值 true 放行 false 不放行(不会再执行后面的拦截器、目标资源)
* 不放行之前一般要重定向到某个页面
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("--------MyInterceptor1 preHandle ---------------");
return false;
}
/**
* 在请求的目标资源(可以是分控制器,或者静态资源)执行之后执行
* @param request
* @param response
* @param handler
* @param modelAndView 每个分控制器方法最终的返回值类型就是ModelAndView
* 此处可以获取分控制器的返回值,并对其中的数据和视图进行处理
* @throws Exception
*/
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("--------MyInterceptor1 postHandle ---------------");
}
/**
* 分控制器之后,SpringMVC流程还没有走完,
* 还要进行
* 视图解析 : result ---> /WEB-INF/templates/result.html
* 视图渲染 <p th:text=${msg}></p> 将Thymeleaf页面的th标签的使用Model中数据替换
* <form th:action=@{/user/login}></form> /springmvc/user/login
*
* 以上工作做完后在执行该方法,比如可以进行异常处理,资源关闭等操作
* @param request
* @param response
* @param handler
* @param ex 请求发生的异常
* @throws Exception
*/
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("--------MyInterceptor1 afterCompletion ---------------");
}
}
配置拦截器
<!--配置拦截器 全局拦截器,拦截经过SpringMVC总控制器的所有请求 -->
<bean class="com.atguigu.interceptor.MyInterceptor1"></bean>
<mvc:interceptors>
<!--配置拦截器 局部拦截器 需要配置拦截的内容 -->
<mvc:interceptor>
<mvc:mapping path="/user/*"/>
<bean class="com.atguigu.interceptor.MyInterceptor2"></bean>
</mvc:interceptor>
</mvc:interceptors>
一个拦截器的各个方法执行顺序:
拦截器preHandle->(如果返回true)执行分控制器handler->拦截器postHandle->视图渲染->拦截器afterCompletion
多个拦截器的执行顺序:
拦截器1的preHandle->拦截器2的preHandle->(如果返回true)执行分控制器handler->拦截器2的postHandle->拦截器1的postHandle->视图渲染->拦截器2的afterCompletion->拦截器1的afterCompletion
不同的拦截器先执行谁:
由配置的顺序确定,和全局局部无关。
经过访问确认,访问分控制器和访问静态资源,都会经过拦截器(总控制器的访问路径是“/”)
拦截器应用
public class MyInterceptor1 implements HandlerInterceptor {
@Autowired
private ServletContext servletContext;
@Autowired
private UserController userController;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.解决中文乱码问题
//request.setCharacterEncoding("utf-8");
//2.判断用户是否已经登录
//如果访问的路径是登录的页面或者分控制器方法的路径,如果访问的路径是注册的页面或者分控制器方法
//如果访问的是验证码的路径,直接放行
/*
String requestURL = request.getRequestURL().toString();
if(requestURL是以上五个路径中的一个){
//直接放行
return true;
}
String user = (String)request.getSession().getAttribute("user");
if(user ==null ){
response.sendRedirect("/xxx/login.html");
return false;
}
*/
System.out.println("--------MyInterceptor1 preHandle ---------------");
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//1.如果Model中有敏感的字符,在此处进行处理
/*
String msg = (String)modelAndView.getModel().get("msg");
if(msg.contains("枪支弹药") || msg.contains("黄赌毒")){
msg.replace("枪支弹药","****");
msg.replace("黄赌毒","???");
modelAndView.addObject("msg",msg);
}
*/
//2.开发了一套新的页面,需要测试通过,再部署到服务器上
//String viewName = modelAndView.getViewName(); //result
//modelAndView.setViewName(viewName+"Test");
System.out.println("--------MyInterceptor1 postHandle ---------------");
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("--------MyInterceptor1 afterCompletion ---------------");
//1.处理异常
//2.关闭资源
}
}
类型转换器
内置类型转换器
使用注解
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer empId;
private String empName;
@NumberFormat(pattern = "##,###,###.##")
private double empSalary;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date hiredate;//!!
}
@Controller
@Slf4j
public class EmployeeController {
@RequestMapping("/emp/saveEmp")
public String saveEmp(Employee emp, BindingResult bindingResult){
log.debug("EmployeeController saveEmp:"+emp);
if(bindingResult.hasErrors()){
return "error";
}
return "result";
}
}
<p th:errors="${employee.hiredate}"></p>
<p th:errors="${employee.empSalary}"></p>
自定义类型转换器
需要实现Converter<>接口
public class AddressConverter implements Converter<String, Address> {
public Address convert(String source) {
// 1.按照约定的规则拆分源字符串
String[] split = source.split(","); //河北省,张家口市,崇礼县
String province = split[0];//河北省
String city = split[1]; //张家口市
String street = split[2];//崇礼县
// 2.根据拆分结果创建 Address 对象
Address address = new Address(province, city, street);
// 3.返回转换得到的对象
return address;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer empId;
private String empName;
@NumberFormat(pattern = "##,###,###.##")
private double empSalary;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date hiredate;//!!
private Address address;
}
<form th:action="@{/emp/saveEmp}" method="post">
薪水<input type="text" name="empSalary" value="11,123.45"><br>
入职时间<input type="text" name="hiredate" value="1999-12-23"><br>
地址<input type="text" name="address" value="河北省,张家口市,崇礼县"><br>
<input type="submit">
</form>
数据校验
JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
| 注解 | 规则 |
|---|---|
| @Null | 标注值必须为 null |
| @NotNull | 标注值不可为 null |
| @AssertTrue | 标注值必须为 true |
| @AssertFalse | 标注值必须为 false |
| @Min(value) | 标注值必须大于或等于 value |
| @Max(value) | 标注值必须小于或等于 value |
| @DecimalMin(value) | 标注值必须大于或等于 value |
| @DecimalMax(value) | 标注值必须小于或等于 value |
| @Size(max,min) | 标注值大小必须在 max 和 min 限定的范围内 |
| @Digits(integer,fratction) | 标注值值必须是一个数字,且必须在可接受的范围内 |
| @Past | 标注值只能用于日期型,且必须是过去的日期 |
| @Future | 标注值只能用于日期型,且必须是将来的日期 |
| @Pattern(value) | 标注值必须符合指定的正则表达式 |
JSR 303 只是一套标准,需要提供其实现才可以使用。Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解:
| 注解 | 规则 |
|---|---|
| 标注值必须是格式正确的 Email 地址 | |
| @Length | 标注值字符串大小必须在指定的范围内 |
| @NotEmpty | 标注值字符串不能是空字符串 |
| @Range | 标注值必须在指定的范围内 |
依赖
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-annotation-processor</artifactId>
<version>6.2.0.Final</version>
</dependency>
public class Employee {
private Integer empId;
private String empName;
@NumberFormat(pattern = "##,###,###.##")
private double empSalary;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date hiredate;//!!
private Address address;
@Size(min=6,max = 12)
@Email
private String email;
}
@Controller
@Slf4j
public class EmployeeController {
@RequestMapping("/emp/saveEmp")
public String saveEmp(@Validated Employee emp, BindingResult bindingResult){
log.debug("EmployeeController saveEmp:"+emp);
if(bindingResult.hasErrors()){
return "error";
}
return "result";
}
}
异常映射
将异常类型和某个具体的视图关联起来,建立映射关系
java.lang.NullPointerException----------->error-null.html
java.lang.FileNotFoundException----------->error-notfoundl.html
java.lang.RuntimeException---------->error-run.html
java.lang.Exception---------->error.html
异常映射的好处
微观:使用声明式代替编程式来实现异常管理
让异常控制和核心业务解耦,二者各自维护,结构性更好。
宏观:整个项目层面使用同一条规则来管理异常。
整个项目代码风格更加统一、简洁。
便于团队成员之间彼此协作。
XML方式实现
1springmvc 配置文件中创建异常映射
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.NullPointerException">exp-null</prop>
<prop key="java.io.FileNotFoundException">exp-notfound</prop>
<prop key="java.lang.RuntimeException">exp-run</prop>
<prop key="java.lang.Exception">exp</prop>
</props>
</property>
<property name="exceptionAttribute" value="atguiguException"></property>
</bean>
2创建相应的异常结果页面
使用thymeleaf接收数据并显示。
3创建发送异常的分控制器并测试
@Controller
@Slf4j
public class UserController {
@RequestMapping("/user/save1")
public String save1(){
String str = null;
System.out.println(str.length());//空指针
return "result";
}
@RequestMapping("/user/save2")
public String save2(){
int n = 10/0; //算术异常 --- >运行异常
return "result";
}
@RequestMapping("/user/save3")
public String save3() throws FileNotFoundException {
FileInputStream fis = new FileInputStream("d:/sdf/adfadf.txt");
return "result";
}
@RequestMapping("/user/save4")
public String save4() throws SQLException {
String url ="adfadsf";
String username ="root";
String password ="root";
Connection conn = DriverManager.getConnection(url,username,password);
return "result";
}
}
注解方式实现
1定义一个类,在其方法指明异常映射关系
/**
* 该注解其实也是一个@Component
*/
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(value = NullPointerException.class)
public String resolveNullPointerException(Exception e,Model model){
model.addAttribute("atguiguException",e);
return "exp-null2";
}
@ExceptionHandler(value = RuntimeException.class)
public String resolveRuntimeException(Exception e,Model model){
model.addAttribute("atguiguException",e);
return "exp-run";
}
}
注意:
- 定义一个异常处理类,添加@ControllerAdvice
- 定义方法处理指定异常,添加注解说明异常类型@ExceptionHandler(value = NullPointerException.class)
2组件扫描,扫描这个类
<!-- 组件(@Controller @Service)扫描 -->
<context:component-scan base-package="com.atguigu.controller,com.atguigu.exceptionmapping"></context:component-scan>
3创建发送异常的分控制器并测试
如果XML和注解两种方式同时存在,注解优先。
没有必要两种都设置。推荐使用注解。
区分请求类型(Ajax和非Ajax)
如果要区分请求类型,给两种请求类型都给出处理方案,只能采用注解方式。
1准备普通请求和Ajax请求
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript" th:src="@{/js/vue.js}"></script>
<script type="text/javascript" th:src="@{/js/axios.min.js}"></script>
</head>
<body>
<h3>异常处理:非Ajax请求</h3>
<a th:href="@{/user/save2}">非Ajax请求</a>
<h3>异常处理:Ajax请求</h3>
<div id="app">
<a href="javascript:void(0)" @click="testAjax()">Ajax请求</a>
</div>
</body>
<script type="text/javascript">
new Vue({
el:"#app",
methods:{
testAjax:function(){
axios({
method:"POST",
url:"[[@{/user/save5}]]"
})
.then(function(response){
console.info(response.data);
})
.catch(function(response){
console.info(response)
});
}
}
});
</script>
</html>
@Controller
@Slf4j
public class UserController {
@RequestMapping("/user/save2")
public String save2(){
int n = 10/0; //算术异常 --- >运行异常
return "result";
}
@RequestMapping("/user/save5")
@ResponseBody
public String save5(){
int n = 10/0; //算术异常 --- >运行异常
return "ok"; //返回字符而不是跳到页面
}
}
2 测试
对于Ajax之前的异常映射也可以起作用,但是返回的是整个异常页面,而不是异常字符串。
对于Ajax请求,出现了异常,跳到异常页面并返回,要在then中来接收,而不是catch中。
3 定义工具类
public class MyUtil {
/**
* 判断当前请求是否为Ajax请求
* @param request 请求对象
* @return
* true:当前请求是Ajax请求
* false:当前请求不是Ajax请求
*/
public static boolean judgeRequestType(HttpServletRequest request) {
// 1.获取请求消息头
String acceptHeader = request.getHeader("Accept");
String xRequestHeader = request.getHeader("X-Requested-With");
// 2.判断
return (acceptHeader != null && acceptHeader.contains("application/json"))
||
(xRequestHeader != null && xRequestHeader.equals("XMLHttpRequest"));//只针对jQuery
}
}
4 在实现异常映射的方法中同时处理两种情况
@ExceptionHandler(value = RuntimeException.class)
public String resolveRuntimeException(Exception e, Model model, HttpServletResponse response, HttpServletRequest request){
if(MyUtil.judgeRequestType(request)){//是Ajax请求
try {
response.getWriter().print(e);
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
model.addAttribute("atguiguException",e);
return "exp-run"; //这是要跳转到某个页面
}
异常处理:Ajax请求
使用异常映射时,Ajax请求出现异常返回的异常信息要在then中获取
文件上传
底层基于commons-fileupload组件,进行封装,简化操作。
1 添加依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
这个依赖依赖于commons-io的jar,导入时会把它导入进来。
2 配置解析器(spring.xml)
<!-- 配置多媒体解析器,和文件上传有关-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="utf-8"></property>
</bean>
3 准备页面
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String userId;
private String realName;
private Integer age;
private String photo;//照片的路径
}
<h3>文件上传</h3>
<form th:action="@{/user/register}" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="userId"><br>
真实姓名:<input type="text" name="realName"><br>
年龄:<input type="text" name="age"><br>
照片:<input type="file" name="photo"><br>
</form>
注意
- 请求方式必须是POST。
- 请求体的编码方式必须是multipart/form-data(通过form标签的enctype属性设置)
- 使用input标签。type属性设置为file来生成文件上传框。
4 准备控制器
@Controller
@Slf4j
public class UserController {
@RequestMapping("/user/register")
public String register(User user, MultipartFile photo1) throws IOException {
log.debug(user.toString());
log.debug(photo1.toString());
user.setPhoto(photo1.getOriginalFilename());
//上传文件到指定的路径
photo1.transferTo(new File("d:/upload/"+photo1.getOriginalFilename()));
return "result";
}
}
5 进行测试
注意:
因为User类中有String photo属性,所以MultipartFile的变量名不要重复。
6 MultipartFile类
属性
log.debug(photo1.getOriginalFilename()); //上传文件的原始名称 zsf.webp zcs.jpg
log.debug(photo1.getContentType()); //MIME类型 tomcat web.xml中有MIME列表 html text/html; jpg images/jpeg txt text/plain
log.debug(photo1.getName()); //表单的上传字段名 <input type="file" name="photo1">
log.debug(photo1.getSize()+""); //文件大小
//log.debug(photo1.getBytes()+""); //文件对对应的字节数组内容
log.debug(photo1.getInputStream()+"");//上传文件需要输入流,下载文件需要输出流
文件上传——完善
1.如果目录不存在就创建
//完善1:如果文件夹不存在,就创建
File dir = new File("d:/upload");
if(!dir.exists()){
//dir.mkdir(); //只能创建一级目录 d:/upload
dir.mkdirs();//可以创建多 级目录 d:/upload/abc/def/ghi
}
2.避免同名文件覆盖(UUID)
UUID是通用唯一识别码的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的便是信息,而不需要通过中央控制端来做辨识信息的指定。如此依赖,每个人都可以创建不与其他人冲突的UUID。这样的情况下,就不需要考虑数据库创建时的名称重复问题。
注意:只保证唯一,不保证有序。
UUID由以下几部分的组合:
- 当前日期和事件,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列。
- 全局唯一的IEEE及其识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
使用
String fileNewName = UUID.randomUUID().toString(); //36f0b33c-e10d-440e-927b-2d96e6d9f051
String extName = photo1.getOriginalFilename().substring(photo1.getOriginalFilename().lastIndexOf(".")); //zadfad.adfad.sf.webp
fileNewName += extName;
3.限制上传文件类型
建议按照按照MIME类型限制。
//方法1:按照扩展名限制
// if(extName!=null &&(extName.equals(".jpg") || extName.equals(".gif"))){ }
//方法2:按照MIME类型限制
String contentType = photo1.getContentType().toLowerCase();
if(!"image/jpeg".equals(contentType) && !"image/gif".equals(contentType)){ //如果不是image/jpeg image/gif
model.addAttribute("error","只能上传jpg和gif文件");
return "error";
4.限制上传文件大小
方式1:接收后,取得属性,看是否符合要求。
{...
//完善4:限制上传文件的大小(不推荐使用该方式)
long size = photo1.getSize();
if(size>10*1024){ //10K
model.addAttribute("error","上传文件不能超过10K");
return "error";
}
缺点:
如果按这种方式来的话,那么就是在分控制器中限制大小,效率比较低。
方式2[推荐]:通过总控制器来限制文件大小(spring-mvc.xml)。
<!-- 配置多媒体解析器,和文件上传有关-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="utf-8"></property>
<!--限制一次性上传的多个文件的总大小 -->
<property name="maxUploadSize" value="100000"></property>
<!--限制每个上传文件的大小 -->
<property name="maxUploadSizePerFile" value="10000"></property>
</bean>
5 使用逻辑路径取代物理路径
原因:
- 在Linux中没有盘符的概念,在windows中盘符的概念。上传文件夹无法统一的指定,比如“d:/upload”.
- 也可以让上传文件夹在项目中(不是开发时目录,是部署后目录),不同的服务器目录可能不同。因为Tomcat、idea的安装位置不同,导致真是目录不同。
解决:使用逻辑路径取代物理路径
如何操作?
通过项目启动时生成的servletContext对象的getRealPath方法即可。它的String参数指的是部署后的项目的根目录路径后边的路径【逻辑路径】,它会返回一个完整的物理路径。
//完善5:使用逻辑路径取代物理路径
String upDir = "/upload"; //逻辑路径
String realPath = servletContext.getRealPath(upDir);
//物理路径 c:/adfa/adfadf/adfad/upload /adfad/adfad/upload
//完善1:如果文件夹不存在,就创建
File dir = new File(realPath);
if(!dir.exists()){
//dir.mkdir(); //只能创建一级目录 d:/upload
dir.mkdirs();//可以创建多 级目录 d:/upload/abc/def/ghi
}
注意
如果发布项目时选择的发布包是war时,会打成一个war包,并自动部署到tomcat的webapp下,启动服务器是改war包自动解压。
如果发布项目是选择的发布包是war exploded,就不会打成war包,也就不会发布到tomcat的webapp下【而是在idea的某个目录下】。
文件上传——使用文件服务器
未使用文件服务器的文件上传的一些缺陷:
- Web应用重新部署时通常都会清理旧的构建结果,此时用户以前上传的文件会被删除,导致数据丢失。
- 项目运行很长时间后,回导致上传的文件基类非常多,体积非常大,从而拖慢Tomcat运行速度。
- 当服务器以集群模式运行时,文件上传到集群中的某一个实例,其他实例中没有这个文件,就会造成数据的不一致。
- 不支持动态扩容,一旦系统增加了新的硬盘或新的服务器实例,那么上传、下载时使用的路径都需要跟着变化,导致Java代码需要重新编写、重新编译,进而导致整个项目重新部署。
解决方案:
提供专门的文件服务器(一个就可以)。运行Tomcat的叫应用服务器(集群下会有多个)。
文件服务器类型:
第三方平台【如阿里的OSS对象存储服务、七牛云】;
自己搭建的服务器:FastDFS。
文件下载
<h3>文件下载</h3>
<img th:src="@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}"><br>
<a th:href="@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}">下载(直接打开)</a>
<hr>
<a th:href="@{/user/download(photoName=35b27821-710e-444a-8f5e-226dd7fb5116.gif)}">下载(弹框提示下载)</a>
@RequestMapping("/user/download")
public void download(String photoName, HttpServletResponse response) throws IOException {
//创建输入流和输出流
String realPath = servletContext.getRealPath("/upload"); /C:\\Users\\Administrator\\IdeaProjects\\springmvc\\pro11_updownload\\target\\pro11_updownload-1.0-SNAPSHOT\\upload
File file = new File(realPath,photoName);//
InputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream(); //写到客户端,那就是下载
//指定三个响应头(其实就一个),控制浏览器的处理,不是直接显示,而是弹框
//response.setContentLength((int)file.length());
//response.setContentType();//MIME类型 也是保存在数据库中
//建议:保存用户信息到数据库时,要保存照片的原始名称、UUID名称、MIME类型
response.setHeader("Content-Disposition","attachment;filename=zwj.gif"); //attachment 附件
//使用输入流和输出流
IOUtils.copy(is,os);
//关闭输入流和输出流
is.close();
os.close();
}
如果文件不在项目目录下,而是在d:\upload项目下,该怎么办?
-
不可以在超链接中直接指定路径
<a th:href="@{/upload/35b27821-710e-444a-8f5e-226dd7fb5116.gif}">下载(直接打开)</a><!--错误的--> -
不管是直接打开,还是附件下载,都要采用编程方式
File file = new File("d:/upload",photoName); response.setHeader("Content-Disposition","attachment;filename=zwj.gif"); //attachment 附件 response.setHeader("Content-Disposition","inline"); //直接打开
补充内容【不重要】
1. springmvc配置文件默认位置和名称
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- <init-param>-->
<!-- <param-name>contextConfigLocation</param-name>-->
<!-- <param-value>classpath:spring-mvc.xml</param-value>-->
<!-- </init-param>-->
<load-on-startup>1</load-on-startup>
</servlet>
2. @RequestMapping的更多参数
| 需求 | 映射方式 |
|---|---|
| 根据 Accept-Language:zh-CN,zh;q=0.8 映射 | @RequestMapping ( value="/xxx", headers= "Accept-Language=zh-CN,en;q=0.8" ) |
| 请求参数中必须包含userName | @RequestMapping(value = "/xxx", params="userName") |
| 请求参数中不能包含userName | @RequestMapping(value = "/xxx", params="!userName") |
3. @ModelAttribute
@ModelAttribute
public void before(){
System.out.println("--------调用分控制器的每个方法之前先执行该方法----------");
}
效果
- 在每个handler方法前执行(作用)
- 可以将某些数据提前存入请求域(应用)
Spring MVC运行原理【重要】
SpringMVC的核心流程
执行流程
- 首先浏览器发送请求——>DipatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他组件进行处理,作为统一访问点,进行全局的流程控制。
- DispatcherServlet——>HandlerMapping,处理器映射器将会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器对象、多个HanlderInterceptor拦截器对象)
- DispatcherServlet——>HandlerAdapter,处理器适配器将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器。
- HandlerAdapter——>调用处理器相应功能处理方法,并返回一个ModelAndView对象(Model部分是业务对象返回的模型数据,View部分为逻辑视图名)
- DispatcherServlet——>ViewResolver,视图解析器将把逻辑视图名解析为物理视图,返回View对象。
- DispatcherServlet——>View,view会根据传进来的Model模型数据进行渲染,此处的Model实际上是一个Map数据结构
- DispatcherServlet——>响应,返回控制权给DispatcherServlet,由它返回响应给用户,到此一个流程结束。
总结强调
- 总控制器不是撒手掌柜,它每次调用其他组件都是要求得到结果的,并且根据结果调用下一个组件。
- 分控制器的返回值:ModelAndView。Model是数据,View是要跳转到的视图。
- 如果要是Ajax请求,就没有ModelAndView了,而是直接返回数据片段。客户端通过回调幻术来处理数据片段。
- SpringMVC流程固定,开发者的主要任务是:开发分控制器、视图文件,可能还有拦截器。
SpringMVC核心API
springMVC四大组件
- DispatcherServlet:总控制器。
- HandlerMapping:建立了请求路径和分控制器之间的映射。
- HandlerExecutionChain:总控制器调用HandlerMapping组件的返回值是一个执行链,不仅有要执行的分控制器方法,还有响应的多个拦截器,组成一个执行链。
- HandlerAdapter:调用分控制器的方法,不是由总控制器之间调用的,而是由HanlderAdapter来调用。
- 【可以有】ViewResolver:逻辑视图(result)——>物理视图(/WEB-INF/templates/result.html)
①中央控制器DispatcherServlet
发现它的上级类中有HttpServlet,而Servlet的执行入口是service()。
②处理器映射器HandlerMapping
项目中会有多个@RequestMapping,每个RequestMapping对应一个类或者一个方法。用户给一个请求路径如何获取该路径对应的方法呢?这就要通过HandlerMapping来实现了。返回的结果就是访问路径所对应的处理器。
③处理器执行链HandlerExecutionChain
为什么请求Handler,要返回HandlerExecutionChain呢?
因为Handler的执行前后会有一个或者多个拦截器执行,并且拦截器是链式执行的。所有的HandlerExecutionChain中就包含了要执行的一个处理器和多个拦截器的信息。
④处理器适配器HandlerAdapter
处理器的执行不是由总控制器直接执行的,而是通过处理器适配器其来执行的。因为会有XML方式、注解方式等处理器形式,具体执行会有不同,通过不同的HandlerAdapter来实现。
其中RequestMappingHandlerAdapter实现类负责解析注解方式的映射。
SimpleServletHandlerAdapter实现类是XML方式的适配器。
⑤视图解析器ViewResolver
ViewResolver实现逻辑视图到物理视图的解析,比如:对于下面的视图解析器。
public interface ViewResolver{
@Nullable
View resolveViewName(String var1,Locale var2)throws Exception;
}
“main”是逻辑视图,而添加了后缀前缀的“WEB-INF/jsp/main.jsp”就是物理视图。
请求处理过程
整个请求处理过程都是doDispatch()方法在宏观上协调和调度,把握了这个方法就理解了SpringMVC总体上是如何处理请求的。
所在类:org.springframework.web.servlet.DispatcherServlet
所在方法:doDispatch()
该方法源码:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
/*第一步,获取执行链(一个handler和多个interceptor)*/
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
/*第二步,得到适配器,用它来执行执行handler*/
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
/*第三步,执行handler之前,限制性拦截器的preHandler方法,如果返回false后面的就不执行了*/
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
/*第四步,执行handler,得到返回的ModelAndView*/
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
/*第五步,执行完handler后执行拦截器的postHandler*/
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
/*第六步,processDispatchResult()方法解析视图、渲染视图,执行拦截器的afterCompletion*/
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
请求处理过程源码解读
1 调用HandlerMapping获取执行链
发现执行链中要执行的分控制器的方法UserController的login,封装成一个HandlerMethod。
可以发现其中有三个拦截器,一个系统默认的ConversionServiceExposingInterceptor,其余两个是自定义拦截器,执行顺序和配置顺序一致。
ConversionServiceExposingInterceptor是Spring MVC的一个HandlerInterceptor,用于向请求添加一个属性,属性名称为ConversionService.class.getName(),值是Spring MVC配置定义的一个类型转换服务。该类型转换服务会在请求处理过程中用于请求参数或者返回值的类型转换。
缺省情况下,Spring MVC配置机制会主动构建一个ConversionServiceExposingInterceptor应用于所有的请求。
2 获取HandlerAdapter
如果使用注解,调用的是RequestMappingHandlerAdapter。
3 正序执行拦截器的PreHandler
注意
如果拦截器的preHandler方法结果是false,就不执行handler、postHandler,但是仍然会执行afterCompletion方法。
4 执行分控制器的方法,返回值是ModelAndView
ModelAndView底层是一个HashMap结构,其中有元素的key有view、model、status、cleared等等属性。view中保存的是逻辑视图,model中保存的是数据。
5 逆序执行拦截器的postHandler
6 逆序执行拦截器的afterComoletion
SpringMVC启动过程
1 初始化操作调用路线图
ApplicationContext就是IoC容器的引用,如果不是web项目,之前我使用的是ClasspathApplicationContext来获得使用ApplicationContext。如果是Web项目,使用的是WebApplicationContext
调试技巧:
在调试页面中前进和后退:crtl+alt+(<-- -->)
2 initWebApplicationContext方法
方法源码
protected WebApplicationContext initWebApplicationContext() {
/*创建IoC容器并返回*/
/*父容器,从ServletContext中找一级存在的IoC容器,如果存在,就会作为现在创建的IoC容器的父容器*/
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
/*wac 现在要创建的容器*/
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
this.configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = this.findWebApplicationContext();
}
if (wac == null) {
/*创建现在的容器,rootContext可能为空*/
wac = this.createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized(this.onRefreshMonitor) {
this.onRefresh(wac);
}
}
if (this.publishContext) {
String attrName = this.getServletContextAttributeName();
/*将当前创建的IoC容器放入ServletContext,所有用户的所有请求都可以获取,相当于全局资源*/
this.getServletContext().setAttribute(attrName, wac);
}
return wac;
}
3 createWebApplicationContext
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = this.getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
} else {
/*创建IoC容器*/
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(this.getEnvironment());
/*指定父容器是谁*/
wac.setParent(parent);
String configLocation = this.getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
/*这个方法用来读取文件,配置容器属性,初始化等..*/
this.configureAndRefreshWebApplicationContext(wac);
/*返回已经创建的IoC容器*/
return wac;
}
}
4 定位获取springmvc配置文件路径名称的代码
可以看到在HttpServletBean类的一个内部类ServletConfigPropertyValues中
private static class ServletConfigPropertyValues extends MutablePropertyValues {
public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException {
Set<String> missingProps = !CollectionUtils.isEmpty(requiredProperties) ? new HashSet(requiredProperties) : null;
Enumeration paramNames = config.getInitParameterNames();
while(paramNames.hasMoreElements()) {
//通过断点可以看到property=contextConfigLocation
String property = (String)paramNames.nextElement();\
//通过断点可以看到value=classpath:spring-mvc.xml
Object value = config.getInitParameter(property);
this.addPropertyValue(new PropertyValue(property, value));
if (missingProps != null) {
missingProps.remove(property);
}
}
if (!CollectionUtils.isEmpty(missingProps)) {
throw new ServletException("Initialization from ServletConfig for servlet '" + config.getServletName() + "' failed; the following required properties were missing: " + StringUtils.collectionToDelimitedString(missingProps, ", "));
}
}
}
5 定位读取springmvc配置文件的内容以及分控制器中@RequestMapping内容中的代码
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
/* 初始化了@RequestMapping*/
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
/* 初始化了视图解析器*/
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
ContextLoaderListener原理源码
注意:本节我们探讨的这个技术方案并不是**『必须』这样做,而仅仅是『可以』**这样做。
1.问题引入
SSM整合后,配置文件内容过多,可以分到两个配置文件中。这两个配置文件夹如何加载。
方法1:DispatcherServlet加载所有的配置文件【使用*号匹配】
<!--配置SpringMVC总控制器,唯一的Servlet -->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<!--指定SpringMVC配置文件的名称和位置,有默认位置 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring*.xml</param-value>
</init-param>
<!-- 启动服务器时就加载总控制器-->
<load-on-startup>1</load-on-startup>
</servlet>
特点:只有一个IoC容器,存放所有的Bean
方法2:DispatcherServlet加载springmvc配置文件,使用ContextLoaderListener加载另一个配置文件。
特点:
- 会有两个IoC容器
- 使用ContextLoaderListener加载另一个配置文件创建的IoC容器时父容器;DispatcherServlet加载的springmvc配置文件的创建的IoC容器是子容器。
注意:
Servlet、Filter、Listener的加载顺序:Linstener>Filter>Servlet
2.使用Linstener创建IoC容器
<!--配置全局上下文参数 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-persist.xml</param-value>
</context-param>
<!--指定监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
ServletContextListener类:监听项目上下文,只有两个方法。
public interface ServletContextListener extends EventListener {
//项目启动时执行该方法
void contextInitialized(ServletContextEvent var1);
//项目销毁时执行该方法
void contextDestroyed(ServletContextEvent var1);
}
两个容器之间的关系:
父子关系。
使用ContextLoaderListener加载另一个配置文件创建的IoC容器是父容器。
DispatcherServlet加载springmvc的配置文件创建的IoC容器是子容器。
3.出现的问题和解决方式
问题:
如果<context:component-scan>的路径设置不合理,就会重复的创建Bean。
如何查看:
将logback的总的日志级别改为DEBUG。
缺点:
- 重复的bean会多占用资源。
- Spring-mvc创建的Controller是调用Spring-mvc自己创建的Service和Dao,但是Spring-mvc的配置文件中并没有关于事务的设置,所以调用springmvc自己创建的Service和Dao将无法使用到事务的,这是很严重的问题。
解决方案1:两个配置文件中扫描不同的包。
<!-- 配置注解扫描基准路径-->
<context:component-scan base-package="com.atguigu.service,com.atguigu.dao"></context:component-scan>
<!-- 配置注解扫描基准路径-->
<context:component-scan base-package="com.atguigu.controller"></context:component-scan>
结果:
springmvc中创建了Controller,Listener中创建了Service并应用了事务。当SpringMVC在自己的IoC容器中找不到Service的时候,就会到父容器中去找Service。问题解决。
解决方案2:两个配置文件中扫描不同的包
方式1:
context:include-filter 指定扫描的包
值得注意的是:
context:include-filter这个方式要谨慎使用,它的模式是默认全部加载的,要把默认的扫描模式关掉【use-default-filters="false"】,他就只会扫描include的文件夹了。
<!-- 配置注解扫描基准路径:
use-default-filters="true" @Controller @Service @Repository @Component
use-default-filters="false" 只扫描基准包下被include-filter指定的注解
-->
<context:component-scan base-package="com.atguigu" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
方式2:
context:exclude-filter排除包,指定不扫描的包。
<!-- 配置注解扫描基准路径:@Controller @Service @Repository @Component-->
<context:component-scan base-package="com.atguigu">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>