# 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对象作为属性值!

自动装配的常用模式有byNamebyType,前者表示根据名称来装配,即:要求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小结

通过学习,应该掌握:

  1. 配置<bean>idclass
  2. 通过SET方式注入属性的值;
  3. 读取*.properties文件;
  4. 理解自动装配中的byTypebyName;
  5. Spring表达式
  6. 常用注解

应该了解:

  1. 单例,作用域,生命周期;
  2. 经常用的注解

# SpringMvc框架

# 1.作用

解决了V-C的交互问题,即视图与控制器的交互问题。

在原生的java EE技术中,使用Servlet作为项目中的控制器,用于接受用户请求,并给予相应结果。这种做法最大的问题在于:在常规做法中,每个Servlet对应一个请求路径,例如LoginServlet处理login.do的请求,而RegisterServlet处理register.do的请求,所以,会导致Servlet数量太多,不便于管理(无论从源文件的数量,还是从配置文件的内容),且占用大量内存空间的问题!

并且,在请求的处理过程中,还存在许多操作不简便的做法!

# 1.2. SpringMVC核心组件

  1. DispatcherServlet 前端控制器,主要职责是接收所有请求(根据配置文件来决定),并将请求转发给对应的控制器,接受控制器的处理结果,确定最终由那个视图完成响应!
  2. HandlerMapping 处理请求路径与控制器的映射关系。
  3. Controller 实际处理请求的组件,例如接收请求参数,决定最终是转发或是重定向的方式来响应。
  4. ModelAndView 控制器的处理结果,其中的Model表示转发的数据(如果是重定向,则Model没有意义),而View表示最终负责响应的视图组件的名称。
  5. ViewResolver 根据视图组件的名称,确定具体使用的是哪个视图的组件。

在这里插入图片描述 SpringMVC工作流程图

  1. DispatcherServlet接收客户端请求
  2. 根据请求路径从HandlerMapping中获取对应的控制器
  3. 调用该控制器,处理请求,以及给予响应结果,也就是数据与视图名。
  4. 根据视图名找到视图组件
  5. 响应客户端

# --------------------------------------

Spring作用 创建和管理对象,使得开发过程中,可以不必使用new关键字创建对象,而是直接获取对象!并且,还可以通过一些配置,使得某些获取到的对象,其中某些属性已经是被赋值的!

Spring注解 常用注解:

  1. @Component 通用注解
  2. @Controller 控制器类的注解
  3. @Service 业务类的注解
  4. @Repository 持久层类的注解
  5. @Scope 是否是单例子,值为:singleton(默认),prototype
  6. @Lazy 懒加载
  7. @PostConstruct 初始化方法,会在构造方法之后被自动调用
  8. @PreDestroy 销毁方法,会在Spring容器销毁并释放资源的前一刻被自动调用
  9. @Autowired 默认按照类型(byType)实现自动装配!
  10. @Resource是javaee注解,默认按照名称(byName)自动装配,如果失败,会继续按照类型(byType)实现自动装配

注:7.8两个方法是在javax包中定义的,使用之前,需要为项目添加Tomcat运行环境,否则无法识别!这两个生命周期方法是建立在单例模式之下的,对于非单例模式而言,以上生命周期方法其实没有意义 1、2、3、9、是常用注解,其他都为不常用注解

SpringMvc框架

  1. 作用:解决了V-C的交互问题,即视图与控制器的交互问题。
  2. SpringMVC核心组件
    1. DispatcherServlet 前端控制器,主要职责是接收所有请求(根据配置文件来决定),并将请求转发给对应的控制器,接受控制器的处理结果,确定最终由那个视图完成响应!
    2. HandlerMapping 处理请求路径与控制器的映射关系。
    3. Controller 实际处理请求的组件,例如接收请求参数,决定最终是转发或是重定向的方式来响应。
    4. ModelAndView 控制器的处理结果,其中的Model表示转发的数据(如果是重定向,则Model没有意义),而View表示最终负责响应的视图组件的名称。
    5. ViewResolver 根据视图组件的名称,确定具体使用的是哪个视图的组件。

SpringMVC工作流程图

  1. DispatcherServlet接收客户端请求
  2. 根据请求路径从HandlerMapping中获取对应的控制器
  3. 调用该控制器,处理请求,以及给予响应结果,也就是数据与视图名。
  4. 根据视图名找到视图组件
  5. 响应客户端

# 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()方法,甚至都不需要考虑类型转换问题,例如所需的ageInteger类的,则直接声明参数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. 常见的响应码

