Spring框架

Spring
Spring 春天(无厘头命名)

框架:软件的半成品
1.框架不是必需品,可以自己实现
2.使用框架可以大大节省开发时间
3.框架采用了很多默认规则,利用规则自动执行

Spring框架:
1.是软件的整体框架,其核心功能两个:
-IOC/DI
-AOP
2.Spring是一个框架家族
-Spring web MVC
-Spring ORM
-Spring data
-Spring Boot
......

IOC 控制反转
控制反转:
1.利用程序控制对象的创建和生命周期管理称为"主动控制"
-主动控制适合管理 创建过程 简单的对象
2.相对"主动控制"来说,将对象的创建和生命周期管理交给"环境"控制称为"控制反转"
-对象的创建过程复杂繁琐,适合控制反转

使用Spring的控制反转
使用步骤
1.导Spring的包



org.springframework
spring-context
4.3.8.RELEASE


junit
junit
4.12


2.创建被Spring IOC管理的对象
3.配置Spring的配置文件:告诉Spring IOC创建哪个类型的对象
在src/main/resources下的applicationContext.xml中添加

4.创建测试案例
1)初始化Spring IOC容器,Spring会自动创建对象
2)从Spring中获得创建好的对象
//测试Spring IOC功能,由Spring创建管理对象称为IOC
//初始化Spring IOC容器
//Spring容器在启动时候会读取xml配置文件,并且根据配置文件创建DemoBean对象
ClassPathXmlApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
//从Spring中获得被管理的对象
DemoBean bean = (DemoBean) ctx.getBean("demo");
//检查对象是否创建成功
System.out.println(bean);
//println方法会自动调用toString

实验目的:Spring创建管理对象,测试类从Spring中获得被管理的对象

JUnit
JUnit是Java测试框架,主要作用是解决单元测试问题
JUnit 测试案例
1.测试案例必须是公有类
2.测试方法必须是公有,无参数,无返回值的方法
3.测试方法上标注@Test 注解
@Before //在测试案例执行之前执行
@After //在测试案例执行之后执行

JavaBean
Java Bean 是指符合一定规范的Java对象
1.必须使用包
2.必须有无参构造器,可以使用默认构造器代替
3.实现序列化接口
4.包含Bean属性:Bean属性是由getXXX setXXX声明的方法
案例:
class Person{
String name;//对象属性
public String getName(){ //Bean属性:name 可读
return name;
}
public void setName(String name){ //Bean属性:name 可改
this.name=name;
}
}
Spring建议:被Spring管理的对象需要按照JavaBean规范定义。但是Spring可以宽泛支持任何类型

Spring的Bean管理功能
Spring为了适应各种软件中的情况,提供了丰富Bean对象管理功能
Spring可以为Bean设置别名
别名就是两个BeanID引用同一个Bean对象
通过在配置文件中使用别名标签实现
1.导入Spring
2.创建JavaBean对象
3.配置Spring文件
-在配置文件中声明Bean组件
-在配置文件中为Bean组件设置别名,用“别名”获取同一个Bean对象
4.测试
-初始化Spring
-利用id获取Bean对象,

 



1.8
1.8

Scope属性
利用scope设置对象是单例或者多个实例
1.单例:在一个软件中,一个类的对象始终是唯一的一个实例,称为单例
2.多个实例:在一个软件中,一个类的对象有多个实例
3.Spring默认是,按照单例管理Bean对象
-任何时候获得的Bean引用都引用同一个对象
//单例情况:多次调用getBean("test")获得同一个对象的引用
TestBean b1=cpx.getBean("test",TestBean.class);
TestBean b2=cpx.getBean("test",TestBean.class); //b1==b2
4.设置属性 scope=prototype以后,Spring按照多个实例管理Bean对象
-每次getBean,或者每次使用对象,都会创建新的实例
//多个实例:多次调用getBean("test1"),获得多个对象的实例
TestBean b3=cpx.getBean("test1",TestBean.class);
TestBean b4=cpx.getBean("test1",TestBean.class);//b1!=b2
根据软件的业务,合理设置对象是单例还是多个实例

对象生命周期管理方法
对象生命周期:对象何时创建,何时使用,何时销毁。一个对象的生命过程称为对象的生命周期
Spring提供对象生命周期支持功能
1.可以在对象创建以后,执行其初始化方法
2.可以在对象销毁之前,执行其销毁方法
-多个实例时候,不会执行其销毁方法
-设置scope=prototype时候destroy-method失效
-解决方案,如果有销毁方法,自行调用
3.利用Spring的配置文件,实现这个功能



懒惰初始化
在使用对象时候再初始化对象,如果不使用对象则不初始化对象,也称为按需初始化对象
Spring对象初始化时机:
1.默认情况下Spring采用立即初始化,也就是Spring容器创建时候,立即初始化单例对象
2.如果设置了lazy-init="true"属性,则对象采用懒惰初始化,也就是使用对象时候(getBean时)才初始化对象。


DI 依赖注入
依赖:一个组件到工作(执行)期间用到另外一个组件,称为依赖
依赖注入:软件组件有依赖关系,在组件工作时将依赖的组件注入到对象中称为依赖注入。简单说就是将用到的对象赋值给目标位置

Spring IOC容器可以管理Bean组件,Spring提供了DI功能,可以在组件有依赖关系的时候,将组件注入到目标位置

依赖关系的业务场景
1.定义斧子类型
2.定义光头强类型
-光头强依赖斧子砍树
利用Spring将斧子注入给光头强




1. Spring 可以注入对象
2. Spring可以注入基本值







容器
web容器,Spring容器,Java Bean容器,容器类
容器:可以承载东西的容器

web容器:承载执行web组件(Servlet、HTML、过滤器)的容器,如:Tomcat、Web Spare,WebLogic, Jetty。因为可以执行Servlet,也称
Servlet引擎

Spring:承载管理JavaBean组件的容器,称为JavaBean容器/Spring容器/Bean容器

集合:承载Java对象的容器,称为容器类/Java容器

Spring注入集合和数组:
Spring注入集合类型的Bean属性
注入类型:
1.数组


3.14
0.618
1.414


2.List


Tom
Jerry
Andy


3.Set


1
2
3


4.Map
子元素




范传奇


张皓岚


渣渣



5.子元素
1)基本值
2)对象 引用其他Bean组件 创建新数组


//引用另一个Bean里的对象



利用DI解耦
紧耦合:组件之间紧密耦合,不方便重构
松耦合:组件之间松散结合在一起,方便重构
解耦:将紧耦合改成松耦合,称为解耦
1.利用接口(面向接口编程)可以实现解耦
2.利用反射API进行解耦
3.Spring的DI支持利用接口解耦

自动注入
可以不用再写标签,根据类型或名称自动完成组件注入
按照名称注入:
1.被注入的bean属性名和bean组件的ID名一致
2.在bean标签上声明自动注入
3.Spring在启动时候,会自动根据名字找出组件进行注入

按照类型注入:
1.设置被注入的Bean为 autowire="byType"
2.Spring启动时根据被注入的Bean的属性类型查找类型兼容的Bean组件,如果找到就进行自动注入,找不到就取消。如果找到两个以上的Bean
组件则报错







Spring读取properties

Spring表达式
语法: #{BeanID.Bean属性}
例子:
1.读取bean属性: #{worker.name} 读取worker组件Bean属性name的值(实际上是调用getName)
2.读取list集合: #{demo.names[0] #{demo.names[1]
3.读取数组: #{demo.weight[0]} #{demo.weight[1]}
4.读取Map: #{demo.table.东}/#{demo.table['东']}
5.读取Properties: #{jdbc.driver} #{jdbc.url}

Spring注解
Spring提供了注解:用于声明Bean,注入Bean属性等功能
1.注解被编译器检查处理,编译器可以检查注解使用错误
2.注解大多采用自动方式执行,减少配置,写得少做的多
3.在源代码中直接书写,自己开发程序可以标注注解
4.注解声明的组件和XML声明的组件地位平等

声明组件
Spring提供了多个可以声明组件的注解,其功能一样
@Component 其他情况下使用
@Service 在业务层组件上使用
@Controller 在表现层控制器组件上使用
@Repository 在数据访问层(持久化层)上使用
Spring建议按照软件的层次使用注解


Spring组件注入注解
提供注解版本注入功能
@Autowired
1.自动按照类型匹配组件,如果类型重复再按照组件名称匹配
2.如果没有匹配成功,就报运行错误
3.注解支持对"Bean属性"注入和"对象属性"注入

Scope
使用注解声明的Spring Bean组件是单例的,使用@Scope("prototype")声明为多个实例
//单例组件加上 @Lazy 设置成懒惰初始化

@Resource 注解
1.@Resource 注解是Java EE注解,使用时候需要导包
2.Spring自动支持这个注解的功能
3.Resource与Autowired类似,都可以实现属性注入
4.Resource处理过程是先检查组件ID是否与属性名一致(byName),如果匹配到名字一致,则注入成功。如果没有匹配到名字,再按照类型查找,
如果找到一个,就注入成功,否则找到多个,注入失败
// 导入Resource的jar包坐标

