Spring MVC 用@Controller及@RestController 注解来标志(自动扫描并注册成bean)该类是一个控制器容器类,在该类下,使用@RequestMapping及其扩展注解来定义处理器。使用注解,可以定义请求的映射、请求的输入、异常处理等。
1 映射请求
使用@RequestMapping 注解来将不同的请求映射到对应的处理器方法上,可以通过URL、请求方法、请求参数、请求头及媒体类型来作匹配。基于其的扩展注解是针对了具体的请求方法做匹配:@GetMapping、@PostMapping等。
1.1 URI 模式
可以使用通配符来匹配请求的URL。
模式 | 描述 | 例子 |
? | 匹配一个任意字符。 | “/user/get?” 匹配:“/user/get1” 不匹配:“/user/get12” |
* | 匹配0个或多个任意字符。 | “/user/*/get” 匹配:“/user/g1/get” 不匹配:“/user/g1/g2/get” |
** | 匹配0或多个路径段,直到路径段结束。 | “/user/*/get” 匹配:“/user/get”、 “/user/g1/g2/get” |
{name} | 匹配一个路径段,并捕获它赋值到变量“name”。 | “/user/{id}/info” 匹配:“/user/1234/info”,变量id 值为1234。 不匹配:“/user/12/34/info” |
{name:[a-z]+} | 匹配一个符合表达式段路径段、并捕获它赋值到变量“name”。 | “/user/{id:\\d+}/info”, 匹配:“/user/1234/info” 不匹配:“/user/12a/info” |
表 @RequestMapping 支持的匹配模式
上面捕获并赋值到变量,可以在处理器方法参数中,通过@PathVariable注解来获取。
@GetMapping("/user/{id:\\d+}/info")
public Pet findPet(@PathVariable Long id) {
// 请求 /user/123/info,则该方法参数id的值为123
}
1.1.1 RFD(反射型文件下载)漏洞
是一个针对客户端的攻击漏洞,主要发生在响应中设置了“Content-Disposition”头,并且该头的filename属性是由用户请求输入的。这是因为Spring 并没有对用户请求传入的文件名进行严格检测。为了防止该漏洞,开发者应该:
- 对filename 参数进行严格的验证和过滤,确保它只包含合法的字符,并且不会造成混淆。
- 使用白名单验证,只允许预定义的、安全的文件名格式。
- 避免将用户提供的输入直接用作文件名。
@Controller
public class FileDownloadController {
// 假设我们有一个文件路径
private static final String FILE_PATH = "path/to/your/file.txt";
@GetMapping("/download")
public void downloadFile(@RequestParam String filename, HttpServletResponse response) throws IOException {
// 这里故意没有验证和过滤filename参数,导致RFD漏洞
Path filePath = Paths.get(FILE_PATH);
String contentType = Files.probeContentType(filePath);
response.setContentType(contentType);
// 使用用户提供的filename作为Content-Disposition中的文件名
String headerValue = String.format("attachment; filename=\"%s\"", filename);
response.setHeader("Content-Disposition", headerValue);
// 将文件内容写入响应输出流
Files.copy(filePath, response.getOutputStream());
response.flushBuffer();
}
}
上面代码中,假如用户的请求是:http://localhost:8080/download?filename=malicious.txt%0D%0AContent-Type:%20text/html%0D%0A%0A<script>alert('Your computer has been compromised!')</script>
那么将可能导致客户端浏览器接收到一个格式不正确的响应,并且可能会执行注入的脚本。
1.2 窄化请求
@RequestMapping 注解还可以通过Media Types、请求参数及请求头等方式来窄化请求匹配。
1.2.1 Media Types
其属性consumes表示请求头的Content-Type,而produces表示响应的Content-Type,由请求头的Accept属性决定。
@PostMapping(path = "/pets", consumes = "application/json",produces = "application/json")
public Object addPet(@RequestBody Pet pet) {
// 表示该请求头的Content-Type 包含 application/json,Accept包含application/json
}
1.2.2 请求参数与请求头
其属性params 用来表示匹配的请求参数值,而headers 则表示匹配的请求头属性值。
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue", headers = "Content-Type =application/json ")
public void findPet(@PathVariable String petId) {
// .请求参数中,参数名为myParam的值要为myValue,请求头的Content-Type属性为//application/json(如果是这个请求头属性,则最好用consumes属性来指定)
}
1.3 其他请求方法
HEAD | 与GET请求类似,但是响应只返回HTTP头信息,不返回实际的响应体。请求会被服务器处理。用于请求资源的元数据,常用于检查资源是否存在、更新状态或其他头部信息。 |
OPTIONS | 用于获取目标资源所支持的请求方法。常用于跨域请求,在发送可能被视为有风险的请求(如POST或PUT)之前,浏览器可能会先发出一个OPTIONS请求来检查服务器是否允许这种请求。 |
表 HEAD 与 OPTION 请求
1.4 动态注册处理器
可以通过动态编程方式来动态地注册映射器。
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, DynamicHandler handler) throws NoSuchMethodException {
RequestMappingInfo mappingInfo = RequestMappingInfo.paths("/dynamic").methods(RequestMethod.GET).build();
Method method = DynamicHandler.class.getMethod("showInfo"); // 处理方法
mapping.registerMapping(mappingInfo,handler,method);
}
2 处理器方法
@RequestMapping 注解方法时,可以为这个方法定义灵活的签名。选择一些列受支持的参数类型和返回值类型。
常见的方法类型参数有:
- HttpServletRequest 和 HttpServletResponse 对象。
- @PathVariable、@RequestParam、@RequestHeader等注解标注的参数,用于从请求中提取信息。
- @ModelAttribute注解标记的参数,用于绑定请求参数到模型对象。
- java.util.Map或org.springframework.ui.Model对象,用于添加模型属性等。
常见的返回值类型有:
- 字符串(通常表示逻辑视图名)。
- ModelAndView对象(包含视图和模型数据)。
- ResponseEntity对象(用于构建HTTP响应)。
- @ResponseBody 注解标记的对象(Spring MVC会将其序列化为JSON或XML并返回给客户端)。
- void(通常与HttpServletResponse对象一起使用以之间写入响应体)。
2.1 参数变量
2.1.1 矩阵变量
URL中,允许在路径段中使用键值对参数。变量用;分割。例如:
/usr;name=黄先生;gender=男/city;address=深圳
注意: 默认情况下,Spring MVC并不支持从URL中解析矩阵变量。要启用对矩阵变量的支持,需要自定义RquestMappingHanddlerMapping的PathMatcher,或者重写WebMvcConfigurer接口的configurePathMatch方法,并设置一个自定义UrIPathHelper,调用其方法setRemoveSemicolonContent(false),来告诉Spring不要移除URL中的分号。
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);// 不要移除URL中的分号
configurer.setUrlPathHelper(urlPathHelper);
}
可以通过@MatrixVariable 来提取这些变量,用MultiValueMap来装载所有的变量。
// /parameter/matrix1/admin;color=blue;name=jack
@GetMapping("/matrix1/{usr}") // 注意,这里一定要使用路径变量,路径变量后的矩阵变量才会生效
// 所以 /parameter/matrix1;color=red,yellow;size=big/admin;color=blue;name=jack 将匹配失败
public void matrix1(@PathVariable String usr,@MatrixVariable MultiValueMap<String,Object> map) {
System.out.println("user:" + usr); // user:admin
System.out.println("map:" + map); //map:{color=[blue], name=[jack]}
}
// parameter/matrix2/admin;subject=English,computer;age=19/sz;subject=math
@GetMapping("/matrix2/{usr}/{address}")
public void matrix2(@PathVariable String usr,@PathVariable String address,@MatrixVariable MultiValueMap<String,Object> map) {
System.out.println("user:" + usr + ";address:" + address); // user:admin;address:sz
System.out.println("map:" + map); // map:{subject=[English, computer, math], age=[19]}
}
// parameter/matrix3/admin;subject=English,computer;age=19/sz;subject=math
@GetMapping("/matrix3/{usr}/{address}")
public void matrix3(@PathVariable String usr,@PathVariable String address,@MatrixVariable(name = "subject", pathVar = "usr") List<String> subject1
,@MatrixVariable(name = "subject", pathVar = "address") String subject2) {
System.out.println("subject1:" + subject1); // subject1:[English, computer]
System.out.println("subject2:" + subject2); // subject2:math
}
2.1.2 会话变量
会话允许开发者在多个请求之间保持用户的状态信息。
HTTP协议本身是无状态的,服务器不会记住每个用户的请求之间的任何信息。然而,在实际开发者,我们经常需要跟踪用户的会话状态,如登录状态。
在Spring MVC中,会话通常由HttpServletSession对象来管理,这是Java Servlet API的一部分。其提供了存储和检索属性的方法,这些属性可以在用户的整个会话期间保持。
当用户第一次访问一个需要会话支持的Web应用时,Servlet容器在会话被创建时自动生成JESSIONID。如果要保持同一会话,则每次请求时,在cookie中需要携带这个会话ID。
@SessionAttributes | 作用于类级别,将模型Model中的属性同步到HTTP会话中。默认请求下,模型中的数据存储到request域。该注解可以指定哪些模型属性存储到session中,使得这些属性可以在其他请求中共享。 |
@SessionAttribute | 从会话中提取属性值 |
表Spring MVC中的会话属性注解
@RequestMapping("/session")
@RestController
@SessionAttributes("user")
public class SessionController {
@GetMapping("/login")
public void login(@RequestParam("username") String username, HttpServletRequest request, Model model) {
System.out.println("login:" + request.getSession().getId());
model.addAttribute("user",username);
model.addAttribute("info","登录了");
}
@GetMapping("/info")
public String info(@SessionAttribute("user") String user,HttpServletRequest request) { // 如果该sessionAttribute的值不存在,则请求会失败
System.out.println("info:" + request.getSession().getId()); // 会话id需与上面的请求保持一致,才能获取到特定会话的值
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + ":" + cookie.getValue());
}
return user;
}
}
2.1.3 @ModelAttribute
可以作用于有返回值的方法及处理器的参数,用于将属性值绑定到model的属性中,以用于在视图中使用。
作用于方法级别时,会在每次请求处理之前被调用,并且返回的模型属性会被添加到模型中,供后续的请求处理方法使用。
作用于处理器参数时,先从上面的方法返回值来填充模型,然后如果请求参数中包含了相应属性,则会用请求参数值进行覆盖。
@RequestMapping("/modelAttribute")
@RestController
public class ModelAttributeController {
@ModelAttribute // 该方法会在每次请求处理之前被调用
public User createUser() {
return new User("刘女士","123456");
}
// /modelAttribute?username=黄先生
@GetMapping
public void get2(@ModelAttribute("user") User user) { // 如果传参的时候没有username及password属性,
// 那么user的值为createUser的返回值,否则request的参数会覆盖其属性值
System.out.println(user); // ModelAttributeController.User(username=黄先生, password=123456)
}
@Data
public static class User {
public String username;
public String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
}
2.1.4 @RequestParam 与 @RequestPart
@RequestPart 主要用于处理multipart/form-data类型的请求。注意,@RequestParam 同样也可以读取该类型的参数。区别在于,@RequestPart能读取参数类型为json的参数,并将其转化为对于的类型,而@RequestParam只能把它当作String类型处理。
@RequestMapping("/paramAndPart")
@RestController
public class ParamAndPartController {
@PostMapping("/param")
public void param(@RequestParam("jsonValue") String jsonValue,@RequestParam("file") MultipartFile file) throws IOException {
System.out.println("jsonValue:" + jsonValue);
System.out.println("file:" + file); // 获取的是文件路径或者文件名等文本信息,而不是文件本身的内容
System.out.println(file.getInputStream()); // 到这里才涉及文件的操作,获取文件本身的内容
}
@PostMapping("/part")
public void part(@RequestPart("jsonValue") User jsonValue, @RequestPart("file") MultipartFile file) throws IOException {
System.out.println("jsonValue:" + jsonValue);
System.out.println("file:" + file); // 会接收客户端发送的数据文件,并将其存储在服务端的临时文件中
System.out.println(file.getInputStream());
}
@Data
public static class User implements Serializable {
String username;
String password;
}
}
图 使用postman时需要注意的地方
2.1.5 @RequestBody 与 HttpEntity
@RequestBody 的主要作用是自动将HTTP请求的请求体(body)中的数据绑定到方法参数上。这些数据通常以JSON或XML格式发送。使用该注解时,必须确保请求头中的Content-Type属性值正确地制定了请求体的格式。例如,对于JSON数据,Content-Type通常设置为application/json。@RequestBody 只能绑定一个请求体。
HttpEntity 用于表示一个HTTP请求和响应的实体。实例类有RequestEntity、ResponseEntity。如果将其用于参数的绑定请求体的格式要求与@RequestBody一样。
@RestController
@RequestMapping("/myRequestBody")
public class RequestBodyController {
@PostMapping
public void requestBody(@RequestBody User user) {
System.out.println(user);
}
@PostMapping("/httpEntity")
public void httpEntity(HttpEntity<User> userHttpEntity) {
System.out.println(userHttpEntity.getBody());
}
@Data
public static class User implements Serializable {
private String username;
private String password;
}
}
2.2 响应
2.2.1 Flash Attributes
当我们进行重定向时,需要向后面的处理器传递参数,可以通过URL参数形式传递,但这样不安全。这时我们可以使用Flash Attributes。
其提供了一种在重定向期间保持状态信息的有效方式,同时避免了将状态信息存储在用户会话中的长期副作用。它一旦被读取,就会自动从session中删除。
@Controller
@RequestMapping("/flash")
public class FlashAttributeController {
@GetMapping("/login")
public String login(@RequestParam String username, RedirectAttributes redirectAttributes) {
System.out.println("login username:" + username);
redirectAttributes.addFlashAttribute("username",username);
return "redirect:info";
}
@GetMapping("/info")
public @ResponseBody String showInfo(@ModelAttribute("username") String username) {
System.out.println("hello " + username);
return username;
}
}
2.2.2 @ReponseBody 与 ResponseEntity
@ResponseBody 主要用于将控制器方法返回的对象转换为特定的格式,并直接写入HTTP响应的body中。
ResponseEntity 表示整个HTTP响应,包括状态代码、响应体。其优先级高于@ResponseBody,即返回类型为ResponseEntity的情况下,即使方法同时标注了@ResponseBody,也不会对响应体产生影响。
2.2.3 JSON Views
Spring MVC 为Jackson的序列化视图提供了内置支持。序列化视图允许你在一个对象中选择性的包含某些字段,而不是所有字段。要与@ResponseBody或ResponseEntity一起使用。
@RestController
@RequestMapping("/jsonView")
public class JsonViewController {
@GetMapping
@JsonView(User.ShowAttribute.class)
public User showUser() {
User user = new User("黄先生", 28, "深圳");
System.out.println(user); // JsonViewController.User(name=黄先生, age=28, address=深圳)
return user; // 返回给前端 {"name": "黄先生", "address": "深圳"}
}
@Data
public static class User {
public interface ShowAttribute {}
@JsonView(ShowAttribute.class)
public String name;
private Integer age;
@JsonView(ShowAttribute.class)
private String address;
public User(String name, Integer age, String address) {
this.name = name;
this.age = age;
this.address = address;
}
}
}
3 Controller的配置
3.1 @InitBinder
可以在控制器中指定多个@InitBinder方法,这些方法会在请求处理之前被调用,用于配置数据绑定。通过这些方法,可以定制WebDataBinder的行为。其作用范围为当前Controller下的所有请求(有数值绑定的)。
其具体作用是:1)自定义数据绑定;2)全局配置;3)处理特定类型的参数;4)处理空值或缺失值。
@RestController
@RequestMapping("/initBinder")
public class InitBinderController {
@InitBinder
public void initBinder1(WebDataBinder webDataBinder) {
System.out.println("initBinder1");
}
@InitBinder
public void initBinder2(WebDataBinder webDataBinder) {
webDataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
@GetMapping
public void req1(@RequestParam String username) {
System.out.println("req1:" + username);
}
}
3.2 @ControllerAdvice
允许开发者定义全局的异常处理、数据绑定等逻辑,而不必在每个单独的controller中重复相同的代码。类似于AOP中的切面,但它专为web层设计。
@ControllerAdvice
public class CustomControllerAdvice {
@InitBinder
public void initBinder(WebDataBinder binder) { // 在请求中,方法参数中每有一个参数执行绑定,则该方法都会执行
System.out.println("CustomControllerAdvice 全局数据绑定处理"); // 可用来配置转换器
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> exceptionHandler(Exception e) { // 全局异常处理
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
@ModelAttribute("webName")
public String webName() {
System.out.println("全局@ModelAttribute");
return "Hello Web";
}
}