SpringMVC简介与配置
SpringMVC
是一种基于Java
实现MCV
模型的轻量级Web
框架,我们该如何使用呢?
首先在Maven
中添加坐标
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
随后我们创建一个SpringMVC
的配置文件,该文件只负责声明这是SpringMVC
的配置,并扫描对应的controller
层。
package configs;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
//创建SpringMVC的配置文件,加载需要的Bean
@ComponentScan("controller")
public class SpringMvcConfig {
}
在配置完后如何让Tomcat
启动时加载呢,我们先前加载Spring
的方式是通过AnnotationConfigApplicationContext
来生成的,但在Web项目中要使用AnnotationConfigWebApplicationContext
来生成。其通过继承AbstractDispatcherServletInitializer
这个类来实现,并且需要重写下面的三个方法来分别做SpringMVC
、数据请求以及Spring
的配置。
package configs;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
public class ServletConfig extends AbstractDispatcherServletInitializer {
//加载SpringMVC配置
@Override
protected WebApplicationContext createServletApplicationContext() {
//在Spring中,使用AnnotationConfigApllication来创建Spring容器,但Web环境下要使用下面的
AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
//上面是一个空的容器,使用register来加载SpringMVC的配置
ctx.register(SpringMvcConfig.class);
return ctx;
}
//设置哪些请求归属SpringMVC处理
@Override
protected String[] getServletMappings() {
return new String[]{"/"};//代表所有请求都要交给SpringMVC去处理
}
//加载Spring容器配置
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext ctx=new AnnotationConfigWebApplicationContext();
//上面是一个空的容器,使用register来加载SpringMVC的配置
ctx.register(SpringConfig.class);
return ctx;
}
}
当然我们也可以使用下面的代码来简化开发,只不过相较于上面的配置我们不知道具体是如何实现的而已。
随后我们定义Controller
package controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
@RequestMapping("/save")
@ResponseBody//将返回的信息作为整体相应内容
public String save(){
System.out.print("controller..");
return "{mode:ok}";
}
}
我们分析一下这个案例的工作流程。
Spring加载控制
SpringMVC
负责加载Controller
中的Bean,而Spring
则应该只负责加载业务层(service)
,数据层(dao)
的Bean
,那么如何让Spring
不重复加载SpringMVC
负责的Bean
呢?
一种方式是精确定位,即在Spring
扫描时只写service
、dao
等,那么自然也就不会加载Controller
的类了
另一种则是排除,在扫描时,依旧扫描全部包,但排除一部分,如Controller
的包。
PostMan工具
这是一款可以模拟http
请求的插件,可以方便我们开发。PostMan
下载地址如下
https://www.postman.com/downloads/
下载后我们需要创建一个账号,随后我们创建一个工作空间,然后写入请求就可以模拟了。
SpringMVC开发Controller
请求映射路径
package controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/users")
public class UserController {
@RequestMapping("/save")
@ResponseBody//将返回的信息作为整体相应内容
public String save(){
System.out.print("controller..");
return "{mode:usersave}";
}
}
不同于Servlet
的doGet
与doPost
方法,SpringMVC
的后端代码是不区分Get
与Post
的,因此我们只需要在PostMan
中选择不同的提交形式即可。
然而,我们在实验过程中,如果传递的参数是汉字的话,则会出现乱码问题:{mode:å½ç¥¥}
该如何解决呢?在原本的Servlet
中我们采用过滤器的方式解决乱码问题,在SpringMVC
中亦是如此,那么该如何实现呢,我们在ServletConfig
中重写getServletFilters()
方法。
package configs;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
import javax.servlet.Filter;
public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter filter=new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
return new Filter[]{filter};
}
}
Controller
中的传参代码如下:参数为name
,而发送http
请求的是:
需要注意的是,SpringMVC
能够将http
传递的参数与形参相同时进行映射。
package controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/users")
public class UserController {
@RequestMapping("/save")
@ResponseBody//将返回的信息作为整体相应内容
public String save(String name){
System.out.print("name:"+name+"\ncontroller..");
return "{mode:"+name+"}";
}
}
然后,如何http
请求的参数与形参不同,则无法映射,从而会导致传参失败。报400错误。
那么此时该如何映射呢,使用@RequestParam
注解即可
public String save(@RequestParam("username") String name){
System.out.print("name:"+name+"\ncontroller..");
return "{mode:"+name+"}";
}
那么如果形参是一个对象呢?如下Users
对象。
package domain;
public class Users {
private int id;
private String name;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public String save(Users user){
System.out.print("name:"+user.getName()+"\ncontroller..");
return "{mode:"+user.getName()+"}";
}
此时发送请求依旧成功。
那如果对于一些类里面嵌套类的情况呢?
后端代码不变,但http
发送的请求参数需要改变:类名.属性名
?name=peng&password=1234&address.city=city
JSON格式传递参数
首先导入JSON
坐标:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.0</version>
</dependency>
由于SpringMVC
是不支持JOON
格式接收的,要想使其支持,我们需要在SpringMVC
文件中开启@EnableWebMvc
注解,即开启自动转换JSON
数据的支持。
package configs;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
//创建SpringMVC的配置文件,加载需要的Bean
@ComponentScan("controller")
@EnableWebMvc
public class SpringMvcConfig {
}
随后我们运行发现SpringMVC
将接收的List
数组当对象了去构建了,导致报错,此时在List
前加上@RequestBody
注解即可,注意,此时不能加@RequestParm
,因为此时数据是封装在JSON
中的。
@RequestMapping("jsonlist")
@ResponseBody
public String jsonList(@RequestBody List<String> likes){
System.out.print(likes);
return "0";
}
注意JSON
数据的封装测试类型。
@RequestMapping("jsonlist")
@ResponseBody
public String jsonList(@RequestBody List<String> likes){
System.out.print(likes);
return "0";
}
@RequestMapping("jsonpojo")
@ResponseBody
public String jsonPOJO(@RequestBody Users user){
System.out.print(user);
return "{user:"+user+"}";
}
@RequestMapping("jsonpojolist")
@ResponseBody
public String jsonPOJOList(@RequestBody List<Users> users){
System.out.print(users);
return "{user:"+users+"}";
}
日期型类型参数传递
还记得@EnableWebMvc
注解吗,这个注解在格式转换中具有非常多的应用。
事实上我们前面所提到的JSON
数据转换并不是真正的JSON
数据,其只是一个字符串而已。真正要返回JSON
格式的对象怎么做呢,很简单,只需要将返回值类型改为对应的对象类型即可。
@RequestMapping("jsonpojo")
@ResponseBody
public Users jsonPOJO(@RequestBody Users user){
System.out.print(user);
return user;
}
@RequestMapping("jsonpojolist")
@ResponseBody
public List<Users> jsonPOJOList(@RequestBody List<Users> users){
System.out.print(users);
return users;
}
REST风格访问资源
我们为什么要使用REST
风格呢,这种方式有什么用呢?最明显的一点的就是简单写法简单,另一个特点就是可以隐藏请求,可以看到我们的查询、修改请求都可以隐藏。
那么REST
叫风格而不叫规范呢?
那么在SpringMVC
的Controller
中该如何写呢?
需要注意的是,在进行修改时,由于我们需要使用路径中的值,因此我们除了指定请求类型外,还需要再请求路径上添加一个占位符,名字与下面的参数相同,同时,还需要在形参前使用路径变量(@PathVaeiable
)注解来声明路径。
@RequestBody
等注解的区别在呢?
RSETful快速开发
由于我们在实际开发中都是将数据转换为JSON
发送Controller
中进行处理的,因此我们对一些功能进行简化。
采用了RESTful
风格后,我们可以之间将@RequestMapping
放到对应的Controller类前面。同时由于返回数据都是JSON格式,因此我们也可以将RequestBody放到Controller类前面,同时将@Controller与@RequestBody
注解合二为一变为:@RestController
@Controller
@ResponseBody//将返回的信息作为整体相应内容
@RequestMapping("/users")
public class UserController {
@RequestMapping(method = RequestMethod.POST)
public String save(@RequestBody Users user){ System.out.print("name:"+user.getAddress().getCity()+"\ncontroller..");
return "{mode:"+user.getName()+"}";
}
}
但是这样我们还要写 @RequestMapping(method = RequestMethod.POST)
,这也是很麻烦的,因此再次简化可以写为 @PostMapping
,其余方法则如法炮制。
@PostMapping
public String save(@RequestBody Users user){
System.out.print(user);
return "{mode:"+user.getName()+"}";
}
@PutMapping("/{id}")
public String edit(Integer id){
System.out.print(id);
return "{mode:1}";
}
@DeleteMapping("/{id}")
public String delete(Integer id){
System.out.print(id);
return "{mode:1}";
}
基于RESTful的页面交互
我们该如何进行RESTful
的页面交互呢?
我们首先创建一个index.html
页面,然后我们访问一下,发现404请求。
这是为什么呢,这是由于SpringMVC
的请求给拦截处理了,还记得我们在SpringMVC的配置吗?
public class ServletConfig extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringMvcConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter filter=new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
return new Filter[]{filter};
}
}
里面的getServletMappings() {return new String[]{"/"};
会拦截所有的请求,导致我们的html
文件被拦截了,那么该怎么办呢?
我们需要配置一个SpringSupport
配置文件,实现一个方法,让来自/pages/**
的请求不发送到SpringMVC
而是直接访问/pages/
,这个配置非常重要,我们可以配置许多静态资源。
package configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
//当路径时pages下内容时,不用走SpringMVC,而要走pages路径
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");
//添加一个资源处理器
}
}
随后我们在SpringMVC
配置中扫描一下configs
@ComponentScan({"controller","configs"})
@EnableWebMvc
public class SpringMvcConfig{
}
OK了。
前后端传输协议
为了方便前后端开发合作,定义一套格式,方便数据交互。
由于这个结果是由表现层输出的,因此我们将结果类(Result)封装在了Controller包中。同时还需要提供Code,同样被封装在Controller中。
package controller;
public class Results {
private Object data;
private String msg;
private Integer code;
public Results( Integer code,Object data) {
this.data = data;
this.code = code;
}
public Results(Object data, String msg, Integer code) {
this.data = data;
this.msg = msg;
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
Code的定义,这是我们与前端人员商量好的。
package controller;
public class Code {
public static Integer Save_OK=20011;
public static Integer DEL_OK=20021;
public static Integer UPDATE_OK=20031;
public static Integer GET_OK=20041;
public static Integer Save_ERR=20010;
public static Integer DEL_ERR=20020;
public static Integer UPDATE_ERR=20030;
public static Integer GET_ERR=20040;
}
随后我们对Controller的结果进行封装修改:
@Autowired
UserService us;
@GetMapping("/{gname}")
public Results MenuList(@PathVariable String gname){
List<Menu> lists= us.list(gname);
System.out.print(lists.toString());
Integer code=lists!=null?Code.GET_OK:Code.GET_ERR;
String msg=lists!=null?"":"查询失败";
return new Results(lists,msg,code);
}
至此,我们便完成了前后端数据的传递。
异常处理
上述过程都是在保证不出现错误与异常的情况下进行的,而我们的实际开发中时常会有多种情况会导致异常的发生。
那么一旦发生了这些异常,就会发生报错,如400,500等错误,而前端人员获取到这种异常也是无法处理的,因为它并不在我们先前规定的前后端协议中,因此,我们要对异常进行处理:
我们在COntroller中定义一个项目异常处理类,这个异常是处理所有异常结果的,一旦发生异常,便会捕获异常并对数据进行封装,变为与前端协商好的格式,同时应该发生通知给运维人员或开发人员。
package controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice//代表是处理rest风格controller异常
public class ProjectExectionAdvice {
@ExceptionHandler(Exception.class)//这是使用一个异常处理器处理所有异常
public Results doException(Exception ex){
System.out.print("发生异常!");
return new Results(666,null,null);
}
}
此时返还给前端的结果便是这种,我们可以让前端人员设计一个页面让用户稍后再试。
事实上,上面的异常是处理的所有异常,而异常是具有不同分类的,项目异常分类如下:
针对项目异常,我们需要做对应的事务处理:
我们可以自己定义不同的异常情况来进行处理。
拦截器
原本的请求处理流程:
加入拦截器的请求处理流程,拦截器可以做权限控制。
拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVc中动态拦截控制器方法的执行作用:
- 在指定的方法调用前后执行预先设定的代码
- 阻止原始方法的执行
讲到这里,我们会发现,拦截器与我们先前所作的过滤器的效果很相近,事实上,两者的功能上的确是很类似的。但仍存在差异:
- 归属不同:Filter属于Servlet技术,Interceptor属于SpringMVC技术
- 拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMVc的访问进行增强
那么拦截器该如何实现呢?
首先,拦截器是在Controller前后使用的,因此我们可以将拦截器定义在表现层,定义如下:
首先该类重写HandlerInterceptor的接口,其实随后我们需要将该类声明为一个Bean,即可以进行扫描,加上@Component即可
package controller.interpector;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class ProjectInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.print("preHandle\n");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.print("postHandle\n");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.print("afterCompletion\n");
}
}
随后我们只是重写了这个方法,那么该在哪里调用呢,还记得先前写的SpringMvcSupport类吗,我们曾在里面定义了过滤器,现在我们的拦截器依旧在里面调用:我们新增一个addInterceptors方法,该方法的使用与过滤器极为相近,随后我们为其添加过滤器,并设置需要拦截的请求。
package configs;
import controller.interpector.ProjectInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class SpringMvcSupport extends WebMvcConfigurationSupport {
@Autowired
private ProjectInterceptor projectInterceptor;
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
//当路径时pages下内容时,不用走SpringMVC,而要走pages路径
registry.addResourceHandler("/pages/**").addResourceLocations("/pages/");//添加一个资源处理器
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
System.out.print("拦截器");
registry.addInterceptor(projectInterceptor).addPathPatterns("/users/*");
}
}
注意,该拦截请求的匹配及其严格,如果写的是/users那么只会过滤对应的请求,如果后面有参数就不符合了,因此我们多写为"/users/*"
,同时,这个可以传递多个拦截请求,如下:
registry.addInterceptor(projectInterceptor).addPathPatterns("/users/*","/goods/*");
学到这里,其实我们可以对拦截器与过滤器进行简化配置,我们让SpringMvcConfig实现WebMvcConfigurer接口即可去除掉原来的SpringMvcSupport,两种方法都可以。但这种 方式由于实现了接口,因此与Spring绑定较强,即侵入性较强。
关于拦截器中的参数定义如下:
此外,当我们配置多个拦截器时,便可以形成拦截器链,一般我们在开发时,一般使用一个拦截器便可以了。