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 文件内
AbstractApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
UserMapper userMapper = ac.getBean("userMapper",UserMapper.class);
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) {
request.setAttribute("msg",
"您输入的用户名" + 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("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()");
HttpSession session = request.getSession();
if (session.getAttribute("username") == null) {
response.sendRedirect("../user/login.do");
return false;
}
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