# Spring
# 作用
创建和管理对象,使得开发过程中,可以不必使用new关键字创建对象,而是直接获取对象!并且,还可以通过一些配置,使得某些获取到的对象,其中某些属性已经是被赋值的!
# Spring注解
在Spring中,定义了一系列的注解,可以取代几乎所有的xml配置!
尽管使用注解可以完成此前的许多配置,但是,基于Spring的项目仍需要Spring的配置文件!
# 常用注解
使用注解的方式来创建和管理对象,首先,必须在Spring的配置文件中添加组件扫描:
<!-- 组件扫描 -->
<!-- 仅在组件扫描的包下的类,才会被Spring管理 -->
<!-- 某个类,如果在扫描范围内,且添加了注解,则会被Spring管理 -->
<!-- base-packge:扫描的根包 -->
<!-- 根包:父级包,Spring扫描时,会扫描各层级子包 -->
<!-- 当配置为cn.tedu.spring时 -->
<!-- cn.tedu.spring.dao或cn.tedu.spring.entity都会被扫描 -->
<context:component-scan base-package="cn.tedu.spring.entity"/>
然后,确保需要被管理的类都在以上配置的包中(也可以在其子包中),并且,在类的声明之前添加注解:
@Component
在默认情况下,被管理的类的bean id是与类的名称相同,且首字母小写的!例如类名是User
,则默认的bean id是user
,或类名是UserDao
,则默认的bean id是userDao
如果需要自定义bean id,可以在注解中添加bean id:
@Component("bean-id")
与@Component
作用相同的注解还有:
@Controller
@Service
@Repository
其中,@Component
是通用注解,即对任意类都可以添加该注解,@Controller
是对控制器类的注解,@Service
是对业务类的注解,@Respository
是对持久层类的注解。
不过,以上这4种的作用和使用完全相同!只是语义不同,即从语法上表达的意义不同,因该根据类的定义来选取其中的某个注解!
# 2.2. 【了解】管理对象的作用域与生命周期
由Spring所管理的对象,默认都是饿汉式单例的,通过@Scope
注解可以配置某个类被Spring管理时,是否是单例的:
@Scope("prototype")
常用的配置方式有@Scope("singleton")
和@Scope("prototype")
。
如果需要配置该类最终是否时懒加载,可以使用@Lazy
注解,当添加了该注解后,就是懒加载模式,即:只有第一次获取对象时才会创建对象,而在加载Spring配置文件的过程中,并不会把对象创建出来。
关于@Lazy
也可以配置值,例如@Lazy(true)
或@Lazy(false)
。
饿汉式:一开始就已经准备好了,随时都有的吃! 懒汉式:不到逼不得已,不干活!
注意:是否懒加载,是建立在单例的基础之上的!如果不是单例的,则懒加载的配置是无效的!
作用域:在多大的范围内是有效的!对于变量/对象而言,作用域就是它存在的时长,即何时创建何时销毁!由于单例的对象都是static实现的,也就涉及创建时间和销毁时间的问题!而非单例的,作用域与普通的局部变量相同!
饿汉式单例代码:
public class King{
private static King king = new King();
private King(){
}
public static King getInstance(){
return king;
}
}
简单的懒汉式单例代码(没有解决线程安全问题):
public class King{
private static King king = null;
private King(){
}
public static King getInstance(){
if(king == null){
king = new King();
}
return king;
}
}
由Spring管理的对象,也存在生命周期问题,毕竟单例模式的类的对象何时创建、何时销毁,是我们无法确定的!为了确保初始化和销毁工作的正常执行,Spring容许在类中自定义初始化方法和销毁方法。使用了@PostConstruct
注解的方法是生命周期初始化方法,会在构造方法之后被自动调用,使用了@PreDestroy
注解的方法是生命周期销毁方法,会在Spring容器销毁并释放资源的前一刻被自动调用。
以上两个方法是在javax包中定义的,使用之前,需要为项目添加Tomcat运行环境,否则无法识别!
注意:以上生命周期方法是建立在单例模式之下的,对于非单例模式而言,以上生命周期方法其实没有意义!
# 2.3. 自动装配
当使用了自动装配后,由Spring管理的对象时,会自动尝试为各属性注入值,值的来源可以是其他bean例如:
public class UserService{
public UserDao userDao;
public void setUserDao(){
}
}
public class UserDao{
}
如果以上2个类都是由Spring管理的,则在创建UserService
对象时,会尝试自动的将UserDao
对象作为属性值!
自动装配的常用模式有byName
和byType
,前者表示根据名称来装配,即:要求bean id与属性名保持一致,后者表示根据类型来装配,即bean的类型与属性的类型保持一致(允许是接口与实现类的关系,也允许是父子级继承的关系),不过,根据类型装配,要求匹配该类型的对象必须是有且仅有1个,如果有2个,则会抛出异常,无法装配!
@Service
public class UserService {
@Autowired
public UserDao userDao;//不需要set方法,是直接赋值
}
使用@Autowired注解,不需要为对应的属性设置set方法,实现原理上是直接为属性赋值的 使用自动装配机制,必须保证当前类和值对应的类都是由Spring管理的,例如在User Service中装配UserDao类型属性的值,则User Service和UserDao都是需要被Spring管理的
除了@Autowired
,还可以使用@Resource
实现自动装配,添加注解的方式与@Autowired
相同
使用@Resource
注解时,如果需要指定装配进来的对象的bean id,可以:
@Resource(name="userDao")
public UserDao userDao;
当显式的配置了注解中的name
时,就不必关系属性的名称了!
@Autowired是Spring注解,默认按照类型(byType)实现自动装配!@Resource是javaee注解,默认按照名称(byName)自动装配,如果失败,会继续按照类型(byType)实现自动装配
由于@Resource是Java EE注解,而并不是每个项目都是WEB项目,就有可能不需要,也不会添加Tomcat运行环境,在这样的项目中,是根本就不识别@Resource的,所以,推荐使用@Autowired注解。
# 2.4. 小结
实际常用的注解有:
@Component,@Controller,@Service,@Autowired
不太常用的注解有:
@Repository、@Scope、@Lazy、@PostConstruct、@PreDestroy、@Resource
# 3. Spring小结
通过学习,应该掌握:
- 配置
<bean>
的id
与class
; - 通过SET方式注入属性的值;
- 读取*.properties文件;
- 理解自动装配中的
byType
和byName
; - Spring表达式
- 常用注解
应该了解:
- 单例,作用域,生命周期;
- 经常用的注解
# SpringMvc框架
# 1.作用
解决了V-C的交互问题,即视图与控制器的交互问题。
在原生的java EE技术中,使用Servlet
作为项目中的控制器,用于接受用户请求,并给予相应结果。这种做法最大的问题在于:在常规做法中,每个Servlet对应一个请求路径,例如LoginServlet
处理login.do
的请求,而RegisterServlet
处理register.do
的请求,所以,会导致Servlet数量太多,不便于管理(无论从源文件的数量,还是从配置文件的内容),且占用大量内存空间的问题!
并且,在请求的处理过程中,还存在许多操作不简便的做法!
# 1.2. SpringMVC核心组件
- DispatcherServlet 前端控制器,主要职责是接收所有请求(根据配置文件来决定),并将请求转发给对应的控制器,接受控制器的处理结果,确定最终由那个视图完成响应!
- HandlerMapping 处理请求路径与控制器的映射关系。
- Controller 实际处理请求的组件,例如接收请求参数,决定最终是转发或是重定向的方式来响应。
- ModelAndView 控制器的处理结果,其中的Model表示转发的数据(如果是重定向,则Model没有意义),而View表示最终负责响应的视图组件的名称。
- ViewResolver 根据视图组件的名称,确定具体使用的是哪个视图的组件。
SpringMVC工作流程图
- DispatcherServlet接收客户端请求
- 根据请求路径从HandlerMapping中获取对应的控制器
- 调用该控制器,处理请求,以及给予响应结果,也就是数据与视图名。
- 根据视图名找到视图组件
- 响应客户端
# --------------------------------------
Spring作用 创建和管理对象,使得开发过程中,可以不必使用new关键字创建对象,而是直接获取对象!并且,还可以通过一些配置,使得某些获取到的对象,其中某些属性已经是被赋值的!
Spring注解 常用注解:
- @Component 通用注解
- @Controller 控制器类的注解
- @Service 业务类的注解
- @Repository 持久层类的注解
- @Scope 是否是单例子,值为:singleton(默认),prototype
- @Lazy 懒加载
- @PostConstruct 初始化方法,会在构造方法之后被自动调用
- @PreDestroy 销毁方法,会在Spring容器销毁并释放资源的前一刻被自动调用
- @Autowired 默认按照类型(byType)实现自动装配!
- @Resource是javaee注解,默认按照名称(byName)自动装配,如果失败,会继续按照类型(byType)实现自动装配
注:7.8两个方法是在javax包中定义的,使用之前,需要为项目添加Tomcat运行环境,否则无法识别!这两个生命周期方法是建立在单例模式之下的,对于非单例模式而言,以上生命周期方法其实没有意义 1、2、3、9、是常用注解,其他都为不常用注解
SpringMvc框架
- 作用:解决了V-C的交互问题,即视图与控制器的交互问题。
- SpringMVC核心组件
- DispatcherServlet 前端控制器,主要职责是接收所有请求(根据配置文件来决定),并将请求转发给对应的控制器,接受控制器的处理结果,确定最终由那个视图完成响应!
- HandlerMapping 处理请求路径与控制器的映射关系。
- Controller 实际处理请求的组件,例如接收请求参数,决定最终是转发或是重定向的方式来响应。
- ModelAndView 控制器的处理结果,其中的Model表示转发的数据(如果是重定向,则Model没有意义),而View表示最终负责响应的视图组件的名称。
- ViewResolver 根据视图组件的名称,确定具体使用的是哪个视图的组件。
SpringMVC工作流程图
- DispatcherServlet接收客户端请求
- 根据请求路径从HandlerMapping中获取对应的控制器
- 调用该控制器,处理请求,以及给予响应结果,也就是数据与视图名。
- 根据视图名找到视图组件
- 响应客户端
# 1. 接收请求参数
# 1.1. 设计目标
在reg.jsp
中添加用户名和密码的输入框,当点击提交按钮后,以POST
的方式将请求提交到/user/handle_reg.do
,并且 ,控制器接收到请求后,能够获取用户填写的用户名和密码。
# 1.2. 【不推荐】通过HttpServletRequest参数获取请求参数
在处理请求的方法中,添加HttpServletRequest
类型的参数,然后调用参数对象的String getParameter(String)
方法即可获取请求参数:
@RequestMapping("/handle_reg.do")
public String handleReg(HttpServletRequest request) {
System.out.println("UserController.handleReg() ");
// getParameter()的参数必须与jsp页面中控件的name保持一致
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("username=" + username);
System.out.println("password=" + password);
return ""; // 暂不关心返回的视图名,后续运行会出现404
}
# 1.3. 【推荐】直接在处理请求的方法中声明所需的参数
将所需的参数列表直接添加在处理请求的方法中即可:
public String handleReg(String username, String password,
Integer age) {
System.out.println("UserController.handleReg() > 准备接收请求参数...");
// getParameter()的参数必须与jsp页面中控件的name保持一致
System.out.println("username=" + username);
System.out.println("password=" + password);
System.out.println("age=" + age);
return "a"; // 暂不关心返回的视图名,后续运行会出现404
}
使用这种做法,无须调用HttpServletRequest
对象的getParameter()
方法,甚至都不需要考虑类型转换问题,例如所需的age
是Integer
类的,则直接声明参数Integer age
即可,SpringMVC框架会自动的转换类型!
注意:使用这种做法,必须前后端使用的名称保持一致,例如前端页面中标签的name值是username,则后端控制器的方法中的参数名称也必须是username,如果不一致,则后端控制器获取到的值将是null值!
注意:如果前端页面没有输入值,则控制器接收到的是空字符串,即"",如果前端页面根本就没有提交这个名称的参数,则控制器接收到的是null!
如果参数名称无法统一,后续有解决方案。
这种做法最大的缺陷是:不使用于数据项目太多的表单!否则,会导致处理请求的方法中需要添加大量的参输!
# 1.3. 【推荐】使用自定义类型获取对象数据
可以自定义某个数据类型,对应所有的请求参数:
public class User {
private String username;
private String password;
private Integer age;
// ...
}
然后,在处理请求的方法中,直接将这个类型作为参数即可:
@RequestMapping("/handle_reg.do")
public String handleReg(User user) {
System.out.println(user);
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
中即可,后续也不需要显示的执行转发,在SpringMVC的控制器中,默认的响应方式就是转发。
@RequestMapping("handle_reg.do")
public String handleReg(User user,HttpServletRequest request) {
//假订输入的用户名已经被占用
//提示:您输入的用户名XXX已经被占用
request.setAttribute("message", "您输入的用户名"+user.getUsername()+"已经被占用");
//返回的视图名,也可以理解为文件的文件名
return "error";
}
# 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("message","[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("message", "[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 UserControlle{...
当方法之前添加了该注解之后,方法内配置的所有请求路径,在最终访问时都必须添加user
路径,例如:http://localhost:8080/SPRING-02-USER/user/reg.do
。
通常,推荐在类之前添加该注解,方便管理路径,例如在某个新闻管理的应用中,可能存在new_list.do
、news-info.do
的请求,而在这个应用中,也会有用户数据,就存在user_list.do
、user_info.do
,可以发现,为了保证请求路径是唯一的,都需要在路径之间添加XXX_
作为前缀,这样的管理方式是非常不方便的,在类之前添加@RequestMapping
注解就可以很好的解决这个问题,每个路径之前根本就不需要配置前缀字符,也不会发生冲突!
在@RequestMapping
的使用过程中,路径可以使用/
作为第1个字符,也可以不需要这个字符,例如:
/user /login.do
uesr /login.do
/user login.do
user login.do
以上四种配置都是正确的!通常推荐使用/
作为第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(){
System.out.println();
return "redirect:login.do";
}
例如以上代码限制了handle_reg.do
必须通过POST方式来请求,如果使用GET方式,则会返回405错误!
只有需要限定请求方式时,才需要显式的配置value="handler_reg.do",否则,直接将"handler_reg.do"配置在注解中即可!
小结:关于@RequestMapping
注解,主要作用是配置请求路径,推荐在控制器类和处理请求的方法之前都添加注解,类之间的注解是用于配置请求路径中的层次,方法之前的注解是用于配置请求的资源,关于路径的配置是该属性的value
属性,如果只配置请求路径,可以不用显示的声明这是配置value
属性,而是直接把值写出来即可,例如不需要写成@RequestMapping(values="login.do")
,而可以直接写成@RequestMapping("login.do")
,在配置路径时,推荐使用/
作为第一个字符,例如@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
注解,默认情况下,参数就是必须的!例如配置了@RequestPara("password") String password
后,如果请求中并不存在名为passwd
的参数,则会出现400错误:
HTTP Status 400 - Required String parameter 'passwd' is not present
没有提交名为passwd的参数,与提交了空值是两码事!即:如果提交了passwd参数却没有值(例如输入框中没有输入值),在服务器将得到空字符串(“”),程序并不会出现错误!如果根本就没有提交名为passwd的参数,则会导致400错误!
如果使用了@RequestMapping
注解,却又不想设置为必须提交参数,可以:
@RequestParam(value="name",required=false)
则将根据name
去接受参数,如果有值,会正确接受,如果没有(没有提交该名称的参数),则会是null值!
当required=false
时,意味着可以不必提交该参数,还可以多配置一项defaultValue
属性,表示**如果请求中没有提交该参数,则默认值是多少!**例如:
@RequestParam(
value="password", required=false, defaultValue="123456")
String password)
以上代码表示:希望请求中包含名为password
的参数,如果有,则值用于方法的String password
的参数,如果没有,也不是必须要提供(required=false
),并且使用"888999"
作为默认值(defaultValue="888999"
),即:在这种情况下,String password
的值是"888999"
。
小结:@RequestParam
注解是用于处理请求的方法中的参数之前,可以配置3项属性,分别是value
表示请求参数名称,required
表示请求中是否必须包含该参数,defaultValue
表示参数的默认值,当有任何一种需求时,都需要配置该注解,即:请求参数名称与处理请求的方法的参数名称不一致;强制必须提交某个参数;为某个参数配置默认值。
# 总结--------------------------------------
- 接收请求参数的3种方法 【不推荐】通过HttpServletRequest参数获取请求参数 【推荐】直接在处理请求的方法中声明所需的参数 【推荐】使用自定义类型获取对项数据
- 常见的响应方式 转发 重定向 redirect:视图路径
- 转发数据 【不推荐】将转发的数据封装在HttpServletRequest对象中 【不推荐】使用ModelAndView 使用ModelMap
- 关于@RequestMapping注解 作用:在方法前添加,绑定请求路径与处理请求的方法。在类前添加,类中所有方法的请求都必须添加类前的value路径 属性:method="":限制请求类型(是get还是post)
- @RequestParam注解 作用:解决请求参数名称与处理请求的方法的参数名称不一致的问题 属性:required=true或false:是否必须提交参数,默认true defaultValue="":当required为false时,若没有收到指定的value参数值,则会赋值给请求处理请求参数,起一个默认值的作用
Session中存储哪些数据
- 用户的唯一标识,例如用户的ID
- 使用频率非常高的数据,例如用户的用户名,昵称,头像等;
- 其他不便于使用其他技术存储的数据......
# SpringMVC中的拦截器(InterCeptor)
# 1.1. 作用
拦截器是运行在DispathcerServlet
之后,在每个Controller
之前,且运行结果可以选择放行或拦截!
除此以外,拦截器还会运行在Controller
之后,关于拦截器,在处理某一个请求时,最多有3次执行!只不过,通常关注最多的是第1次执行,即在Controller
之前的那次!
# 1.2. 基本使用
需要自定义类,例如名为LoginInterCeptor
,实现HandlerInterceptor
接口,重写3个抽象方法。
拦截器需要在spring的配置文件中进行配置后才可以生效!所以,需要添加配置:
<!-- 配置拦截器链 -->
<mvc:interceptors>
<!-- 配置第1个拦截器 -->
<mvc:interceptor>
<!-- 指定拦截路径,不在拦截路径之内的将不予处理,即拦截器根本就不运行 -->
<mvc:mapping path="/user/info.do"/>
<mvc:mapping path="/user/password.do"/>
<!-- 指定拦截器 -->
<bean class="cn.tedu.spring.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
在拦截器类中,运行在Controller
之前的是preHandler()
方法,该方法返回true
表示放行,返回false
表示拦截!
对于登录拦截器而言,可以判断session中的用户数据,如果数据存在,视为已登录,可直接放行,如果没有数据,则视为没登录,需要重登录,则拦截!
在配置拦截器时,根节点是<mvc:interceptors>
,用于匹配拦截器链,即任何一个SpringMVC项目,都可以有若干个拦截器,从而形成拦截器链,如果某个请求符合多个拦截器的拦截配置,则会依次被各拦截器进行处理,任何一个拦截,都会导致后续不再将请求交给Controller
去处理!
在<mvc:interceptors>
节点子集,可以配置多个<mvc:interceptor>
字级 节点,表示多个拦截器,拦截器链的执行顺序取决于这里各节点的配置先后顺序!
在<mvc:interceptor>
中,<bean>
节点用于指定拦截器类;<mvc:mapping>
节点,用于配置拦截的请求路径,每个拦截器可以配置多个该节点,并且,在配置时,支持使用星号*
作为通配符,例如:<mvc:mapping path="/user/*"/>
,为了避免拦截范围过大,可以通过<mvc:exclude-mapping />
配置排除在外的请求路径,可以理解为白名单,该节点也可以配置多个:
<!-- 配置拦截器链 -->
<mvc:interceptors>
<!-- 配置第1个拦截器 -->
<mvc:interceptor>
<!-- 指定拦截路径,不在拦截路径之内的将不予处理,即拦截器根本就不运行 -->
<mvc:mapping path="/user/*"/>
<!-- 指定白名单, 列举的请求路径将不予处理,即拦截器根本就不运行 -->
<mvc:exclude-mapping path="/user/reg.do"/>
<mvc:exclude-mapping path="/user/login.do"/>
<mvc:exclude-mapping path="/user/handle_reg.do"/>
<mvc:exclude-mapping path="/user/handle_login.do"/>
<!-- 指定拦截器 -->
<bean class="cn.tedu.spring.interceptor.LoginInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
注意:以上配置黑名单或白名单,都可以使用星号*
作为通配符,但是,它只能匹配1层路径!例如配置的是/user/*
,可以匹配/user/reg.do
或/user/login.do
却无法匹配到/user/a/reg.do
或/user/a/b/login.do
这样的路径!如果需要表示多级路径中的通配,则需要使用2个星号**
!例如配置/user/**
则可以匹配以上任意路径!
# 乱码
# 2.1. 关于乱码
在处理中文或非ASCII字符(需要使用输入法才可以输入的)时,如果存,取时,使用的字符编码不统一,就会出现乱码!
所以,出现乱码原因就是因为字符编码不统一,而解决问题的方案就是使用统一的编码!
需要统一编码的位置有:项目源码、数据库、处理数据的服务端组件、数据传输过程、显示界面
# 2.2. 解决控制器中接受请求参数的乱码
通常,在java EE项目中,解决问题的方式是: request.setCharacterEncoding("utf-8");
由于Controller
是运行在DispatcherServlet
之后的,在Controller
内部再执行编码格式已经晚了,事实上SpringMVC框架在DispatcherServlet
之前就已经通过CharacterEncodingFilter
确定了请求与响应的编码格式,所以,在SpringMVC中,无法通过Controller
或Interceptor
来解决请求和响应的乱码问题。
在SpringMVC框架的CharacterEncodingFilter
中,把使用的字符编码涉及为了变量,可以在web.xml
中添加配置加以应用,来统一设置编码:
<!-- 配置字符编码过滤器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filterclass>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>
# 2.3. 拦截器与过滤器有什么区别
拦截器是Interceptor,过滤器是Filter;
拦截器是SpringMVC中的组件,过滤器是Java EE中的组件;
拦截器是配置在Spring的配置文件中的,过滤器是配置在web.xml中的;
拦截器是运行在DispatcherServlet
之后,Controller
之前的,且在Controller
执行完后还会调用2各方法,而过滤器是运行在所有的Servlet
之前的;
拦截器的配置非常灵活,可以配置多项黑名单,也可以配置多项白名单,过滤器的配置非常单一,只能配置1项过滤路径;
拦截器与过滤器也有很多相似之处,例如:都可拒绝掉某些访问,也可以选择放行;都可以形成链。
相比之下,在一个使用SpringMVC框架的项目中,拦截器会比过滤器要好用一些,但是,由于执行时间的节点的原因,它并不能完全取代过滤器!
# 异常
# 3. 关于异常
异常的体系结构:
Throwable
Error:错误
OutOfMemoryError:OOM,使用的内存超出了限制
Exception:异常
SqlException
IOException
FileNotFoundException
RuntimeException
NullPointerException
ClassCastException
IndexOutOfBoundsException
ArrayIndexOutOfBoundsException
IllegalArgumentException
ArithmeticException
ClassNotFoundException
关于以上异常,非RuntimeException
是必须处理的,而RuntimeException
是不要求处理的!处理异常的方式可以是:
使用
try...catch
包裹可能出现异常的代码块;在方法的声明中添加
throws
声明抛出,由当前方法的调用者去处理;
RuntimeException
及其子孙类异常,从语法上没有任何处理异常相关的约束,主要原因是:
这些异常出现的频率可能极高,如果一定要处理,可能通篇代码都在处理范围之内!
这些异常都属于可以杜绝的异常,通过严谨的编程,可以使得异常一定不会出现!
# SpringMVC提供了统一处理异常的方式:
在Java中,RuntimeException
及其子孙类异常不是必须处理的,也就是既不需要try...catch
也不需要通过throw
和throws
抛出,但是,如果出现这些异常,例如NullPointerException
或ClassCastException
等,Tomcat会将异常的跟踪信息显示在页面,一方面会造成不好的用户体验,另一方面还可能暴露程序的执行流程甚至相关参数等,是非常不合适的,所以,还是应该对这些异常进行处理!但是,这些异常出现的频率极高,在每个处理请求的方法中都进行处理,工作量大,且不便于管理!
SpringMVC提供了统一处理异常的方式,使得开发者在编写处理请求的方法时,不必关心这些异常,而是在统一的方式中处理即可,一旦出现异常,也是由统一处理异常的方案来解决。
SimpleMappingExceptionResolver
它是一个配置异常与页面的映射关系的类,即:当出现指定的异常时,会自动转发到对应的页面!使用时,在Spring的配置文件中进行配置即可:
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.RuntimeException">null</prop>
</props>
</property>
</bean>
这种做法存在的问题:无法精准提示!不一定可以反馈给客户端,因为客户端需要的可能不是一个页面,而只是一些数据!还可能存在其它转发的弊端!
# -----------------------------------
SpringMVC拦截器
- 作用: 拦截器是运行在DispathcerServlet之后,在每个Controller之前,且运行结果可以选择放行或拦截!在Controller之后还会执行2次。
- 使用方法:
- 实现HandlerInterceptor接口,重写3个抽象方法。
- 在spring配置文件中进行配置
spring中解决控制器接受请求参数乱码问题 由于Controller是运行在DispatcherServlet之后的,在Controller内部再执行编码格式已经晚了,SpringMVC框架在DispatcherServlet之前就已经通过CharacterEncodingFilter确定了请求与响应的编码格式,在SpringMVC框架的CharacterEncodingFilter中,把使用的字符编码涉及为了变量,可以在web.xml中添加配置加以应用,来统一设置编码
拦截器与过滤器的区别
- 拦截器是Interceptor,过滤器是Filter;
- 拦截器是SpringMVC中的组件,过滤器是Java EE中的组件;
- 拦截器是配置在Spring的配置文件中的,过滤器是配置在web.xml中的;
- 拦截器是运行在DispatcherServlet之后,Controller之前的,且在Controller执行完后还会调用2各方法,而过滤器是运行子所有的Servlet之前的;
- 拦截器的配置非常灵活,可以配置多项黑名单,也可以配置多项白名单,过滤器的配置非常单一,只能配置1项过滤路径;
- 相同点:拦截器与过滤器也有很多相似之处,例如:都可拒绝掉某些访问,也可以选择放行;都可以形成链。
为什么RuntimeException及其子孙类异常,不是必须抛出:
- 这些异常出现的频率可能极高,如果一定要处理,可能通篇代码都在处理范围之内!
- 这些异常都属于可以杜绝的异常,通过严谨的编程,可以使得异常一定不会出现!
SpringMVC提供了统一处理异常的方式: SimpleMappingExceptionResolver 当出现指定的异常时,会自动转发到对应的页面!使用时,在Spring的配置文件中进行配置即可:
# 3.3 处理异常-@ExceptionHandler
可以在控制器中自定义一个处理异常的方法,在方法之前添加@ExcptionHandler
,然后凡是约定范围内的异常,都可以由该方法来决定来如何处理!
使用
public
权限;设计返回值的规则与处理请求的方法相同;
方法名称不限;
必须添加异常为参数;
必须添加
@ExceptionHandler
注解;
@ExceptionHandler
public String handlerException(Exception e) {
System.out.println("ExceptionController.handleException()");
if(e instanceof NullPointerException) {
return "null";
}else if(e instanceof ArrayIndexOutOfBoundsException) {
return "index";
}
return "runtime";
}
在处理异常时,如果需要转发数据,可以将返回值修改为ModelAndView
,或者,在处理异常的方法参数列表中添加HttpServletRequest
,但是,却不可以使用ModelMap
来封装转发的数据!
@ExceptionHandler
public String handlerException(Exception e,
HttpServletRequest request) {
System.out.println("ExceptionController.handleException()");
if(e instanceof NullPointerException) {
return "null";
}else if(e instanceof ArrayIndexOutOfBoundsException) {
request.setAttribute("msg", e.getMessage());
return "index";
}
return "runtime";
}
允许使用多个不同的方法来处理异常! 处理异常的方法必须和发生异常的方法必须在同一个类里面,但是,这样存在不合理性,会导致每一个controller都需要写处理异常代码!解决办法:可以创建一个BaseController,作为当前项目的控制器类的基类,然后,把处理异常的代码编写在这个类中即可!通过其他类继承该基类,从而解决问题
关于@ExceptionHandler
,还可以用于限制其对应的方法处理的异常的种类!
@ExceptionHandler(IndexOutOfBoundsException.class)
以上代码表示接下来的方法只处理IndexOutOfBoundsException
及其子孙类异常,而其他的异常不处理。
# 3.4 小结
处理异常的本质并不可以“撤销”异常,无法让已经出现的问题还原到没有问题!所为,处理异常的本质应该是尽量的补救已经出现的问题,并且,尝试通过一些提示信息等方式告知使用者,尽量执行正确的操作,避免后续再次出现同样的问题!
并且,对于开发者而言,应该处理每一个可能出现的异常,因为,如果没有处理,就会继续抛出,最终被Tomcat捕获,而Tomcat处理的方式是把异常的跟踪信息显示在页面上,是极为不合适的!
在SpringMVC,处理异常有2种方式,第1种是通过SimpleMappingExceptionResolver
设置异常与转发目标的对应关系,第2种是使用@ExceptionHandler
为处理异常的方法进行注解,推荐使用第2种方式。
通常,会把处理异常的方法写在项目的控制器类的基类中,即写在BaseController
中,在使用@ExceptionHandler
时,可以明确的指定所处理的异常类型,例如@ExceptionHandler(NullPointerException)
,且,在控制器类中,可以编写多个处理异常的方法!
关于处理异常的方法,应该是public
方法,返回值类型与处理请求的方法形同,可以是String
或ModelAndView
或其他允许的类型,方法的名称可以自定义,参数中必须包括Exception
类型的参数,还允许存在HttpServletRequest
、HttpServletResponse
,不允许使用ModelMap
。
# 手写spring核心
# 1. 添加配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<!--
配置处理器。
class属性:指定处理器的类名。
-->
<bean class="demo.HelloController"/>
<bean class="demo.LoginController"/>
</beans>
# 2. 在web.xml中配置dispetcheSevlet和初始化参数
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>base.web.DispatcherServlet</servlet-class>
<!--
指定配置文件名
-->
<init-param>
<param-name>configLocation</param-name>
<param-value>smartmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>
# 3. 在DispatcherServlet中重写初始化方法
# 3.1 init方法
public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private HandlerMapping handlerMapping;
@Override
public void init() throws ServletException {
/*
* 读取smartmvc框架的配置文件(smartmvc.xml),
* 然后将该配置文件中的配置好的处理器实例化。
*/
String fileName = getServletConfig()
.getInitParameter("configLocation");
InputStream in = getClass().getClassLoader().
getResourceAsStream(fileName);
SAXReader reder = new SAXReader();
try {
//读取配置文件的内容,dom4j会将依据xml文件中的内容
//构造一颗对应的树。
Document doc = reder.read(in);
//找出根节点
Element root = doc.getRootElement();
//找出根节点的所有子节点
List<Element> elements = root.elements();
//遍历所有子节点
List beans = new ArrayList();
for(Element ele : elements) {
//读取处理器类名
String className = ele.attributeValue("class");
System.out.println("className:"+className);
//将处理器实例化
Object bean = Class.forName(className).newInstance();
//将处理器实例添加到集合里面,方便管理
beans.add(bean);
}
System.out.println("处理器集合:" + beans);
//创建HandlerMapping对象
handlerMapping = new HandlerMapping();
//process方法负责建立请求路径与处理器的对应关系。
handlerMapping.process(beans);
} catch (Exception e) {
e.printStackTrace();
throw new ServletException(e);
}
}
}
# 3.2 HandlerMapping
/**
* 映射处理器:
* 负责建立请求路径与处理器的对应关系。
*
*/
public class HandlerMapping {
/*
* 用来存放请求路径与处理器的关系
* 注:
* Handler对象封装了处理器及method对象
*/
private Map<String,Handler> handlerMap =
new HashMap<String,Handler>();
/**
* 依据请求路径返回Handler对象
*/
public Handler getHandler(String path) {
return handlerMap.get(path);
}
/**
* 负责建立请求路径与处理器的对应关系
*/
public void process(List beans) {
//遍历由处理器实例组成的集合
for(Object bean : beans) {
//获得Class对象
Class clazz = bean.getClass();
//获得处理器所有方法
Method[] methods = clazz.getDeclaredMethods();
//遍历所有方法
for(Method mh : methods) {
//获得加在方法前的@RequestMapping注解
RequestMapping rm =
mh.getDeclaredAnnotation(RequestMapping.class);
//获得请求路径
String path = rm.value();
System.out.println("path:"+path);
//建立请求路径与处理器的对应关系
handlerMap.put(path,new Handler(mh,bean));
}
}
System.out.println("handlerMap:" + handlerMap);
}
}
# 3.2 RequestMapping注解
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
public String value();
}
# 3.3 Handler.java
/**
* 该类封装了处理器及方法对象,目的是为了利用
* java反射去调用处理器的方法。
* @author chengyi
*
*/
public class Handler {
private Method mh;
private Object obj;
public Handler(Method mh, Object obj) {
super();
this.mh = mh;
this.obj = obj;
}
public Method getMh() {
return mh;
}
public void setMh(Method mh) {
this.mh = mh;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
# 3.4 以HelloControllee为例
/**
* 处理器:
* 负责业务逻辑的处理。
* 注:
* 也可以调用其他的类来完成业务逻辑的处理。
*
*/
public class HelloController {
/*
* @RequestMapping注解用来告诉框架,
* 该方法用来处理那个请求。
*/
@RequestMapping(value="/hello.do")
public String hello() {
System.out.println("helloController's hello()");
/*
* 返回视图名。
* DispatcherServlet(控制器)会依据视图名来生成真正的
* 目标地址。
*
*/
return "hello";
}
}
# 4. 请求访问
当有请求访问时,会根据web.xml中的配置访问到DispatcherServlet的Service方法中
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
//获得请求资源路径
String uri = request.getRequestURI();
System.out.println(uri);
//为了方便比较,截取请求资源路径的一部分。
//这一部分路径称为请求路径
String contextPath = request.getContextPath();
String path = uri.substring(contextPath.length());
System.out.println("请求路径(path):"+path);
//调用HandlerMapping对象的getHandler方法
//来获得Handler对象
Handler handler = handlerMapping.getHandler(path);
System.out.println("handler:"+handler);
//利用Handler对象来调用处理器的方法。
Method mh = handler.getMh();
Object obj = handler.getObj();
try {
/*
* 查看处理器的方法带不带参,如果带参,则要将参数值传递给
* 处理器的方法。
*/
Class[] types = mh.getParameterTypes();
Object returnVal = null;
if(types.length == 0) {
//调用不带参的方法
returnVal = mh.invoke(obj);
}else {
//params数组用来存放参数值
Object[] params = new Object[types.length];
for(int i=0;i<types.length;i++) {
if(types[i] == HttpServletRequest.class) {
params[i] = request;
}
if(types[i] == HttpServletResponse.class ) {
params[i] = response;
}
}
//调用带参的方法
returnVal = mh.invoke(obj, params);
}
//获得视图名
String viewName = returnVal.toString();
System.out.println("viewName:"+viewName);
/*
* 处理视图名。
* 默认情况下,转发到"/WEB-INF/"+视图名+".jsp"。
* 如果视图名是以:"redirect:"开头,则重定向。
*/
if(viewName.startsWith("redirect:")) {
//重定向
String redirectPath = contextPath+"/"
+ viewName.substring("redirect:".length());
response.sendRedirect(redirectPath);
}else {
//转发
String jspPath = "/WEB-INF/"+viewName+".jsp";
request.getRequestDispatcher(jspPath).forward(request, response);
}
} catch (Exception e) {
e.printStackTrace();
throw new ServletException(e);
}
}