被服务器接收到的每个请求,在最终响应时,服务器都会给出一个响应码,例如200404等。通常:

  • 2xx: 正确的响应,例如200206等.....
  • 3xx: 重定向,例如302301等.....
  • 4xx: 请求错误,例如请求的资源不存在,或者请求类型错误、或者请求参数错误等等,例如400404405406等.....
  • 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.donews-info.do的请求,而在这个应用中,也会有用户数据,就存在user_list.douser_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表示参数的默认值,当有任何一种需求时,都需要配置该注解,即:请求参数名称与处理请求的方法的参数名称不一致;强制必须提交某个参数;为某个参数配置默认值。

# 总结--------------------------------------

  1. 接收请求参数的3种方法 【不推荐】通过HttpServletRequest参数获取请求参数 【推荐】直接在处理请求的方法中声明所需的参数 【推荐】使用自定义类型获取对项数据
  2. 常见的响应方式 转发 重定向 redirect:视图路径
  3. 转发数据 【不推荐】将转发的数据封装在HttpServletRequest对象中 【不推荐】使用ModelAndView 使用ModelMap
  4. 关于@RequestMapping注解 作用:在方法前添加,绑定请求路径与处理请求的方法。在类前添加,类中所有方法的请求都必须添加类前的value路径 属性:method="":限制请求类型(是get还是post)
  5. @RequestParam注解 作用:解决请求参数名称与处理请求的方法的参数名称不一致的问题 属性:required=true或false:是否必须提交参数,默认true defaultValue="":当required为false时,若没有收到指定的value参数值,则会赋值给请求处理请求参数,起一个默认值的作用

Session中存储哪些数据

  1. 用户的唯一标识,例如用户的ID
  2. 使用频率非常高的数据,例如用户的用户名,昵称,头像等;
  3. 其他不便于使用其他技术存储的数据......

# 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中,无法通过ControllerInterceptor来解决请求和响应的乱码问题。

在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是不要求处理的!处理异常的方式可以是:

  1. 使用try...catch包裹可能出现异常的代码块;

  2. 在方法的声明中添加throws声明抛出,由当前方法的调用者去处理;

RuntimeException及其子孙类异常,从语法上没有任何处理异常相关的约束,主要原因是:

  1. 这些异常出现的频率可能极高,如果一定要处理,可能通篇代码都在处理范围之内!

  2. 这些异常都属于可以杜绝的异常,通过严谨的编程,可以使得异常一定不会出现!

# SpringMVC提供了统一处理异常的方式:

在Java中,RuntimeException及其子孙类异常不是必须处理的,也就是既不需要try...catch也不需要通过throwthrows抛出,但是,如果出现这些异常,例如NullPointerExceptionClassCastException等,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>

这种做法存在的问题:无法精准提示!不一定可以反馈给客户端,因为客户端需要的可能不是一个页面,而只是一些数据!还可能存在其它转发的弊端!

# -----------------------------------

  1. SpringMVC拦截器

    1. 作用: 拦截器是运行在DispathcerServlet之后,在每个Controller之前,且运行结果可以选择放行或拦截!在Controller之后还会执行2次。
    2. 使用方法:
      1. 实现HandlerInterceptor接口,重写3个抽象方法。
      2. 在spring配置文件中进行配置
  2. spring中解决控制器接受请求参数乱码问题 由于Controller是运行在DispatcherServlet之后的,在Controller内部再执行编码格式已经晚了,SpringMVC框架在DispatcherServlet之前就已经通过CharacterEncodingFilter确定了请求与响应的编码格式,在SpringMVC框架的CharacterEncodingFilter中,把使用的字符编码涉及为了变量,可以在web.xml中添加配置加以应用,来统一设置编码

  3. 拦截器与过滤器的区别

    1. 拦截器是Interceptor,过滤器是Filter;
    2. 拦截器是SpringMVC中的组件,过滤器是Java EE中的组件;
    3. 拦截器是配置在Spring的配置文件中的,过滤器是配置在web.xml中的;
    4. 拦截器是运行在DispatcherServlet之后,Controller之前的,且在Controller执行完后还会调用2各方法,而过滤器是运行子所有的Servlet之前的;
    5. 拦截器的配置非常灵活,可以配置多项黑名单,也可以配置多项白名单,过滤器的配置非常单一,只能配置1项过滤路径;
    6. 相同点:拦截器与过滤器也有很多相似之处,例如:都可拒绝掉某些访问,也可以选择放行;都可以形成链。
  4. 为什么RuntimeException及其子孙类异常,不是必须抛出:

    1. 这些异常出现的频率可能极高,如果一定要处理,可能通篇代码都在处理范围之内!
    2. 这些异常都属于可以杜绝的异常,通过严谨的编程,可以使得异常一定不会出现!
  5. 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方法,返回值类型与处理请求的方法形同,可以是StringModelAndView或其他允许的类型,方法的名称可以自定义,参数中必须包括Exception类型的参数,还允许存在HttpServletRequestHttpServletResponse,不允许使用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);
		} 
	}

# Spring源码架构图

RUNOOB 图标

Last Updated: 12/16/2022, 8:39:10 AM