javax.annotation
jsr250-api
1.0

声明周期管理方法
注解原理:
1.@PostConstruct 注解标注在初始化方法上,Spring在创建对象以后,立即执行这个初始化方法
2.@PreDestroy 标注在销毁方法,Spring在关闭容器时候,会自动执行对象销毁方法。如果是多例对象,不会执行


Spring MVC
是Spring提供的MVC框架,其封装Java web(Servlet)功能,简化Java web的开发。其功能大多是自动化完成。可以大量减少代码量。

Spring MVC Hello World
搭建Spring MVC的基础环境:
1.创建Maven web项目
-web项目
-创建部署描述文件web.xml
-设置目标运行环境 Tomcat
2.导入Spring-webmvc包

1.8
1.8




org.springframework
spring-webmvc
4.3.10.RELEASE


junit
junit
4.12


mysql
mysql-connector-java
5.1.6



commons-dbcp
commons-dbcp
1.4


javax.annotation
jsr250-api
1.0


3.配置前置控制器,需要依赖一个xml配置文件

DispatcherServlet
org.springframework.web.servlet.DispatcherServlet

contextConfigLocation
classpath:spring-mvc.xml

1 //立即创建DispatcherServlet(默认是懒惰初始化)


DispatcherServlet
*.do

resources里的spring-mvc.xml 配置




4.部署测试

编写Hello World:
@RequestMapping("hello.do") //声明浏览器请求路径:作用是当浏览器请求hello.do时,会找到当前方法hello()
//并执行hello()方法,处理用户的请求
@ResponseBody //将控制器方法hello()的返回自动处理以后作为response body发送到客户端
public String hello() {
System.out.println("Hello World!");
return "Hello World!";
}


利用模板引擎显示信息
显示完整的网页,可以自动解决中文问题


org.thymeleaf
thymeleaf-spring4
3.0.11.RELEASE

近期课程安排 成恒
SpringMVC(2) / MyBatis(2) /AJAX(1) /SpringBoot(1) 项目(12)

回顾Spring
框架(Framework):框架是一套已经编写完成的程序代码,它解决了一些特定的问题,并且,很有可能改变了传统的一些开发方式!程序员在使用框架
的基础上进行编程,就可以不必关心很多基础功能的实现

Spring框架:解决了创建和管理对象的问题,使用了Spring框架后,程序员就可以不必再自行"创建"某些对象,而是改为通过Spring窗口"获取"对象!
甚至获取到的对象的某些属性已经被注入了指定的值!

为什么需要通过Spring创建和管理对象,主要源自于希望实现解耦!

解耦=降低对象之间的耦合度=降低对象之间的依赖关系

假设存在:
public class UserJdbcDao {
public void login() {
// 通过JDBC实现了用户登录的验证
}
}
public class UserLoginServlet {
UserJdbcDao userDao = new UserJdbcDao();
public void doPost() {
userDao.login(); // 实现登录验证
}
}
则`UserLoginServlet`是依赖于`UserDao`的!

如果该项目后续不再使用JDBC技术实现增删改查,例如需要改为使用MyBatis相关技术来实现!则可能创建出`UserMyBatisDao`,希望使用这个新
的类取代原有的`UserJdbcDao`!则原有的:
public UserJdbcDao userDao = new UserJdbcDao();
需要改为:
public UserMyBatisDao userDao = new UserMyBatisDao();
当某个组件发生改变时,对原有的代码需要做的修改较多,则称之为耦合度较高!

如果希望解决这个问题,可以先创建一个接口:
public interface UserDao {
void login();
}

然后,使得`UserJdbcDao`和`UserMyBatisDao`都实现这个接口,则原有的代码可以调整为使用接口来声明对象的类型:
public UserDao userDao = new UserJdbcDao();
为了保证切换实现类时,以上代码可以不必再调整,可以使用设计模式中的工厂模式:
public class UserDaoFactory {
public static UserDao getInstance() {
return new UserJdbcDao();
}
}

则原有代码可以调整为:
public UserDao userDao = UserDaoFactory.getInstance();

则后续需要将`UserJdbcDao`替换为`UserMyBatisDao`时,只需要修改以上工厂类的方法的返回值即可!项目中其它的代码中并没有体现任何实现
类的名称,也就不就需要进行调整了!

经过以上的调整后,可以视为:`UserLoginServlet`类就不再依赖于`UserJdbcDao`类,如果一定要说依赖于谁,也只依赖于`UserDao`接口,这种
做法就是解耦!

在实际开发项目时,可以创建很多接口,但是,如果还要创建大量的工厂类,是比较繁琐的!所以,就产生了Spring框架,可以把Spring理解为一
个“大型工厂”,或者“综合性工厂”,可以生产任何指定类型的数据,当需要这些数据时,从“工厂”(Spring容器)中获取即可!

关于Spring框架,需要掌握的:
1.理解Spring的作用
2.了解Spring管理的对象的作用域(是否单例,是否懒加载)和生命周期
3.掌握通过Set方式为属性注入值;
4.理解自动装配中的"byName"和"byType"这2种装配模式
5.掌握组件扫描,及4个通用注解: @Component @Controller @Service @Respository
6.了解关于作用域与生命周期的注解: @Scope @lazy @PostConstruct @PreDestroy
7.理解 @Autowired 与 @Resource
8.理解依赖、解耦相关概念
9.理解IOC和DI
10.关于Spring AOP会在项目的最后再讲

SpringMVC框架的作用
MVC=Model(数据模型)+View(视图)+Controller(控制器)
SpringMVC的作用是解决了V-C和C-V之间的交互问题
传统的Controller的具体表现就是Servlet,每个Serlvet是由1个继承自HttpServlet的Java类,和在web.xml中的配置组成!每个Servlet的配置信息
大致是

.... ....
.... .... 在使用Servlet时,通常每个Servlet处理1种请求,例如登录有登录的Servlet,注册也有注册的Servlet,如果一个项目中有100中请求,就需要创建
100个Servlet类,并至少使用800行代码进行配置!如果某个项目有1000种甚至5000种请求,则需要创建1000个或5000个Servlet类,并至少使用
8000行甚至40000行代码进行配置,在实际运行时,还需要创建1000个或5000个对象!对内存的开销也是非常大的

SpringMVC中控制器与请求的对应关系是1对多的!在同一个控制器中可以添加若干个处理请求的方法!无论是从配置的代码量、Java类文件的数
量、运行时控制器对象的数量都大大的减少了!使得整个项目运行时开销更小,更加易于管理和维护
当然,在实际开发时,SpringMVC也表现得比原生技术的使用方式更加简单

SpringMVC框架的核心处理流程
在SpringMVC框架的核心处理流程中,会涉及到的5个组件
DispatcherServlet:前端控制器,用于接收所有请求,并组织整个执行路程
HandlerMapping:用于配置请求路径与处理请求的控制器的映射关系
Controller:实际处理请求的组件
ModelAndView:控制器的处理结果,包括Model(数据)和View(视图)
ViewResolver:视图解析器

SpringMVC+HelloWorld
案例目标
在浏览器中访问`http://localhost:8080/项目名称/hello.do`能打开某个页面,该页面中显示`Hello, SpringMVC!!!`的字样。

创建项目
创建**Maven Project**,勾选**Create a simple project**,**Group id**为`cn.tedu.spring`,**Artifact id**为`SpringMVC-02`,
**Packaging**选择`war`。
当项目创建出来后,先生成**web.xml**文件;对项目点击右键设置属性,勾选**Targeted Runtimes**中的Tomcat;在**pom.xml**
中添加`spring-webmvc`的依赖(建议使用4.2或以上版本);在前序项目中复制Spring的配置文件到当前新项目中。

配置DispatcherServlet
首先,需要在**web.xml**中配置`DispatcherServlet`,其基本配置如下:

SpringMVC
org.springframework.web.servlet.DispatcherServlet


SpringMVC
*.do

SpringMVC框架是基于Spring的,后续还会在Spring的配置文件(**spring-mvc.xml**)中进行配置,则希望项目启动时,就加载Spring
的配置文件。可以:在以上配置中,为`DispatcherServlet`配置初始化参数`contextConfigLocation`,该参数的值是Spring配置文件的位置,
一旦配置了该参数,当DispatcherServlet被初始化时,就会自动加载Spring的配置文件!然后,将DispatcherServlet配置为默认启动的,
**即Tomcat启动时,就会初始化DispatcherServlet,进而导致spring-mvc.xml被读取并加载。**

则补充配置为:

SpringMVC
org.springframework.web.servlet.DispatcherServlet

contextConfigLocation
classpath:spring-mvc.xml

1


SpringMVC
*.do

注意:以上配置的``和``节点是区分先后顺序的!

如果需要测试以上配置是否成功,可以先在**spring-mvc.xml**中配置组件扫描:


然后,在被扫描的包下创建任意类,为类添加`Compontent`注解,自定义构造方法,输出某个字符串,例如:
package cn.tedu.spring;
import org.springframework.stereotype.Component;
@Component
public class User {
public User() {
System.out.println("创建了User类的对象!");
}
}
最后,启动项目,在Eclipse的控制台中应该可以看到构造方法中输出的内容!

