https://juejin.im/post/5d4c2886518825371070d83d
https://zhuanlan.zhihu.com/p/51728248
每篇一句
改我们就改得:取其精华,去其糟粕。否则木有意义
前言
如果说知道@SessionAttributes
这个注解的人已经很少了,那么不需要统计我就可以确定的说:知道@RequestAttribute
注解的更是少之又少。我觉得主要有如下两个原因:
@RequestAttribute
这个注解很新,Spring4.3
后才有- 我们可以使用API调用的方式(
ServletRequest.getAttribute()
)来达到目的,而不用注解。且成本也不太高
既然Spring推出了这个注解,那必然有它的优点。本文就带大家领略一下它的风骚之处。
Spring
提供的这些注解比如@ModelAttribute
、@SessionAttributes
、@RequestAttribute
都是为了简化开发,提高复用性。同时另外一个目的是希望完全屏蔽掉源生Servlet API,增加它的扩展性。本文我以
@RequestAttribute
为例进行讲解,因为@SessionAttribute
(也是Spring4.3后推出的注解)不管从使用和原理上都是一模一样的。你可以理解成唯一区别是ServletRequest.getAttribute()
和HttpSession.getAttribute()
的区别此处再强调一次,这里指的是:
org.springframework.web.bind.annotation.SessionAttribute
,而非org.springframework.web.bind.annotation.SessionAttributes
@RequestAttribute
它比前面介绍的那些@ModelAttribute
、@SessionAttributes
等注解要简单很多,它只能使用在方法入参上。作用:从request中取对应的属性值。
很多小伙伴对
getParameter()
和getAttribute()
相关方法傻傻分不清楚。建议你可以先弄清楚param
和attribute
的区别~
// @since 4.3
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestAttribute {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
// 默认情况下 这个属性是必须的(没有就报错了)
boolean required() default true;
}复制代码
接下来这句话很重要:@RequestAttribute
只负责从request里面取属性值,至于你什么时候往里放值,是有多种方式的可以达到的:
@ModelAttribute
注解预存HandlerInterceptor
拦截器中预存- 请求转发带过来
下面分别按照这三种使用场景,给出使用Demo
:
@ModelAttribute
注解预存
比较简单,在@ModelAttribute
标注的方法上使用源生的HttpServletRequest
放值即可
@RestController
@RequestMapping
public class HelloController {
// 放置attr属性值
@ModelAttribute
public Person personModelAttr(HttpServletRequest request) {
request.setAttribute("myApplicationName", "fsx-application");
return new Person("非功能方法", 50);
}
@GetMapping("/testRequestAttr")
public void testRequestAttr(@RequestAttribute("myApplicationName") String myApplicationName, HttpServletRequest request, ModelMap modelMap) {
System.out.println(myApplicationName); //fsx-application
// 从request里获取
System.out.println(request.getAttribute("myApplicationName")); //fsx-application
// 从model里获取
System.out.println(modelMap.get("myApplicationName")); // null 获取不到attr属性的
System.out.println(modelMap.get("person")); // Person(name=非功能方法, age=50)
}
}复制代码
请求/testRequestAttr
,结果打印如下:
fsx-application
fsx-application
null
Person(name=非功能方法, age=50)复制代码
这里务必注意:@RequestAttribute("myApplicationName")
注解如果省略,是绑定不到attr属性的哦(必须要有注解)~
但是,这样是可行的:
@RequestAttribute String myApplicationName
(若注解没有指定,Spring MVC
会再去看形参的名字来确认自动绑定)但若你写成了这样
@RequestAttribute String aaa
,那请求就直接400错误了抛出异常:org.springframework.web.bind.ServletRequestBindingException
HandlerInterceptor
拦截器中预存
简单,直接上代码:
public class SimpleInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setAttribute("myApplicationName", "fsx-application");
return true;
}
...
}复制代码
测试代码:略。
forward
请求转发带过来
形如这样:
request.setAttribute("myApplicationName", "fsx-application");
request.getRequestDispatcher("/index").forward(request, response); 复制代码
其实往里放置属性值只需要遵循一个原则:在调用处理器目标方法之前
(参数封装之前)任意地方放置即可,属性值是都能被取到的。
原理剖析
按照我的习惯,即使它很简单,我也会扒开来看看它的原理部分嘛。根据经验很容易想到解析它的是一个HandlerMethodArgumentResolver
,它就是RequestAttributeMethodArgumentResolver
RequestAttributeMethodArgumentResolver
很明显,它也是@since 4.3
才出现的,命名上也很配套嘛。
public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
// 只处理标注了@RequestAttribute注解的入参
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestAttribute.class);
}
// 封装此注解的属性到NamedValueInfo 这里关于参数名的处理有这么一个处理
// info.name.isEmpty()也就说如果自己没有指定,就用形参名parameter.getParameterName()
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestAttribute ann = parameter.getParameterAnnotation(RequestAttribute.class);
Assert.state(ann != null, "No RequestAttribute annotation");
return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE);
}
// 从request请求域去找属性值
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){
return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
// 若值不存在,抛出异常ServletRequestBindingException
@Override
protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException {
throw new ServletRequestBindingException("Missing request attribute '" + name +
"' of type " + parameter.getNestedParameterType().getSimpleName());
}
}复制代码
源码短小精悍,非常简单。其实它解析入参方面的核心解析流程在其父类AbstractNamedValueMethodArgumentResolver
身上,但并不是本文的重点,请详见HandlerMethodArgumentResolver
的章节~
@RequestAttribute
属性required
默认为true,request.getAttribute
获取不到参数就会抛出异常ServletRequestBindingException
;required设置为false,即使没有从request中获取到就忽略跳过,赋值为null;
总结
这篇文章介绍了@RequestAttribute
的使用Demo
,以及它很简单的原理分析。相较于之前所有文章,这篇是非常轻松的,希望可以提供给大家一个思路,来使用@RequestAttribute
提高你的逼格,哈哈(友情提示:装逼需谨慎哦~)
说明:因为
@SessionAttribute
的使用甚至原理几乎一毛一样,所以不用再重复篇幅了
总结
这篇文章介绍了@SessionAttributes
的核心处理原理,以及也给了一个Demo
来介绍它的基本使用,不出意外阅读下来你对它应该是有很好的收获的,希望能帮助到你简化开发~
1. 接收请求参数
1.1. 【不推荐】通过HttpServletRequest
在处理请求的方法中,添加HttpServletRequest
对象作为参数,在方法体中,直接调用参数对象的getParameter()
或类似功能的方法,即可获取请求参数:
@RequestMapping("handle_reg.do")
public String handleReg(
HttpServletRequest request) {
System.out.println("UserController.handleReg()");
String username
= request.getParameter("username");
String password
= request.getParameter("password");
Integer age
= Integer.valueOf(request.getParameter("age"));
String phone
= request.getParameter("phone");
String email
= request.getParameter("email");
System.out.println("username=" + username);
System.out.println("password=" + password);
System.out.println("age=" + age);
System.out.println("phone=" + phone);
System.out.println("email=" + email);
return null;
}
1.2. 【推荐】在处理请求的方法中声明同名参数
假设用户提交的参数是username=root
,则参数名是username
,当需要获取这个参数的值时,直接在处理请求的方法中声明String username
即可,框架会把root
值直接用于调用处理请求的方法,即String username
的值就已经是root
了:
@RequestMapping("handle_reg.do")
public String handleReg(
String username, String password,
Integer age, String phone, String email) {
System.out.println("[2] username=" + username);
System.out.println("[2] password=" + password);
System.out.println("[2] age=" + (age + 1));
System.out.println("[2] phone=" + phone);
System.out.println("[2] email=" + email);
return null;
}
使用这种做法时,可以无视数据类型,例如希望age
是Integer
类型的,则直接声明为Integer
类型即可,无须自行转换!
使用这种做法时,必须保证提交的请求参数的名称,与处理请求的方法中的参数名称是一致的!如果不一致,则处理请求的方法中,对应的参数值会是null
值!
如果参数名称无法统一,后续有解决方案。
这种做法最大的缺陷是:不适用于数据项目太多的表单!否则,会导致处理请求的方法中需要添加大量的参数!
1.3. 【推荐】使用自定义类型获取多项数据
假设请求参数中包含多项数据,例如:username=admin&password=123456&age=22&phone=13900139001&email=admin%40tedu.cn
,而这些数据都可以封装在同一个类型中,则直接使用该类型作为处理请求的参数即可:
@RequestMapping("handle_reg.do")
public String handleReg(User user) {
System.out.println("[3] username=" + user.getUsername());
System.out.println("[3] password=" + user.getPassword());
System.out.println("[3] age=" + (1 + user.getAge()));
System.out.println("[3] phone=" + user.getPhone());
System.out.println("[3] email=" + user.getEmail());
return null;
}
这种做法,适用于请求参数较多的场合!
注意:如果请求参数的参数名称,与类中的属性名称不一致,则类对象中对应的属性值为null
!
注意:这种做法可以与前序介绍的第2种做法组合来使用!
1.4. 小结
关于获取请求参数,首先,并不推荐使用HttpServletRequest
,主要原因是相对比较原始,编码比较繁琐!而声明同名参数,或声明对象,都是推荐的做法,至于使用哪一种,可以根据参数的数量及数据是否适合被封装到同一个类中,综合评定,并且,这2种做法可以组合使用!
2. 控制器的响应
2.1. 常见的响应方式
【转发】在转发过程中,客户端只发出过1次请求!在浏览器的地址栏中,也只会显示第1次请求的路径!转发是在服务器内部完成的,可以传递数据!
【重定向】当服务器响应重定向时,客户端会发出第2次请求!最终,在浏览器的地址栏中,会显示第2次请求的路径!由于是2次不同的请求,基于Http协议是无状态协议,没有经过特殊处理(Session/Cookie/数据库存取……)的数据是无法在2次请求之间传递的!
2.2. 常见的响应码
被服务器接收到的每个请求,在最终响应时,服务器端都会给出一个响应码,例如200
、404
等。通常:
- 2xx:正确的响应,例如
200
、206
等…… - 3xx:重定向,例如
302
、301
等…… - 4xx:请求错误,例如请求的资源不存在,或者请求类型错误、或者请求参数错误等等,例如
400
、404
、405
、406
等…… - 5xx:服务器内部错误,通常可能是出现某种异常,例如
500
等……
3. 转发数据
3.1. 【不推荐】将转发的数据封装在HttpServletRequest对象中
可以为处理请求的方法添加HttpServletRequest request
参数,当需要转发数据时,将数据封装在request
中即可,后续也不需要显式的执行转发,在SpringMVC的控制器中,默认的响应方式就是转发。
@RequestMapping("handle_reg.do")
public String handleReg(User user,
HttpServletRequest request) {
// 假定输入的用户名已经被占用
// 提示:您输入的用户名XXX已经被占用
request.setAttribute("msg",
"您输入的用户名" + user.getUsername() + "已经被占用!");
// 返回视图名,也可以理解为文件的文件名
return "error"; // 页面:/WEB-INF/error.jsp
}
3.2. 【不推荐】使用ModelAndView
可以将处理请求的方法的返回值设置为ModelAndView
类型,该类型的常用构造方法有:
ModelAndView()
ModelAndView(String viewName)
ModelAndView(String viewName, Map<String, ?> model)
当需要转发数据时,可以使用以上3种中的最后一种:
@RequestMapping("handle_reg.do")
public ModelAndView handleReg(String username) {
String viewName = "error";
Map<String, Object> model
= new HashMap<String, Object>();
model.put("msg",
"[2] 您输入的用户名" + username + "已经被占用!");
ModelAndView mav
= new ModelAndView(viewName, model);
return mav;
}
由于这种方式使用相对比较复杂,所以,一般不推荐使用这种做法!
3.3. 【推荐】使用ModelMap封装需要转发的数据
使用ModelMap
的流程与使用HttpServletRequest
完全相同,即:方法的返回值依然使用String
类型,在方法中声明该参数,然后在方法体中直接封装数据,最后,返回视图名:
@RequestMapping("handle_reg.do")
public String handleReg(String username,
ModelMap modelMap) {
modelMap.addAttribute("msg",
"[3] 您输入的用户名" + username + "已经被占用!");
return "error";
}
3.4. 小结
在SpringMVC中,转发数据共有3种做法,第1种使用HttpServletRequest
的做法简单直接,但是,并不推荐这样处理,主要是因为框架已经帮我们处理了request需要执行的任务,而我们在编写代码时应该尽量不干预框架的处理过程,第2种使用ModelAndView
,是框架的核心处理方式,但是,因为使用方式过于麻烦,所以,也不推荐这样使用,第3种使用ModelMap
,使用简洁,推荐使用。
3.5. 附:重定向
在SpringMVC中,当需要重定向时,首先,应该保证处理请求的方法的返回值是String
类型(与转发一样),然后,返回值使用redirect:
作为前缀即可,例如:
@RequestMapping("handle_reg.do")
public String handleReg() {
// 假设注册成功,需要登录
return "redirect:login.do";
}
需要注意的是:在redirect:
右侧的不是视图名,而是重定向的目标的路径,可以是绝对路径,也可以是相对路径。
当处理的请求的返回值类型是String时,如果返回值使用redirect:作为前缀,是重定向,否则,是转发!
4. 关于@RequestMapping注解
通过配置@RequestMapping
,可以绑定请求路径与处理请求的方法,例如:
@RequestMapping("login.do")
public String showLogin() { ...
即:通过以上配置,当接收到login.do
请求时,SpringMVC会自动调用showLogin()
方法。
除了在方法之前添加该注解以外,该注解还可以添加在控制器类的声明之前,例如:
@RequestMapping("user")
@Controller
public class UserControler { ...
当方法之前添加了该注解之后,方法内配置的所有请求路径,在最终访问时都必须添加user
路径,例如:http://localhost:8080/SPRINGMVC-02-USER/user/reg.do
。
通常,推荐在类之前也添加该注解,方便管理路径,例如在某个新闻管理的应用中,可能存在news_list.do
、news_info.do
的请求,而在这个应用中,也会有用户数据,就存在user_list.do
、user_info.do
,可以发现,为了保证请求路径是唯一的,都需要在路径之前添加xxx_
作为前缀,这样的管理方式是非常不方便的,在类之前添加@RequestMapping
注解就可以很好的解决这个问题,每个路径之前根本就不需要配置前缀字符,也不会发生冲突!
在@RequestMapping
的使用过程中,路径可以使用/
作为第1个字符,也可以不需要这个字符,例如:
/user /login.do
user login.do
/user login.do
user /login.do
以上4种配置都是正确的!通常,推荐使用/
作为第1个字符,即以上第1种方式!
除了配置请求路径以外,使用@RequestMapping
还可以限制请求方式,即某个路径可以设置为只允许POST
请求,而不接收GET
请求!
【GET】会将请求的参数与值体现在URL中;请求的参数与值会受到URL长度限制,不适用于传递大量的数据;
【POST】请求的参数与值不会体现在URL中;可以传递大量的数据;
【选取】请求的参数与值涉及隐私(例如密码)则必须使用POST;数据量可能较大时必须使用POST;需要共享URL且其中包含参数时必须使用GET;支持页面刷新必须使用GET。
【复杂度】如果要发出POST请求,只能通过<form>
中的<input type="submit" />
或<button />
,或者通过JS技术,否则,在Web领域无法发出POST请求,而这2种方式也都可以用于发出GET请求,除此以外,直接在浏览器中输入某个URL发出的也是GET请求,总的来说,发GET请求要简单得多。
【小结】参考以上“选取”原则,选择请求方式,如果两者均可,则使用GET即可。
在@RequestMapping
中配置method
属性可以限制请求类型:
@RequestMapping(value="handle_reg.do",
method=RequestMethod.POST)
public String handleReg() {
例如以上代码限制了handle_reg.do
必须通过POST方式来请求,如果使用GET方式,则会返回405错误!
只有需要限定请求方式时,才需要显式的配置value="handlereg.do",否则,直接将"handlereg.do"配置在注解中即可!
小结:关于@RequestMapping
注解,主要作用是配置请求路径,推荐在控制器类和处理请求的方法之前都添加该注解,类之前的注解是用于配置请求路径中的层次,方法之前的注解是用于配置请求的资源,关于路径的配置是该属性的value
属性,如果只配置请求路径,可以不用显式的声明这是配置value
属性,而是直接把值写出来即可,例如不需要写成@RequestMapping(values="login.do")
,而可以直接写成@RequestMapping("login.do")
,在配置路径时,推荐使用/
作为第1个字符,例如@RequestMapping("/login.do")
,如果还需要限制请求方式,则必须显式的声明路径为value
属性的值,并且添加配置method
属性,例如:@RequestMapping(value="handle_reg.do", method=RequestMethod.POST)
。
5. 关于@RequestParam注解
使用@RequestParam
注解,可以解决请求参数名称与处理请求的方法的参数名称不一致的问题,例如:
public String handleLogin(
@RequestParam("name") String username,
String password) { ...
则请求参数的名称是name
,而处理请求的方法中的参数名称却是username
,这是可以正常运行的!
一旦使用了@RequestParam
注解,默认情况下,参数就是必须的!例如配置了@RequestParam("passwd") String password
后,如果请求中并不存在名为passwd
的参数,则会出现400错误:
HTTP Status 400 - Required String parameter 'passwd' is not present
没有提交名为passwd的参数,与提交了空值,是两码事!即:如果提交了passwd参数却没有值(例如输入框中没有输入值),在服务器将得到空字符串(""),程序并不会出现错误!如果根本就没有提交名为passwd的参数,则会导致400错误!
如果使用了@RequestParam
注解,却又不想设置为必须提交该参数,可以:
@RequestParam(value="name", required=false)
则将根据name
去接收参数,如果有值,会正确接收,如果没有(没有提交该名称的参数),则会是null值!
当required=false
时,意味着可以不必提交该参数,还可以多配置一项defaultValue
属性(The default value to use as a fallback when the request parameter value is not provided or empty. Supplying a default value implicitly sets required() to false.
),表示如果请求中没有提交该参数,则默认值是多少!例如:
@RequestParam(value="passwd", required=false, defaultValue="888999") String password
以上代码表示:希望请求中包含名为passwd
的参数,如果有,则值用于方法的String password
的参数,如果没有,也不是必须要提供(required=false
),并且使用"888999"
作为默认值(defaultValue="888999"
),即:在这种情况下,String password
的值是"888999"
。
小结:@RequestParam
注解是用于处理请求的方法中的参数之前,可以配置3项属性,分别是value
表示请求参数名称,required
表示请求中是否必须包含该参数,defaultValue
表示参数的默认值,当有以上任何一种需求时,都需要配置该注解,即:请求参数名称与处理请求的方法的参数名称不一致;强制必须提交某个参数;为某个参数配置默认值。