java spring 详细笔记

Spring war 包  5步骤
1.右键Deployment Descriptor:.../generate Deployment...
2.打开Tomcat。右键项目-Properties-Targeted Runtimes 打勾 Apache Tomcat...-OK
3.java Resources-src/main/resources里spring.xlm文件
4.项目下pon.xml
5.src/main/webapp/WEB-INF/web.xml

测试包4件事 src/test/java 文件内
//1.加载Spring配置文件,获取Spring容器
		AbstractApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
//2.从Spring容器中获取对象
			//bean-id 与接口名一致,首字母为小写
		UserMapper userMapper = ac.getBean("userMapper",UserMapper.class);
//3.测试功能
//4.释放资源
		ac.close();


# 1.Spring    
## (1)Spring是什么?
是一个轻量级的、用来简化企业级应用开发的开发框架。

	注:
		a.简化开发:
		Spring对常用的api做了简化,比如,使用Spring
		jdbc来访问数据库,就不用再考虑如何获取连接、关闭连接、处理异常等等。
		b.解耦:
		Spring容器(Spring框架的一个模块)帮我们管理
		对象(包括对象的创建及对象之间的依赖关系),
		这样一来,对象之间的耦合度会大大降低,提供高了
		系统的维护性。
		c.集成其它框架:
		Spring可以将其它一些常用的框架集成进来,比如
		可以将Quartz,MyBatis等集成进来。

## (2)Spring容器
### 1)Spring容器是什么?
Spring框架的一个核心模块,用于管理对象。

	注:
		管理对象指的是,spring容器会用我们创建对象,
	并且建立对象之间的依赖关系。

### 2)如何启动Spring容器?
step1.导包。
	<dependencies>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-webmvc</artifactId>
		<version>3.2.8.RELEASE</version>
	</dependency>
	</dependencies>
step2.添加配置文件(applicationContext.xml)。

	将配置文件添加到main/resources下。

step3.启动Spring容器。
	
	ApplicationContext ac = 
		new ClassPathXmlApplicationContext(
				"applicationContext.xml");

### 3)创建对象
a.方式一  使用无参构造器

	step1. 给类添加无参构造器(缺省构造器)。
	step2.  在配置文件当中,使用bean元素来进行配置。
		
	<!-- 
		使用无参构造器来创建对象。
		id属性要求唯一。
		class属性指定类名(要写完整的类名)。
		注:
			在spring框架当中,将Spring容器所管理
			的对象称之为一个bean。	
	 	-->
		<bean id="ap1" class="first.Apple"/>

	step3. 启动容器,调用容器提供的getBean方法。

		//通过容器获得对象
		Apple ap1 = 
			ac.getBean("ap1",Apple.class);

b.方式二  使用静态工厂方法 (了解)

	<!-- 
		使用静态工厂方法来创建对象。
		factory-method属性用于指定一个静态
		方法(static修饰的方法)。
		注:
		  容器会调用指定类的静态方法来创建
		对象。
	 -->
	<bean id="cal1" 
	class="java.util.Calendar" 
	factory-method="getInstance"/>

c.方式三  使用实例工厂方法 (了解)
	
	<!-- 
		使用实例工厂方法来创建对象。
		factory-bean属性指定bean的id,
		factory-method属性指定一个方法。
		注:
			容器会调用该bean的方法来创建一个
			对象。
	 -->
	 <bean id="date2" factory-bean="cal1" 
	 factory-method="getTime"/>


### 4)作用域
a.默认情况下,对于某个bean,容器只会创建一个实例。

b.如果要让容器创建多个实例,需要设置bean的作用域。

	<!-- 
			scope属性用于指定作用域,
			缺省值是singleton,如果将值设置
			为prototype,则容器会创建多个实例。
		 -->
		<bean id="eb1" 
			class="basic.ExampleBean" 
			scope="prototype"/>	

### 5)生命周期
spring容器在创建好对象之后,会调用初始化方法,
在销毁该对象之前,会调用销毁方法。

		<!-- 
			init-method属性:	指定初始化方法
			destroy-method属性:指定销毁方法。
			注:
				销毁方法只有当作用域为singleton
				才有效。
		 -->	
		<bean id="mb1" 
		class="basic.MessageBean" 
		init-method="init" 
		destroy-method="destroy" 
		scope="singleton"/>	

### 6)延迟加载
a.默认情况下,容器启动之后,会将所有作用域为singleton的bean实例化。

b.延迟加载指的是,通过设置lazy-init="true"告诉容器,
启动之后不再创建其实例。

		<!-- 
			lazy-init属性如果值为true,表示
			延迟加载。
		 -->
		<bean id="lb1" class="basic.LazyBean" 
		lazy-init="true"/>

## (3)IOC和DI   
### 1)IOC是什么? (Inversion Of Controll 控制反转)
对象之间的依赖关系应该由容器来建立。

### 2)DI是什么? (Dependency Injection 依赖注入)
容器通过调用set方法或者构造器来建立对象之间的依赖关系。

	注:
		IOC是目标,DI是手段。

### 3)set方法注入
step1. 为类添加set方法。

step2. 在配置文件当中,使用property元素来配置set
方法注入。
	
		<!-- 
			property元素表示采用set方法注入。
			name属性用于指定set方法名
			(容器会将b大写,前面添加set来
			构造方法名)。
			ref属性用于指定被注入的bean的id。
		 -->
		<bean id="a1" class="ioc.A">
			<property name="b" ref="b1"/>
		</bean>

step3. 启动容器,调用getBean方法。


# 练习 
使用无参构造器创建一个Student对象。
添加初始化方法和销毁方法,
作用域为singleton,
不使用延迟加载。

—————————————————————————————————

### 3)set方法注入

![](set.png)

### 4)构造器注入
step1. 添加相应的带参的构造器。

step2. 使用<constructor-arg>元素来配置构造器注入。
	
		<bean id="cpu" class="ioc.Cpu"/>
		<!-- 
			constructor-arg元素用来配置构造器
			注入。
			index属性用于指定参数的下标。
			ref属性用于指定被注入的bean的id。
		 -->
		<bean id="cp" class="ioc.Computer">
			<constructor-arg index="0" 
			ref="cpu"/>
		</bean>

step3. 启动容器,调用getBean方法。



### 4)自动装配 (了解)
a.什么是自动装配?

	容器依据某些规则,自动查找符合要求的对象,
	然后建立对象之间的依赖关系。
	注: 容器仍然要调用set方法或者构造器。

b.如何自动装配?
	
	使用autowire属性指定装配的规则:
	<bean id="wt1" class="autowire.Waiter"/>
		<!-- 
			autowire属性表示让容器自动建立
			对象之间的依赖关系,有三个值:
			byName:
				以属性名作为bean的id,查找
				对应的bean,然后调用set方法。
				注:如果找不到对应的bean,
				不会调用set方法。
			byType:
				以属性类型作为bean的类型,查找
				对应的bean,然后调用set方法。
				注:
					如果找不到对应的bean,
					不会调用set方法。
					如果找到多个bean,会出错。
			constructor:
				类似于byType,只不过是调用构造器来
				完成	注入。	 
		 -->
	<bean id="rest" 
		class="autowire.Restaurant" 
		autowire="byType"/> 

### 5)注入基本类型的值

	<bean id="vb" class="value.ValueBean">
		<property name="name" value="小包总"/>
		<property name="age" value="20"/>
	</bean>

### 6)注入集合类型的值
a.支持四种集合类型的值的注入,分别是List,Set,Properties,Map。

b.注入方式如下:

		<property name="interest">
				<list>
					<value>打猎</value>
					<value>看书</value>
					<value>编程</value>
					<value>编程</value>
				</list>
			</property>
		<property name="city">
			<set>
				<value>洛阳</value>
				<value>金陵</value>
				<value>开封</value>
			</set>
		</property>
		<property name="score">
			<map>
				<entry key="english" 
					value="59.5"/>
				<entry key="math" 
					value="17"/>	
			</map>
		</property>
		<property name="db">
			<props>
				<prop key="username">tom</prop>
				<prop key="pwd">1234</prop>
			</props>
		</property>

### 7)引用的方式注入集合类型的值

		<!-- 
			将集合类型的值配置成一个bean
		 -->
		<util:list id="interestBean">
			<value>打猎</value>
			<value>看书</value>
			<value>编程</value>
		</util:list>
		<util:set id="cityBean">
			<value>北京</value>
			<value>南京</value>
			<value>东京</value>
		</util:set>
		<util:map id="scoreBean">
			<entry key="english" value="80"/>
			<entry key="math" value="90"/>
		</util:map> 
		<util:properties id="dbBean">
			<prop key="username">John</prop>
			<prop key="pwd">1234</prop>
		</util:properties>
		<bean id="vb2" class="value.ValueBean">
			<property name="interest" 
			ref="interestBean"/>
			<property name="city"
			ref="cityBean"/>
			<property name="score"
			ref="scoreBean"/>
			<property name="db"
			ref="dbBean"/>
		</bean>