通过控制器接收用户提交的请求
通过`@RequestMapping`注解,可以设置请求路径与处理请求的方法的映射关系,所以,在实际编程时,并不需要显式的使用`HandlerMapping`。
所以,可以直接创建`cn.tedu.spring.HelloController`控制器类,并为类添加`@Controller`注解,这样,Spring框架才会创建该控制器类的对象,
最终,被SpringMVC框架所识别并使用。然后,在控制器类中添加处理请求的方法,关于方法的设计:
1. 应该使用`public`权限;
2. 暂时使用`String`作为返回值类型;
3. 方法名称可以自定义;
4. 方法的参数列表暂时为空。
则可以添加方法:
public String showHello() {
return null;
}
需要在方法之前使用`@RequestMapping`以配置映射关系,则:
@RequestMapping("hello.do")
public String showHello() {
System.out.println("HelloController.showHello()");
return null;
}
注意:以上控制器类必须放在`cn.tedu.spring`包中,因为此前设置组件扫描就是这个包!通常,控制类应该使用`Controller`单词作为名称的后缀
。控制器类只能使用`@Controller`注解,不可以使用`@Component`或另几个注解。
完成后,重新启动项目,打开浏览器,通过`http://localhost:8080/SpringMVC-02/hello.do`进行测试,正确的结果应该是:在Eclipse的控制
台中,可以看到以上方法输出的内容!而页面尚未处理,可能出现404错误。

显示页面
默认情况下,控制器中的方法如果返回`String`类型的数据,表示“视图名称”,框架需要根据视图名称来确定由哪个视图组件负责最终响应
时的显示。则需要在**spring-mvc.xml**中进行配置:



class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
value="/WEB-INF/templates/" />
value=".html" />
value="utf-8" />
value="HTML" />
value="false" />


class="org.thymeleaf.spring4.SpringTemplateEngine">
ref="templateResolver" />



ref="templateEngine" />
value="utf-8" />

然后,将控制器中处理请求的方法的返回值改为`"hello"`。
最后,在**webapp/WEB-INF/templates/**文件夹下创建**hello.html**,并在HTML中自行设计页面内容。通过
`http://localhost:8080/SpringMVC-02/hello.do`进行测试。

接收客户端的请求参数
准备工作
希望通过`http://localhost:8080/SpringMVC-02/reg.do`能打开注册页面,在注册页面中,至少包含用户名、密码、年龄、
手机号码、邮箱这5项数据的输入框,及1个提交按钮。
首先,可以创建新的`cn.tedu.spring.UserController`控制器类(把代码写在原有的`HelloController`中也行,但是,不建议),
在类之前添加`@Controller`注解,并在类中添加处理请求的方法:
@RequestMapping("reg.do")
public String showReg() {
return "reg";
}
然后,在**webapp/WEB-INF/tempaltes/**下创建**reg.html**,并设计页面内容。

*/
【不推荐】使用HttpServletRequest接收请求参数
可以在处理请求的方法的参数列表中添加`HttpServletRequest`类型的参数,然后,按照传统方式获取请求参数的值即可:
@RequestMapping("handle_reg.do")
public String handleReg(HttpServletRequest request) {
System.out.println("UserController.handleReg()");
String username = request.getParameter("username");
String password = request.getParameter("password");
String age = 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. 如果期望的参数类型不是`String`,需要自行转换数据类型;
3. 不便于执行单元测试。

【推荐】将所需的请求参数添加在处理请求的方法的参数列表中
可以在处理请求的方法的参数列表中,将所需的参数一一列举出来,且不区分先后顺序,但是,必须与客户端提交的参数保持名称一致:
@RequestMapping("handle_reg.do")
public String handleReg(String username, String password, Integer age, String phone, String email) {
System.out.println("username=" + username);
System.out.println("password=" + password);
System.out.println("age=" + (age + 1));
System.out.println("phone=" + phone);
System.out.println("email=" + email);
return null;
}
使用这种做法时,可以将参数直接声明为所期望的类型。如果定义的某个参数的名称是客户端没有提交的参数,则方法中的参数值将是`null`。
这种做法并不适用于请求参数的数量较多的应用场景!

【推荐】将所需的请求参数封装在自定义类型中并作为处理请求的方法的参数,可以将若干个请求参数进行封装,例如:
public class User {
private String username;
private String password;
private Integer age;
private String phone;
private String email;
// 添加匹配的SET/GET方法
}
然后,在处理请求的方法的参数列表中,添加以上自定义的数据类型的参数即可:
@RequestMapping("handle_reg.do")
public String handleReg(User user) {
System.out.println(user);
return null;
}
小结:
首先,第1种做法(使用`HttpServletRequest`)的做法是不推荐的,无论在任何应用场景中,在控制器(`Controller`)中接收请求参数时,都不
使用这种做法!
第2种做法(将参数穷举)和第3种做法(将参数封装)各有优点,当请求参数的数量较少(通常不超过4个),且固定时,应该优先使用第2种做法,这样
做会使得源代码非常直观,阅读时更加容易理解代码的意义,反之,当请求参数的数量较多时(通常超过6个),或请求参数的数量可能发生变化时
(未来调整需求时,数量可能增加或减少,或改为需要其它的参数),应该优先使用第3种做法!
另外,以上第2种做法和第3种做法可以同时使用!

转发数据
以“登录”功能为例,假设`root`/`1234`是正确的用户名和密码,在处理登录时,如果用户名或密码错误,则使用专门错误页面进行提示,如果
成功,暂不处理。
则新建**error.html**页面,用于显示错误信息,然后,在处理登录的`handleLogin()`方法中,添加登录信息的判断:
// 判断用户名
if ("root".equals(username)) {
// 用户名正确,需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功
// TODO 处理登录成功
} else {
// 密码错误
return "error";
}
} else {
// 用户名错误
return "error";
}

【不推荐】将需要转发的数据封装在HttpServletRequest中
可以在处理请求的方法的参数列表中添加`HttpServletRequest`类型的参数,当需要转发数据时,调用参数对象`setAttribute(String name,
Object value)`方法进行封装,然后转发即可:
@RequestMapping("handle_login.do")
public String handleLogin(String username, String password, HttpServletRequest request) {
System.out.println("UserController.handleLogin()");
System.out.println("username=" + username);
System.out.println("password=" + password);
// 判断用户名
if ("root".equals(username)) {
// 用户名正确,需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功
// TODO 处理登录成功
} else {
// 密码错误
request.setAttribute("errorMessage", "密码错误");
return "error";
}
} else {
// 用户名错误
request.setAttribute("errorMessage", "用户名不存在");
return "error";
}
return null;
}
然后,在Thymeleaf的模版页面中,使用表达读取以上封装的数据:

xxxxxxx


这种做法并不利于执行单元测试,通常不推荐使用这种做法!

【推荐】将需要转发的数据封装在ModelMap中
使用方法与使用`HttpServletRequest`基本一致!需要在处理请求的方法的参数列表中添加`ModelMap`类型的参数,当需要转发数据时,调用
该参数对象的`addAttribute(String name, Object value)`封装所需要转发的数据:
@RequestMapping("handle_login.do")
public String handleLogin(String username, String password, ModelMap modelMap) {
System.out.println("username=" + username);
System.out.println("password=" + password);
// 判断用户名
if ("root".equals(username)) {
// 用户名正确,需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功
// TODO 处理登录成功
} else {
// 密码错误
// request.setAttribute("errorMessage", "密码错误");
modelMap.addAttribute("errorMessage", "[M] 密码错误");
return "error";
}
} else {
// 用户名错误
// request.setAttribute("errorMessage", "用户名不存在");
modelMap.addAttribute("errorMessage", "[M] 用户名不存在");
return "error";
}
return null;
}
`ModelMap`是继承自`LinkedHashMap`的,其中的`addAttribute()`方法本质就是调用了`Map`的`put()`方法来实现的。

【不推荐】使用ModelAndView作为处理请求的方法的返回值
将处理请求的方法的返回值声明为`ModelAndView`类型,在需要返回结果时,调用`ModelAndView(String viewName, Map
model)`构造方法,确定视图名称和数据即可:
@RequestMapping("handle_login.do")
public ModelAndView handleLogin(String username, String password) {
System.out.println("username=" + username);
System.out.println("password=" + password);
Map map = new HashMap();
// 判断用户名
if ("root".equals(username)) {
// 用户名正确,需要判断密码
if ("1234".equals(password)) {
// 密码也正确,则登录成功
// TODO 处理登录成功
} else {
// 密码错误
// request.setAttribute("errorMessage", "密码错误");
// modelMap.addAttribute("errorMessage", "[M] 密码错误");
map.put("errorMessage", "[MAV] 密码错误");
return new ModelAndView("error", map);
}
} else {
// 用户名错误
// request.setAttribute("errorMessage", "用户名不存在");
// modelMap.addAttribute("errorMessage", "[M] 用户名不存在");
map.put("errorMessage", "[MAV] 用户名不存在");
return new ModelAndView("error", map);
}
return null;
}

