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方法注入

### 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.用法如下图:

# 练习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
根据视图组件的名称,确定具体使用的是哪个视图组件。

### 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`:

接下来,需要执行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,在启动日志中如果能够看到以上输出语句,则成功!

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

所有的控制器都应该是由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;
}
实际运行效果是:页面会显示错误:

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

#### 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`的文件完成最终的显示:

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

#### 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`文件夹中:

其实,在`mappers`下的映射文件的名称并不重要!但是,为了便于管理,通常会使用与接口对应的名称,所以,将`SomeMapper.xml`重命名为`UserMapper.xml`。
所有映射文件中,根节点都是``节点,且该节点必须配置名为`namespace`的属性,属性值是对应的Java接口,例如:
经过以上配置,指定了XML映射文件与接口文件的对应关系。
然后,在该文件内部,使用各级子节点配置与抽象方法的对应关系,子节点名称的选取,取决于要执行的操作的类型,例如要执行的数据操作是insert类型,则使用``节点,这些节点都必须指定id属性,属性值是与之对应的抽象方法的方法名:
在``节点中,添加`parameterType`属性,用于指定参数的类型,即抽象方法中的参数类型:

然后,在节点内部,编写需要执行的SQL语句:
INSERT INTO t_user (
username, password,
age, phone, email
) VALUES (
#{username}, #{password},
#{age}, #{phone}, #{email}
)
执行SQL语句时的参数值均使用`#{}`类似的语法,其中的名称是`User`类中的属性名:

#### 1.2.5. 最后的配置
首先,需要配置`SqlSessionFactoryBean`,通过它指定数据源与XML映射的文件位置:
以上配置中,XML映射文件的位置使用了`mappers/*.xml`,即:在`mappers`文件夹下的所有XML文件都应该是MyBatis的映射文件,所以,后续使用时,不可以在这个文件夹中存放其它XML文件。
然后,还需要配置`MapperScannerConfigurer`,用于指定接口文件在哪里:

至此,配置完成!
#### 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映射中,每个查询对应的都是`