### 8)读取properties文件的内容
	
		<!-- 
			读取properties文件的内容。
			location属性用于指定要读取的文件的位置。
			注:
				classpath:表示依据类路径去查找相应的
				文件。
		 -->
		 <util:properties id="config" 
		 location="classpath:config.properties"/>
	
### 9)spring表达式
a.spring表达式的作用?

	可以用来读取其它bean的属性

b.用法如下图:

![](spel.png)


# 练习1
 将Waiter注入到Restaurant,采用构造器注入。

# 练习2
 将盔甲和无限手套注入到钢铁侠,采用自动装配
(使用byName)

	IronMan类  钢铁侠
	Armour类   盔甲 (String name )
	Glove类  无限手套

# 练习3
 为ExampleBean注入四种集合类型的值。

# 练习4  
使用Spring表达式读取jdbc.properties文件的
内容,并注入到DataSourceBean对象。

	DataSourceBean类
	private String driver;
	private String username;
	private String pwd;
	private String url;

## 提示:
step1.将jdbc.properties文件拷贝到resources下。

step2.添加DataSourceBean类

	public class DataSourceBean {
		private String driver;
		private String url;
		private String username;
		private String pwd;
		
		@Override
		public String toString() {
			return "DataSourceBean [driver=" + driver + ", url=" + url + ", username=" + username + ", pwd=" + pwd + "]";
		}
		
		public void setDriver(String driver) {
			this.driver = driver;
		}
		public void setUrl(String url) {
			this.url = url;
		}
		public void setUsername(String username) {
			this.username = username;
		}
		public void setPwd(String pwd) {
			this.pwd = pwd;
		}
	}

step3. 在配置文件(lab.xml)中,添加如下配置

	<util:properties id="jdbc" 
		location="classpath:jdbc.properties"/>
	 	
 	<bean id="ds" 
 		class="lab.DataSourceBean">
 		<property name="driver" 
 		value="#{jdbc.driver}"/>
 		<property name="url"
 		value="#{jdbc.url}"/>
 		<property name="username"
 		value="#{jdbc.username}"/>
 		<property name="pwd"
 		value="#{jdbc.password}"/>
 	</bean>

step4. 测试

	ApplicationContext ac = 
		new ClassPathXmlApplicationContext(
			"lab.xml");
	DataSourceBean ds = 
			ac.getBean("ds",DataSourceBean.class);
	System.out.println("ds:" + ds);

—————————————————————————————————
## Spring

### 1. 作用

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

### 2. Spring注解

在Spring中,定义了一系列的注解,可以取代几乎所有的XML配置!

**尽管使用注解可以完成此前的许多配置,但是,基于Spring的项目仍需要Spring的配置文件!**

#### 2.1. 常用注解

使用注解的方式来创建和管理对象,首先,必须在Spring的配置文件中添加组件扫描:

	<!-- 组件扫描 -->
	<!-- 仅在组件扫描的包下的类,才会被Spring管理 -->
	<!-- 某个类,如果在扫描范围中,且添加了注解,则会被Spring管理 -->
	<!-- base-package:扫描的根包 -->
	<!-- 根包:父级包,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`是对业务类的注解,`@Repository`是对持久层类的注解。

不过,以上这4种的作用和使用方式完全相同!只是语义不同,即从语法上表达的意义不相同,应该根据类的定位来选取 其中的某个注解!

#### 2.2. 【了解】管理对象的作用域与生命周期

由Spring所管理的对象,默认都是饿汉式单例的,通过`@Scope`注解可以配置某个类被Spring管理时,是否是单例的:

	@Scope("prototype")
	public class ...

常用的配置方式有`@Scope("singleton")`和`@Scope("prototype")`。

如果需要配置该类最终是否是懒加载的,可以使用`@Lazy`注解,当添加了该注解后,就是懒加载模式,即:只有第1次获取对象时,才会创建对象,而在加载Spring配置文件的过程中,并不会把对象创建出来!

关于`@Lazy`也可以配置值,例如`@Lazy(true)`或`@Lazy(false)`。

> 饿汉式:一开始就已准备好了,随时都有的吃!

> 懒汉式:不到逼不得已,不干活!

**注意:是否懒加载,是建立在单例的基础之上的!如果不是单例的,则懒加载的配置是无效的!**

**作用域:在多大的范围内是有效的!对于变量/对象而言,作用域就是它存在的时长,即何时创建及何时销毁!由于单例的对象都是static实现的,
也就涉及创建时间和销毁时间的问题!而非单例的,作用域与普通的局部变量相同!**

简单的懒汉式单例代码(没有解决线程安全问题):

	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容器销毁并释放资源的前一刻被自动调用。

**注意:以上2个方法是在javax包中定义的,使用之前,需要为项目添加Tomcat运行环境,否则无法识别!**

**注意:以上生命周期方法是建立在单例模式之下的,对于非单例模式而言,以上生命周期方法其实没有意义!**

#### 2.3. 自动装配

当使用了自动装配后,由Spring管理的对象时,会自动尝试为各属性注入值,值的来源可以是其它bean,例如:

	public class UserService {
		public IUserDao userDao;
	}

	public class UserDao1 implements IUserDao {
	}

如果以上2个类都是由Spring管理的,则在创建`UserService`对象时,会尝试自动的将`UserDao`对象作为其属性值!

自动装配的常用模式有`byName`和`byType`,前者表示根据名称来装配,即:要求bean id与属性名保持一致,后者表示根据类型来装配,
即bean的类型与属性的类型保持一致(允许是接口与实现类的关系,也允许是父子级继承的关系),不过,根据类型装配,
要求匹配该类型的对象必须是有且仅有1个,如果有2个,则会抛出异常,无法装配!
	

## SpringMVC框架

### 1. 作用

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

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

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

### 1.2. SpringMVC核心组件

1. DispatcherServlet

前端控制器,主要职责是接收所有请求(根据配置文件来决定),并将请求转发给对应的控制器,接收控制器的处理结果,确定最终由哪个视图完成响应!

2. HandlerMapping

处理请求路径与控制器的映射关系。

3. Controller

实际处理请求的组件,例如接收请求参数,决定最终是转发或重定向的方式来响应。

4. ModelAndView

控制器的处理结果,其中的Model表示转发的数据(如果是重定向,则Model没有意义),而View表示最终负责响应的视图组件的名称。

5. ViewResolver

根据视图组件的名称,确定具体使用的是哪个视图组件。

![](springmvc.png)

### 1.3. SpringMVC-HelloWorld

#### 1.3.1. 创建项目

创建`Maven Project`,勾选`Create a simple project`,然后,`Group Id`值为`cn.tedu.spring`,`Artifact Id`值为`SPRINGMVC-01-HELLOWORLD`,`Packaging`选择`war`:

![](springmvc-helloworld-1.png)

接下来,需要执行4个步骤(以后每次创建项目时都需要做的事情):

1. 生成`web.xml`文件;
2. 添加依赖:在此前的项目中找到`pom.xml`,将其中的依赖的代码复制到当前项目中,关于依赖的jar包,会越用越多,学习时,只加不减;
3. 复制`spring.xml`文件到当前项目中,并删除其中的配置,除非你确定这些配置是当前项目中必须使用的;
4. 为项目添加Tomcat运行环境;

#### 1.3.2. 配置DispatcherServlet

由于需要`DispatcherServlet`接收所有请求,所以,首先必须在`web.xml`配置它:

	<!-- 配置DispatcherServlet -->
	<servlet>
		<servlet-name>SpringMVC</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>SpringMVC</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

经过以上配置,则任何以`.do`作为后缀的请求,都将由`DispatcherServlet`来处理!

由于项目是基于Spring的,希望项目启动时就加载Spring的配置文件,并使得`DispatcherServlet`已经完成初始化可以随时接收请求,所以,还需进一步的配置:

	<!-- 配置DispatcherServlet -->
	<servlet>
		<servlet-name>SpringMVC</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<!-- 配置spring的配置文件 -->
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:spring.xml</param-value>
		</init-param>
		<!-- 启动Tomcat时即初始化该Servlet -->
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>SpringMVC</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

至此,当项目启动时,就会初始化`DispatcherServlet`,并且,在初始化过程中,会加载Spring的配置文件。

**测试**

如果Spring能正常工作,则配置是成功的!

在`src\main\java`中创建一个测试类`cn.tedu.spring.test.Test`,并且,显式的添加构造方法,并输出内容:

	@Component
	public class Test {

		public Test() {
			System.out.println("已经加载Spring配置文件!");
		}
	
	}

然后,在Spring的配置文件中添加组件扫描:

	<context:component-scan base-package="cn.tedu.spring" />

最终,把项目部署到Tomcat,并启动Tomcat,在启动日志中如果能够看到以上输出语句,则成功!

![](springmvc-helloworld-2.png)

#### 1.3.3. 设计目标

发出请求`http://localhost:8080/项目名称/hello.do`,要求在界面中显示`HelloWorld`字样。