重定向
假设登录成功后,将“跳转”到欢迎页(**hello.html**)!
在处理请求的方法中,如果方法的返回值是`String`类型的,返回以`redirect:`作为前缀,并拼接上目标路径,就表示重定向!
return "redirect:hello.do";

关于@RequestMapping
在处理请求的方法之前,添加`@RequestMapping`注解,可以配置请求路径与处理请求的方法的映射关系!
除了在方法之前添加该注解以外,还可以在类之前也添加该注解!一旦在类之前添加了该注解并配置了值之后,该类中所有映射的请求路径都需要
添加这次配置的值作为路径的前缀,相当于添加了一层“文件夹”:
@Controller
@RequestMapping("user")
public class UserController {
@RequestMapping("reg.do")
public String showReg() {
return "reg";
}
}
如果是以上配置,需要显示注册页面,则请求路径应该是`user/reg.do`!
通常,建议在每一个控制器类之前都添加该注解!
在类和方法之前都添加了该注解后,配置时,左侧的第1个`/`和右侧最后多余的`/`是可有可无的,例如:
user reg.do
user /reg.do
/user reg.do
/user /reg.do
/user/ reg.do
/user/ /reg.do
以上6种配置方式是完全等效的!推荐始终使用第1种组合,或第4种组合!

在`@RequestMapping`内部,声明了:
String[] value() default {};
即:可以在注解中配置`value`属性,其值是`String[]`类型的,在使用时,可以使用1个字符串作为值,也可以使用`{}`框住多个字符串作为值,例如:
@RequestMapping(value= {"hello.do", "index.do"})
public String showHello() {
System.out.println("HelloController.showHello()");
return "hello";
}
以上代码表示无论通过`hello.do`还是`index.do`都会导致`showHello()`方法被执行,也就是将2个请求路径映射到同1个方法!

以上注解的代码中还添加了`@AliasFor("path")`注解,则表示`value`与`path`是等效的,在后续的代码中也可以看到:
@AliasFor("value")
String[] path() default {};
以上`path`是从SpringMVC 4.2版本起加入的,仅当4.2及更高版本可用!
注意:如果同时为注解配置多个属性,则每个属性之前都必须显式的声明属性名!例如`@RequestMapping(name="这是配置的xxxx", "reg.do")`
是错误的!必须配置为`@RequestMapping(name="这是配置的xxxx", value="reg.do")`!

另外,在注解的源代码中还有:RequestMethod[] method() default {};
@RequestMapping(path= {"hello.do","index.do"},method=RequestMethod.POST)//用GET方法会报405错误
该属性的作用是用于“限制请求方式”!如果限制了请求方式,并且客户端使用了错误的请求方式,则会导致405错误:
HTTP Status 405 – Method Not Allowed
在错误页面通常会有详细描述,例如:
Request method 'GET' not supported

关于@RequestParam注解
可以在处理请求的方法的参数列表中,为方法的参数添加`@RequestParam`注解。
当客户端提交的请求参数的名称,与控制器中处理请求的方法的参数名不一致时,可以使用该注解,在注解中配置请求参数的名称即可:
@RequestMapping("handle_login.do")
public String handleLogin(
@RequestParam("uname") String username,
String password,
ModelMap modelMap) {
// ...
}

当配置了该注解后,该请求参数就是必须提交的,如果没有提交,就导致400错误:
HTTP Status 400 – Bad Request
详细错误可能是:
Required String parameter 'uname' is not present
之所以添加了注解后,就必须要提交对应的请求参数,是因为在注解的源代码中有:
boolean required() default true;
以上`required`属性表示“是否必须”,默认值为`true`,所以,默认情况下,必须提交该请求参数,可以将值设置为`false`,则客户端没有提交
请求参数时也不会出错,只不过服务器端控制器中处理请求的参数值为`null`:
@RequestMapping("handle_login.do")
public String handleLogin(
@RequestParam(name="uname", required=false) String username,
String password,
ModelMap modelMap) {
}
在该注解的源代码中还有:
String defaultValue() default ValueConstants.DEFAULT_NONE;
以上`defaultValue`表示“默认值”,即:客户端没有提交请求参数时,视为提交的是某个值!具体使用方式例如:
@RequestMapping("handle_login.do")
public String handleLogin(
@RequestParam(name="uname", required=false, defaultValue="admin") String username,
String password,
ModelMap modelMap) {
// ...
}


关于HttpSession
案例目标
登录成功后,应该“跳转”到显示用户个人信息的页面(**user_info.html**),在该页面中显示成功登录的用户的用户名,不要求显示其它数据。

案例分析
此次的“跳转”应该是**重定向**,当使用重定向,必须指定**目标路径**,目前并没有哪个路径用于显示个人信息!所以,应该先创建
**user_info.html**页面,然后在`UserController`中添加处理`user/info.do`的请求,处理时,转发到**user_info.html**页面,最后,在登录
成功时,重定向到`user/info.do`。

使用Session存取数据
为了保证登录的用户名可以显示在个人信息页面,可以选择:登录成功时,将用户名存入到Session中,当用户访问个人中心时,从Session中取出
数据。在SpringMVC中,当需要使用Session时,只需要将`HttpSession`声明为处理请求的方法的参数即可!
所以,解决以上问题时,首先,在处理请求的方法的参数列表中添加`HttpSession session`参数,当登录成功时,通过`session.setAttribute
("username", username);`将用户名存入到Session中,最后,在页面中,通过`某某某`显示
用户名!
这种使用Session的方式是不利于执行单元测试的!

SpringMVC框架建议的做法是将需要保存到Session的数据存入到`ModelMap`对象中,当然,这样做会导致转发的数据和Session的数据都在
`ModelMap`对象中,所以,还需要在控制器类之前添加`@SessionAttributes`注解,以说明“ModelMap中的哪些数据是需要保存到Session
中的”,例如:
@SessionAttributes({"uid", "username"})
则`ModelMap`对象中,名为`uid`和`username`数据就是保存到了Session中!

这种做法也是存在一定缺陷的,例如`ModelMap`中的数据作用域比较乱,默认情况下,还会将Session中的数据显示到URL中,并且,通过这种方
式管理的Session数据并不能通过传统的`session.invalidate()`进行清除……
所以,一般情况下,还是有很多开发者直接使用`HttpSession`参数对象来管理Session数据,并不使用SpringMVC提供的这种做法!即使使用
`HttpSession`是不便于执行单元测试的,也无所谓!通常,关于控制器的测试,可以选择在浏览器直接发送请求进行简单的测试,也可以通过专业
的测试软件进行测试,而不执行单元测试!

拦截器(Interceptor)
案例目标
为“个人中心”页面添加新的规则:必须登录后才允许打开该页面,如果未登录,就尝试打开该页面,则重定向到登录页!

案例分析
可以在处理“显示个人中心”的请求,即`showUserInfo()`方法中,将`HttpSession`作为参数列表中的参数,在方法运行初期,就尝试取出
`username`,如果取出的值不是`null`,则表示已经登录,可以显示页面,如果取出的值是`null`,即Session中并没有`username`,则表示未登录,
将重定向到登录页!
代码示例:
@RequestMapping("info.do")
public String showUserInfo(HttpSession session) {
if (session.getAttribute("username") == null) {
return "redirect:login.do";
}
return "user_info";
}

问题分析
在一个软件系统中,可能需要登录才可以进行的操作种类非常多,如果把以上判断Session数据的代码在很多次处理请求时都写一遍,是非常不现
实的,也不易于统一管理!应该制定一种策略,使得需要检验Session的过程中,都统一执行相同的代码!

拦截器简介
拦截器(Interceptor)是SpringMVC中的组件!
拦截器可以使得若干种请求(可以自行配置)都执行拦截器中的代码,在拦截器中,可以选择对这些请求的处理进行阻止,则这些请求就不会向后继
续执行,也可以选择放行,让这些请求继续执行!

拦截器的基本使用
首先,需要创建`cn.tedu.spring.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的配置文件中进行配置才可以使用!所以需要在**spring-mvc.xml**中添加配置:









通过运行效果可以发现:
1. 如果访问的路径不是拦截器配置的拦截路径,拦截器是完全不工作的;
2. 在拦截器类中,真正具有“拦截”功能的只有`preHandle()`方法,该方法返回`true`时表示放行,返回`false`时表示阻止继续运行;
3. 在拦截器类中,还有`postHandle()`和`afterCompletion()`方法,这2个方法是在处理请求的方式之后运行的!

所以,如果需要使用拦截器实现“登录拦截”,可以:
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) {
String contextPath = request.getContextPath();
System.out.println("contextPath=" + contextPath);
response.sendRedirect(contextPath + "/user/login.do");
return false; // 阻止继续运行
}
return true; // 放行
}

关于拦截器的配置
每个拦截器的配置中,都可以使用若干个``节点,配置若干个拦截路径!例如:














在编写控制器的代码时,可以直接视为“能执行控制器中相关代码,就肯定是登录过的”,因为,没有登录的话,会被拦截器阻止运行,是不会
执行控制器中的代码的!
在配置拦截路径时,还可以使用星号`*`作为通配符,所以,以上配置的与`blog`相关的路径,可以写为:

以上1项配置就可以表示此前的3条配置!

需要注意的是,使用1个`*`作为通配符时,只能匹配1层资源,例如配置为`/blog/*`时,可以匹配到`/blog/addnew.do`、`/blog/delete.do`等,
但是,不能匹配到`/blog/2019/list.do`,如果需要无视层级,需要使用2个连续的星号,即配置为: */

