一 SpringMVC概述
1.1 什么是设计模式
设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。
使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。
设计模式使代码编写真正工程化;
设计模式是软件工程的基石脉络,如同大厦的结构一样。
设计模式就是一种模子,经过多年实践锤炼形成一套行之有效的完成某个特定任务的步骤和方式。
例如:飞天茅台的酿造过程,酿造工序,前后不能变,温差不能变,这样做就是好喝,稍微改动就变味道了。
1.2 MVC设计模式
MVC设计模式是一种通用的软件编程思想,MVC开始是存在于桌面程序中的,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。
在MVC设计模式中认为, 任何软件都可以分为三部分:
-
模型(Model)
模型是应用程序的核心部分,负责管理数据和业务逻辑。它直接与数据库交互,检索数据并处理前端的命令。
-
视图(View)
视图是用户界面的部分,负责将数据以图形界面的形式展示给用户。它仅仅展示数据,不包含业务逻辑处理。
-
控制器(Controller)
控制器作为模型和视图之间的中介,处理用户的输入,将命令传递给模型,并选择视图来显示模型的数据。
MVC 属于[架构](模式的一种,所谓架构就是如何设计一个程序的结构。MVC 将程序结构划分为三层,每一层都对外提供了可供上层调用的接口,既能维系三层之间的联系,也能保持相对的独立性。
这种将业务逻辑、数据和界面分离的代码组织形式,降低了模块间的耦合度,有利于日后的维护与扩展。
1.3 SpringMVC
springmvc是基于spring Framwork衍生出来的一个mvc框架,主要解决原有mvc架构中,控制器(Controller)的问题,常见的控制器有servlet,struts2等,控制器的核心功能是根据用户的请求调用对应业务功能,然后依据业务处理的结果,控制程序的运行流程。
而servlet实现控制器存在的问题如下:
-
接收客户端请求参数时,存在代码的冗余
String value = request.getParameter("name");
-
只能接收字符串类型的数据,其它数据类型需要手动的转换
Integer num = Integer.parseInt(request.getParameter("number"));`
-
无法接收对象类型的参数
-
调用业务对象存在耦合 (new)
-
流程跳转存在耦合(路径耦合,视图耦合)
1.4 核心组件
Spring MVC 涉及到的组件有 DispatcherServlet(前端控制器)、HandlerMapping(映射处理器)、HandlerAdapter(处理适配器)、Handler(处理器)、ViewResolver(视图解析器)和 View(视图)。下面对各个组件的功能说明如下:
-
DispatcherServlet
DispatcherServlet 是前端控制器,从图 1 可以看出,Spring MVC 的所有请求都要经过 DispatcherServlet 来统一分发。DispatcherServlet 相当于一个转发器或中央处理器,控制整个流程的执行,对各个组件进行统一调度,以降低组件之间的耦合性,有利于组件之间的拓展。
-
HandlerMapping
HandlerMapping 是处理器映射器,其作用是根据请求的 URL 路径,通过注解或者 XML 配置,寻找匹配的处理器(Handler)信息。
-
HandlerAdapter
HandlerAdapter 是处理器适配器,其作用是根据映射器找到的处理器(Handler)信息,按照特定规则执行相关的处理器(Handler)。
-
Handler
Handler 是处理器,和 Java Servlet 扮演的角色一致。其作用是执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装至 ModelAndView 对象中。
-
View Resolver
View Resolver 是视图解析器,其作用是进行解析操作,通过 ModelAndView 对象中的 View 信息将逻辑视图名解析成真正的视图 View(如通过一个 JSP 路径返回一个真正的 JSP 页面)
-
View
View 是视图,其本身是一个接口,实现类支持不同的 View 类型(JSP、FreeMarker、Excel 等)。
以上组件中,需要开发人员进行开发的是处理器(Handler,常称Controller)和视图(View)。通俗的说,要开发处理该请求的具体代码逻辑,以及最终展示给用户的界面
1.5 SpringMVC执行流程
Spring MVC 框架是高度可配置的,包含多种视图技术,例如 JSP、FreeMarke和 POI。Spring MVC 框架并不关心使用的视图技术,也不会强迫开发者只使用 JSP。
SpringMVC 的执行流程如下:
-
用户点击某个请求路径,发起一个 HTTP request 请求,该请求会被提交到 DispatcherServlet(前端控制器);
-
由 DispatcherServlet 请求一个或多个 HandlerMapping(处理器映射器),并返回一个执行链(HandlerExecutionChain)。
-
DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
-
HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(常称为 Controller);
-
Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC的底层对象,包括 Model 数据模型和 View 视图信息);
-
HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
-
DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
-
ViewResolver 根据 View 信息匹配到相应的视图结果,并返回给 DispatcherServlet;
-
DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
-
视图负责将结果显示到浏览器(客户端)
二 SpringMVC入门案例
需求:
(1)通过浏览器访问 http://localhost:8088/项目名称/hello
地址,在控制台输出 "hello Springmvc"
(2)将请求转向(跳转到) /WEB-INF/pages/hello.jsp
页面
2.1 搭建步骤简述
-
创建maven的Module
-
在pom.xml中引入springmvc所需jar包
-
在web.xml中配置前端控制器
-
创建并配置springmvc-config.xml
-
创建并实现HelloController类
-
创建并实现hello.jsp
-
访问测试
2.2 具体实现
1)创建maven的Module, 命名为spring-mvc01, 同时添加web框架支持。
2)在pom.xml中引入springmvc所需jar包
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.10.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies>
3)在web.xml中配置前端控制器
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 配置Springmvc前端控制器, 将所有请求交给Springmvc来处理 --> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置Springmvc核心配置文件的位置,默认Springmvc的配置文件是在WEB-INF目录下, 默认的名字为Springmvc-servlet.xml,如果要放在其他目录,则需要指定如下配置: --> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc-config.xml</param-value> </init-param> </servlet> <!-- 其中的斜杠(/)表示拦截所有请求(除JSP以外), 所有请求都要经过Springmvc前端控制器 --> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
4)创建并配置springmvc-config.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 1.配置前端控制器放行静态资源(html/css/js等,否则静态资源将无法访问) --> <mvc:default-servlet-handler/> <!-- 2.配置注解驱动,用于识别注解(比如@Controller)和json、xml自动转换等功能 --> <mvc:annotation-driven/> <!-- 3.配置需要扫描的包:Spring自动去扫描 base-package 下的类, 如果扫描到的类上有 @Controller、@Service、@Component等注解, 将会自动将类注册为bean --> <context:component-scan base-package="com.sldl.controller"/> <!-- 4.配置内部资源视图解析器 prefix:配置路径前缀 suffix:配置文件后缀 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
5)创建并实现HelloController类
package com.sldl.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class HelloController { @RequestMapping("/hello") public String sayhello(){ System.out.println("hello spring mvc"); /* 跳转路径,默认使用转发,会经过视图解析器。 返回的字符串,就是jsp页面的名称 。 */ return "hello"; } }
6)创建并实现hello.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>springmvc...hello.jsp....</h1> </body> </html>
7)配置tomcat, 修改虚拟项目名称,项目构建=>put into output root
8)启动tomcat,浏览器访问
http://localhost:8088/spring_mvc01/hello
三 SpringMVC参数绑定
当项目中引入Springmvc框架后,所有的请求流转将由Springmvc进行控制,当客户端发送的请求中包含数据(也就是请求参数)时,那么该如何在controller层获取这些参数呢?
Springmvc会自动的将请求中包含的参数和方法的参数进行匹配,也就是说只要保证请求中的参数名称和方法中的参数名称相对应(另,参数的格式也要正确),在方法中就可以使用这些参数—即请求中的参数。
3.1 基本数据类型绑定
形参的名字和传递参数的名字保持一致,参数需要全部传递否则报500错误,为了解决不传参报错,可以给基本类型的参数设置默认值
1)添加方法
@RequestMapping("/login1") public String login1(int id,String name,double price,boolean flag){ System.out.println(id); System.out.println(name); System.out.println(price); System.out.println(flag); return "hello"; }
2)测试
http://localhost:8088/spring_mvc01/login1?id=10&name=张三&price=10.4&flag=true
3)测试结果
4)如果参数不全,会报500错误,需要使用@RequestParam(defaultValue = "xxx")设置默认值。 如果传值了,则以传的值为最终值。
@RequestMapping("/login1") public String login1(int id, String name, @RequestParam(defaultValue = "20") double price, boolean flag){ System.out.println(id); System.out.println(name); System.out.println(price); System.out.println(flag); return "hello"; }
3.2 包装数据类型的传递
使用包装类型可以解决基本类型不传递值,出现500错误的问题但是还是要保持参数名字和形参保持一致,
1)添加方法
@RequestMapping("/login2") public String login2(Integer id,String name,Double price,boolean flag){ System.out.println(id); System.out.println(name); System.out.println(price); System.out.println(flag); return "hello"; }
2)测试
http://localhost:8088/spring_mvc01/login2?id=20&name=李四
3)测试结果
3.3 字符串类型绑定
1)添加方法
@RequestMapping("req3") public String testRequest3(String name,String address){ System.out.println(name); System.out.println(address); return "hello"; }
2)测试
http://localhost:8088/spring_mvc01/req3?name=zhangsan&address=长春
3)测试结果
3.4 数组类型绑定
1)添加方法
@RequestMapping("/login4") public String login4(String[] names){ for (String name : names) { System.out.println(name); } return "hello"; }
2)测试
http://localhost:8088/spring_mvc01/login4?names=王一&names=张二&names=李三&names=赵四
3)测试结果
3.5 javaBean类型
参数名的字段和Javabean中的属性保持一致即可。 底层调用的是javaBean的setter方法。
准备pojo类型
package com.sldl.pojo; public class User { private int id; private String name; private String gender; private double balance; public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; } public void setGender(String gender) { this.gender = gender; } public void setBalance(double balance) { this.balance = balance; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", gender='" + gender + '\'' + ", balance=" + balance + '}'; } }
1)添加方法
@RequestMapping("/login5") public String login5(User user){ System.out.println(user); return "hello"; }
2)测试
http://localhost:8088/spring_mvc01/login5?id=1001&name=张三&gender=女&balance=200.3
3)测试结果
3.6 日期类型
Springmvc默认是以斜杠(yyyy/MM/dd)接收日期类型的参数, 因此如果日期参数不是yyyy/MM/dd 格式,就会出现400错误!
如果提交的日期就是以斜杠分隔的, Springmvc就可以接收这个日期类型的参数, 否则将无法接收。
如果提交的日期参数的格式是自定义的, 也可以修改Springmvc默认的接收格式,改为自定义的方式!!
1)添加方法
@RequestMapping("/login6") public String login6(int id,Date date,String remark){ System.out.println(id); System.out.println(date); System.out.println(remark); return "hello"; }
2)测试
http://localhost:8088/spring_mvc01/login6?id=1001&date=2024/06/12 12:20:30&remark=哈哈哈
备注: %20 其实就是空格,序列化之后就变成%20了。在url传递参数的时候,一般都会序列化一下,以保证参数的安全。
3)测试结果
4)自定义日期格式
在Springmvc中,提供了@InitBinder注解,用于指定自定义的日期转换格式,因此,我们只需要在Controller类中添加下面的代码即可,在接受日期类型的参数时,会自动按照自定义的日期格式进行转换。
只需要在类中添加如下方法
@InitBinder public void InitBinder (ServletRequestDataBinder binder){ binder.registerCustomEditor(java.util.Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), true) ); }
5)测试
http://localhost:8088/spring_mvc01/login6?id=1001&date=2024-05-31%2012:20:30&remark=哈哈哈
6)测试结果
四 SpringMVC响应数据
4.1 ModelAndView方式
1)添加方法
@Controller public class ResponseController { @RequestMapping("/resp1") public ModelAndView test1(){ ModelAndView mav = new ModelAndView(); mav.setViewName("hello"); mav.addObject("scheme","ModelAndView"); mav.addObject("content","日照香炉生紫烟"); return mav; } }
2)hello.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>springmvc响应数据</title> </head> <body> <br> 模式:${scheme} <br><br> 内容: ${content} </body> </html>
3)测试
4.2 Model方式
Model对象实际上是一个Map集合,例如:往model中添加一个属性
model.addAttribute(String name, Object value);
其中,addAttribute方法会将属性保存到request域中,再通过转发将属性数据带到相应的JSP中,通过${}取出并显示。
1)添加方法
@RequestMapping("/resp2") public String test2(Model model){ model.addAttribute("scheme","Model"); model.addAttribute("content","遥看瀑布挂前川"); return "hello"; }
2)测试
4.3 ModelMap方式
1)添加方法
@RequestMapping("/resp3") public String test3(ModelMap map){ map.addAttribute("scheme","ModelMap"); map.addAttribute("content","飞流直下三千尺 疑是银河落九天"); return "hello"; }
2)测试结果
五 SpringMVC的跳转
5.1 转发
在前面request对象的学习中,通过request对象可以实现请求转发(即资源的跳转)。同样的,Springmvc也提供了请求转发的方式,具体实现如下
1)hello.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>springmvc跳转测试</title> </head> <body> <br> 主题:${theme} <br><br> 内容:${content} </body> </html>
2)代码
@Controller public class SkipController { @RequestMapping("f1") public String test1(Model model){ model.addAttribute("theme","forward"); model.addAttribute("content","时维九月,序嘱三秋"); /* 字符串格式: forward: xxxx * forward: 转发关键字 * xxxx: 转发目的地的名称(页面) * */ return "forward:hello"; } }
3)测试结果
4)简写方式
即SpringMVC的默认方式,就是转发
@RequestMapping("f2") public String test2(Model model){ model.addAttribute("theme","forward"); model.addAttribute("content","潦水尽而寒潭清,烟光凝而暮山紫"); /* 简写方式: 直接书写页面的名字。 */ return "hello"; }
5)测试结果
6)总结
forward方式相当于:
request.getRequestDispatcher("url").forward(request,response);
转发是一次请求,一次响应;
转发后地址栏地址没有发生变化(还是访问testForward的地址);
转发前后的request和response对象也是同一个。
5.2 重定向
在前面response对象的学习中,通过response对象可以实现请求重定向(即资源的跳转)。同样的,SpringMVC也提供了请求重定向的方式,具体实现如下:
1)添加方法
@RequestMapping("re1") public String test3(){ /** * 重定向的格式: "redirect:xxxx" * redirect: 重定向的关键字 * xxxx: 重定向的新地址。 也就是浏览器收到这个新地址,然后向该地址再次发送请求 * 因此:浏览器的地址会发生变化 * * 该案例: 浏览器的最初访问地址:http://localhost:8088/项目名/re1 * 改变后的地址: http://localhost:8088/项目名/f1 * * 逻辑如下: * 1. 浏览器最初发送了re1请求, * 2. 然后走到该方法test3 * 3. 遇到返回值里的关键字redirect和新地址f1 * 4. 服务器会将新地址f1以及302状态码发送回浏览器 * 5. 浏览器收到信息后,会向新地址f1再次发送请求 * */ return "redirect:f1"; }
2)测试
http://localhost:8088/spring_mvc01/re1
3)测试结果
4)总结
redirect方式相当于:
response.sendRedirect("url");
重定向是两次或多次请求,两次或多次响应;
重定向后地址栏地址发生了变化(变为转发后的地址);
并且在重定向前后,request和response对象不是同一个。
六 SpringMVC处理JSON
6.1 数据准备
1)引入js文件
在web的根目录下创建目录statics,在目录statics下创建子目录js。将jquery-1.8.3.min.js文件放进去
2)创建regist.jsp文件
3)pojo类型
package com.sldl.pojo; public class Student { private String username; private String password; private int age; private String gender; public Student() { } public Student(String username, String password, int age, String gender) { this.username = username; this.password = password; this.age = age; this.gender = gender; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + ", gender='" + gender + '\'' + '}'; } }
6.2 JSON响应数据
6.2.1 @ResponseBody注解的应用
-
@ResponseBody的作用其实是将java对象转为json格式的数据。
-
@responseBody注解的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到response对象的body区,通常用来返回JSON数据或者是XML数据。一般用于AJAX
-
注意:在使用此注解之后==不会再走==视图处理器,而是直接将数据写入到输入流中,他的效果等同于通过response对象输出指定格式的数据。
-
注意:在使用 @RequestMapping后,返回值通常解析为跳转路径,但是加上 @ResponseBody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP response body 中。
6.2.2 方案1-JSON字符串
1)编写html代码
<h2> <button οnclick="getJson1()">服务端返回json--方案1</button> <button οnclick="getJson2()">服务端返回json--方案2</button> </h2>
2)编写后端代码
@Controller public class LoginController { @RequestMapping("toRegist") public String toRegistPage(){ return "regist"; } @RequestMapping("getJson1") @ResponseBody public Object testJson1(){ return "{\"username\":\"张三\",\"age\":21}"; } }
3)编写Ajax代码
function getJson1(){ $.ajax({ url:"getJson1", type:"post", dataType:"json", success:function (data){ console.log(data); console.log(data.username); console.log(data.age) }, error:function (){ alert("系统错误") }, async:true }) }
4)解决乱码情况
//解决乱码情况 @RequestMapping(value = "getJson1",produces ="application/json;charset=utf-8" )
6.2.3 方案2-JavaBean写法
1)后端代码编写
@RequestMapping("getJson2") @ResponseBody public User testJson2(){ User user = new User("xiaoming","123123",23,"女"); /** 如果想要将student对象转成json对象返回给浏览器,需要在方法上 * 添加@ResponseBody注解 * * 如果想要序列化到浏览器,需要添加另外一个依赖jackson-databind,否则报如下错误 * No converter found for return value of type * */ return user; }
2)编写Ajax代码
function getJson2(){ $.ajax({ url:"getJson2", type:"post", dataType:"json", success:function (data){ console.log(data); console.log(data.username); console.log(data.password); console.log(data.age); console.log(data.gender); }, error:function (){ alert("系统错误") }, async:true }) }
6.3 JSON请求数据
1)编写regist.jsp代码
<br> <h2> <button οnclick="getJson1()">服务端返回json--方案1</button> <button οnclick="getJson2()">服务端返回json--方案2</button> </h2> <h2>注册页面</h2> <form> 用户: <input type="text" id="username" name="username" placeholder="请输入用户名"> <br> 密码: <input type="password" id="pwd" name="pwd" placeholder="请输入密码"> <br> 年龄: <input type="text" id="age" name="age" placeholder="请输入年龄"> <br> 性别: <input type="text" id="gender" name="gender" placeholder="请输入性别"> <br> <input type="button" value="注册" οnclick="regist()"> </form>
2)编写ajax代码
function regist(){ var u1 = $("#username").val(); var p1 = $("#pwd").val(); var a1 = parseInt($("#age").val()); var g1 = $("#gender").val(); //组织json对象 var jsonData = {"username":u1,"pwd":p1,"age":a1,"gender":g1} console.log(jsonData) $.ajax({ url:"regist", data:jsonData, type:"post", dataType:"text", success:function (data){ alert(data); }, error:function (){ alert("系统错误") }, async:true }) }
3)编写后端代码
@RequestMapping("regist") @ResponseBody public String regist(String username,String pwd,int age, String gender){ System.out.println(username); System.out.println(pwd); System.out.println(age); System.out.println(gender); return "hello"; }
4)后端代码的另外一种写法
@RequestMapping("regist") @ResponseBody public String regist(User user){ System.out.println(user); return "hello"; }
七 SSM框架整合
7.1 简述
SSM框架,即是将SpringMVC框架、Spring框架、MyBatis框架三个框架,整合到一起使用,用于构建企业级的Web应用程序。这样可以大大的简化在web开发中繁琐、重复的操作,让开发人员的精力专注于业务处理的开发上。
-
spring框架和mybatis框架整合到一起
-
spring容器来管理dataSource,SqlSessionFactory等
-
-
spring框架与springmvc框架整合到一起。
-
SpringMVC容器:主要对Controller控制器对象,视图等用户请求和请求结果进行管理。
-
Spring容器:主要对Service、Dao、工具类等对象进行管理。
-
两个容器的关系:SpringMVC容器为Spring容器的子容器,进而两容器中的对象进行间接管理。
-
那么如何整合到一起呢,需要做哪些工作?其实只需要整合以下四个配置文件即可。
-
mybatis的主配置文件
-
spring的配置文件
-
springMVC的配置文件
-
web.xml
7.2 SSM整合项目演示
7.2.1 创建项目/模块,引入坐标
1)创建模块,命名spring-ssm01。
2)添加web应用支持
3)引入坐标依赖
<!--junit的依赖jar包--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> <!-- servlet依赖 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- jsp依赖 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2.1-b03</version> <scope>provided</scope> </dependency> <!-- jstl --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- jackson依赖 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.0</version> </dependency> <!-- springmvc依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!--mybatis的依赖jar包--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.5</version> </dependency> <!--mysql的依赖jar包--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> <!-- druid连接池依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!-- spring事务依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!-- 参考版本对应 http://www.mybatis.org/spring/ --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.3</version> </dependency> <!-- commons 文件上传jar --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.3.1</version> </dependency>
7.2.2 编写配置文件
1)SSM的web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!--解决SpringMVC的中文乱码问题的过滤器--> <filter> <filter-name>encodingFilter</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>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--配置SpringMVC的前端控制器--> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--初始化SSM的主配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:beans.xml</param-value> </init-param> <!--容器启动就加载--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <!--斜杠表示所有的请求都拦截--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
2)mybatis的核心配置文件mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "https://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> </configuration>
3) 数据源的配置文件jdbc.properties
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/ssm_db?serverTimezone=Asia/Shanghai&useTimezone=true user=root password=123456
4) spring的核心配置文件applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!--开启注解扫描,比如扫描Dao,Service--> <context:component-scan base-package="com.sldl"/> <!--配置加载jdbc.properties文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置druid的连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"></property> <property name="url" value="${url}"/> <property name="username" value="${user}"/> <property name="password" value="${password}"/> </bean> <!--配置SqlSessionFactory的bean--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--绑定数据源--> <property name="dataSource" ref="dataSource"></property> <!--加载mybatis的核心配置文件--> <property name="configLocation" value="classpath:mybatis-config.xml"></property> <!--配置加载各个pojo对应的XXXXMapper.xml--> <property name="mapperLocations" value="classpath:com/sldl/mapper/*.xml"/> </bean> <!--配置可以扫描mapper/dao接口的类型--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> <property name="basePackage" value="com.sldl.dao"></property> </bean> </beans>
5) SpringMVC的核心配置文件SpringMVC.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!--开启SpringMVC的注解,主要是Controller注解--> <mvc:annotation-driven/> <!--静态资源的访问通过配置--> <mvc:default-servlet-handler/> <!--视图解析器的配置--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages"/> <property name="suffix" value=".jsp" /> </bean> </beans>
6) ssm的主配置文件beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--整合两个配置文件--> <import resource="classpath:applicationContext.xml"/> <import resource="classpath:SpringMVC.xml"/> </beans>
八 拦截器
8.1 回顾过滤器:
Servlet规范中的三大接口:==Servlet接口,Filter接口、Listener接口。==
过滤器接口,是Servlet2.3版本以来,定义的一种小型的,可插拔的Web组件,可以用来拦截和处理Servlet容器的请求和响应过程。以便查看,提取或以某种方式操作正在客户端与服务器端之间交换的数据。
过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截。Filter 接口中定义了三个方法。
-
init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。「注意」:这个方法必须执行成功,否则过滤器会不起作用。
-
doFilter() :容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
-
destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次。
8.2 拦截器的简介
SpringMVC里的拦截器是面向切面编程AOP的一个具体实现,用于对请求做预处理。
1)什么是拦截器:
在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。拦截是AOP的一种实现策略
2)为什么需要拦截器:
在做身份认证或者是进行日志的记录时,我们需要通过拦截器达到我们的目的。最常用的登录拦截、或是权限校验、或是防重复提交、或是根据业务像12306去校验购票时间,总之可以去做很多的事情
8.3 拦截器的应用
步骤1):自定义一个类,实现HandlerInterceptor接口,或者继承HandlerInterceptorAdapter抽象类
步骤2):根据自己的需求,重写方法
方法1:boolean preHandle() - 想要在执行Controller之前执行拦截,就重写该方法。 - 存在多个interceptor时,它们基于链式方式调用,按照注册的先后顺序依次执行。 - 方法返回true时,后续有拦截器,就继续执行拦截器,没有就执行controller. - 方法返回false时,后续任何内容都不执行了,直接返回浏览器 方法2:void postHandle(): 会在Controller执行后,视图渲染之前调用该方法。因此可以在这个阶段,对将要返回给客户端的ModelAndView进行处理。 方法3:void afterCompletion: - 该方法会在视图渲染后被调用,主要是用来进行资源清理工作。 - 多个拦截器时,依旧是先执行先注册的拦截器的afterCompletion方法 - 不管处理器是否抛出异常,该方法都将执行。。
步骤3)在spring的配置文件中配置拦截器(或者配置类中也可以)
8.4 拦截器执行流程
8.4.1 拦截器的执行时机
8.4.2 拦截器和过滤器一起应用时的执行时机
参考图1:
参考图2:
8.5 拦截器的应用场景
日志记录:记录请求信息的日志,以便进行信息监控、信息统计等;
权限检查:如登录校验,在处理器处理之前先判断是否已经登录;
性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。
通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用。还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的都可以用拦截器来实现。
8.6 当前路径绑定权限拦截器
package com.ssm.netctoss.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 步骤1):自定义一个类,实现HandlerInterceptor接口,或者继承HandlerInterceptorAdapter抽象类 * 步骤2):根据自己的需求,重写方法 * 步骤3)在spring的配置文件中配置拦截器(或者配置类中也可以) */ public class CurrentPathPrivilegeInterceptor implements HandlerInterceptor { /** * * @param request 请求对象 * @param response 响应对象 * @param handler 请求路径对应的Controller里的方法对象(反射机制) * /fee/findByPage * public String findByPage(){ * ........ * } * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("----------preHandle------------"); System.out.println("-------注意:需要给每个用户添加0,8,9权限,在数据库中-----------"); System.out.println("-------注意:需要给每个用户添加0,8,9权限,在数据库中-----------"); System.out.println("-------注意:需要给每个用户添加0,8,9权限,在数据库中-----------"); //获取请求路径: String uri = request.getRequestURI(); int currentPrivilege = -1; if(uri.contains("/toIndex")){ currentPrivilege = 0; }else if(uri.contains("/role/")){ currentPrivilege = 1; }else if(uri.contains("/admin/")){ currentPrivilege = 2; }else if(uri.contains("/fee/")) { currentPrivilege = 3; }else if(uri.contains("/account/")) { currentPrivilege = 4; }else if(uri.contains("/service/")) { currentPrivilege = 5; }else if(uri.contains("/bill/")) { currentPrivilege = 6; }else if(uri.contains("/report/")) { currentPrivilege = 7; }else if(uri.contains("/user/show")) { currentPrivilege = 8; }else if(uri.contains("/user/toUpdate")) { currentPrivilege = 9; } request.getSession().setAttribute("currentPrivilege", currentPrivilege); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("----------postHandle------------"); HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("----------afterCompletion------------"); HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
编写导航高亮提示
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <div id="navi"> <ul id="menu"> <li><a href="../index/toIndex" class="<c:choose><c:when test="${currentPrivilege==0}">index_on</c:when><c:otherwise>index_off</c:otherwise></c:choose>"></a></li> <li><a href="../role/findByPage" class="<c:choose><c:when test="${currentPrivilege==1}">role_on</c:when><c:otherwise>role_off</c:otherwise></c:choose>"></a></li> <li><a href="../admin/findByPage" class="<c:choose><c:when test="${currentPrivilege==2}">admin_on</c:when><c:otherwise>admin_off</c:otherwise></c:choose>"></a></li> <li><a href="../fee/findByPage" class="<c:choose><c:when test="${currentPrivilege==3}">fee_on</c:when><c:otherwise>fee_off</c:otherwise></c:choose>"></a></li> <li><a href="../account/findByPage" class="<c:choose><c:when test="${currentPrivilege==4}">account_on</c:when><c:otherwise>account_off</c:otherwise></c:choose>"></a></li> <li><a href="../service/searchService" class="<c:choose><c:when test="${currentPrivilege==5}">service_on</c:when><c:otherwise>service_off</c:otherwise></c:choose>"></a></li> <li><a href="../bill/bill_list.html" class="bill_off"></a></li> <li><a href="../report/report_list.html" class="report_off"></a></li> <li><a href="../user/showUserInfo" class="<c:choose><c:when test="${currentPrivilege==8}">information_on</c:when><c:otherwise>information_off</c:otherwise></c:choose>"></a></li> <li><a href="../user/toUpdatePwd" class="<c:choose><c:when test="${currentPrivilege==9}">password_on</c:when><c:otherwise>password_off</c:otherwise></c:choose>"></a></li> </ul> </div>
8.6 登录拦截
package com.ssm.netctoss.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * 使用拦截器,来完成用户是否登录过。 如果没有登录,应该跳转到登录页面,强制其登录 * * 返回值: * false: 不执行后续的代码,包括Controller * true: 执行后续的代码,如果有下一个拦截器,就执行下一个拦截器的preHandle。如果没有拦截器,执行Controller */ public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("----------LoginInterceptor的preHandle-----------"); //获取session,从中获取绑定的信息 Object loginAdmin = request.getSession().getAttribute("LOGINADMIN"); if (loginAdmin == null) { //没有登录过,就跳转 response.sendRedirect(request.getContextPath()+"/login/toLogin"); return false; } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("----------LoginInterceptor的postHandle----------"); HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("----------LoginInterceptor的afterCompletion----------"); HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
注册:
<!--注册拦截器: 拦截器的执行顺序与配置的先后有关系。 先配置的先执行--> <mvc:interceptors> <!--配置登录拦截器--> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login/login"/> <mvc:exclude-mapping path="/login/getCheckCode"/> <mvc:exclude-mapping path="/login/toLogin"/> <bean class="com.ssm.netctoss.interceptor.LoginInterceptor"></bean> </mvc:interceptor> <!-- 当前路径的权限绑定拦截器 --> <mvc:interceptor> <!-- 需要拦截的各种路径:--> <mvc:mapping path="/**"/> <!-- <mvc:mapping path="/login/toIndex"/>--> <!-- <mvc:mapping path="/login/showUserInfo"/>--> <!-- 不需要拦截的路径:--> <mvc:exclude-mapping path="/login/toLogin"/> <mvc:exclude-mapping path="/login/login"/> <mvc:exclude-mapping path="/login/logout"/> <!-- 手动配置拦截器的Bean对象 --> <bean class="com.ssm.netctoss.interceptor.CurrentPathPrivilegeInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
8.7 权限验证拦截器
package com.ssm.netctoss.interceptor; import com.ssm.netctoss.pojo.Admin; import com.ssm.netctoss.pojo.Privilege; import com.ssm.netctoss.pojo.Role; import com.ssm.netctoss.service.AdminService; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashSet; import java.util.List; import java.util.Set; /** * 登录用户的权限校验拦截: * 比如:登录用户,只能访问管理员,资费,账务,没有其他权限,那么相应的导航栏,有的能访问,有的不能访问 * * * admin_info ----> admin_role---->role_info--->role_privilege-->privilege_info * caocao 管理员1 100 1 2 3 * 营业员 200 3 4 5 6 * 经理 300 7 */ public class PrivilegeInterceptor extends HandlerInterceptorAdapter { /** * 因为拦截器是运行在Spring容器中维护的。(Bean),因此可以使用DI注入其他业务层/控制层的各种类型属性 */ // @Resource // private AdminService adminService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取当前登录用户的所有权限 Admin admin = (Admin) request.getSession().getAttribute("LOGINADMIN"); //创建一个Set集合,用于存储该用户的所有权限(privilegeId,name) Set<Privilege> privileges = new HashSet<Privilege>(); // Privileges实体类要重新hashCode和equals方法 List<Role> roles = admin.getRoles(); for (Role role : roles) { //添加进集合,并去重 privileges.addAll(role.getPrivileges()); } /*从当前的请求路径上获取对应的绑定权限*/ int currentPrivilege = (Integer)(request.getSession().getAttribute("currentPrivilege")); /*查看当前路径的绑定权限是否在当前用户的权限集合里,如果没有,就跳转进行提示*/ for (Privilege privilege : privileges) { if(privilege.getPrivilegeId()==currentPrivilege){ return true; } } //如果在循序期间,没有遇到return true,说明要访问的路径用户是没有该权限的。因此要做一个跳转 response.sendRedirect(request.getContextPath()+"/login/nopower"); return false; } }
注册拦截器
<!--注册拦截器: 拦截器的执行顺序与配置的先后有关系。 先配置的先执行--> <mvc:interceptors> <!--配置登录拦截器--> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login/login"/> <mvc:exclude-mapping path="/login/getCheckCode"/> <mvc:exclude-mapping path="/login/toLogin"/> <bean class="com.ssm.netctoss.interceptor.LoginInterceptor"></bean> </mvc:interceptor> <!-- 当前路径的权限绑定拦截器 --> <mvc:interceptor> <!-- 需要拦截的各种路径:--> <mvc:mapping path="/**"/> <!-- <mvc:mapping path="/login/toIndex"/>--> <!-- <mvc:mapping path="/login/showUserInfo"/>--> <!-- 不需要拦截的路径:--> <mvc:exclude-mapping path="/login/toLogin"/> <mvc:exclude-mapping path="/login/login"/> <mvc:exclude-mapping path="/login/logout"/> <!-- 手动配置拦截器的Bean对象 --> <bean class="com.ssm.netctoss.interceptor.CurrentPathPrivilegeInterceptor"></bean> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login/*"/> <bean class="com.ssm.netctoss.interceptor.PrivilegeInterceptor"></bean> </mvc:interceptor> </mvc:interceptors>
8.8 拦截器与过滤器的比较
① 拦截器是基于java的反射机制的,而过滤器是基于函数回调。
② 拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
③ 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
④ 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
⑤ 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
⑥ 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
⑦ 过滤器和拦截器触发时机、时间、地方不一样
⑧过滤器包裹住servlet,servlet包裹住拦截器。
扩展:
总结:简单来讲就是每个进入到具体的逻辑业务类中的请求都属于Action请求。
例如请求:https://xxxxxxxxxxxxx/creation/editor?spm=xxxxxx
普通请求: 包括但不限于访问静态资源的请求,如:静态页面、css文件、js文件等
九 异常处理
SpringMVC处理异常的方式有三种,当然也可以使用AOP面向编程,自定义一个类进入切入。
第一种:使用SpringMVC提供的简单异常处理器SimpleMappingExceptionResolver
<!--SpringMVC提供的异常处理器类型:SimpleMappingExceptionResolver--> <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <!-- 异常映射属性: 是一个map散列表 用于配置不同异常跳转到不同页面 --> <property name="exceptionMappings"> <props> <!--key: 用于指定异常类型, value用于指定跳转的页面名称--> <prop key="java.lang.Exception">error</prop> </props> </property> </bean>
第二种:实现HandlerExceptionResolver接口,自定义异常处理器,并注册
/** * 用户【】 IP[] * 在【时间】 * 操作【Controller.find】 发生如下异常 * xxxxxxxxxxxxxxxxxxxxx * yyyyyyyyyyyyyyyyyyyyy * */
package com.ssm.netctoss.util; import org.springframework.http.HttpStatus; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MyExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) { ModelAndView modelAndView = new ModelAndView(); System.out.println("---------执行了自定义异常处理器------------"); // 根据不同的异常类型,设置不同的响应状态码 if (e instanceof MyCustomException) { response.setStatus(HttpStatus.BAD_REQUEST.value()); // 可以添加更多的自定义处理逻辑 System.out.println("-----"); }else if (e instanceof IllegalArgumentException) { // }else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 记录日志或其他处理逻辑 } // 可以将异常信息放入模型中供视图展示 modelAndView.addObject("errorMessage", e.getMessage()); // 设置视图名称 modelAndView.setViewName("error"); return modelAndView; } } class MyCustomException extends Exception { public MyCustomException(String message) { super(message); } }
<bean class="com.ssm.netctoss.util.MyExceptionResolver"/>
第三种:使用@ExceptionHandler注解实现异常处理
1. 编写如下方法 2. 在方法上添加注解 3. 其他Controller继承即可
package com.ssm.netctoss.util; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; public class BaseController { @ExceptionHandler public String executeEx(HttpServletRequest request,Exception e){ // request.setAttribute("msg", e.getMessage()); // request.setAttribute("code", 1); System.out.println("---------------注解异常"); //根据不同异常类型,返回不同视图 return "error"; } }
第四种:使用AOP,自定义异常处理类型
可以正常统一处理异常信息: 另外一个需求: 出现异常后跳转到error页面,增加用户体验度。(自己思考一下)
开启注解扫描,别忘记了
package com.ssm.netctoss; import java.text.SimpleDateFormat; import java.util.Date; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import com.ssm.netctoss.pojo.Admin; import org.apache.log4j.Logger; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; /** * 用户【】 IP[] * 在【时间】 * 操作【Controller.find】 发生如下异常 * xxxxxxxxxxxxxxxxxxxxx * yyyyyyyyyyyyyyyyyyyyy * */ @Component @Aspect public class ExceptionLogger { @Resource private HttpServletRequest request; @Around("within(com.ssm.netctoss.controller..*)") public Object log(ProceedingJoinPoint p) throws Exception{ Object obj = null; try { obj = p.proceed(); } catch (Throwable e) { // 记录异常信息 Admin admin = (Admin)request.getSession().getAttribute("LOGINADMIN"); String msg=""; if(admin!=null){ String adminCode = admin.getAdminCode(); String ip = request.getRemoteHost(); String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); String className = p.getTarget().getClass().getName(); String methodName = p.getSignature().getName(); msg+="用户["+adminCode+"],IP["+ip+"],在"+now+"],操作["+className+"."+methodName+"]时,发生如下异常:\n"; } StackTraceElement[] elems = e.getStackTrace(); for(StackTraceElement elem:elems){ msg+="\t"+elem.toString()+"\n"; } Logger logger = Logger.getLogger(ExceptionLogger.class); logger.error(msg); //记录日志后,抛出异常,交给后面的代码继续处理 throw new Exception(e); } return obj; } }
十 声明式事务处理
10.1 事务简介
10.1.1 编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); }
编程式的实现方式存在缺陷:
-
细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
-
代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用
10.1.2 声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
好处1:提高开发效率
好处2:消除了冗余的代码
好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
所以,我们可以总结下面两个概念:
-
编程式:自己写代码实现功能
-
声明式:通过配置让框架实现功能
10.1.3 SpringMVC的声明式事务
声明式事务(declarative transaction management)是Spring提供的对程序事务管理的方式之一。
Spring的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明。用在Spring配置文件中声明式的处理事务来代替代码式的处理事务。这样的好处是,事务管理不侵入开发的组件,具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可;在不需要事务管理的时候,只要在设定文件上修改一下,即可移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便。
Spring使用AOP来完成声明式的事务管理,因而声明式事务是以方法为单位,Spring的事务属性自然就在于描述事务应用至方法上的策略,在Spring中事务属性有以下五个参数:
1.传播策略
2.隔离机制
3.只读策略
4.超时策略
5.回滚策略
10.2 准备工作
数据库表
drop table t_book; CREATE TABLE `t_book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称', `price` int(11) DEFAULT NULL COMMENT '价格', `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)', PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'盗墓笔记',80,100),(2,'凡人修仙传',50,100); drop table t_user; CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(20) DEFAULT NULL COMMENT '用户名', `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
pom.xml
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.10.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.14</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.1.10.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.1.10.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> </dependency> </dependencies>
前端控制器:
<!--配置SpringMVC的前端控制器--> <servlet> <servlet-name>SpringMVC</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--初始化SSM的主配置文件--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:mainConfig.xml</param-value> </init-param> <!--容器启动就加载--> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>SpringMVC</servlet-name> <!--斜杠表示所有的请求都拦截--> <url-pattern>/</url-pattern> </servlet-mapping>
spring-ioc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"> <!--开启注解扫描,比如扫描Dao,Service--> <context:component-scan base-package="com"/> <!--配置加载jdbc.properties文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置druid的连接池--> <bean id="ds" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${driverClassName}"></property> <property name="url" value="${url}"/> <property name="username" value="${user}"/> <property name="password" value="${password}"/> </bean> <!--配置SqlSessionFactory的bean--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--绑定数据源--> <property name="dataSource" ref="ds"></property> <!--加载mybatis的核心配置文件--> <!-- <property name="configLocation" value="classpath:mybatis-config.xml"></property>--> <!--配置加载各个pojo对应的XXXXMapper.xml--> <property name="mapperLocations" value="classpath:com/mapper/*.xml"/> </bean> <!--配置可以扫描mapper/dao接口的类型--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> <property name="basePackage" value="com.mapper"></property> </bean> </beans>
jdbc.properties
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/ssm_db?serverTimezone=Asia/Shanghai&useTimezone=true&useSSL=false user=root password=mmforu
spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation=" http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 1.配置前端控制器放行静态资源(html/css/js等,否则静态资源将无法访问) --> <mvc:default-servlet-handler/> <!-- 2.配置注解驱动,用于识别注解(比如@Controller)和json、xml自动转换等功能 --> <mvc:annotation-driven/> <!-- 3.配置需要扫描的包:Spring自动去扫描 base-package 下的类, 如果扫描到的类上有 @Controller、@Service、@Component等注解, 将会自动将类注册为bean --> <context:component-scan base-package="com"/> <!-- 4.配置内部资源视图解析器 prefix:配置路径前缀 suffix:配置文件后缀 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
mainConfig.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--整合两个配置文件--> <import resource="classpath:spring-ioc.xml"/> <!-- <import resource="classpath:spring-mvc.xml"/>--> </beans>
实体类
package com.pojo; import lombok.*; //@Setter //@Getter //@AllArgsConstructor //@NoArgsConstructor //@EqualsAndHashCode //@ToString @Data // 该注解会帮我们生成构造器,getter/setter,hashCode/equals,toString public class Book { private Integer bookId; private String bookName; private Integer price; private Integer stock; }
package com.pojo; import lombok.Data; @Data public class User { private Integer userId; private String userName; private Integer balance; }
Mapper接口
package com.mapper; import org.apache.ibatis.annotations.Param; public interface BookMapper { //根据图书id查询图书价格 Integer getPriceByBookId(Integer bookId); //更新图书库存 void updateStock(Integer bookId); //更新用户的余额 void updateBalance(@Param("userId") Integer userId, @Param("price") Integer price); }
映射文件:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "https://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.mapper.BookMapper"> <!-- void updateStock(Integer bookId);--> <update id="updateStock"> update t_book set stock = stock -1 where book_id = #{book_id} </update> <!--void updateBalance(@Param("userId") Integer userId, @Param("price") Integer price);--> <update id="updateBalance"> update t_user set balance = balance - #{price} where user_id = #{userId} </update> <!-- Integer getPriceByBookId(Integer bookId);--> <select id="getPriceByBookId" resultType="java.lang.Integer"> select price from t_book where book_id = #{book_id} </select> </mapper>
Service接口
package com.service; import org.apache.ibatis.annotations.Param; public interface BookService { Integer getPriceByBookId(Integer bookId); //更新图书库存 void updateStock(Integer bookId); //更新用户的余额 void updateBalance(@Param("userId") Integer userId, @Param("price") Integer price); }
package com.service; public interface UserService { void buyBook(Integer bookId, Integer userId); void buyBooks(Integer[] bookIds, Integer price); }
实现类:
package com.service; import com.mapper.BookMapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class BookServiceImpl implements BookService{ @Resource private BookMapper bookMapper; @Override public Integer getPriceByBookId(Integer bookId) { return bookMapper.getPriceByBookId(bookId); } @Override public void updateStock(Integer bookId) { bookMapper.updateStock(bookId); } @Override public void updateBalance(Integer userId, Integer price) { bookMapper.updateBalance(userId, price); } }
package com.service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service public class UserServiceImpl implements UserService{ @Resource private BookService bookService; /** * 用户买一本书 * @param bookId */ @Transactional public void buyBook(Integer bookId,Integer userId) { /*获取要买的书的价格*/ Integer price = bookService.getPriceByBookId(bookId); /*更新书的库存*/ bookService.updateStock(bookId); /*修改用户的余额*/ bookService.updateBalance(userId, price); } @Override public void buyBooks(Integer[] bookIds, Integer price) { } }
控制器层:
package com.controller; import com.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("user") public class UserController { @Autowired private UserService userService; @RequestMapping("buyBook") public String buyBook1111(Integer bookId,Integer userId){ System.out.println("用户id:"+userId+",书籍ID:"+bookId); userService.buyBook(bookId,userId); //随便写返回值,404也没关系。我们不研究视图解析器,我们研究的是事务,也就是数据库里是否发生了安全隐患问题 return "aaaa"; } }
10.3基于注解的声明式事务
在Spring-IOC中添加事务支持
<!--配置事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务的注解驱动: 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务--> <!--transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>
注解的位置
因为service层表示业务逻辑层,一个方法表示一个完整的功能,因此处理事务一般在service层添加注解@Transactional @Transactional标识在方法上,就只会影响该方法 @Transactional标识的类上,会影响类中所有的方法
10.4五个属性
10.4.1 只读
语法:
@Transactional(readOnly = true)
意义:
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化 对增删改操作设置只读会抛出下面异常:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
10.4.2 超时
语法:
@Transactional(timeout = 3)
意义:
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。 概括来说就是一句话:超时回滚,释放资源。
10.4.3 回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。 可以通过@Transactional中相关属性设置回滚策略
-
rollbackFor属性:需要设置一个异常类的Class对象
-
rollbackForClassName属性:需要设置一个字符串类型的全类名
-
noRollbackFor属性:需要设置一个Class类型的对象
-
noRollbackFor属性:需要设置一个字符串类型的全类名
语法:
@Transactional(noRollbackFor = ArithmeticException.class)
10.4.4 事务隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交(READ UNCOMMITTED): 允许Transaction01读取Transaction02未提交的修改。
-
读已提交(READ COMMITTED): 要求Transaction01只能读取Transaction02已提交的修改。
-
可重复读(REPEATABLE READ):确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。(MySQL默认为可重复读。)
-
串行化(SERIALIZABLE):确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
语法:
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
10.4.5 事务传播
语法:
@Transactional(propagation = Propagation.REQUIRED)
传播的概念:
事务传播:指的是调用者将自己事务传递给被调用者,比如A.fa()里调用了B.fb(): A.fa(){ B.fb(); } A的方式的事务传播给了B的方法事务
七个值:研究的对象是B的方法事务如何被A的方式事务影响的。 因此B的方法的设置可能是如下七个值中的一个
1. PROPAGATION_REQUIRED:默认值 (外面影响里面,里面不影响外面) A有事务, 则B加入A的当前事务。 合成一个事务。有异常就整体回滚。 A没有事务,B就使用自己的事务。 2. PROPAGATION_REQUIRES_NEW:(外面不影响里面,里面(不)影响外面) 若A的方法有事务, B就新建事务,使用新建的,将A的事务挂起。 如果B执行完了,A的后续代码继续使用A的事务 若A的方法没有事务,B就新建事务,使用新建的。 3. PROPAGATION_NESTED :(外面的影响里面的,里面的(不)影响外面的) 若A的方法有事务, B就在嵌套一个相同事务。 如果B的方法抛出异常,则B回滚 如果A的方法不捕获异常,则回滚,捕获则不回滚 如果A的方法抛出异常,则都回滚 4. PROPAGATION_SUPPORTS:(外面的影响里面的,里面的影响外面的) 如果A有事务,B就用A的事务。 A的方法抛出异常,A和B都会回滚。 B的方法抛出异常,A和B都会回滚。 如果A没有事务,B就没收到事务。因此A和B都没有事务,则A和B的运行出现异常都不会回滚 5. PROPAGATION_NOT_SUPPORTED: 如果A没有事务,B就以非事务方式执行,如果A有,B就将A传过来的事务的挂起。 白话: A有没有事务,B都不使用。 6. PROPAGATION_NEVER: 如果A没有事务,就以非事务方式执行,如果有,就抛出异常。 白话:我都说了不要了,你非给我,我就错给你看 7. PROPAGATION_MANDATORY:(外面的影响里面的,里面的影响外面的) 如果A没有事务,B就抛出异常,如果A有事务,B就使用A的当前事务。 白话:我强制需要事务,你不传给我,我就错给你看 如果A有事务 如果A的方法抛出异常,则A和B都会回滚 如果B的方法抛出异常,则A和B也都会回滚
Spring框架定义了七种事务传播行为: Propagation.REQUIRED(默认):所修饰的方法必须运行在事务中。当调用方抛出异常时,被调的方法也会回滚。 Propagation.REQUIRES_NEW:所修饰的方法只会运行在它自己的事务中,与调用方的事务互不影响。调用方抛出异常时,被调方法不会回滚。 Propagation.SUPPORTS:所修饰的方法自身不运行在事务中,若调用方有事务,则运行在调用方的事务中;若无事务,则以非事务方式运行。 Propagation.MANDATORY:所修饰的方法必须在事务中运行。若调用方无事务,则抛出异常并回滚。 Propagation.NOT_SUPPORTED:以非事务方式执行操作,若当前存在事务,则将当前事务挂起。 Propagation.NEVER:以非事务方式运行,若调用方有事务,则抛出异常。 Propagation.NESTED:若调用方有事务,则嵌套事务内执行;若无事务,则执行与PROPAGATION_REQUIRED类似的操作2。 这些传播行为帮助开发者在复杂的调用关系中更好地控制事务行为,确保数据的一致性和安全性
10.5 基于XML的声明式事务
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>
<!-- 注册事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="ds"/> </bean> <!-- tx:advice标签:配置事务通知 id属性:给事务通知标签设置唯一标识,便于引用 transaction-manager属性:关联事务管理器 --> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <!-- tx:method标签:配置具体的事务方法 name属性:指定方法名,可以使用星号代表多个字符 --> <tx:method name="find*" read-only="true" timeout='3'/> <tx:method name="query*" read-only="true"/> <tx:method name="toUpdate*" read-only="true"/> <!-- read-only属性:设置只读属性 rollback-for属性:设置回滚的异常 no-rollback-for属性:设置不回滚的异常 isolation属性:设置事务的隔离级别 timeout属性:设置事务的超时属性 propagation属性:设置事务的传播行为 --> <tx:method name="buy*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRED"/> </tx:attributes> </tx:advice> <aop:config> <!-- 配置事务通知和切入点表达式 --> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.service.*.*(..))"></aop:advisor> </aop:config>