#### 1.3.4. 处理请求

以上完成了流程中的第1步,接下来,如果发出`hello.do`请求,就会被`DispatcherServlet`接收得到,
然后,`DispatcherServlet`应该根据`hello.do`请求路径去询问`HandlerMapping`得知由哪个控制器去处理请求,
原始的实现方式是使用`SimpleUrlHandlerMapping`类去进行配置,不过,这种配置方式非常麻烦,而且,
对应关系不明确(所有的路径与控制器的配置都在同一个配置文件中,导致不便于管理),所以,后续都是使用注解完成相关配置,则在开发过程中,
跳过第2步和第3步(只是开发时不用关注,框架的执行流程不变)。

为了确保有控制器处理请求,所以,创建`HelloController`:

![](springmvc-helloworld-3.png)

所有的控制器都应该是由Spring管理的,所以,需要为控制器类添加`@Controller`注解:

	@Controller
	public class HelloController {
	
	}

然后,在类中自定义处理请求的方法,方法的访问权限是`public`的,返回值暂时使用`String`,方法的名称可以自由定义,方法的参数暂时留空:

	public String showHello() {
	}

在SpringMVC中,请求对应的可以是某个方法,而不一定每个请求对应一个控制器类,如果请求对应的是以上方法,则需要在方法的声明之前添加`@RequestMapping("/hello.do")`:

	@RequestMapping("/hello.do")
	public String showHello() {
	}

完成以上步骤后,即表示:路径为`/hello.do`的请求将触发`showHello()`方法被执行!

**测试**

在以上方法中输出任意语句,然后,启动项目,请求对应的路径,观察控制台输出:

	@RequestMapping("/hello.do")
	public String showHello() {
		System.out.println(
			"HelloController.showHello()");
		return null;
	}

实际运行效果是:页面会显示错误:

![](springmvc-helloworld-4.png)

控制台可以看到输出语句:

![](springmvc-helloworld-5.png)

#### 1.3.5. 显示页面

至此,执行流程已经完成前4步,接下来,控制器应该向`DispatcherServlet`返回结果:

	@RequestMapping("/hello.do")
	public String showHello() {
		// 测试
		System.out.println(
			"HelloController.showHello()");
		// 返回视图组件的名称
		return "helloworld";
	}

以上返回的字符串表示最终负责显示的视图组件的名称,但是,并不表示是哪个文件!

接下来,需要配置`ViewResolver`,使得`DispatcherServlet`通过它可以知道刚才控制器返回的`"helloworld"`到底是哪个页面!

实际使用的`ViewResolver`是`InternalResourceViewResolver`,需要在`spring.xml`进行配置:

	<!-- 配置视图解析器ViewResolver -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	</bean>

`InternalResourceViewResolver`的工作方式是以项目的根路径为参考(在开发时,对应的是`webapp`文件夹),使用**前缀+视图组件名+后缀**得到视图文件的位置,
例如实际存在的jsp文件是`webapp/WEB-INF/helloworld.jsp`,
由于它本身使用`webapp`作为根级,所以,前缀是`/WEB-INF/`,后缀是`.jsp`,结合控制器返回的`"helloworld"`进行拼接,就可以得到以上路径,
从而确定视图组件的位置,例如配置为:

	<!-- 配置视图解析器ViewResolver -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<!-- 配置前缀 -->
		<property name="prefix" value="/WEB-INF/" />
		<!-- 配置后缀 -->
		<property name="suffix" value=".jsp" />
		<!-- 经过以上配置后, -->
		<!-- 当前项目中的jsp文件都应该放在/WEB-INF/-->
	</bean>

基于视图解析器的前缀是`/WEB-INF/`,后缀是`.jsp`,结合控制器返回的`"helloworld"`字符串,则需要在`WEB-INF`文件夹下创建名为`helloworld.jsp`的文件完成最终的显示:

![](springmvc-helloworld-6.png)

通过浏览器再次访问,实际效果应该是:

![](springmvc-helloworld-7.png)

#### 1.3.6. 小结

1. 本案例中关于`web.xml`中的`DispatcherServlet`的配置,和`spring.xml`中的`InternalResourceViewResolver`的配置,是相对固定的配置,在后续的项目开发时,可以直接复制使用,但是,注意理解配置的值的意义;

2. 处理请求的方法的声明语句应该例如:`public String xx()`

3. 处理请求的方法的返回值可以理解为JSP文件的文件名;

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

# 作业

#### 1. 把笔记中HelloWorld的开发流程读一遍

#### 2. 参考springmvc.png流程图,口述SpringMVC的工作流程

#### 3. 创建新项目 SPRINGMVC-02-USER,要求对 /reg.do/login.do 能作出响应,响应方式是显示 reg.jsp 和 login.jsp 页面,并且,
注册页(reg.jsp)要求有用户名、密码、年龄、手机、邮箱这5个输入项和1个提交按钮,而登录页(login.jsp)要求有用户名、密码这2个输入项和1个提交按钮,
不要求页面之间可以跳转。注意:代码请排版!


—————————————————————————————————### 【回顾】

#### 3. 创建新项目 SPRINGMVC-02-USER,要求对 /reg.do/login.do 能作出响应,响应方式是显示 reg.jsp 和 login.jsp 页面,并且,
注册页(reg.jsp)要求有用户名、密码、年龄、手机、邮箱这5个输入项和1个提交按钮,而登录页(login.jsp)要求有用户名、
密码这2个输入项和1个提交按钮,不要求页面之间可以跳转。注意:代码请排版!

## 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="handle_reg.do",否则,直接将"handle_reg.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`表示参数的默认值,当有以上任何一种需求时,
都需要配置该注解,即:请求参数名称与处理请求的方法的参数名称不一致;强制必须提交某个参数;为某个参数配置默认值。

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

## 作业

在今天的案例基础之上,实现:

- 如果注册时,提交的用户名是`root`,将视为用户名已经被占用,提示错误页面;

- 如果注册成功,跳转到登录页面;

- 如果登录时,提交的用户名是`root`,且密码是`1234`,则跳转到主页(请求路径为`/main/index.do`,显示的页面是`index.jsp`),如果登录错误,根据错误类型在错误页面提示“用户名不存在”或“密码错误”


—————————————————————————————————

## 【回顾】作业

在今天的案例基础之上,实现:

- 如果注册时,提交的用户名是`root`,将视为用户名已经被占用,提示错误页面;

- 如果注册成功,跳转到登录页面;

- 如果登录时,提交的用户名是`root`,且密码是`1234`,则跳转到主页(请求路径为`/main/index.do`,显示的页面是`index.jsp`),如果登录错误,根据错误类型在错误页面提示“用户名不存在”或“密码错误”

## 练习

- 设计`/user/info.do`请求,响应方式是转发到`user_info.jsp`页面,页面中使用文字标识是该页面即可

- 设计`/user/password.do`请求,响应方式是转发到`user_password.jsp`页面,页面中使用文字标识是该页面即可

- 在主页(`/main/index.do`)中添加超链接,点击后分别进入以上2个页面。

- 以上2个页面,必须登录以后,才允许访问,如果没有登录时尝试访问这2个页面,将直接重定向到登录页。

**分析**

如何判断用户已经登录?可以在用户登录成功的情况下,通过Session记录用户的相关信息,然后,当需要判断是否登录时,直接判断Session中是否存在对应的信息即可!

**Session中存储哪些数据**

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

## 1. SpringMVC中的拦截器(Interceptor)

### 1.1. 作用

拦截器是运行在`DispatcherServlet`之后,在每个`Controller`之前的,且运行结果可以选择放行或拦截!

除此以外,拦截器还会运行在`Controller`之后,关于拦截器,在处理某一个请求时,最多有3次执行!只不过,通常关注最多的是第1次执行,即在`Controller`之前的那次!

### 1.2. 基本使用

需要自定义类,例如名为`LoginInterceptor`,实现`HandlerInterceptor`接口,重写3个抽象方法:

	public class LoginInterceptor implements HandlerInterceptor {

		public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
				throws Exception {
			System.out.println("LoginInterceptor.preHandle()");
			return false;
		}
	
		public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
				ModelAndView modelAndView) throws Exception {
			System.out.println("LoginInterceptor.postHandle()");
		}
	
		public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
				throws Exception {
			System.out.println("LoginInterceptor.afterCompletion()");
		}
		
	}