另外,还可以使用``配置白名单,在白名单中的请求路径,是拦截器不予处理的!每个拦截器可以配置若干项白名单
,在配置白名单时,也可以使用通配符!
注意:在配置每个拦截器时,必须先配置拦截路径,即``,再配置白名单,即``,最后再配置拦截
器类,即``。

使用Filter解决SpringMVC项目中的乱码问题
在SpringMVC框架中,定义了`CharacterEncodingFilter`过滤器类,其中有`encoding`属性,如果为该属性指定了值,所指定的值将成为请求和
响应时使用的字符编码!
假设需要将请求与响应的编码设置为`utf-8`,可以是:

CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter

encoding
utf-8



CharacterEncodingFilter
/*

*/
SpringMVC小结
1. 理解SpringMVC的作用;
2. 理解SpringMVC的核心执行流程图;
3. 理解SpringMVC项目的相关配置,包括**web.xml**和**spring-mvc.xml**中的配置;
4. 掌握使用控制器接收请求;
5. 掌握接收请求中的参数(2种做法);
6. 掌握`@RequestMapping`注解的使用;
7. 理解转发与重定向;
8. 了解转发与重定向的开发步骤;
9. 了解`@RequestParam`注解的使用;
10. 掌握`HttpSession`的使用;
11. 掌握拦截器的使用与配置。
12. 关于SpringMVC中其它知识点在后续课程或项目中再补充!

什么情况下需要使用Session
1. 存储用户的身份时,存储用户的唯一标识,例如:用户的id;
2. 存储高频率使用的数据时,例如:用户名;
3. 不便于使用其它技术存取时。

拦截器与过滤器的区别
过滤器(`Filter`)是Java EE中的组件,则任何一个Java的WEB项目都可以使用过滤器;而拦截器(`Interceptor`)是SpringMVC中的组件,仅当使用
了SpringMVC框架才可以使用这个拦截器,并且,仅当被SpringMVC框架处理的请求才有可能被拦截器进行处理,假如将`DispatcherServlet`的
路径映射为`*.do`,则只有以`.do`作为后缀的请求才可能被拦截器处理,其它请求是不会被拦截器处理的!
在实际使用时,过滤器的配置相对非常单一,例如:








在整个配置过程中,就只能配置1个映射的URL,尽管可以使用通配符,但是,可能配置的过滤范围不够精准,而拦截器的配置过程中,可以配置若
干个拦截路径,也可以配置若干个例外,也可以使用通配符,配置非常灵活!
另外,过滤器是执行在所有`Servlet`之后的组件,而拦截器是执行在`DispatcherServlet`之后,在所有`Controller`之前的组件!
所以,一般情况下,会优先选取拦截器!

字符编码
计算机能够直接识别并处理的只有二进制,在二进制的世界中,只有0和1这2种数字,所有的数据都是由0和1组成的序列!
二进制数据中的每个0,或者每个1,所需占据的存储空间是1个位(bit),通常使用字节(byte)作为最基本的存储单位,每个字节占8个位。
1位:0, 1
2位:00, 01, 10, 11
3位:000, 001, 010, 011, 100, 101, 110, 111

每个字节占8个位,可以表达256种不同的0、1组成的序列,除去最左侧的固定作为符号位,还剩余7个位,则可以表达128种不同的0、1组成的序列。
最早制定的ASCII码表中就记录了所有的英文字母、数据、标点符号、特殊按键与0、1组成的二进制序列的对应关系,例如大写字母`a`对应的就是
`110 0001`,转换成十进制就是`97`。当需要计算机处理`a`时,计算机就会根据`110 0001`来处理,反之,当计算机处理的结果是`110 0001`时,
就会在屏幕中显示出`a`。
所以,所谓的编码表,其实就是记录了对应关系的表!
但是,ASCII码表只能适用英文语言,却无法适用于使用其它语言的国家和地区!因为ASCII码表是使用1字节来记录对应关系的,只能表达128种
不同的意义,而中文的汉字种类太多,使用1个字节是无法记录各种对应关系的!所以,在任何一门编程语言中,如果要表达汉字,每个汉字至少需
要占2个字节!
由于ASCII码表中并没有记录汉字与二进制序列的对应关系,所以,使用中文,需要支持中文的编码表,例如GBK、UTF-8。
Java语言默认使用2个字节作为1个字符,使用的是Unicode编码,默认情况下可以表示32768种不同的二进制序列,也就可以记录32768种字符!
但是,在实际传输时,一次传输的字符较多时,可能会出现无法正确区分的问题,例如传输的是`1010 1111 0001 1100 1011 1100 1011 1101`时
,共4个字节,接收方可能就无法区分这个序列到底是1个ASCII码+1个中文+1个ASCII码,还是2个中文,还是2个ASCII码+1个中文,或1个中文+
2个ASCII码,甚至是4个ASCII码!
所以,在数据需要传输时,就需要使用到UTF-8这种传输编码(UTF:Unicode Transfermation Format)。
在UTF-8中,对于多字节组成的二进制序列的编码规则例如:
2字节: 110 ????? 10 ??????
3字节: 1110 ???? 10 ?????? 10 ??????
4字节: 11110 ??? 10 ?????? 10 ?????? 10 ??????
UTF-8编码的细节版本又有utf8mb3和utf8mb4,分别表示Max Byte 3和Max Byte 4的意思!
在MySQL数据库中,使用UTF-8,其实表示的是utf8mb3,也支持utf8mb4,需要显式的指定为utf8mb4。


MyBatis框架
1. MyBatis框架的作用
MyBatis的主要作用是减化持久层开发。
使用Java语言,可以通过JDBC实现数据库的增删改查相关功能的开发!在使用JDBC的过程中,始终是建立与数据库的连接 > 设计所需要执行的
SQL语句 > 获取Statement/PreparedStatement对象 > 执行SQL语句 > 处理结果 > 释放资源/归还连接这样的处理模式,代码相对比较固定!
MyBatis框架可以简化以上开发过程,在使用MyBatis实现增删改查相关功能时,只需要:设计所需要执行的功能的抽象方法;设计该抽象方法对
应的SQL语句。

2. 创建数据库与数据表
CREATE DATABASE tedu_ums;
USE tedu_ums;
CREATE TABLE t_user (
id INT AUTO_INCREMENT COMMENT '用户id',
username VARCHAR(16) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(16) NOT NULL COMMENT '密码',
age INT COMMENT '年龄',
phone VARCHAR(20) COMMENT '手机号码',
email VARCHAR(30) COMMENT '电子邮箱',
PRIMARY KEY (id)
) DEFAULT CHARSET=utf8mb4;

4. 创建MyBatis项目
创建`Maven Project`,**Group Id**使用`cn.tedu.mybatis`,**Artifact Id**使用`MyBatis`,**Packaing**选择`war`(也可以选择`jar`)。
项目创建出来后,需要:生成**web.xml**文件;在**pom.xml**中添加依赖;从前序项目中复制**spring-mvc.xml**到当前项目;添加Tomcat
运行环境(此案例可以不需要);打开前序项目的**web.xml**,将`DispatcherServlet`和`CharacterEncodingFilter`的配置复制到当前项目中。
本次另外需要添加依赖:


org.mybatis
mybatis
3.5.1



org.mybatis
mybatis-spring
2.0.1

然后,将**spring-mvc.xml**复制一份,得到**spring-dao.xml**,删除**spring-dao.xml**文件内的配置!

5. 数据库连接
在**src/main/resources**下创建**db.properties**文件,用于配置数据库连接的相关信息:
url=jdbc:mysql://localhost:3306/tedu_ums?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
driver=com.mysql.cj.jdbc.Driver
username=root
password=root
initialSize=2
maxActive=10

在配置`driver`属性时,需要注意所使用的`mysql-connector-java`的版本,如果使用的`5.x`版本,该属性值应该是`com.mysql.jdbc.Driver`,如
果使用的是`6.x`或更高版本,该属性值应该是`com.mysql.cj.jdbc.Driver`,如果不确定到底应该用哪个值,可以展开`mysql-connector-java`的
jar包文件来观察!另外,在该jar包中还有**META-INF/services/java.sql.Driver**文件,该文件内记录的就是使用该jar包时应该使用的`Driver`类。
如果使用的是`6.x`或以上版本的`mysql-connector-java`,在配置连接数据库的URL时,还必须显式的设置`serverTimezone`参数的值!在中国
大陆地区可使用的值有`Asia/Shanghai`和`Asia/Chongqing`。

需要在**spring-dao.xml**中添加配置,读取以上**db.properties**中的配置信息:

