spring MVC
1.web开发常见模式:
-
同步开发:每次请求客户端发起url,服务端解析并渲染页面(jsp),输出流把渲染的静态html返回客户端。
-
半分离的异步开发:
前端html代码与后端java代码仍然在一个项目中,一起部署到tomcat,用户先请求tomcat获取html页,客户端拿到html后,再发起ajax异步请求获取数据,客户端拿到数据后再通过前端技术来渲染动态html页(DOM操作)。
-
全分离异步开发(前后端分离):
用户先请求nginx获取html页,再发起针对tomcat服务器的异步请求,拿到数据,客户端动态渲染页面。
前端html代码独立项目,独立部署nginx静态服务器;
后端java代码独立项目,独立部署tomcat动态服务器(处理数据);
http协议:
请求头:content-type:application/x-www-form-urlencoded:前端数据格式key=value&key=value
content-type:application/json:前端数据格式:{key:value,key:value,key:value}
content-type:multipart-formdata:文件上传场景:字节流
http-method:请求方法:get、post、put、delete,options
use-agent:客户端信息(浏览器,移动端)
响应头:content-type:text/html
application/json
第二阶段的servlet中request.getParameter()不能获取json数据,只能获取key=value&key=value
(这个处理前端发来json字符串数据)request.getInputStream(),读取输入流中的数据为String,后端得到json字符串,再通过fastjson或gson组件把json字符串转对象。
2.springmvc作用
springMVC的spring框架中的一个子框架,是web模块中的一个小组件。
对servlet进行封装,解决以下一些servlet开发中繁琐的代码。
- 一个servlet只能够处理一件事情;
- 繁琐的getParameter,繁琐的转型;
- 返回json数据麻烦。response.getWriter().print(JSON.toJsonString(Object obj));
3.springMVC初了解
搭建springMVC环境
- 引入spring核心jar,引入spring-web核心jar(spring-web;spring-webMVC),引入springMVC内部依赖的json组件包jackson(三个:core;databind;annotation)
- 创建springMVC的xml配置文件.
<context:component-scan base-package="com.javasm"></context:component-scan>
<!--开启mvc注解识别:@RequestMapping,@GetMapping,@PostMapping,@PutMaping,@DateTimeFormated.@RequestBody,@RequestParam,@ResponseBody-->
<!--@JSONFormated-->
<mvc:annotation-driven></mvc:annotation-driven>
<!--jsp视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/page/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
-
在web.xml配置springmvc的前端控制器DispatcherServlet。
两个作用:tomcat启动时加载spring的xml文件初始化容器;
用户请求时,解析请求url,根据url去springmvc容器中找到对应的处理器对象。
-
写springmvc风格的控制层对象controller(处理器对象)
例子:
//1.创建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">
<context:component-scan base-package="com.javasm"></context:component-scan>
<!--开启mvc注解识别:
@RequestMapping,@GetMapping,@PostMapping,@PutMaping,@DateTimeFormated.@RequestBody,@RequestParam,@ResponseBody-->
<!--@JSONFormated-->
<!--选择描述最末尾是MVC的那个 别问为啥写这句 问就是我卡了一个晚上-->
<mvc:annotation-driven></mvc:annotation-driven>
<!--jsp视图解析器 一般同步开发才用-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/page/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
</beans>
//2.在web.xml配置springmvc的前端控制器DispatcherServlet
<?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">
<!--init.service,doGet,dopost-->
<!--任何请求全部进入DispatcherServlet,解析url分发请求,加载指定的spring风格的xml配置文件,初始化spring容器-->
<!--servlet什么时候实例化,用户第一次请求才实例化这个servlet,希望在tomcat启动时实例化DispatcherServlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--初始化spring容器-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springMVC.xml</param-value>
</init-param>
<!--在tomcat启动时实例化DispatcherServle-->
<load-on-startup>1</load-on-startup>
</servlet>
<!--"/"---任何请求全部进入DispatcherServlet -->
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
//Tomcat现在是设置的deployment 那个项目名简化成"/"
//3.写springmvc风格的控制层对象controller
@Controller
public class SysuserHandler {
//用来做url映射,把一个url字符串映射到一个处理器方法上
//http://localhost:8080/hc 走这个方法
@RequestMapping("hc")
public String hellomvc(){
System.out.println("进入了hellomvc方法");
//跳转到"/page/hello.jsp"
return "hello";
}
}
springMVC执行流程
-
tomcat启动:DispatcherServlet加载springmvc.xml,初始化XmlWebApplicationContext容器对象;
解析@Controller注解的类里面的@RequestMapping注解,进行url映射(url字符串/hm–>HandlerMethod)
–>Map<String,HandlerMethod>.put("/hm",hellomvc)
-
用户请求:
- dispatcherServlet接收请求,解析url字符串(/hm或/login),RequestMappingHandlerMapping查找处理器方法,把找到的方法返回dispatcherServlet
- 在dispatcherServlet中,调用RequestMappingHandlerAdapter对象执行处理器方法,获取前端参数,解析封装,调用处理器方法,调用service,调用doa,得到方法的返回值,把返回值返回给dispatcherServlet;
- 在dispatcherServlet中,调用视图解析器InternalResourceViewResolver得到视图路径。后端进行页面渲染,返回给前端。
4.接收前端参数
4.1 key=value格式
接收key=value&key=value格式的参数,content-type:application/x-www-form-urlencoded
加形参,底层通过反射,获取处理器方法的形参数组,根据形参名去getParameter获取数据。
可以通过@DateTimeFormated指定日期格式
可以通过@RequestParam注解指定参数默认值,该注解只能用在简单类型上,获取key=value数据.
@Controller
public class SysuserHandler {
//@DateTimeFormat(pattern = "yyyy-MM-dd")
//简单类型,springMVC底层根据形参名去getParameter,习惯写包装类对象。
//http://localhost:8080/feiqi_addUser?uname=111&upwd=222&ubirthday=2019-11-11
@RequestMapping("feiqi_addUser")
public String feiqi_addUser(String uname,String upwd,Integer uscore,@DateTimeFormat(pattern = "yyyy-MM-dd") Date ubirthday){
System.out.println("进入了feiqi_addUser方法"+uname+"---"+upwd+"---"+ubirthday);
return "hello";
}
//底层判断形参不是简单类型(String,date.包装对象),反射得到对象的成员变量列表,根据成员变量名去getParameter
//实体类对象如果有时间类型date 可以在实体类里直接注解
// @DateTimeFormat(pattern = "yyyy-MM-dd")
// private String createTime;
//http://localhost:8080/add?loginUserr=111&uname=222&createTime=2019-11-11
@RequestMapping("add")
public String addUser(Sysuser suser){
System.out.println("进入了add方法"+suser);
System.out.println("进入了add方法"+suser.getUname()+"---"+suser.getCreateTime());
System.out.println(loginUser);
return "hello";
}
//直接给两个对象的成员变量赋值
//http://localhost:8080/addUserAndRole?uname=fyt&upwd=123&rname=users
//Sysuser{uid=null, uname='fyt', upwd='123'}--Sysrole{rid=null, rname='users'}
//如果两个对象有相同的成员变量 这个变量赋值的时候两个对象都会被赋值
//http://localhost:8080/addUserAndRole?uname=fyt&upwd=123&rid=333&rname=users
//Sysuser{uid=null, uname='fyt', upwd='123', rid=333}--Sysrole{rid=333, rname='users'}
//如果是对象.成员变量=?? 这样值给不上
//http://localhost:8080/addUserAndRole?uname=fyt&upwd=123&srole.rname=users
//Sysuser{uid=null, uname='fyt', upwd='123'}--Sysrole{rid=null, rname='null'}
@RequestMapping("addUserAndRole")
public String addUserAndRole(Sysuser suser, Sysrole srole){
System.out.println("进入了addUserAndRole方法"+suser+"--"+srole);
return "hello";
}
//Sysuser实体类里持有Srole
//http://localhost:8080/addUserAndRole2?uname=fyt&upwd=123&srole.rname=users
//Sysuser{uid=null, uname='fyt', upwd='123', srole=Sysrole{rid=null, rname='users'}}
@RequestMapping("addUserAndRole2")
public String addUserAndRole2(Sysuser suser){
System.out.println("进入了addUserAndRole2方法"+suser);
return "hello";
}
//给传入的参数默认值
@RequestMapping("usersList")
public String getUserList(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue ="10") Integer pageSize){
// if(pageNum==null)pageNum=1;
// if(pageSize==null)pageSize=10;
System.out.println("进入了usersList方法"+pageNum+"--"+pageSize);
//进入了usersList方法1--10
return "hello";
}
}
4.2 json格式
接收前端{key:value,key:value}json格式的参数,即请求头content-type:application/json
处理器方法的形参加注解@RequestBody注解
百分百异步开发
@Controller
public class SysuserHandler {
//一个实体类对象往前端传 @RequestBody不能注解简单数据类型
@PostMapping("json")
public ResponseEntity addRole(@RequestBody Sysrole role){ //在这里直接注解
System.out.println("addRole:"+role);
role.setRid(100);
return ResponseEntity.ok(role);
}
//多个对象往前端传
//参数以json传输,不能在通过@RequestBody直接注解两个对象,需要注解到map
//再通过BeanUtils来分数据到对象。
@PostMapping("addUserAndRole")
public ResponseEntity addRole(@RequestBody Map map){
System.out.println("addRole:"+map);
//数据全部在map中,有时需要把map中的数据分到不同的对象中,apache:commons-beanUtils组件
Sysuser user = new Sysuser(); //其中一个实体类对象
Sysrole role = new Sysrole(); //另一个实体类对象
try {
BeanUtils.populate(user,map);
BeanUtils.populate(role,map);
} catch (Exception e) {
e.printStackTrace();
}
map.put("aa","dd");
return ResponseEntity.ok(map);
}
// {
// "uname": "fyt",
// "upwd": "111",
// "rname": "sss",
// "aa": "dd"
// }
}
5.同步开发中返回数据给前端
同步开发中:(服务器端渲染视图)request.setAttribute(key,value),在jsp页jstl+el。
springMVC中使用Model或者ModelAndView对象来把数据传输到视图层。
//控制层 public class SysuserHandler代码
//第一种:通过Mode
@RequestMapping("login")
public String loginPage(String uname, String upwd, Model m){
System.out.println("进入了loginPage方法"+uname+"---"+upwd);
m.addAttribute("modelDatas","model对象中的数据");//request.setAttribute()
return "user/login";// /page/user/login.jsp
}
//第二种:通过ModelAndView
@RequestMapping("login")
public ModelAndView loginPage(String uname, String upwd){
System.out.println("进入了loginPage方法"+uname+"---"+upwd);
ModelAndView mv = new ModelAndView();
mv.setViewName("user/login");//视图名
mv.addObject("modelDatas","modelAndView对象中的数据");//数据
return mv;// /page/user/login.jsp
}
//login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
登录页面
<hr>
${modelDatas} //使用后端传来的数据
</body>
</html>
6.异步请求返回json数据给前端
springMVC内部使用的jackson进行json处理。
- 在处理器方法上加@ResponseBody注解,springMVC对方法返回值走json转换器(MappingJackson2HttpMessageConverter)进行转json字符串
- 处理器方法的返回值类型改为ResponseEntity对象.
- ResponseEntity.ok(数据)
- new ResponseEntity(数据,响应头,状态码)主要用在下载,刷新token
@Controller
@RequestMapping("role")
public class SysroleHandler {
//第一种方法
@RequestMapping("add")
@ResponseBody //带这个注解,表示返回值不在走视图解析器,而是走json转换器,把对象转 json字符串
public Sysrole addRole(Sysrole role){
System.out.println("addRole:"+role);
role.setRid(100);
return role;
//{"rid":100,"rname":null,"rdate":null}
}
//第二种方法(必须推荐使用)
@RequestMapping("add2")
public ResponseEntity addRole2( Sysrole role){
System.out.println("addRole2:"+role);
role.setRid(100);
return ResponseEntity.ok(role);//该对象可以指定响应头,响应码,响应体
// HttpHeaders headers = new HttpHeaders();
// headers.add("token","adfadfasfd");//下载文件,前后端分离开发给前端返回token
// return new ResponseEntity(role,null,HttpStatus.OK);
}
}
7.如何限定请求方法:
- 在RequestMapping注解进行url映射时,通过注解的属性method限定请求方法
@RequestMapping(path = "add",method = RequestMethod.POST) //注解的属性method限定请求方法
public ResponseEntity addRole(@RequestBody Sysrole role){
.....................
}
- 使用GetMapping,PostMapping,PutMapping,DeleteMapping进行url映射
@PostMapping("add")
8.乱码解决
get:tomcat8及以上版本不需要在server.xml中配置URIEncoding=UTF-8,useDefaultEncdoing=true。
post:加编码过滤器,CharacterEncodingFilter
//web,xml 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>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
9.如何获取servlet的核心对象
在处理器方法的形参上添加HttpServletRequest,HttpServletResponse,HttpSession。
如果需要获取ServletContext对象的话,通过request或session对象间接获取。
@Controller
public class SysuserHandler {
//前端vue默认数据格式json,直接传后端
@PostMapping("login2")
public ResponseEntity loginPage2(@RequestBody Sysuser user, HttpSession session){
//先将数值传进session
session.setAttribute("loginUser",user);
return ResponseEntity.ok("loginSuccess");
}
@RequestMapping("add")
public String addUser(Sysuser suser,HttpSession session){
System.out.println("进入了loginPage方法"+suser);
//从session里取数值
Object loginUser = session.getAttribute("loginUser");
System.out.println(loginUser);
return "user/login";// /page/user/login.jsp
}
}
11.springMVC的几个概念
- 前端控制器(中央处理器):DispatcherServlet作用:
作用:初始化springMVC子容器;
接收请求,响应结果,相当于转发器,是springMVC的中央处理器。
处理器映射器:RequestMappingHandlerMapping:解析带有@Controller的类中的RequestMapping注解,进行url映射注册。当用户请求时,根据url得到对应的映射的处理器方法。
处理器适配器:RequestMappingHandlerAdapter:用来封装表单参数,执行处理器方法。
作用:找到Handler处理器方法对象后,由DispatcherServlet前端控制器调用HandlerAdapter来执行HandlerMethod,Handler处理器方法返回给适配器ModelAndView对象。
视图解析器:ViewResolver:设置视图的公共前缀和后缀,通过返回的ModelAndView对象得到完整的视图路径。
json转换器:Converter,MappingJackson2HttpMessageConverter,把对象转json字符串。
12. springMVC请求处理流程图
12.与DateTimeFormat结合使用的JsonFormated
通过DateTimeFormat注解把获取的日期字符串指定日期格式,转成Date对象
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") //把毫秒格式转换成yyyy-MM-dd HH:mm:ss
//jackson在序列化对象的时候,把日期序列化这种格式字符串,timezone = "GMT+8"
private Date rdate;
小总结:
- 做url映射:
@RequestMapping注解,用来做url映射,把url映射一个处理器方法上。可以限定客户端提交请求的方法。
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping
- 用于接收参数:
@RequestParam,针对key=value格式的参数,注解到简单类型的形参上(String,Date,包装类,基本类型),可以设置默认值。
@RequestBody,针对{key:value}json格式的参数,注解到对象类型的形参上。也可以注解到Map上。
- 用于返回数据:
@ResponseBody,表示处理器方法返回值不执行视图解析器,而是执行jackson消息转换器把返回值对象转 json字符串。
ResponseEntity:处理器方法的返回值类型,灵活的设置响应体,响应头,状态码.
ResponseEntity.ok(响应体数据)
new ResponseEntity(Obejct,HttpHeaders,HttpStatus)
# 13. 转发与重定向:不重要
同步开发中,处理器方法之间的转发与重定向。
在返回的String以forward:或redirect:开头.
@Controller
@RequestMapping("role")
public class SysroleHandler {
@GetMapping("select")
public String select(){
// int a=1/0;
System.out.println("查询角色列表");
return "roles";
}
@GetMapping("add")
public String add(){
System.out.println("添加角色");
return "redirect:select"; //return "forward:select";
//forward是转发 页面还是http://localhost:8080/role/add
//redirect是重定向 服务器发起二次请求 http://localhost:8080/role/select
}
}
14. resturl:重要
同步开发中不能使用,前后端分离的项目中使用最多
rest是对url的写法,描述性状态转移,url中不含动词
注意: 1. 前端以post方法提交---- 对应 —>add方法
2. 每个类里只能有一个post/get/put/delete方法提交的 resturl
url (以前) | RequestMethod | data | resturl(现在) | RequestMethod |
---|---|---|---|---|
/user/add | post | 请求体:{k:v,k,v} | /user | POST添加 |
/user/selectById?uid=1 | GET | /user/1 | GET查询 | |
/user/updateById | POST | 请求体:{k:v,k:v} | /user | PUT修改 |
/user/deleteById?uid=1 | GET | /user/1 | DELETE删除 |
//四种注解对应四种请求方式
//请求方式:GET负责查询、POST负责添加、DELETE负责删除、PUT负责更新
//注解后面没有"id" 就代表这四个方法
@GetMapping("{uid}")
//例: "/user/1"
//@PathVariable("uid")可以在 通过@GetMapping("{uid}") 取得值
public ResponseEntity selectUserById(@PathVariable("uid") Integer uid){
System.out.println("selectUserById:"+uid);
Sysuser user = us.selectUserById(uid);
return ResponseEntity.ok(user);
}
// /user
@PostMapping
public ResponseEntity addUser(@RequestBody Sysuser user){
//添加用户,暂时不维护用户的角色
boolean isok = us.addUser(user);
return ResponseEntity.ok("suc");
}
@PutMapping
public ResponseEntity updateUserById(Sysuser user){
System.out.println("updateUserByID:"+user);
boolean isok = us.addUser(user);
return ResponseEntity.ok("suc");
}
// /user/1
@DeleteMapping("{uid}")
public ResponseEntity delUserById(@PathVariable("uid") Integer uid,@RequestBody Sysuser user){
System.out.println("delUserById:"+uid);
return ResponseEntity.ok("suc");
}
注意点:
put和delete请求:如果请求体中的数据是key=value&key=value格式的话,收不到数据。可以通过配置FormContentFilter过滤器来接收数据。
//web.xml中配置
<filter>
<filter-name>formContentFilter</filter-name>
<filter-class>org.springframework.web.filter.FormContentFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>formContentFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
15. 静态资源处理
同步开发,半分离异步开发。(前端资源和后端代码在一个项目中)
在web目录下的前端资源无法直接访问。
为什么jsp能访问,也是因为tomcat,tomcat中有默认的serlvet:default,jsp,当访问XXX.jsp进入JspServlet中处理。当我们访问XXX.html,XXX.css等非jsp后缀的时候,进入DispatcherServlet,前端控制器解析url,找处理器对象,找不到,则404.
- 解决方法1:在web.xml中配置defaultServlet
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>*.html</url-pattern>
<url-pattern>*.css</url-pattern>
<url-pattern>*.jpg</url-pattern>
<url-pattern>*.png</url-pattern>
<url-pattern>*.js</url-pattern>
</servlet-mapping>
- 解决方式2:springMVC中有标签自动配置defaultServlet,建议使用这种。
<!--所有的静态资源都走DefaultServlet, default-servlet-name="default"-->
<mvc:default-servlet-handler></mvc:default-servlet-handler>
- 解决方式3:springMVC中通过静态资源映射器,对静态资源进行处理。
<!--mapping:uri以/static/ /page/开头的请求,都进入静态资源处理器。
该处理器对/static/css/a.css-->
<!--如果mapping="/a/**" (可随意更改) 地址就是/a/css/a.css -->
<mvc:resources mapping="/static/**" location="/static/"></mvc:resources>
<mvc:resources mapping="/page/**" location="/page/"></mvc:resources>
16. 异常处理:重要
把服务器端处理器方法内部产生的各种信息进行处理。
- 在web.xml中对不同的错误码进行错误页面配置(只适合前后端未分离)。
<error-page>
<error-code>404</error-code>
<location>/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/500.html</location>
</error-page>
缺点:提示信息无法动态定义。只能够返回页面,在前端分离的情况下,不能使用。
- 局部异常处理:只对一个处理器类中的异常生效。
//只对该类写了这个的有效
//也可以返回页面 返回ModelAndView
@ExceptionHandler(Exception.class)
public ResponseEntity doException(Exception e){
String message = e.getMessage();
Map<String,String> datas = new HashMap<>();
datas.put("msg",message);
datas.put("date",System.currentTimeMillis()+"");
return ResponseEntity.ok(datas);
}
缺点:范围太小,只对一个类有效
- 全局异常处理:对所有处理器类中的异常生效。
//异常处理类
public class MyExceptionHandler implements HandlerExceptionResolver {
//适合于同步开发,当出现异常,转到一个自定义的异常页。
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
HandlerMethod hm = (HandlerMethod)o;
Method method = hm.getMethod();
String clzName = method.getDeclaringClass().getName();
String methodName = method.getName();
ModelAndView mv = new ModelAndView();
mv.setViewName("error"); //转到error页面
mv.addObject("msg",e.getMessage());
mv.addObject("date",System.currentTimeMillis()+"");
return mv;
}
缺点:只能够同步开发中使用。异步开发中不支持。
-
全局统一异常处理(真正使用):
项目中使用,在局部异常处理的基础上结合着@ControllerAdvice注解一起使用
前端程序员调用后端接口,后端需要把执行结果返回前端(数字状态码,中文文字信息,数据)。
状态码枚举类;StatusBean;ResponseBean;自定义异常;定义全局的异常处理类;
//1.创建枚举
public enum StatusCode {
OPS_SUC(20000,"操作成功"),
OPS_ERROR(50001,"操作失败"),
PWD_ERROR(50002,"密码错误"),
UNAME_ERROR(50003,"用户名未注册"),
;
private Integer code;
private String msg;
StatusCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {return code;}
public void setCode(Integer code) {this.code = code;}
public String getMsg() {return msg;}
public void setMsg(String msg) {this.msg = msg;}
}
//2.用于返回不带数据的异常信息类
public class StatusBean {
private Integer code;
private String msg;
public StatusBean(StatusCode sc) {
this.code =sc.getCode();
this.msg = sc.getMsg();
}
public Integer getCode() {return code;}
public void setCode(Integer code) {this.code = code;}
public String getMsg() {return msg;}
public void setMsg(String msg) {this.msg = msg;}
}
//3.用于返回带数据的异常信息类
public class ResponseBean extends StatusBean{
private Object data;
public ResponseBean(StatusCode sc,Object data) {
super(sc);
this.data = data;
}
public Object getData() { return data; }
public void setData(Object data) {this.data = data;}
}
//5.定义一个每个项目都会创建的属于项目的异常
//用于一些非正确操作来抛出异常 返回枚举数据
public class MvcException extends RuntimeException {
private StatusCode sc;
public MvcException(StatusCode sc) {this.sc=sc;}
public StatusCode getSc() {return sc;}
public void setSc(StatusCode sc) {this.sc = sc;}
}
//6.配置全局的异常发生后的处理
//处理器增强注解,对所有的RequestMapping生效。
@ControllerAdvice
public class MyGlobExceptionHandler {
//Exception发生类型的异常处理
@ExceptionHandler(Exception.class)
public ResponseEntity doException(Exception e){
return ResponseEntity.ok(new StatusBean(StatusCode.OPS_ERROR));
}
//MvcException发生类型的异常处理
@ExceptionHandler(MvcException.class)
public ResponseEntity doException(MvcException e){
return ResponseEntity.ok(new StatusBean(e.getSc()));
}
}
@Controller
@RequestMapping("user")
public class SysuserHandler {
@Resource
private ISysuserService us;
@GetMapping("login")
public ResponseEntity login(Sysuser user){
//判断用户名是否已注册
Sysuser isRegisuser = us.isRegisUser(user.getUname());
if(isRegisuser==null){
//非正确操作
throw new MvcException(StatusCode.UNAME_ERROR);
}
//密码校验执行登录
boolean isLogin = isRegisuser.getUpwd().equals(user.getUpwd());
if(!isLogin){
//非正确操作
throw new MvcException(StatusCode.PWD_ERROR);
}
return ResponseEntity.ok(new ResponseBean(StatusCode.OPS_SUC,isRegisuser));
}
}
17. 文件上传:重要
springMVC的文件上传仍然基于apache common-fileupload。
- 添加fileupload;io两个文件上传的jar包;
- 在springmvc.xml中配置文件上传解析对象。
<!--文件上传解析器-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"></property><!--上传中文文件名-->
<property name="maxUploadSize" value="50000000"></property><!--上传文件最大大小-->
<property name="maxInMemorySize" value="10000000"></property><!--临时文件的存储域-->
<property name="uploadTempDir" value="/upload/tmp"></property><!--临时文件存储目录-->
</bean>
- 写文件上传处理器
@Controller
public class FileHandler {
//文件上传
@PostMapping("upload")
//形参传前端表单参数名 MultipartFile-文件名 大小 输入流都在这里放着
public ResponseEntity doupload(MultipartFile ifile, HttpServletRequest req){
String name = ifile.getName();//参数名
String fileName = ifile.getOriginalFilename();//文件名
long size = ifile.getSize();//文件大小
String realPath = req.getServletContext().getRealPath("/"); //得到根路径
String savePath = "/upload/"+fileName;
String saveFile = realPath+savePath; //项目最终上传文件的路径位置
try {
byte[] bytes = ifile.getBytes();//文件内容,适合于小文件
// InputStream inputStream = ifile.getInputStream();//文件内容,适合于大文件
FileUtils.writeByteArrayToFile(new File(saveFile),bytes);
} catch (IOException e) {
e.printStackTrace();
}
Map<String,String> map = new HashMap<>();
map.put("SAVE_PATH",savePath);// /upload/aaaa.md
map.put("REAL_NAME",fileName);
map.put("UPLOAD_TIME",DateUtils.getCurrentTime()); //传给前端上传时间
return ResponseEntity.ok(new ResponseBean(StatusCode.OPS_SUC,map));
//上传成功到out里 E:\code\Java\javaSpringa\out\artifacts\day0915mvc_war_exploded\upload
}
//同步文件下载,前端不能使用ajax或axios异步调用。
@GetMapping("down")
public ResponseEntity download(String path,String realName,HttpServletRequest req){
System.out.println(path);
String realPath = req.getServletContext().getRealPath("/");
String absPath = realPath+path;
//想把这个文件读取到内存,byte[]
byte[] bytes = null;
HttpHeaders headers = new HttpHeaders();
try {
bytes = FileUtils.readFileToByteArray(new File(absPath));
//需要指定响应头 可百度header("Content-Disposition: attachment; filename=abc.txt"); //指定文件名
//Content-type自动会找不需要设置
headers.add("Content-Disposition","attachment;filename="+URLEncoder.encode(realName,"UTF-8"));
} catch (IOException e) {
e.printStackTrace();
}
return new ResponseEntity(bytes,headers,HttpStatus.OK);
}
}
//时间工具类
public class DateUtils {
public static String getCurrentTime(){
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return df.format(new Date());
}
}
可用postman测试
18. 拦截器:interceptor
类似于过滤器filter,拦截器是框架中的概念。
1.登陆拦截器是个项目中都会使用。2.权限拦截器
- 编写拦截器类
public class LoginInterceptor implements HandlerInterceptor {
//前拦截 只重写这个就行了
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//一旦跨域了 拦截器里需要 对预检请求放行
String method = request.getMethod();
if("OPTIONS".equals(method))
return true;
HttpSession session = request.getSession();
Object login_user = session.getAttribute("LOGIN_USER");
if (login_user != null) {
return true;
}
throw new MvcException(StatusEnum.NO_LOGIN);
}
}
- 配置拦截器
<!--springmvc.xml-->
<mvc:interceptors>
<mvc:interceptor>
<!--拦截路径-->
<mvc:mapping path="/**"/>
<!--忽略路径-->
<mvc:exclude-mapping path="/u/login"></mvc:exclude-mapping>
<!--拦截器类-->
<bean class="com.javasm.common.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
19. 跨域处理:
发起的请求与当前页面所在服务, 协议,ip,端口三者只要有一个不同,违反了浏览器的同源策略,造成跨域问题。
需要在服务器端配置响应头,告诉浏览器服务端允许这个客户端的请求:
响应头需要配置如下信息:
允许的域:http://127.0.0.1:8848
允许的请求头:
允许的请求方法:GET/POST/PUT/DELETE/OPTIONS预检.
是否允许请求携带cookie信息:
可以暴露给客户端的响应头:
跨域产生的问题:
问题1:
请求不能正常响应。因为浏览器的同源保护(推荐 springmvc.xml中配置全局跨域)
方法1:@CrossOrigin注解
该注解添加到在处理器方法或类上。
缺点:只能够针对当个类处理。
* 方法2:springMVC的全局配置
//spring,xml
<mvc:cors>
<mvc:mapping path="/**" allowed-origins="http://127.0.0.1:8848" allowed-methods="*" allowed-headers="*"/>
</mvc:cors>
方法3:CrosFilter过滤器
<!-- 这个配置需要放到Spring的配置文件中,不能放到SpringMVC的配置文件,因为SpringMVC的加载是基于Servlet,它是晚于Filter的 -->
<bean id="corsFilter" class="org.springframework.web.filter.CorsFilter">
<constructor-arg name="configSource">
<bean class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
<property name="corsConfigurations">
<map>
<entry key="/**">
<bean class="org.springframework.web.cors.CorsConfiguration">
<property name="allowCredentials" value="true"/>
<property name="allowedMethods">
<list>
<value>GET</value>
<value>POST</value>
<value>HEAD</value>
<value>PUT</value>
<value>Delete</value>
<value>OPTIONS</value>
</list>
</property>
<property name="allowedHeaders" value="*"/>
<property name="allowedOrigins" value="*"/>
</bean>
</entry>
</map>
</property>
</bean>
</constructor-arg>
</bean>
<!-- web.xml
由于CorsFilter跟通常的Filter不一样,Spring对其做了很多改造,所以加载的方式要使用DelegatingFilterProxy,通过Spring的方式把它放到容器中
-->
<filter>
<filter-name>myCorsFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetBeanName</param-name>
<param-value>corsFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>myCorsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
方法4:自定义跨域处理过滤器
public class MyCrosFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 预检请求,请求头Origin是客户端地址,要求跨域头不能是*
String origin = httpRequest.getHeader("Origin");
if (origin == null) {
httpResponse.addHeader("Access-Control-Allow-Origin", "*");
} else {
httpResponse.addHeader("Access-Control-Allow-Origin", origin);
}
httpResponse.addHeader("Access-Control-Allow-Headers",
"Origin, x-requested-with, Content-Type, Accept,X-Cookie,token");
//httpResponse.addHeader("Access-Control-Expose-Headers", "token");
httpResponse.addHeader("Access-Control-Allow-Credentials", "true");
httpResponse.addHeader("Access-Control-Allow-Methods", "GET,POST,PUT,OPTIONS,DELETE");
//预检请求,直接通过。
if (httpRequest.getMethod().equals("OPTIONS")) {
httpResponse.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
}
}
@Override
public void destroy() {
}
}
方法5:springboot的解决方式:
@Configuration
public class MyConfiguration {
//springmvc中 没有FilterRegistrationBean这个类
@Bean
public FilterRegistrationBean这个类 corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
方法6:其他方案(了解)
基于nginx做url代理;未学习nginx,暂不学习。
基于node的一个包http-proxy-middleware,基于node组件启动一个后端程序做代理,把所有程序转发到服务器。(属于前端技能)。
问题2:
httpSession保存用户会话信息:每次请求服务器都会创建新的HttpSession对象,无法做到多次请求之间共享session中数据。
解决方法:前端axios请求配置withCredentials=true,表示请求时携带cookie
服务端跨越配置中允许接收客户端cookie,allow-credentials="true"
如果使用chrome发现配置后仍然不行,开发中暂时先chrome://flags/,把samesite禁用了
仍然存在的问题:
tomcat分布式部署的时候,多服务器之间的session共享问题(通过spring-session,结合redis使用)。
chrome浏览器的samesite同站点默认配置lax,不保存第三方的cookie问题。
//vue
//base.js
axios.defaults.baseURL = 'http://localhost:8080'; //这里省略axios网址的书写
axios.defaults.withCredentials = true; //客户端请求仍然携带cookie信息。
//axios前拦截
axios.interceptors.request.use(function (config) {
//前端进行登录校验,前端存储用户的登录信息。
return config;
}, function (error) {
return Promise.reject(error);
});
//axios后拦截
axios.interceptors.response.use(function (response) {
//服务端返回的错误状态码,统一处理
//token的刷新重置。
//统一获取数据
return response.data;
}, function (error) {
return Promise.reject(error);
});
//
methods: {
doLogin() {
//这里axios网址前缀就是http://localhost:8080;了
axios.post('/u/login',this.loginForm).then(resp => {
// debugger;
if(resp.code==50004){
this.showLoginMsg = true;
}else{
debugger;
this.showLoginMsg = false;
localStorage.setItem("loginuser",resp.data);
location.href="main.html";
}
});
}
}
//后端部分
//跨域这里加一句 allow-credentials="true" 允许请求携带cookie
<mvc:cors>
<mvc:mapping path="/**" allowed-origins="http://127.0.0.1:8848" allowed-methods="*" allowed-headers="*" allow-credentials="true"/>
</mvc:cors>
解决方法2:JWT组件,token工具包
服务端不保存会话状态信息,当用户登陆,服务端生成token字符串,返回客户端保存localStorage或cookie。每次请求都携带token信息在请求头中,发送服务端,服务端校验是否合法请求。
依赖组件:auto0或jjwt,两者都可以用来生成token。
缺点:token过长,效率低;服务端无法控制token强制失效,不安全;需要自己实现token刷新。
注意点:
在跨域的情况下,浏览器对POST,PUT,DELETE三种请求,会先向服务器发送预检请求,(检查服务端是否支持客户端的本次请求).
20. aop事务切面
定义出事务管理器对象。
定义事务切面,切入点表达式使用注解方式。
//service
public interface ISysuserService {
public boolean addUsers() throws Exception;
public boolean delUserByID(Integer uid);
}
@Service
public class SysuserServiceImpl implements ISysuserService {
@Resource
private ISysuseDao ud;
//addUsers()执行多次添加在一个事务里
@TransAnno //表示该方法进行事务管理
@Override
public boolean addUsers() throws Exception {
Sysuser u = new Sysuser("aaa","aaa");
ud.addUser(u);
Sysuser u1 = new Sysuser("bbd","bbb");
ud.addUser(u1);
return true;
}
@Override
public boolean delUserByID(Integer uid) {
ud.delUser(uid);
return false;
}
}
//dao
public interface ISysuseDao {
public Boolean addUser(Sysuser u) throws Exception;
void delUser(Integer uid);
}
@Repository
public class SysuserDaoImpl implements ISysuseDao {
@Resource
private CurrentConnection cc;
@Override
public Boolean addUser(Sysuser u) throws Exception{
PreparedStatement pst = cc.getCurrentConnection().prepareStatement("insert into sysuser(uname,upwd) values(?,?)");
pst.setString(1,u.getUname());
pst.setString(2,u.getUpwd());
return pst.execute();
}
@Override
public void delUser(Integer uid) {
try {
Connection currentConnection = cc.getCurrentConnection();
PreparedStatement pst = currentConnection.prepareStatement("delete from sysuser where uid=?");
pst.setInt(1,uid);
pst.execute();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//tm
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TransAnno {
}
@Component
@Aspect
public class TmAspect {
@Resource
private MyTransactionManager tm;
//不用另一种方法是因为这个具体
//不可能每个方法都要事务管理
@Pointcut("@annotation(com.javasm.tm.TransAnno)")
public void tmpc(){}
@Pointcut("execution(* com.javasm.service.*.*(..))")
public void all(){}
@After("all()")
public void closeConnection(){
tm.close();
}
@Around("tmpc()")
public Object execute(ProceedingJoinPoint jp){
Object proceed = null;
try {
tm.openConnection();
tm.beginTransaction();
proceed = jp.proceed();
tm.commit();
} catch (Throwable throwable) {
tm.rollback();
}finally {
tm.close();
}
return proceed;
}
}
@Component
public class MyTransactionManager {
@Resource
private CurrentConnection cc;
public void openConnection(){
cc.openConneciton();
}
public void beginTransaction(){
Connection currentConnection = cc.getCurrentConnection();
try {
currentConnection.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}
public void commit(){
Connection currentConnection = cc.getCurrentConnection();
try {
currentConnection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void rollback(){
Connection currentConnection = cc.getCurrentConnection();
try {
currentConnection.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
public void close(){
Connection currentConnection = cc.getCurrentConnection();
try {
if(currentConnection!=null){
currentConnection.close();
currentConnection=null;
cc.removeThreadLocal();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Component
public class CurrentConnection {
//线程变量ThreadLocal,为每个独立的线程创建一个变量副本。set/get
private ThreadLocal<Connection> conn=new ThreadLocal<>();
@Resource
private DataSource ds;
public void openConneciton(){
try {
Connection connection = ds.getConnection();
conn.set(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
public Connection getCurrentConnection(){
Connection connection = conn.get();
//如果没有事务帮我打开 那就自己打开
if(connection==null){
try {
connection = ds.getConnection();
//如果没有事务打开
//这个在后面tm.close()事务中用于connection关闭
conn.set(connection);
} catch (SQLException e) {
e.printStackTrace();
}
}
return connection;
}
public void removeThreadLocal() {
conn.remove();
}
}
//spring.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:aop="http://www.springframework.org/schema/aop"
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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.javasm"></context:component-scan>
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<property name="url" value="${jdbc.url}"></property>
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
</beans>
//test
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class TestTransaction {
@Resource
private ISysuserService us;
@Test
public void addUsers() throws Exception {
us.addUsers();
}
@Test
public void delUser() throws Exception {
us.delUserByID(30);
}
}
其他
Dom4j解析xml:核心对象:SAXReader
jdk子代的Properties对象解析.properties:new Properties().load()
spring的xml配置文件的方式是在ssm整合框架中使用。
spring的类配置的方式是在springboot框架中使用。