拦截器需要在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`之前的是`preHandle()`方法,该方法返回`true`表示放行,返回`false`表示拦截!

对于登录拦截器而言,可以判断Session中的用户数据,如果数据存在,视为已登录,可直接放行,如果没有数据,则视为没登录,需要重定向,并拦截:

	public boolean preHandle(
			HttpServletRequest request, 
			HttpServletResponse response, Object handler)
			throws Exception {
		System.out.println("LoginInterceptor.preHandle()");
		// 判断Session中的数据,得知是否登录
		HttpSession session = request.getSession();
		if (session.getAttribute("username") == null) {
			// Session中没有用户名,即:没有登录,则:重定向,并拦截
			response.sendRedirect("../user/login.do");
			// 返回false表示拦截
			return false;
		}
		// 返回true表示放行
		return true;
	}

在配置拦截器时,根节点是`<mvc:interceptors>`,用于配置拦截器链,即任何一个SpringMVC项目,都可以有若干个拦截器,从而形成拦截器链,
如果某个请求符合多个拦截器的拦截配置,则会依次被各拦截器进行处理,任何一个拦截,都会导致后续不再将请求交给`Controller`去处理!

在`<mvc:interceptors>`(有s)节点子级,可以配置多个`<mvc:interceptor>`(没有s)子级节点,表示多个拦截器,拦截器链的执行顺序取决于这里各节点的配置先后顺序!

在`<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. 乱码

### 2.1. 关于乱码

在处理中文或非ASCII字符(需要使用输入法才可以输入的)时,如果存、取时,使用的字符编码不统一,就会出现乱码!

所以,出现乱码原因就是因为字符编码不统一,而解决问题的方案就是**使用统一的编码**!

需要统一编码的位置有:项目源码、数据库、处理数据的服务端组件、数据传输过程、#显示界面

### 2.2. 解决控制器中接收请求参数的乱码

通常,在Java EE项目中,解决问题的方式是:

	request.setCharacterEncoding("utf-8");

由于`Controller`是运行在`DispatcherServlet`之后的,在`Controller`内部再执行更改编码格式已经晚了,
事实上SpringMVC框架在`DispatcherServlet`之前就存在`CharacterEncodingFilter`可以确定请求与响应的编码格式,所以,在SpringMVC中,
无法通过`Controller`或`Interceptor`来解决请求和响应的乱码问题。