然后,连接数据库时,使用到的数据源将是`BasicDataSource`,则需要将以上读取到的数据库连接信息注入到`BasicDataSource`的属性中去:

class="org.apache.commons.dbcp.BasicDataSource">







然后,可以在测试类中,编写并执行单元测试,以检查配置是否正确。

6. 接口与抽象方法
暂定目标:使用MyBatis向数据表中插入新的用户数据。
使用MyBatis时,需要定义各功能对应的抽象方法,这些抽象方法必须定义在接口中!
首先,应该创建`cn.tedu.mybatis.UserMapper`接口文件,然后,在接口中定义“插入用户数据”的抽象方法,关于抽象方法的设计原则:
- 如果需要执行的是增、删、改类型的操作,则使用`Integer`作为方法的返回值,将表示“受影响的行数”,可用于判断操作成功与否,也可以
设计为`int`,还可以设计为`void`表示不关心受影响的行数;
- 方法名称可以自定义,不可以重载;
- 参数列表根据所需要的参数来决定,可以参考此前学习JDBC时,SQL中有哪些问号,就写哪些参数。

此次“插入用户数据”需要执行的SQL语句大致是:
insert into (username, password, age, phone, email) values (?,?,?,?,?)
则抽象方法需要写5个参数,如果后续该数据表增加了更多的字段,则需要写更多参数,这种做法是有缺陷的,应该将这些参数封装到
`cn.tedu.mybatis.User`类中:
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String phone;
private String email;
}
则抽象方法可以设计为:
Integer insert(User user);

接下来,需要配置“接口文件在哪里”,以保证后续MyBatis框架工作时,能找到该接口:




7. 配置SQL映射
从`http://doc.tedu.cn/config/mybatis-mapper.zip`下载编写配置文件的XML文件压缩包。
在**src/main/resources**下创建名为**mappers**的文件夹,并将压缩包中的**SomeMapper.xml**复制到该文件夹下。
**注意:在MyBatis中用于配置SQL语句的XML文件必须包括以下声明,否则将无法正常使用!**

在配置该XML文件时,首先,需要添加根节点``,且必须配置`namespace`属性,该属性值表示对应哪个接口文件:


然后,根据所需要执行的SQL语句的种类,从``、``、``、`节点,该节点必须配置resultType或resultMap属性!
当查询某条指定的记录时,如果存在匹配的结果,将返回正确结果,如果没有匹配的结果,则返回null;如果查询结果可能有多条记录,则抽象方
法必须设计返回List集合类型的数据,最终查询结果会是一个有效的List对象,即使没有匹配的结果,也只是List集合中没有数据(即:size()返回0),
返回的List集合本身不会是null。

3. 处理多个参数的抽象方法
MyBaits框架默认只支持抽象方法中没有参数,或只有1个参数!
可以尝试将多个参数进行封装,封装为某个自定义的数据类型,或封装到`Map`集合中均可,但是,使用封装后,会降低代码的可读性,方法的调用
者可能也不知道如何正确的调用方法!所以,MyBatis框架提供了`@Param`注解,该注解是添加在抽象方法的参数之前的,使用该注解后,在执行,
MyBatis框架会自动的将各参数进行封装,最终满足“只能有1个参数”的要求!具体的使用方式示例:
Integer updateEmailById(@Param("id") Integer id, @Param("email") String email);

小结:当使用MyBatis时,只要抽象方法的参数可能有多个(超过1个),则必须在每个参数之前添加@Param注解,在注解中指定参数名称,后续
在配置XML映射时,占位符中也使用注解配置的参数名称!

4.动态SQL--foreach
动态SQL:在使用MyBaits配置SQL语句的映射时,可以添加例如``、``等节点,实现SQL语句中的判断、循环等,使得参数不同时,
可能生成不同的SQL语句!
以“一次性删除多条数据”为例,在设计抽象方法时,多条数据的`id`可以使用数组或`List`集合类型,则抽象方法应该是:
Integer deleteByIds(List ids);
还可以是:
Integer deleteByIds(Integer[] ids);
Integer deleteByIds(Integer... ids);

在配置SQL映射时:

DELETE FROM t_user WHERE id IN

#{id}

在配置``节点时,关于属性的配置:
- `collection`:被遍历的对象,如果被遍历的对象是`List`集合类型的,则配置为`collection="list"`,如果被遍历的对象是数组类型的,则配置
为`collection="array"`;
- `item`:遍历过程中得到的元素的名称,在``子级文本节点中,使用占位符表示元素值时,就必须使用该名称;
- `separator`:遍历过程中,值与值之间使用什么作为分隔符;
- `open`与`close`:遍历生成的SQL语句部分的最左侧字符串和最右侧字符串。

5. 动态SQL--if
在配置SQL语句时,如果需要对参数值进行判断,就可以使用``标签。

6. 关于#{}和${}占位符
使用`#{}`格式的占位符表示某个值,可以简单的理解为“以前在使用JDBC技术时,可以写问号的位置,都应该使用`#{}`格式的占位符,反之,不可
以写问号的位置,就只能使用`${}`格式的占位符”。
MyBatis在处理`${}`占位符时,是:先将原有SQL语句拼接`${}`占位符,然后尝试编译,最终执行;
MyBatis在处理`#{}`占位符时,是:先将SQL语句进行编译(预编译),然后使用值取代占位符,最终执行。
使用预编译,可以有效的防止SQL注入的问题!
所以,在能够使用`#{}`的应用场景中,应该尽量使用`#{}`格式的占位符,如果一定需要使用`${}`格式的占位符,需要评估是否存在SQL注入的风险,
如果可能存在,则需要对参数值进行验证。

7. 解决字段名与属性名不一致的查询问题
为用户数据表添加新的字段:
ALTER TABLE t_user ADD COLUMN is_delete INT; // 0, 1
并为数据表中已存在的数据设计新字段的值:
UPDATE t_user SET is_delete = 0;
然后,还需要在`User`类中添加新的属性:
public class User {
...
private Integer isDelete; // 新增
}

MyBatis在执行查询时,可以将查询结果中的数据自动的封装到结果对象中!以查询用户数据为例,当查询到某个用户数据后,如果查询结果中`id`这
一列的值是`10`,MyBatis就会自动创建查询结果`User`类的对象,并尝试调用名为`setId`的方法,将`10`作为方法的参数值,以将查询结果封装进去!
当然,如果名为`setId`的方法不存在,就不会调用该方法!也就说:如果希望MyBatis能够正确的封装查询结果,需要**查询结果中的列名(column)
与返回值类型中的属性(property)名保持完全一致**!

如果字段名(filed)与属性名不一致时,在编写SQL语句时,可以自定义别名,该别名最终会作为查询结果的列名,所以,别名与属性名一致,即可保证
MyBatis能够正确的封装查询结果!

1. 使用
在配置映射时,可以配置``节点,用于**指导MyBatis如何将查询到的数据封装到返回值对象中**!
关于``的配置例如:















在使用``解决名称不一致的问题时,如果查询结果的列名与属性名原本就是完全一致的,则可以不配置这些名称一致的项!例如以上代码
只需要配置为:
!-- id:自定义名称 -->









从功能的表现来说,``和``节点的功能表现是一样的!即使使用``节点来配置主键,也是完全可行的!但是,MyBatis框架会基于
``节点的配置实现缓存,所以,主键应该使用``节点来配置!

2. 多表联合查询
在`t_user`表中添加字段`department_id`,用于表示该用户归属哪个部门:
ALTER TABLE t_user ADD COLUMN department_id INT;
然后,为现有的用户数据分配部门:
UPDATE t_user SET department_id=1 WHERE id IN(2,16,24);
UPDATE t_user SET department_id=2 WHERE id IN(19,21,22);
UPDATE t_user SET department_id=3 WHERE id IN(4,10,23);
因为修改了数据表结构,所以,还需要在`User`类中添加新的属性!

需求:查询指定id的用户的数据时,要求显示该用户所归属的部门的名称
需要查询用户数据,则查询的是`t_user`表,另外,要求查询出部门名称,就需要查询`t_department`表,所以,该功能需要联合查询2张表:
select
*
from
t_user
left join
t_department
on
t_user.department_id=t_department.id
where
t_user.id=1
由于此次执行的是联合查询,目前并没有哪个实体(Entity)类可以封装此次的查询结果!所以,需要先创建新的VO(Value Object)类,用于存放
查询结果!
public class UserVO {
private Integer id;
private String username;
private String password;
private Integer age;
private String phone;
private String email;
private Integer isDelete;
private Integer departmentId;
private String departmentName;
}
>从编写代码的角度来看,实体类和VO类并没有区别,但是,从定位上来说,实体类是与数据表一一对应的,即每张数据表都有一个与之对应的实体
类,数据表中有多少个字段,实体类中就有多少个属性;而VO类是与查询需求相对应的,某次查询会得到哪些结果,VO类就设计与之对应的属性,
VO类的数量与数据表并没有明确的关系,通常VO类的数量取决于有多少种关联查询。

通过MyBatis实现时,设计抽象方法:
UserVO findVOById(Integer id);
然后,在`UserMapper.xml`中配置该抽象方法的映射:

3. 处理1对多关系的查询
需求:查询某个部门信息时,显示该部门所有的用户的信息!
SELECT
*
FROM
t_department
LEFT JOIN
t_user
ON
t_department.id=t_user.department_id
WHERE
t_department.id=1
在开发功能之前,还需要创建某个VO类,用于封装此次的查询结果:
public class DepartmentVO {
private Integer id;
private String name;
private List users;
}
然后,在`DepartmentMapper.java`接口中添加抽象方法:
DepartmentVO findVOById(Integer id);
由于这样的查询,结果中可能有多条数据记录,默认情况下,MyBatis并不知道如何将多条查询结果封装到1个`DepartmentVO`对象中!所以,需要
为这次的查询配置``:















然后再配置SQL语句:


注意:尽管使用了``,仍需要自定义别名,与此前不同,此前定义别名是为了实现“查询结果的列名与返回结果的属性名一致”,
此次使用了``,就不需要通过定义别名的方式来保证名称一致,而定义别名的目的是“保持查询结果中的每一列的名称都不相同”。


AJAX
1. 服务器向客户端的响应方式
服务器向客户端的常规响应方式有:转发、重定向。
无论是转发,还是重定向,都有一个共同的特点:服务器端会确定客户端即将显示的页面!
这种做法是现阶段正规开发中趋于淘汰的做法!
因为使用这种做法,服务器端的开发人员需要掌握页面的开发技术(HTML/CSS/JS),并承担相应的开发任务,不利于实现前后端开发人员的工作
职责的分离!
现今的客户端技术种类也比较多,例如使用浏览器访问时,会使用网页作为客户端的显示技术,如果使用手机访问时,就需要开发iOS/Android
手机端的APP,如果使用的是平板电脑,就需要开发iOS/Android平板电脑端的APP。这样来看,不应该让程序员既承担服务器端的开发任务,
又承担各种客户端的开发任务!
另外,使用网页呈现数据,并不适用于现今客户端种类较多的应用场景!
推荐的做法是:服务器处理完客户端的请求后,仅向客户端响应关键的数据,而不考虑这些数据是如何呈现的!

2. 服务器向客户端响应数据
在使用SpringMVC框架时,在控制器处理请求的方法之前,添加`@ResponseBody`注解,就表示接下来的方法处理请求后,会**响应正文**,
例如:
@Controller
public class UserController {
@RequestMapping("hello.do")
@ResponseBody
public String showHello() {
return "hello, ajax!!!";
}
}
即对`hello.do`发出请求,最终客户端得到的响应结果就只是`"hello, ajax!!!"`字符串。
当服务器只向客户端响应正文,而不再是响应某个页面,会使得响应的数据量更小,进而会导致响应速度更快,用户体验更好!

3. 服务器向客户端响应的数据的格式
假设客户端的请求是需要“显示某个用户的基本信息”,该用户的信息可能是:
用户名:David
年龄:26
手机号码:13800138026
电子邮箱:[email protected]
由于响应时,程序代码中的表现就只是一个字符串,则需要使用1个字符串表示以上4项属性的值!为了客户端接收到这个字符串之后,能够从中获
取到任意所需的属性的值,需要使用特定的格式将以上4项属性的值组织起来!

组织数据,可以使用XML语法:

David
26
13800138026
[email protected]

目前比较流行的组织数据格式的做法是使用JSON数据格式:
{
"name":"David",
"age":26,
"phone":"13800138026",
"email":"[email protected]"
}

4. JSON数据格式
JSON(JavaScript Object Notation, JS对象简谱)是一种轻量级的数据交换格式。它基于ECMAScript(欧洲计算机协会制定的js规范)的一个子集,采
用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON 成为理想的数据交换语言。易于人阅读和编写,同时也易于
机器解析和生成,并有效地提升网络传输效率。
JSON示例代码:
{
"name":"David",
"age":26,
"phone":"13800138026",
"email":"[email protected]",
"skill":["JavaSE", "JavaOOP", "MySQL"],
"department":{
"id":3,
"name":"软件研发部"
}
}
JSON只是一种组织数据格式的语法,并且,它可以被JavaScript直接识别(JSON是JavaScript默认能识别的一种对象)!
1. 整个JSON数据是一个JSON对象,JSON对象是使用大括号`{}`框住的;
2. JSON对象中可以有若干个属性与值的配置,各配置之间使用逗号`,`分隔;
3. JSON对象中的属性名称是字符串类型的,使用一对双引号`""`框住;属性名与属性值之间使用冒号`:`分隔;属性的值可以是不同的类型,如果是字
符串类型,需要使用一对双引号框住,如果是数值型或布尔型,则不需要使用双引号;
4. JSON对象的属性值还可以是数组类型的,使用一对中括号`[]`框住,可以通过`JSON对象名.属性名[下标]`来访问某个数组元素,也可以通过`JSON对
象名.属性名.length`获取数组的长度,并组合循环语法实现遍历;
5. JSON对象的属性值还可以是另一个对象,值使用一对大括号`{}`框住;
6. 任务一个属性的值都可以是数值、布尔值、字符串、数组、对象!数组中的元素也可以是以上类型中的任意某一种!

在JavaScript中使用JSON示例:
let obj = {
"name":"David",
"age":26,
"phone":"13800138026",
"email":"[email protected]",
"skill":["JavaSE", "JavaOOP", "MySQL"],
"department":{
"id":3,
"name":"软件研发部"
}
};
console.log(obj.name);
console.log(obj.age);
console.log(obj.phone);
console.log(obj.email);
console.log(obj.skill.length);
for (let i = 0; i < obj.skill.length; i++) {
console.log(obj.skill[i]);
}
console.log(obj.department.id);
console.log(obj.department.name);

如果在JavaScript获取到的是一个符合JSON语法的字符串,可以通过`JSON.parse(str)`将其转换为JSON对象!

5. 服务器端向客户端响应JSON数据
首先,需要在当前项目中添加`Jackson`依赖:

com.fasterxml.jackson.core
jackson-databind
2.9.8

然后,自定义某个类,用于封装即将响应给客户端的数据:
public class User {
private String name;
private Integer age;
private String phone;
private String email;
}

在处理请求时,将自定义的类型作为方法的返回值类型:
@RequestMapping("info.do")
@ResponseBody
public User showUserInfo() {
User user = new User();
user.setName("David");
user.setAge(26);
user.setPhone("13800138026");
user.setEmail("[email protected]");
return user;
}

还需要在Spring的配置文件中添加注解驱动:

如果没有添加该注解驱动,会导致406错误:
HTTP Status 406 – Not Acceptable

完成后,向服务器发送请求后,服务器端会响应:
{"name":"David","age":26,"phone":"13800138026","email":"[email protected]"}

工作原理:在SpringMVC中,如果控制器中处理请求的方法添加了@ResponseBody注解,框架会通过转换器(Converter)来处理结果,在SpringMVC
中有多种不同的转换器,根据处理请求的方法的返回值不同,使用的转换器也不相同,例如返回String时,会使用StringHttpMessageConverter,如
果返回的数据类型是SpringMVC默认不识别的,并且使用了Jackson框架,就会自动使用Jackson框架中的转换器,而Jackson框架中的转换器会把响
应结果对象处理为JSON格式的对象!另外,Jackson框架还将响应头的Content-Type设置为application/json,charset=utf-8,即响应结果是JSON数
据,且使用utf-8编码.