在SpringMVC框架的`CharacterEncodingFilter`中,把使用的字符编码设计为变量,可以在`web.xml`中添加配置加以应用,来统一设置编码:

	
	
		CharacterEncodingFilter
		org.springframework.web.filter.CharacterEncodingFilter
		
			encoding
			utf-8
		
	
	
	
		CharacterEncodingFilter
		/*
	

### 2.3. 拦截器与过滤器有什么区别

拦截器是Interceptor,过滤器是Filter;

拦截器是SpringMVC中的组件,过滤器是Java EE中的组件;

拦截器是配置在Spring的配置文件中的,过滤器是配置在web.xml中的;

拦截器是运行在`DispatcherServlet`之后、`Controller`之前的,且在`Controller`执行完后还会调用2个方法,而过滤器是运行在所有的`Servlet`之前的;

拦截器的配置非常灵活,可以配置多项黑名单,也可以配置多项白名单,过滤器的配置非常单一,只能配置1项过滤路径;

拦截器与过滤器也有很多相似之处,例如:都可拒绝掉某些访问,也可以选择放行;都可以形成链。

相比之下,在一个使用SpringMVC框架的项目中,拦截器会比过滤器要好用一些,但是,由于执行时间节点的原因,它并不能完全取代过滤器!

## 3. 异常

### 3.1. 基本概念

在Java中,异常的体系结构是:

	Throwable
		Error
			OutOfMemoryError
		Exception
			IOException
				FileNotFoundException
			SQLException
			UnsupportedEncodingException
			RuntimeException
				NullPointerException
				ClassCastException
				ArithmeticException
				IllegalArgumentException
				IndexOutOfBoundsException
					ArrayIndexOutOfBoundsException

在这些异常中,`RuntimeException`及其子孙类异常,在Java语法中并不要求必须处理!主要的原因有:这些异常出现的频率可能非常高,
如果一定要处理,例如`try...catch`,则几乎所有的代码都需要放在`try`代码块中!并且,这些异常是可以杜绝的异常,通过严谨的编程,可以保证这些异常绝对不会出现!

处理异常有2种方式:使用`try...catch`处理异常,或者使用`throw`抛出异常对象,并且在方法的声明中使用`throws`语法声明抛出!

通常,异常都是必须处理的,如果没有处理异常,会导致异常不断向上抛出,最终,Java EE中的异常会由Tomcat来处理,会把跟踪日志显示在页面中!
而这些跟踪日志是普通用户看不懂的,对于专业人士而言,还可能分析出项目中的一些内容,对于后续解决问题也没有任何帮助,所以,从开发原则上来说,必须处理异常!

### 3.2 处理异常-SimpleMappingExceptionResolver

非`RuntimeException`是从语法上强制要求处理的,所以,每次调用了抛出异常的方法,就必须`try...catch`或继续声明抛出!

而`RuntimeException`及其子孙类异常并不从语法上强制要求处理,但是,一旦出现,会项目的安全、用户体验等各方面都会产生负面影响!

SpringMVC提供了统一处理异常的方式:使用`SimpleMappingExceptionResolver`类,该类通过`private Properties exceptionMappings;
`属性来配置异常种类与转发到的页面的对应关系,即每一种异常都可以有一个与之对应的页面,一旦项目中出现配置过的异常,就会自动转发到对应的页面来提示错误信息!

	
	
		
			
				runtime
				null
				index
			
		
	

这种方式处理异常非常的简单,但是,也有一个比较大的问题:无法提示更详细的信息!例如出现`ArrayIndexOutOfBoundsException`时,其实,异常对象中还封装了详细的错误信息,即:越界值是多少。

—————————————————————————————————

## 【续】3. 异常

### 3.3 处理异常-@ExceptionHandler

可以在控制器类中自定义一个处理异常的方法,在方法之前添加`@ExceptionHandler`,然后,凡是约定范围内的异常,都可以由该方法来决定如何处理:

	@ExceptionHandler
	public String handleException(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 handleException(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";
	}

思考:是否可以在类中添加3个处理异常的方法,分别只处理`NullPointerException`和`ArrayIndexOutOfBoundsException`和`RuntimeException`?

**答案:可以!允许使用多个不同的方法来处理异常!**

思考:假设处理异常的方法在A控制器中,而B控制器中处理请求时出现异常,会被处理吗?

**答案:不可以!通常,可以创建一个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`。


## 1. MyBatis

### 1.1. 作用

MyBatis是持久层框架,它是支持JDBC的!简化了持久层开发!

使用MyBatis时,只需要通过接口指定数据操作的抽象方法,然后配置与之关联的SQL语句,即可完成!

> 持久化存储:在程序运行过程中,数据都是在内存(RAM,即内存条)中的,内存中的数据不是永久存储的,例如程序可以对这些数据进行销毁,
> 或者由于断电也会导致内存中所有数据丢失!而把数据存储到硬盘中的某个文件中,会使得这些数据永久的存储下来,常见做法是存储到数据库中,
> 当然,也可以使用其它技术把数据存储到文本文件、XML文件等其它文件中!

### 1.2. 基本使用

#### 1.2.1. 创建项目

使用此前相同的创建流程即可!注意:请检查有没有多余的配置,如果有,请删除,例如在`spring.xml`是否存在拦截器的配置!

此次使用MyBatis框架,所以,需要添加新的依赖:

	
		org.mybatis
		mybatis
		3.4.6
	

> 如果下载的依赖jar包是损坏的,应该先关闭Eclipse,然后删除对应的jar包文件,再次启动Eclipse,对项目点击右键,选择Maven > Update Project,并且在弹出的对话框中勾选`Force Update ...`选项即可。

MyBatis是一个独立的框架,即只添加该依赖就可以实现持久层编程,但是,开发过程相对比较繁琐,而实际应用中,通常会与Spring、SpringMVC一起使用,整合使用时,可以简化大量的配置,使得开发更加简便!整合时,还需要添加相关依赖:

	
		org.mybatis
		mybatis-spring
		1.3.2
	

整合的SSM框架是基于JDBC的,所以,还需要添加spring-jdbc的依赖:

	
		org.springframework
		spring-jdbc
		3.2.8.RELEASE
	

**添加以上依赖时,直接将此前的spring-webmvc的依赖代码复制一次,将spring-webmvc改成spring-jdbc即可!凡是Spring官方(Group ID是org.springframework)推出的以spring-作为前缀的依赖,必须使用相同的版本,否则,可能存在不兼容的风险!**

在实现过程中,肯定得先建立与数据库的连接,然后再继续编程,所以,还应该添加数据源管理的依赖,即数据库连接池的依赖:

	
		commons-dbcp
		commons-dbcp
		1.4
	

由于本次将使用MySQL数据库,所以,还需要该数据库的连接驱动的依赖:

	
		mysql
		mysql-connector-java
		5.1.8
	

#### 1.2.2. 创建数据库与数据表

创建数据库`tedu_mybatis`:

	CREATE DATABASE tedu_mybatis;

创建数据表`t_user`:

	CREATE TABLE t_user(
		id INT AUTO_INCREMENT,
		username VARCHAR(20) UNIQUE NOT NULL,
		password VARCHAR(20) NOT NULL,
		age INT,
		phone VARCHAR(20)	,
		email VARCHAR(30),
		PRIMARY KEY(id)
	) DEFAULT CHARSET=UTF8;

#### 1.2.3. 配置数据源

在`src\main\resources`下创建`db.properties`文件,用于配置与数据库连接相关的信息:

	url=jdbc:mysql://localhost:3306/tedu_mybatis?useUnicode=true&characterEncoding=utf8
	driver=com.mysql.jdbc.Driver
	username=root
	password=root
	initialSize=2
	maxActive=10

**以上配置的值以自己使用的数据库为准!**

以上配置需要被应用到项目中,则在Spring的配置中通过``可以读取该文件:

	
	

最后,需要把这些配置应用到数据源(数据库连接池)中,当前项目使用的是Apache的`commons-dbcp`,则对应的数据源是`BasicDataSource`类:

	
	
		
		
		
		
		
		
	

完成以上配置后,可以测试到目前为止的配置是否正确,做法就是:获取`BasicDataSource`的对象,调用它的`getConnection()`方法,尝试在Java程序中获取与数据库的连接,如果能够正常连接,则配置无误,如配置有误,将无法获取连接:

	@Test
	public void getConnection() throws SQLException {
		AbstractApplicationContext ac
			= new ClassPathXmlApplicationContext(
				"spring.xml");
		
		BasicDataSource ds
			= ac.getBean("dataSource", BasicDataSource.class);
		
		Connection conn = ds.getConnection();
		System.out.println(conn);
		
		ac.close();
	}

#### 1.2.4. 通过MyBatis插入数据

MyBatis的编码模式是:

1. 创建接口,并声明数据访问的抽象方法;
2. 配置与抽象方法对应的XML映射。

首先,创建`cn.tedu.mybatis.entity.User`实体类,并添加与`t_user`数据表匹配的属性。

> 通常,每张数据表都有一个与之匹配的实体类!

创建`cn.tedu.mybatis.mapper.UserMapper`接口,并在接口中声明抽象方法:

	Integer insert(User user);

**在MyBatis中,执行insert/update/delete操作时,均返回受影响的行数,所以,设计抽象方法时,如果对应的是这几种操作,返回值均设计为Integer类型。**

通常,一个完整的项目中,会存在许多MyBatis的映射文件,为了便于管理,会在`src\main\resources`下创建一个名为`mappers`的文件夹,然后,下载共享的`SomeMapper.zip`,将解压得到的XML文件复制到刚才创建的`mappers`文件夹中:

![](01.png)

其实,在`mappers`下的映射文件的名称并不重要!但是,为了便于管理,通常会使用与接口对应的名称,所以,将`SomeMapper.xml`重命名为`UserMapper.xml`。

所有映射文件中,根节点都是``节点,且该节点必须配置名为`namespace`的属性,属性值是对应的Java接口,例如:

	
	

经过以上配置,指定了XML映射文件与接口文件的对应关系。

然后,在该文件内部,使用各级子节点配置与抽象方法的对应关系,子节点名称的选取,取决于要执行的操作的类型,例如要执行的数据操作是insert类型,则使用``节点,这些节点都必须指定id属性,属性值是与之对应的抽象方法的方法名:

	
	
	

在``节点中,添加`parameterType`属性,用于指定参数的类型,即抽象方法中的参数类型:

	
	
	
	

![](03.png)

然后,在节点内部,编写需要执行的SQL语句:

	
	
	
		INSERT INTO t_user (
			username, password, 
			age, phone, email
		) VALUES (
			#{username}, #{password}, 
			#{age}, #{phone}, #{email}
		)
	

执行SQL语句时的参数值均使用`#{}`类似的语法,其中的名称是`User`类中的属性名:

![](02.png)

#### 1.2.5. 最后的配置

首先,需要配置`SqlSessionFactoryBean`,通过它指定数据源与XML映射的文件位置:

	
	
		
		
		
		
	

以上配置中,XML映射文件的位置使用了`mappers/*.xml`,即:在`mappers`文件夹下的所有XML文件都应该是MyBatis的映射文件,所以,后续使用时,不可以在这个文件夹中存放其它XML文件。

然后,还需要配置`MapperScannerConfigurer`,用于指定接口文件在哪里:

	
	
		
		
	

![](04.png)

至此,配置完成!

#### 1.2.6. 执行单元测试

在`src\test\java`下创建测试类,并添加测试方法,以执行测试:

	@Test
	public void insert() {
		// 加载Spring配置文件,获取Spring容器
		AbstractApplicationContext ac
			= new ClassPathXmlApplicationContext(
				"spring.xml");
		
		// 从Spring容器中获取对象
		// bean-id与接口名一致,首字母为小写
		UserMapper userMapper
			= ac.getBean("userMapper", UserMapper.class);
		
		// 测试功能
		User user = new User();
		user.setUsername("root");
		user.setPassword("1234");
		Integer rows = userMapper.insert(user);
		System.out.println("rows=" + rows);
		
		// 释放资源
		ac.close();
	}

#### 1.2.7. 小结

**关于配置**

使用MyBatis添加了一些新的依赖,及一些配置,这些属于固定的一次性操作,在后续的项目中,每个项目中只需要做1次即可。

关于这些配置,需要记住配置的作用,及可能需要修改的位置,至少包括:

1. db.properties的文件名,因为``需要使用它;
2. 在db.properties中配置的值,重点是数据库名称、访问数据库的密码,在更换项目或更换计算机后都可能需要调整;
3. 在配置`SqlSessionFactoryBean`时指定的映射文件的位置;
4. 在配置`MapperScannerConfigurer`时指定的接口文件所在的包;

**关于开发**

1. 每张数据表,都应该有1个与之对应的实体类;
2. 每种数据(视为每个实体类)的处理都应该有对应的接口文件,例如项目中有`User`实体类,则应该有`UserMapper`接口;
3. 在接口中声明抽象方法时,如果最终将执行insert/delete/update操作,返回值类型应该是`Integer`;
4. 每个持久层接口,都应该有对应的XML映射文件,例如有`UserMapper.java`接口,就应该有`UserMapper.xml`文件;
5. 在配置XML文件内部,根据执行操作选择节点,如果执行的是insert操作,则通过``节点进行配置;
6. 在XML映射中,每个节点都必须配置`id`属性,取值是接口中抽象方法的名称,由于id具有唯一的特性,所以,在接口中声明抽象方法时,不要使用重载;
7. 在配置SQL语句时,使用`#{}`表示预编译时的`?`对应的值,括号中的名称是参数名称,或参数对象中的属性名称;
8. 当执行delete/update时,配置的节点不需要指定`parameterType`属性。

#### 1.2.7. 删除指定的数据

设置目标为**根据id删除指定的数据**。

先在接口中声明抽象方法:

	Integer deleteUserById(Integer id);

然后,在XML映射中配置以上方法对应的节点:

	
		DELETE FROM t_user WHERE id=#{id}
	

然后,执行单元测试:

	@Test
	public void deleteUserById() {
		// 加载Spring配置文件,获取Spring容器
		AbstractApplicationContext ac
			= new ClassPathXmlApplicationContext(
				"spring.xml");
		
		// 从Spring容器中获取对象
		// bean-id与接口名一致,首字母为小写
		UserMapper userMapper
			= ac.getBean("userMapper", UserMapper.class);
		
		// 测试功能
		Integer id = 3;
		Integer rows = userMapper.deleteUserById(id);
		System.out.println("rows=" + rows);
		
		// 释放资源
		ac.close();
	}

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

## 作业

1. 完成**根据用户名删除数据**的功能;
2. 完成**将所有用户的密码设置为某个值**的功能;
3. 完成**删除用户表中所有数据**的功能。

> 在MyBatis中,默认不允许添加2个参数,否则会报错,解决方案待续。
	

		
 SELECT c.id class_id , c.name class_name,s.id sturnt_id,s.name student_name FROM t_class c INNER JOIN t_student s ON c.id=s.class_id where c.name="JSD1807";

public classs classs{
	
} 
public class Student{
	private Integer id;
	private String name;
	private x
}

—————————————————————————————————

## 【回顾】作业

1. 完成**根据用户名删除数据**的功能;
2. 完成**将所有用户的密码设置为某个值**的功能;
3. 完成**删除用户表中所有数据**的功能。

> 在MyBatis中,默认不允许添加2个参数,否则会报错,解决方案待续。

## 1. 单元测试

在单元测试中,每个测试方法都需要执行相同的前置代码和后置代码,则可以自定义2个方法,分别在这2个方法中执行前置代码和后置代码,
并为这2个方法添加`@Before`和`@After`注解,然后,在每个测试方法中,就不必再编写这些代码,最终,在执行测试方法之前,
会自动调用添加了`@Before`注解的方法,在执行测试方法之后,会自动调用添加了`@After`注解的方法:

	private AbstractApplicationContext ac;
	private UserMapper userMapper;
	
	@Before
	public void doBefore() {
		// 加载Spring配置文件,获取Spring容器
		ac = new ClassPathXmlApplicationContext(
				"spring.xml");
				
		// 从Spring容器中获取对象
		// bean-id与接口名一致,首字母为小写
		userMapper
			= ac.getBean("userMapper", UserMapper.class);
	}
	
	@After
	public void doAfter() {
		// 释放资源
		ac.close();
	}

	@Test
	public void deleteUserById() {
		// 测试功能
		Integer id = 3;
		Integer rows = userMapper.deleteUserById(id);
		System.out.println("rows=" + rows);
	}

## 2. 在MyBatis中查询数据

使用MyBatis执行查询操作时,抽象方法的设计中,返回值应该根据查询需求来决定,例如查询用户列表时,可以使用`List`作为返回值类型,查询某个用户时,可以使用`User`作为返回值类型,查询的是计数等操作时,可以使用`Integer`作为返回值类型。

**其实,无论执行什么样的查询,MyBatis的查询结果都是List集合,只不过,如果抽象方法声明的不是集合,而是具体的某个类型,例如User时,MyBatis会尝试从集合中获取第1个元素作为返回值!**

**查询是可能失败,即没有匹配的数据,在这种情况下,如果返回值是List集合,则返回的是空集合(集合对象是存在的,但是集合中没有任何元素),如果返回值是某个类型,则返回null。**

在配置的XML映射中,每个查询对应的都是`
		SELECT
			id, username, password, age, phone, email
		FROM t_user
	

**目标:查询指定id的用户的数据**

在接口中添加抽象方法:

	User findUserById(Integer id);

配置以上方法的映射:

	

**练习:根据用户名查询用户数据。**

**目标:获取当前用户表中用户的数量**

在接口中添加抽象方法:

	Integer getCount();

配置以上方法的映射:

	 

## 2. 涉及多参数的数据访问操作

在许多操作中,也许1个参数并不能满足需求,可能需要2个或更多个参数,例如**修改某个用户的密码**,设计的抽象方法可能是:

	Integer changePassword(Integer id, String password);

然后,配置的映射应该是:

	
		UPDATE t_user
		SET password=#{password}
		WHERE id=#{id}
	

在MyBatis中,其实只支持1个参数,并且,Java源文件编译为class字节码文件后,也会丢失参数名称,所以,需要为每一个参数添加`@Param`注解:

	Integer changePassword(
		@Param("id") Integer arg0, 
		@Param("password") String arg1);
	
则MyBatis会把这些参数全部封装在一个`Map`中,注解中的名称就是每个参数的Key,后续,在配置映射时,在`#{}`中填入的也就是注解的名称,即各参数在Map中的Key:

![](01.png)

**在设计MyBatis中的抽象方法时,如果参数超过1个,则每个参数之前都必须添加@Param注解,在映射的SQL语句中,#{}中填入也是注解中使用的名称!**

## 3. 插入数据时获取数据自增长的id

在配置映射时,为``节点添加2个属性:`useGeneratedKeys="true"`表示需要获取自动生成的键,通常就是数据表中的主键字段,即id字段,
然后配置`keyProperty="id"`,表示获取到的键的值(即自增长的id值)将要封装到哪个属性中(即实体类中的属性名):

![](02.png)

经过以上配置后,当成功插入数据时,就会获取到该数据的自增长的id值,并且,会将值封装到参数对象中,即:调用`Integer insert(User user)`方法时,假设使用`user1`作为调用时的参数,当方法执行结果后,参数`user1`中就已经包含了id值!

	@Test
	public void insert() {
		// 测试功能
		User user = new User();
		user.setUsername("java");
		user.setPassword("1234");
		
		System.out.println("执行前:" + user);
		Integer rows = userMapper.insert(user);
		System.out.println("rows=" + rows);
		System.out.println("执行后:" + user);
	}

执行结果例如:

![](03.png)

## 4. 关联表查询操作

**问题描述**

存在**班级**和**学生**数据,要求显示某个班级信息时,同时显示出该班级的所有学生。

**准备工作**

创建班级(1)表:

	CREATE TABLE t_class (
		id INT AUTO_INCREMENT,
		name VARCHAR(20),
		PRIMARY KEY(id)
	) DEFAULT CHARSET=UTF8;

创建学生(N)表:

	CREATE TABLE t_student (
		id INT AUTO_INCREMENT,
		name VARCHAR(20),
		class_id INT,
		PRIMARY KEY(id)
	) DEFAULT CHARSET=UTF8;

**当数据表之间存在1对多关系时,在“多”的表中需要存储“1”的表的唯一标识,即存储“1”的表的id。**

为了保证后续的数据查询,还应该添加一部分的测试数据:

	INSERT INTO t_class (name) VALUES 
	("JSD1806"), ("JSD1807"), ("JSD1808");

	INSERT INTO t_student (name, class_id) VALUES
	("Jack", 1),	("Rose", 1),
	("LiLei", 2),	("HanMM", 2),
	("Lucy", 3), 	("LiLi", 3),
	("Bob", 1),	("LiMing", 1),
	("Kitty", 2),	("Tom", 1);

基于以上数据表,如果要**获取班级信息的同时,还获取该班级的所有学生的列表**,则执行的SQL查询应该是:

	SELECT
		c.id AS class_id,
		c.name AS class_name,
		s.id AS student_id,
		s.name AS student_name
	FROM t_class AS c
	INNER JOIN t_student AS s
	ON c.id=s.class_id
	WHERE c.name="JSD1806";

![](04.png)

基于**每张数据表都应该有1个与之对应的实体类**的原则,则在项目中应该有:

	public class Clazz {
		private Integer id;
		private String name;
	}

	public class Student {
		private Integer id;
		private String name;
		private Integer classId;
	}

即使有了以上2个类,却都无法满足查询结果的需求,即尝试在接口中声明抽象方法时:

	??? getClassInfo(String className);

无法确定返回值类型!

针对这种情况,通常会在项目中创建**VO类**,即Value Object类!例如:

	public class ClazzVO {
		private Integer classId;
		private Integer className;
		private List students;
	}

> 通常,实体类与数据表是对应的,而VO类是与实际使用需求对应的!

当设计好了VO类,则查询的抽象方法可以是:

	ClazzVO getClassInfo(String className);

可以发现,即使使用了VO类,查询的字段与`ClazzVO`类中的属性名称等都无法直接对应,则,在映射文件中,需要使用到``!

![](05.png)

最后的执行结果为:

	ClazzVO [
		classId=1, 
		className=JSD1806, 
		students=[
			Student [id=1, name=Jack, classId=null], 
			Student [id=2, name=Rose, classId=null], 
			Student [id=7, name=LiMing, classId=null], 
			Student [id=8, name=Bob, classId=null], 
			Student [id=10, name=Tom, classId=null]
		]
	]

**小结**

如果某次查询涉及多张表,存在关联查询,通常是没有匹配的实体类可以直接使用的,在这种情况下,就需要自定义VO类。

VO类与实体类的代码表现基本相似,只是定位不同,实体类是与数据表对应的,而VO类是为了满足编码需求,更方便的获取查询结果而存在的!

VO类的属性的设计原则完全取决于所需要执行的查询的结果。

产生了关联查询后,可以直接用VO类作为`resultType`,但是,如果查询结果中存在数据之间的1对多等关系,则需要配置``。

## 5. 动态SQL

在MyBatis中配置映射时,允许使用例如``此类的标签,使得每次执行的SQL语句可以产生动态调整,则称之为动态SQL。

**目标:实现根据id修改用户信息,可修改的字段有:密码、年龄、手机号码、电子邮件,如果执行的参数中,某项数据为null,则不修改原有值,例如修改时,参数中没有年龄值,则不修改原有的年龄,其它字段也是相同的处理方式。**

首先,在接口中声明抽象方法:

	Integer changeInfo(
		@Param("id") Integer id,
		@Param("password") String password,
		@Param("age") Integer age,
		@Param("phone") String phone,
		@Param("email") String email);

然后,配置以上方法的映射:

	UPDATE t_user
	SET
		password=#{password},
		age=#{age},
		phone=#{phone},
		email=#{email}
	WHERE id=#{id}

以上配置的执行效果会是:如果没有提供某个值,将会把对应的字段的值设置为null,而并非不修改原有值。

此类问题可以通过动态SQL的``标签来解决:

	UPDATE t_user
	SET
		password=#{password},
			
		
		age=#{age},
		
			
		phone=#{phone},

		
		email=#{email}
		
	WHERE id=#{id}

执行以上代码时,如果提供了有效的age值(非null),则SQL语句为:

	UPDATE t_user SET password=?, age=?, phone=?, email=? WHERE id=?

但是,如果没有提供age值,则SQL语句为:

	UPDATE t_user SET password=?, phone=?, email=? WHERE id=?

**注意:在编写动态SQL时,参数直接写名字即可,例如test="age != null"中的age就是参数的名称,不需要使用#{}这类的语法!**

**目标:一次删除多条数据,这些数据的id是作为参数体现的,但是,是没有规律的。**

在接口中声明抽象方法:

	Integer deleteUserByIds(List ids);

以上方法的设计,参数可以是`List`,也可以是`Integer[]`。

然后,配置映射:

	
	
	
	
	
	
		DELETE FROM t_user 
		WHERE id IN 
		
			#{id}
		
	

**注意:在中的collection属性,当抽象方法只有1个参数时,取值为list或array,根据参数类型决定,当抽象方法的参数超过1个时,该属性的值为参数的名称(参数的注解中使用的名称)!**

关于动态SQL,主要掌握``和``的使用!

## 【附】关于配置MyBatis映射没有代码提示的解决方案**

先连接达内公司内网,下载:http://schema.tedu.cn/proxy/dtd/ibatis-3-mapper.dtd

下载的文件存储到任意位置均可。

在Eclipse中打开设置,左侧选择XML > XML Catelog,并在右侧点击Add按钮:

![](06.png)

然后,在左侧选择第1项,右侧选择到刚才下载的文件,然后在Key这一栏输入`-//ibatis.apache.org//DTD Mapper 3.0//EN`(在映射文件顶部`Public`字样右侧的字符串):

![](07.png)

—————————————————————————————————

## 1. AJAX

### 1.1. 响应数据而不再响应页面

传统的WEB响应时处理方式有转发或重定向,无论是哪一种,最终,客户端都将得到一个新的页面!这样的处理方式存在比较明显的问题:

- 客户端要得到响应结果(页面),产生的流量消耗较大;
- 访问速度较慢;
- 不便于扩展,例如可能不适用于其它客户端,例如手机端;
- ……

理想的解决方案是:服务端只负责处理响应,并且,结果是成功与否,或客户端所需要的数据,而并不响应整个页面!

### 1.2. 关于@ResponseBody

使用`@ResponseBody`添加在处理请求的方法之前,如果返回`String`类型,则表示返回值将成为响应正文,而不是再是转发或重定向。

在实际使用时,默认情况下,并不支持中文!

### 1.3. JSON

使用XML组织数据存在的问题:解析过程较复杂,且数据量较大,例如:

	
		
			1
			Jack
		
		
			2
			Rose
		
	

JSON也是一种组织数据的格式,它更加轻量级, 例如:

	{ "id":1, "user": "Jack" }

它的语法特征是:

- 整个数据是使用大括号框住的;
- 各组属性之间使用逗号分隔;
- 使用冒号分隔属性名称与属性值;
- 属性名称使用双引号框住;
- 如果属性值是字符串,也需要使用双引号框住;

基于以上规则,如果需要添加一个`age`属性,则应该是:

	{ "id":1, "name": "Jack", "age": 23 }

可以发现,这种组织数据的格式,相比XML更加简洁:

	
		1
		Jack
		23
	

并且,JSON格式是Javascript默认识别的一种数据类型:

	

在JSON数据中,每个属性的值,也可以是另一个对象:

	{ 
		"id":1, 
		"name": "Jack", 
		"age": 23, 
		"from": { 
			"name": "BeiJing", 
			"zip": "110000" 
		}
	}

在数据读取方面,直接通过`.`调用对象中的属性即可:

	

在JSON中,还支持数组,用于组织批量数据:

	{ 
		"id":1, 
		"name": "Jack", 
		"age": 23, 
		"from": { 
			"name": "BeiJing", 
			"zip": "110000" 
		},
		"skill": [
			{"id": 101, "name": "Java"}, 
			{"id": 102, "name": "Spring"}, 
			{"id": 103, "name": "SpringMVC"}
		]
	}

数组的元素可以是基本值,也可以是另一个对象!与Java中的数组一样,各元素之间使用逗号进行分隔即可!

在使用JSON数组时,在Javascript中也可以使用循环语法:

	for (var i = 0; i < json.skill.length; i++) {
		console.log("id=" + json.skill[i].id);
		console.log("name=" + json.skill[i].name);
		console.log("----------------");
	}

在实际应用中,可能无法直接得到JSON对象,而是JSON格式的字符串,则可以通过`JSON.parse(str)`将字符串转换为JSON对象:

	var str = '{"id":1, "name": "jack"}';
	var json = JSON.parse(str);
	alert(json.name);

**小结**

- JSON是一种比XML更加轻量级的组织数据的方式,数据量更小,且解析更加方便;
- Javascript默认识别JSON类型的数据;
- 在JSON数据中,使用`{}`框住整个对象,所有属性之间使用逗号分隔,属性名称均使用双引号框住,属性值可以是基本型数据(数值型、字符串、布尔值),也可以是另一个对象(使用`{}`),或数组(使用`[]`);
- 使用`JSON.parse(str)`可以把字符串`str`转换为JSON对象。

### 1.4. 在服务端如何高效的返回JSON

在服务端,添加新的依赖Jackson:

	
		com.fasterxml.jackson.core
		jackson-databind
		2.9.7
	

添加成功后,会多依赖3个jar包:

![](01.png)

当添加了Jackson依赖后,如果控制器(Controller)中处理请求的方法返回的数据格式不是SpringMVC默认识别的格式(例如不是`String`等),
SpringMVC会自动应用Jackson框架,把返回的对象(例如自定义的`ResponseResult`)以JSON格式来组织,返回给客户端!

除此以外,Jackson框架还会在响应头(Response Headers)中配置Content-Type为`application/json, charset=utf-8`,即声明响应的内容是JSON数据,且字符编码是UTF-8,解决了原有默认编码是ISO-8859-1导致的乱码问题。

由于Jackson框架还是设置响应头,默认是不支持的,所以,在Spring的配置文件中还需要加添加注解驱动的配置:

	

### 1.5. 异步请求

在传统开发模式中,浏览器向服务器提交请求的方式有:改变浏览器地址栏中的URL、FORM表单,这2种方式发出请求后,由浏览器直接处理服务器的响应,而浏览器的响应方式就是直接把响应正文显示在浏览器中!

为了使得客户端可以自行发出请求,并自行处理结果,而不是由浏览器把响应的结果直接显示出来,则需要使用到异步请求!

AJAX是在网页客户端通过Javascript发出异步请求的做法,传统的AJAX开发比较麻烦,通常,会结合jQuery框架来实现开发过程:

	
	

在以上AJAX处理中,主要是使用jQuery中的`$.ajax()`函数来实现,该函数的参数是一个JSON对象,所以,通过`{}`来组织其中的内容,通常,需要指定5项属性:

- url:将请求提交到哪里;
- data:提交的请求参数,参数的数据格式例如:`xx=xx&xx=xx&xx=xx`,如果某些参数的值来自输入框等控件,可以通过`$("#控件id").val()`获取;
- type:提交的请求类型,例如`get`或`post`,该值不区分大小写;
- dataType:服务器端响应的结果的数据类型,在服务器端响应时,响应头(Response Headers)中会包含Content-Type,确定了响应的数据类型,根据此处的类型,从而决定当前属性的值是`json`或`xml`或`text`;
- success:当服务器端正确的响应时,将如何处理响应结果,该属性的值是一个函数,通常使用匿名函数,且,函数中可以添加参数,该参数就是服务器响应的内容,如果服务器端响应的是JSON字符串,则在该函数内部,可以将参数直接当作JSON对象来使用;
- error:当服务器端的响应码是3xx、4xx、5xx时,`$.ajax()`并不会调用`success`对应的函数,而是调用`error`对应的函数。 

### 1.6. 练习

**题目**

假设正确的用户名是`root`,匹配的密码是`1234`,请设计登录页面`login.html`,当提交登录时,服务器端完成用户名与密码的验证,并响应JSON结果,客户端获取结果,如果登录成功,重定向到`index.html`,如果失败,则在对应的输入框右侧显示错误信息。

**开发-1-服务器端**

在控制器类`UserController`中添加处理登录的方法:

	@RequestMapping("/handle_login.do")
	@ResponseBody
	public ResponseResult handleLogin(
		@RequestParam("username") String username,
		@RequestParam("password") String password) {
		// ...
	}

**注意:由于登录成功、用户名不存在、密码错误时,均有不同的处理方式,所以,在不同的分支,返回结果的state属性值应该是不相同的!**

完成后,通过`http://localhost:8080/AJAX-01-SAMPLE/user/handle_login.do?username=root&password=1234`路径进行测试,观察是否得到了正确的JSON数据,测试完成后,将请求方式限定为POST请求:

	@RequestMapping(value="/handle_login.do", 
		method=RequestMethod.POST)

**开发-2-设计前端显示**

当前登录页面至少应该包括:

- 用户名的输入框
- 密码的输入框
- 用户名出错时的提示区域
- 密码错误时的提示区域
- 登录按钮

由于将使用AJAX提交,所以,可以不需要`
`标签,也可以不需要`submit`类型的按钮。 由于需要从2个输入框中获取值,可能有提示信息要显示到2个提示区域,按钮也要绑定Javascript事件,所以,这5个标签都应该设置id属性:
请输入用户名:
请输入密码:
**开发-3-请求与响应** 由于将使用jQuery框架的`$.ajax()`函数,所以,应该先添加对jQuery文件的引用。 然后,通过`$("#login").click()`为登录按钮绑定点击事件。 注意:在HTML中``,如果写成` ————————————————————————————————— ## 1. 案例 ### 1.1. 目标 实现用户注册、登录功能,用户的数据将需要写入到数据库中,并且,注册时,用户名必须是唯一的!前端界面将通过AJAX提交请求并处理结果。 ### 1.2. 分析 大致功能分为2项:注册,登录。 在每项功能的开发过程中,应该:持久层(建库建表、实体类、增删改查),业务层,控制器层,前端界面。 MVC = Model(DAO + Service) + View + Controller ### 1.3. 注册-持久层 应该先确定数据库、数据表,然后创建与数据表对应的实体类。 检查`spring-dao.xml`中的配置是否正确。 然后,创建`cn.tedu.project.UserMapper`接口,然后添加抽象方法: Integer insert(User user); 接下来,在`src\main\resources`下创建`mappers`文件夹,并复制映射文件到此文件夹中,确认文件名为`UserMapper.xml`,并配置以上抽象方法的映射: INSERT INTO t_user ( username, password, age, phone, email ) VALUES ( #{username}, #{password}, #{age}, #{phone}, #{email} ) 为了保证用户名的唯一,应该实现**根据用户名查询用户数据**的功能,从而,后续在业务层中可以**先查询尝试注册的用户名是否已经被注册,如果没有被注册,此允许,否则,如果查询到对应的数据,表示用户名已经被占用,不允许注册**! 所以,还应该在持久层的接口中声明: User findUserByUsername(String username); 然后配置映射: 以上查询过程中,只查询了`id`,主要原因是本次查询根本就不关注结果中`User`对象的各项属性值,而只需要关注返回的结果是否为`null`即可! ### 1.4. 注册-业务层 每个用户名都是唯一的,不允许注册已经被占用的用户名: ![](01.png) 关于业务层的设计,首先,应该创建`cn.tedu.project.service.IUserService`业务层接口,然后,在接口中声明抽象方法: User reg(User user) throws UsernameConflictException; 通常,抽象方法的名称,与需要执行的业务(需要实现的功能)是对应的! 关于方法的返回值,依据**操作成功时需要返回什么数据**来设计,而不考虑可能存在的操作失败!并且,关于操作失败的设计,统统抛出异常! 当设计成这样,方法的调用者可能会: try { User user = userService.reg(xx); // 继续后续操作 } catch (xx异常) { // 处理操作失败 } catch (xx异常) { // 处理操作失败 } 所以,方法的调用者既可以获取操作成功时的数据,并在此基础上继续编程,也可以知晓是否出现某种错误,并对错误进行处理! **关于自定义异常,必须继承自RuntimeException,或其子孙类异常。** 所以,还需要创建`cn.tedu.project.service.ex.UsernameConflictException`,继承自`RuntimeException`。 然后,创建`cn.tedu.project.serivce.impl.UserServiceImpl`类,实现以上接口,并重写其中的抽象方法: public User reg(User user) throws UsernameConflictException { // 根据用户名查询用户数据 // 判断是否查询到有效数据(数据是否为null) // 是:用户名没有被占用,执行注册 // 否:用户名已经被占用,抛出异常 } **关于业务层的方法** 1. 通常,业务层的方法,相对持久层的方法,只多,不少; 2. 通常,每个持久层的方法,在业务层中,都有一个调用它的方法; 3. 通常,业务层的方法,都是通俗易懂,简单易用的; 4. 对于不允许外部直接访问的方法,应该声明为私有的,而允许外部直接访问的方法,应该在业务层接口中声明,然后再在实现类中实现; 5. 在设计业务层方法时,返回值应该是操作成功的情况下返回的结果,而操作失败时,均抛出自定义异常。 ### 1.5. 注册-控制器层 首先,设计请求: 请求路径:/user/handle_reg.do 请求类型:POST 请求参数:User 响应方式:ResponseResult 则,先创建`cn.tedu.project.entity.ResponseResult`类,在其中声明`Integer state`和`String message`。 然后,在`cn.tedu.project.controller.UserController`控制器类,该类应该是被SpringMVC所管理的,所以,需要添加`@Controller`注解,并且,检查`sping-mvc.xml`中的组件扫描的根包是否是`cn.tedu.project.controller`。 然后,在该类的声明之前添加`@RequestMapping("/user")`。 由于控制器中实现功能都依赖于业务层对象,所以,声明全局变量: @Autowired private IUserService userService; 然后,添加处理请求的方法: @RequestMapping(value="/handle_reg.do", method=RequestMethod.GET) @ResponseBody public ResponseResult handleReg(User user) { // 声明返回值 // try { // 调用业务层对象的reg()方法 // 在返回值对象中封装state值为1 // } catch (XXXException e) { // 在返回值对象中封装state值为0 // 在返回值对象中封装message值为e.getMessage // } // 返回 } 编写完成后,代码如下: @RequestMapping(value="/handle_reg.do", method=RequestMethod.POST) @ResponseBody public ResponseResult handleReg(User user) { // 声明返回值 ResponseResult rr = new ResponseResult(); try { // 调用业务层对象的reg()方法 userService.reg(user); // 在返回值对象中封装state值为1 rr.setState(1); } catch (UsernameConflictException e) { // 在返回值对象中封装state值为0 rr.setState(0); // 在返回值对象中封装message值为e.getMessage rr.setMessage(e.getMessage()); } // 返回 return rr; } 最后,请检查`spring-mvc.xml`中是否添加了注解驱动: 并且,在依赖中,必须有Jackson框架。 全部完成后,通过`http://localhost:8080/SSM-AJAX-01-SAMPLE/user/handle_reg.do?username=mybatis&password=1234`,如果测试无误,将处理请求的方法限制为:只允许提交POST请求。 ### 1.6. 注册-前端界面 由于使用AJAX提交请求,只需要在前端页面发出并获取JSON结果即可,关于AJAX的处理流程,相对比较固定,核心代码是: var url = "user/handle_reg.do"; var data = $("#reg_form").serialize(); // 根据控件的name和value拼接出参数数据 $.ajax({ "url": url, "data": data, "type": "POST", "dataType": "json", "success": function(json) { } }); ### 1.6. 登录-持久层 不用重新写,只需要检查`findUserByUsername()`的返回结果中是否包含所需的数据,如果没有,则在SQL语句中添加对应的字段! 即将原有的`SELECT id`调整为`SELECT id, username, password`。 ### 1.7. 登录-业务层 在接口中声明新的抽象方法: User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException; 然后,声明以上抛出的2个异常,它们都应该继承自`RuntimeException`。 然后,在实现类实现以上方法: public User login(String username, String password) throws UserNotFoundException, PasswordNotMatchException { // 调用当前类findUserByUsername获取用户数据 // 判断是否获取到了用户数据 // 是:用户名存在,判断密码是否匹配 // 是:密码正确,返回用户数据 // 否:密码错误,抛出异常 // 否:用户名不存在,抛出异常 }

你可能感兴趣的:(java)