6. AJAX
Ajax即“Asynchronous Javascript And XML”(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。
function login() {
// alert("准备登录");
// jQuery中的$.ajax()函数用于提交异步请求并处理结果
// jQuery中的$.ajax()函数的参数是一个JSON对象
// url:将请求提交到哪里去
// data:请求参数
// type:请求类型
// dataType:服务器端响应的数据的类型,取值可以是text、xml、json,取决于响应头(Response Header)中的Content-Type
// success:响应成功(HTTP响应码是200)时的回调函数
$.ajax({
"url":"handle_login.do",
"data":$("#form-login").serialize(),
"type":"post",
"dataType":"json",
"success":function(obj) {
if (obj.state == 1) {
alert("登录成功");
} else if (obj.state == 2) {
alert("用户名不存在");
} else if (obj.state == 3) {
alert("密码错误");
}
}
});
}


SpringBoot
1. SpringBoot简介
SpringBoot是一个默认就集成了Spring、SpringMVC及相关常用框架的集合,并默认完成了常规的配置。关于配置,SpringBoot的理念是“约定
大于配置”,在使用过程中,不必做配置的同时,也要遵守SpringBoot完成的配置值。

2. 创建SpringBoot项目
打开https://start.spring.io,在页面中填写创建项目的参数,必要的话,勾选所需要添加的依赖,点击Generate按钮,以下载项目。
解压下载的项目压缩包,得到项目文件夹,应该将该文件夹移动到Workspace中,在Eclipse中,通过Import > Existing Maven Projects导入项目。
导入时,务必保证当前计算机能够连接到Maven服务器!此次项目需要从Maven服务器下载大量依赖的jar包!
如果项目没有开始自动更新,可以对项目点击右键,选择Maven > Update Project,并在弹出的对话框中勾选Force update ...并尝试更新,如果
仍无法更新,可以尝试降低依赖的springboot的版本。
推荐使用Eclipse Oxygen(4.7)以上版本,如果使用的是较低版本的Eclipse,在pom.xml文件中可能会提示Maven版本错误!可以无视这个错误,
并不影响项目的运行!

3. SpringBoot项目结构
创建好的项目结构例如:
[Java Resources]
[src/main/java]
[cn.tedu.springboot]
ServletInitializer.java
SpringbootApplication.java
[src/main/resources]
[static]
[templates]
application.properties
[src/test/java]
[cn.tedu.springboot]
SpringbootApplicationTests.java

注意:以上cn.tedu.springboot包是根据创建项目时设置的Group和Artifact决定的,也是当前项目设置的组件扫描的根包!所以,后续在编程时,
所有的组件类(例如控制器类)必须放在这个包或其子包中!
注意:SpringbootApplication类的名称也是根据创建项目时设置的Artifact决定的,这个类是SpringBoot项目的启动类,当需要运行时,执行该
类中的main()方法即可!
注意:SpringBoot项目内置了Tomcat,运行启动类时,就会将项目部署到内置的Tomcat中!
注意:在src/main/resources下的static文件夹是用于存放静态资源的文件夹,例如存放.html、.css、.js及图片等!
注意:在src/main/resources下的templates文件夹是用于存放模版页面的,例如使用Thymeleaf时,就应该把相关的模版页面放在这里!
注意:在src/main/resources下的application.properties文件是SpringBoot项目唯一的配置文件,所有自定义配置都应该配置在该文件中!也有
一些SpringBoot项目使用的配置文件是application.yml,与application.properties的区别只是配置的语法不同,功能与定位完全相同!
注意:在src/test/java下有cn.tedu.springboot包,这个包名也是创建项目时决定的,在SpringBoot项目中所有的单元测试类都必须放在这个包或
子包中!否则,执行单元测试时,将无法加载Spring环境!在SpringbootApplicationTests测试类中,类的声明语句之前的2条注解,也是每个单
元测试类必须添加的注解!

4. 在SpringBoot项目中添加静态网页
在src/main/resources/static下创建index.html,内容可以自行设计,完成后,启动项目(执行SpringbootApplication类中的main()方法),打开
浏览器,通过http://localhost:8080/ 即可访问到该页面!
因为index.html是默认的欢迎页面的文件名,所以,在访问时,不必在URL中显式的使用http://localhost:8080/index.html。

5. 修改SpringBoot项目中内置Tomcat的端口号
在src/main/resources下的application.properties中添加配置:
server.port=8888
HTTP协议的默认端口号是80,也可以将端口号修改为这个值,如果使用的是Linux操作系统,使用80端口需要进行相关的权限设置。

6. 在SpringBoot项目中处理请求
在项目的根包cn.tedu.springboot下创建子包controller,并在该子包中创建控制器类HelloController,并在类之前添加@RestController注解:
package cn.tedu.springboot.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
}
@RestController相当于@Controller + @ResponseBody,即:添加该注解后,当前类就是控制器类,并且,当前类中所有处理请求的方法都是
响应正文,相当于每个处理请求的方法之前都添加了@ResponseBody!@RestController并不是SpringBoot项目才有的注解,在普通的
SpringMVC项目中也可以使用该注解,只是需要另行配置!

注意:SpringBoot项目默认已经配置了SpringMVC的DispatcherServlet,并且,映射的路径是/*,所以,在使用SpringBoot开发时,设计请求
路径并不需要使用.do作为后缀!
(*/
然后,在控制器类中添加处理请求:
@GetMapping("hello")
public String showHello() {
return "Hello, SpringBoot!!!";
}
以上@GetMapping相当于@RequestMapping(method=RequestMethod.GET),即限制请求方式必须是GET类型的请求!除此以外,还有
@PostMapping……

7. 使用SpringBoot开发用户注册功能(正常的开发流程,应该是最后做页面!)
数据库连接
SpringBoot项目默认并没有集成数据库编程所需的依赖!可以在创建项目时添加,也可以在项目创建成功后再在**pom.xml**中补充相关依赖!
关于MyBatis和MySQL的依赖是:

org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.1


mysql
mysql-connector-java
runtime

注意:如果在SpringBoot项目中添加了数据库相关依赖,却没有配置数据库连接相关信息,则启动项目时会报错,因为SpringBoot在启动时就
会加载连接数据库的相关信息!

然后,需要在**application.properties**中添加配置:
spring.datasource.url=jdbc:mysql://localhost:3306/tedu_ums?useUnicode=true&characterEncoding=utf-8&serverTimezone=
Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

在配置数据库连接时,可以不必配置driverClassName,SpringBoot会自动从数据库连接的jar包中查找!
添加以上配置后,即使配置值是错误的,也不影响项目启动!但是,应该及时检查配置是否正确!则可以通过单元测试,来尝试获取数据库连接,
如果能够连接,则配置正确,如果无法连接,则配置错误,或者相关jar包是损坏的!

在`SpringbootApplicationTests`编写并执行单元测试:
@Autowired
private DataSource dataSource;
@Test
public void getConnection() throws SQLException {
Connection conn = dataSource.getConnection();
System.err.println(conn);
}
由于SpringBoot的单元测试在执行之前,会加载整个项目中的配置和Spring环境,所以,在传统的SpringMVC项目中可以通过`getBean()`获取的
对象,在SpringBoot项目中都可以自动装配!

通过MyBatis实现插入数据
创建`cn.tedu.springboot.mapper.UserMapper`接口,并在接口中添加抽象方法:
Integer insert(User user);
然后,需要配置接口文件的位置!可以直接在接口的声明语句之前添加`@Mapper`注解!使用这种做法时,每个MyBatis的接口都需要添加该
注解!也有另一种做法,在启动类之前添加`@MapperScan`注解进行配置:
@SpringBootApplication
@MapperScan("cn.tedu.springboot.mapper")
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
在**src/main/resources**下创建名为**mappers**的文件夹,专门用于存放配置SQL语句的XML文件,并在该文件夹下复制粘贴得到
**UserMapper.xml**文件,进行配置:


INSERT INTO t_user (
username, password,age, phone,email, is_delete,department_id
) VALUES (
#{username}, #{password},#{age}, #{phone},#{email}, #{isDelete},#{departmentId}
)


在SpringBoot项目中,仍然需要配置XML文件的位置,则需要在**application.properties**中添加配置:
mybatis.mapper-locations=classpath:mappers/*.xml
*/
完成后,编写并执行单元测试:
@Autowired
private UserMapper userMapper;
@Test
public void insert() {
User user = new User();
user.setUsername("boot");
user.setPassword("1234");
Integer rows = userMapper.insert(user);
System.err.println("rows=" + rows);
}
由于SpringBoot的单元测试在执行之前,会加载整个项目中的配置和Spring环境,所以,在传统的SpringMVC项目中可以通过`getBean()`获取的
对象,在SpringBoot项目中都可以自动装配!

开发前端页面
在**src/main/resources/static**下创建用户注册页面。

使用控制器接收并处理请求
为了便于接收用户提交的注册数据,首先,应该创建`cn.tedu.springboot.entity.User`用户数据的实体类:
创建`cn.tedu.springboot.controller.UserController`控制器类,在控制器类之前添加`@RestController`和`@RequestMapping("user")`:
@RequestMapping("user")
@RestController
public class UserController {
}
然后,添加方法以接收并处理请求:
@PostMapping("reg")
public void reg(User user) {
System.err.println("UserController.reg()");
System.err.println("\t" + user);
// TODO 将用户数据写入到数据库中
}
在前端页面中确定注册请求是提交到`/user/reg`的,如果无误,则启动项目,观察用户的注册数据是否可以提交到服务器端的控制器中!

为了保证“用户名唯一”,可以在用户尝试注册之前,检查用户名是否已经被注册,则“根据用户名查询用户数据”,判断查询结果是否为`null`,
即可判断得到答案!
所以,在`UserMapper.java`接口中添加:
User findByUsername(String username);
并在`UserMapper.xml`中配置SQL映射:

完成后,还是应该编写并执行单元测试:
@Test
public void findByUsername() {
String username = "root";
User user = userMapper.findByUsername(username);
System.err.println(user);
}
然后,将以上方法应用到控制器的处理请求的过程中:
@PostMapping("reg")
public JsonResult reg(User user) {
System.err.println("UserController.reg()");
System.err.println("\t" + user);
JsonResult jsonResult = new JsonResult();
// 检查该用户名是否已经被注册
String username = user.getUsername();
User result = userMapper.findByUsername(username);
// 判断查询结果是否为null
if (result == null) {
// 将用户数据写入到数据库中
userMapper.insert(user);
jsonResult.setState(1);
} else {
// 用户名已经被注册
jsonResult.setState(2);
jsonResult.setMessage("您尝试注册的用户名(" + username + ")已经被占用!");
}
return jsonResult;
}

页面中添加AJAX对应的接收代码

 

 

 


你可能感兴趣的:(Spring框架)