spring入门

spring简介

spring是分层的Java SE/EE应用 轻量级开源框架。提供了表现层 SpringMVC和持久层 Spring JDBC Template以及 业务层 事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。

spring有两大核心:

  • IOC Inverse Of Control:控制反转
  • AOP Aspect Oriented Programming:面向切面编程

IOC

IOC概念

IOC 控制反转,是一种思想,实际上是指对一个对象的控制权的反转。

控制:在java中指对象的控制权限(创建、销毁)

反转:指对象控制权由原来 程序员在类中手动控制 反转到 由spring容器控制

比如这样一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Book{
private Integer id;
private String name;
private Double price;
}
......

public class User{
private Integer id;
private String name;
private Integer age;

public void read(){
Book book = new Book();
book.setId(1);
book.setName("聊斋志异");
book.setPrice(20.56);
}
}

上述代码中,Book的对象控制权在User对象里面,你要使用User对象的方法就必须要创建一个Book对象,这样User和Book就高度耦合。你在其他地方使用Book对象,必须自己手动创建、初始化、销毁等。

有了spring,我们就可以把对象的创建等操作交给spring容器来管理。在项目启动时,所有的Bean对象都注册到了spring容器中,如果使用到了哪个Bean就去spring容器中要,而不需要自己手动创建,这样就可以专注于业务,可以从对象的创建中解脱出来。

自己实现IOC

需求:实现service层与dao层代码解耦合

实现:

  • 创建Java项目,导入依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
  • 编写Dao接口和实现类

Dao接口:

1
2
3
4
5
6
7
package com.cwz.dao;

public interface IUserDao {

public void save();

}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
package com.cwz.dao.impl;

import com.cwz.dao.IUserDao;

public class UserDaoImpl implements IUserDao {
@Override
public void save() {

System.out.println("dao被调用了...");

}
}
  • 编写service接口和实现类

service接口:

1
2
3
4
5
6
7
package com.cwz.service;

public interface IUserService {

public void save();

}

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.cwz.service.impl;

import com.cwz.dao.IUserDao;
import com.cwz.service.IUserService;
import com.cwz.utils.BeanFactory;

public class UserServiceImpl implements IUserService {
@Override
public void save() {

IUserDao userDao = (IUserDao) BeanFactory.getBean("userDao");

userDao.save();

}
}
  • 编写beans.xml

把所有需要创建对象的信息定义在配置文件中

1
2
3
<beans>
<bean id="userDao" class="com.cwz.dao.impl.UserDaoImpl"></bean>
</beans>
  • 编写BeanFactory工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.cwz.utils;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class BeanFactory {

private static Map<String, Object> iocmap = new HashMap<>();

// 程序启动时,初始化对象实例
static {
// 1.读取配置文件
InputStream resourceAsStream = BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml");

SAXReader saxReader = new SAXReader();

try {
// 2.解析xml
Document document = saxReader.read(resourceAsStream);
// 3.编写xpath表达式
String xpath = "//bean";
// 4.获取所有的bean标签
List<Element> list = document.selectNodes(xpath);
// 5.遍历并创建对象实例,设置到map集合中
for (Element element : list) {

String id = element.attributeValue("id");
String className = element.attributeValue("class");

Object o = Class.forName(className).newInstance();

iocmap.put(id, o);

}

} catch (DocumentException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}


}

// 获取指定id的对象实例
public static Object getBean(String beanId) {
Object o = iocmap.get(beanId);
return o;
}


}
  • 编写测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.cwz.test;

import com.cwz.service.impl.UserServiceImpl;
import org.junit.Test;

public class SpringTest {

@Test
public void test() {
UserServiceImpl userService = new UserServiceImpl();
userService.save();
}

}

这样就手动实现了类似与IOC容器的功能,实际上BeanFactory就是一个简单的Spring的IOC容器所具备的功能。

IOC体验

首先创建一个普通的Maven项目,引入spring-context依赖

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.6</version>
</dependency>
</dependencies>

然后在resources目录下创建一个spring配置文件:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>

在这个文件中,我们可以配置所有需要注册到spring容器的bean:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="org.javaboy.User" id="user"/>

</beans>

class 属性表示需要注册的bean的 全路径,id表示bean的唯一标识,也可以用name属性作为bean的标志。

接下来,写一个main方法去加载这个配置文件,从容器中获取对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.javaboy.ioc;

import org.javaboy.ioc.model.User;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
public static void main(string[] args) {
// 加载配置文件
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

// 获取User对象
User user = ctx.getBean("user", User.class);
System.out.println("user = " + user);

}
}

上述代码中获取bean对象是通过id和class组合来获取一个bean的,一般这样不会出错。嫌麻烦可以直接使用id或者name来获取,但一般不建议直接使用class来获取(如果xml文件中存在两个相同的bean就不好办了)

属性的注入

构造方法注入

通过 Bean 的构造方法给 Bean 的属性注入值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
private String username;
private String address;
private Integer id;

// 省略tostring、get、set方法

// 有参构造方法
public User(String username, String address, Integer id) {
this.username = username;
this.address = address;
this.id = id;
}
}

在 xml 文件中注入 Bean

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="org.javaboy.ioc.model.User" id="user">
<constructor-arg name="id" value="1"/>
<constructor-arg name="username" value="javaboy"/>
<constructor-arg name="address" value="javaboy.org"/>
</bean>

</beans>

如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。

set方法注入

1
2
3
4
5
<bean class="org.javaboy.ioc.model.User" id="user2">
<property name="id" value="2"/>
<property name="username" value="javaboy2"/>
<property name="address" value="www.javaboy.org"/>
</bean>

set 方法注入,有一个很重要的问题,就是属性名。很多人会有一种错觉,觉得属性名就是你定义的属性名,这个是不对的。在所有的框架中,凡是涉及到反射注入值的,属性名统统都不是 Bean 中定义的属性名,而是通过 Java 中的内省机制分析出来的属性名,简单说,就是根据 get/set 方法分析出来的属性名。

p 名称空间注入

1
<bean class="org.javaboy.ioc.model.User" id="user3" p:username="javaboy" p:id="3" p:address="itboy.com"></bean>

p 名称空间注入,使用的比较少,它本质上也是调用了 set 方法。

外部 Bean 的注入

有时候,我们使用一些外部 Bean,这些 Bean 可能没有构造方法,而是通过 Builder 来构造的,这个时候,就无法使用上面的方式来给它注入值了。

例如在 OkHttp 的网络请求中,原生的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package org.javaboy.ioc;

import okhttp3.*;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;

public class OkHttpTest {

public static void main(String[] args) {

OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
OkHttpClient okHttpClient = ctx.getBean("okHttpClient", OkHttpClient.class);
Request request = new Request.Builder()
.get()
.url("https://www.baidu.com")
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println("e.getMessage() = " + e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {

System.out.println("response.body() = " + response.body().string());

}
});

}

}

这个 Bean 有一个特点,OkHttpClient 和 Request 两个实例都不是直接 new 出来的,在调用 Builder 方法的过程中,都会给它配置一些默认的参数。这种情况,我们可以使用 静态工厂注入或者实例工厂注入来给 OkHttpClient 提供一个实例。

静态工厂注入

提供一个 OkHttpClient 的静态工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.javaboy.ioc;

import okhttp3.OkHttpClient;

public class OkHttpStaticFactory {

private static OkHttpClient okHttpClient;

public static OkHttpClient getInstance() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder().build();
}
return okHttpClient;
}

}

在 xml 文件中,配置该静态工厂:

1
<bean class="org.javaboy.ioc.OkHttpStaticFactory" factory-method="getInstance" id="okHttpClient"/>

这个配置表示 OkHttpStaticFactory类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。然后,在 Java 代码中,获取到这个实例,就可以直接使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.javaboy.ioc;

import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.io.IOException;

public class OkHttpTest {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
OkHttpClient okHttpClient = ctx.getBean("okHttpClient", OkHttpClient.class);
Request request = new Request.Builder()
.get()
.url("https://www.baidu.com")
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println("e.getMessage() = " + e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {

System.out.println("response.body() = " + response.body().string());

}
});

}

}

实例工厂注入

实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.javaboy.ioc;

import okhttp3.OkHttpClient;

public class OkHttpFactory {

private OkHttpClient okHttpClient;

public OkHttpClient getInstance() {
if (okHttpClient == null) {
okHttpClient = new OkHttpClient.Builder().build();
}
return okHttpClient;
}

}

此时,在 xml 文件中,需要首先提供工厂方法的实例,然后才可以调用工厂方法:

1
2
<bean class="org.javaboy.ioc.OkHttpFactory" id="okHttpFactory"/>
<bean class="okhttp3.OkHttpClient" factory-bean="okHttpFactory" factory-method="getInstance" id="okHttpClient"/>

自己写的 Bean 一般不会使用这两种方式注入,但是,如果需要引入外部 jar,外部 jar 的类的初始化,有可能需要使用这两种方式。

复杂属性的注入

1
2
3
4
5
6
7
8
9
public class User {
private String username;
private String address;
private Integer id;
private Cat cat;
private Cat[] cats;
private Map<String, Object> detail;
private Properties info;
}
1
2
3
4
public class Cat {
private String name;
private Integer age;
}

对象注入

1
2
3
4
5
6
7
8
<bean class="org.javaboy.User" id="user">
<property name="cat" ref="cat"/>
</bean>

<bean class="org.javaboy.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>

可以通过 xml 注入对象,通过 ref 来引用一个对象

数组注入

数组注入和集合注入在 xml 中的配置是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<bean class="org.javaboy.User" id="user">
<property name="cat" ref="cat"/>
<property name="favorites">
<array>
<value>足球</value>
<value>篮球</value>
<value>乒乓球</value>
</array>
</property>
</bean>

<bean class="org.javaboy.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>

array节点也可使用list节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<bean class="org.javaboy.User" id="user">
<property name="cat" ref="cat"/>
<property name="favorites">
<list>
<value>足球</value>
<value>篮球</value>
<value>乒乓球</value>
</list>
</property>
<property name="cats">
<list>
<ref bean="cat"/>
<ref bean="cat2"/>
<bean class="org.javaboy.Cat" id="cat3">
<property name="name" value="小花"/>
<property name="color" value="花色"/>
</bean>
</list>
</property>
</bean>

<bean class="org.javaboy.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>

<bean class="org.javaboy.Cat" id="cat2">
<property name="name" value="小黑"/>
<property name="color" value="黑色"/>
</bean>

注意,既可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。

Map注入

1
2
3
4
5
6
<property name="map">
<map>
<entry key="age" value="99"/>
<entry key="name" value="javaboy"/>
</map>
</property>

properties注入

1
2
3
4
5
6
<property name="info">
<props>
<prop key="age">99</prop>
<prop key="name">javaboy</prop>
</props>
</property>

Java配置

在 Spring 中,想要将一个 Bean 注册到 Spring 容器中,整体上来说,有三种不同的方式:

  • xml注入
  • Java配置,通过 Java 代码将 Bean 注册到 Spring 容器中
  • 自动化扫描

Java 配置这种方式在 Spring Boot 出现之前,其实很少使用,自从有了 Spring Boot,Java 配置开发被广泛使用,因为在 Spring Boot 中,不使用一行 XML 配置

例如我有如下一个 Bean:

1
2
3
4
5
public class SayHello {
public String sayHello(String name) {
return "hello " + name;
}
}

在 Java 配置中,我们用一个 Java 配置类去代替之前的 applicationContext.xml 文件:

1
2
3
4
5
6
7
@Configuration
public class JavaConfig {
@Bean
SayHello sayHello() {
return new SayHello();
}
}

首先在配置类上有一个 @Configuration 注解,这个注解表示这个类不是一个普通类,而是一个配置类,它的作用相当于 applicationContext.xml。 然后,定义方法,方法返回对象,方法上添加 @Bean 注解,表示将这个方法的返回值注入的 Spring 容器中去。也就是说,@Bean 所对应的方法,就相当于applicationContext.xml 中的 bean 节点。

既然是配置类,我们需要在项目启动时加载配置类。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
SayHello hello = ctx.getBean(SayHello.class);
System.out.println(hello.sayHello("哈哈哈哈哈"));
}
}

注意,配置的加载,是使用 AnnotationConfigApplicationContext 来实现。

Bean 的默认名称是方法名。以上面的案例为例,Bean 的名字是 sayHello。 如果开发者想自定义方法名,也是可以的,直接在 @Bean 注解中进行过配置。如下配置表示修改 Bean 的名字为 javaboy:

1
2
3
4
5
6
7
@Configuration
public class JavaConfig {
@Bean("javaboy")
SayHello sayHello() {
return new SayHello();
}
}

自动化配置

在我们实际开发中,大量的使用自动配置。

自动化配置既可以通过 Java 配置来实现,也可以通过 xml 配置来实现。

准备工作

例如我有一个 UserService,我希望在自动化扫描时,这个类能够自动注册到 Spring 容器中去,那么可以给该类添加一个 @Service,作为一个标记。

和 @Service 注解功能类似的注解,一共有四个:

  • @Component
  • @Repository
  • @Controller
  • @Controller

这四个中,另外三个都是基于 @Component 做出来的,而且从目前的源码来看,功能也是一致的,那么为什么要搞三个呢?主要是为了在不同的类上面添加时方便。

  • 在 Service 层上,添加注解时,使用 @Service
  • 在 Dao 层,添加注解时,使用 @Repository
  • 在 Controller 层,添加注解时,使用 @Controller
  • 在其他组件上添加注解时,使用 @Component
1
2
3
4
5
6
7
8
9
10
@Service
public class UserService {
public List<String> getAllUser() {
List<String> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add("javaboy:" + i);
}
return users;
}
}

添加完成后,自动化扫描有两种方式,一种就是通过 Java 代码配置自动化扫描,另一种则是通过 xml 文件来配置自动化扫描。

Java 代码配置自动扫描

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "org.javaboy.javaconfig.service")
public class JavaConfig {

}

然后,在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包(如果不指定,默认情况下扫描的是配置类所在的包下载的 Bean 以及配置类所在的包下的子包下的类),然后就可以获取 UserService 的实例了:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService userService = ctx.getBean(UserService.class);
System.out.println(userService.getAllUser());
}
}

注意:

  • 默认情况下,Bean 的名字是类名首字母小写。例如上面的 UserService,它的实例名,默认就是 userService。如果开发者想要自定义名字,就直接在 @Service 注解中添加即可。
  • 上面的配置,我们是按照包的位置来扫描的。也就是说,Bean 必须放在指定的扫描位置,否则,即使你有 @Service 注解,也扫描不到。

除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:

1
2
3
@Configuration
@ComponentScan(basePackages = "org.javaboy.javaconfig",useDefaultFilters = true,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)})
public class JavaConfig {}

这个配置表示扫描 org.javaboy.javaconfig 下的 除了 Controller之外 所有 Bean

XML 配置自动化扫描

下面这行配置表示扫描 org.javaboy.javaconfig 下的所有 Bean。当然也可以按照类来扫描。

1
<context:component-scan base-package="org.javaboy.javaconfig"/>

XML 配置完成后,在 Java 代码中加载 XML 配置即可。

1
2
3
4
5
6
7
8
public class XMLTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean(UserService.class);
List<String> list = userService.getAllUser();
System.out.println(list);
}
}

也可以在 XML 配置中按照注解的类型进行扫描:

1
2
3
<context:component-scan base-package="org.javaboy.javaconfig" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

对象注入问题

自动扫描时的对象注入有三种方式:

  • @Autowired, @Autowired 是根据类型去查找,然后赋值,这就有一个要求,这个类型只可以有一个对象,否则就会报错
  • @Resources, @Resources 是根据名称去查找,默认情况下,定义的变量名,就是查找的名称,当然开发者也可以在 @Resources 注解中手动指定
  • @Injected
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserService {

@Autowired
UserDao userDao;
public String hello() {
return userDao.hello();
}

public List<String> getAllUser() {
List<String> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add("javaboy:" + i);
}
return users;
}
}

如果一个类存在多个实例,那么就应该使用 @Resources 去注入,如果非得使用 @Autowired,也是可以的,此时需要配合另外一个注解,@Qualifier,在 @Qualifier 中可以指定变量名,两个一起用(@Qualifier 和 @Autowired)就可以实现通过变量名查找到变量。

混合配置

混合配置就是 Java 配置+XML 配置。混用的话,可以在 Java 配置中引入 XML 配置

1
2
3
@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {}

在 Java 配置中,通过 @ImportResource 注解可以导入一个 XML 配置。

条件注解

条件注解的示例

在 Windows 中如何获取操作系统信息?Windows 中查看文件夹目录的命令是 dir,Linux 中查看文件夹目录的命令是 ls,我现在希望当系统运行在 Windows 上时,自动打印出 Windows 上的目录展示命令,Linux 运行时,则自动展示 Linux 上的目录展示命令。

首先定义一个显示文件夹目录的接口:

1
2
3
public interface ShowCmd {
String showCmd();
}

然后,分别实现 Windows 下的实例和 Linux 下的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WinShowCmd implements ShowCmd {
@Override
public String showCmd() {
return "dir";
}
}

public class LinuxShowCmd implements ShowCmd {
@Override
public String showCmd() {
return "ls";
}
}

接下来,定义两个条件,一个是 Windows 下的条件,另一个是 Linux 下的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
}
}

public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("linux");
}
}

接下来,在定义 Bean 的时候,就可以去配置条件注解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class JavaConfig {
@Bean("showCmd")
@Conditional(WindowsCondition.class)
ShowCmd winCmd() {
return new WinShowCmd();
}

@Bean("showCmd")
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd() {
return new LinuxShowCmd();
}
}

这里,一定要给两个 Bean 取相同的名字,这样在调用时,才可以自动匹配。然后,给每一个 Bean 加上条件注解,当条件中的 matches 方法返回 true 的时候,这个 Bean 的定义就会生效。

1
2
3
4
5
6
7
public class JavaMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
ShowCmd showCmd = (ShowCmd) ctx.getBean("showCmd");
System.out.println(showCmd.showCmd());
}
}

条件注解有一个非常典型的使用场景,就是多环境切换。

多环境切换

开发中,如何在 开发/生产/测试 环境之间进行快速切换?Spring 中提供了 Profile 来解决这个问题,Profile 的底层就是条件注解。这个从 @Profile 注解的定义就可以看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();

}
class ProfileCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}

}

我们定义一个 DataSource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class DataSource {
private String url;
private String username;
private String password;

@Override
public String toString() {
return "DataSource{" +
"url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

然后,在配置 Bean 时,通过 @Profile 注解指定不同的环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean("ds")
@Profile("dev")
DataSource devDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/dev");
dataSource.setUsername("root");
dataSource.setPassword("123");
return dataSource;
}

@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://192.168.33.113:3306/dev");
dataSource.setUsername("jkldasjfkl");
dataSource.setPassword("jfsdjflkajkld");
return dataSource;
}

最后,在加载配置类,注意,需要先设置当前环境,然后再去加载配置类:

1
2
3
4
5
6
7
8
9
10
public class JavaMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(JavaConfig.class);
ctx.refresh();
DataSource ds = (DataSource) ctx.getBean("ds");
System.out.println(ds);
}
}

这个是在 Java 代码中配置的。

环境的切换,也可以在 XML 文件中配置,如下配置在 XML 文件中,必须放在其他节点后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans profile="dev">
<bean class="org.javaboy.DataSource" id="dataSource">
<property name="url" value="jdbc:mysql:///devdb"/>
<property name="password" value="root"/>
<property name="username" value="root"/>
</bean>
</beans>
<beans profile="prod">
<bean class="org.javaboy.DataSource" id="dataSource">
<property name="url" value="jdbc:mysql://192.168.33.113:3306/devdb"/>
<property name="password" value="jsdfaklfj789345fjsd"/>
<property name="username" value="root"/>
</bean>
</beans>

启动类中设置当前环境并加载配置:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("prod");
ctx.setConfigLocation("applicationContext.xml");
ctx.refresh();
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
System.out.println(dataSource);
}
}

Bean 的作用域

在 XML 配置中注册的 Bean,或者用 Java 配置注册的 Bean,如果我多次获取,获取到的对象是否是同一个?

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = ctx.getBean("user", User.class);
User user2 = ctx.getBean("user", User.class);
System.out.println(user==user2);
}
}

如上,从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例。

通过在 XML 节点中,设置 scope 属性,我们可以调整默认的实例个数。scope 的值为 singleton(默认),表示这个 Bean 在 Spring 容器中,是以单例的形式存在,如果 scope 的值为 prototype,表示这个 Bean 在 Spring 容器中不是单例,多次获取将拿到多个不同的实例。

1
<bean class="org.javaboy.User" id="user" scope="prototype" />

除了 singleton 和 prototype 之外,还有两个取值,request 和 session。这两个取值在 web 环境下有效。

上面的是在 XML 中的配置,我们也可以在 Java 中配置。

1
2
3
4
5
6
7
8
@Configuration
public class JavaConfig {
@Bean
@Scope("prototype")
SayHello sayHello() {
return new SayHello();
}
}

在 Java 代码中,我们可以通过 @Scope 注解指定 Bean 的作用域。

在自动扫描配置中,也可以指定bean的作用域

1
2
3
4
5
6
7
@Repository
@Scope("prototype")
public class UserDao {
public String hello() {
return "userdao";
}
}

id 和 name 的区别

在 XML 配置中,我们可以看到,即可以通过 id 给 Bean 指定一个唯一标识符,也可以通过 name 来指定,大部分情况下这两个作用是一样的,有一个小小区别:

name 支持取多个。多个 name 之间,用,隔开

1
<bean class="org.javaboy.User" name="user,user1,user2,user3" scope="prototype"/>

此时,通过 user、user1、user2、user3 都可以获取到当前对象:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = ctx.getBean("user", User.class);
User user2 = ctx.getBean("user2", User.class);
System.out.println(user);
System.out.println(user2);
}
}

而 id 不支持有多个值。如果强行用 , 隔开,它还是一个值。例如如下配置:

1
<bean class="org.javaboy.User" id="user,user1,user2,user3" scope="prototype" />

这个配置表示 Bean 的名字为 user,user1,user2,user3,具体调用如下:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = ctx.getBean("user,user1,user2,user3", User.class);
User user2 = ctx.getBean("user,user1,user2,user3", User.class);
System.out.println(user);
System.out.println(user2);
}
}

Aware接口

Aware 接口,从字面上理解就是感知捕获。单纯的一个 Bean 是没有知觉的。

在实际开发中,我们可能会遇到一些类,需要获取到容器的详细信息,那就可以通过 Aware 接口来实现。

Aware 是一个空接口,有很多实现类:

这些实现的接口,有一些公共特性:

  • 都是以 Aware 结尾
  • 都继承Aware
  • 接口内均定义了一个 set 方法

每一个子接口均提供了一个 set 方法,方法的参数就是当前 Bean 需要感知的内容,因此我们需要在 Bean 中声明相关的成员变量来接受这个参数。接收到这个参数后,就可以通过这个参数获取到容器的详细信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package org.javaboy.aware;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

@Service
@PropertySource(value = "javaboy.properties")
public class AwareService implements BeanNameAware, BeanFactoryAware, ResourceLoaderAware, EnvironmentAware {

private String beanName;
private ResourceLoader resourceLoader;
private Environment environment;

public void output() throws IOException {
System.out.println("beanName = " + beanName);
Resource resource = resourceLoader.getResource("javaboy.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()));
String s = br.readLine();
System.out.println("s = " + s);
br.close();
String address = environment.getProperty("javaboy.address");
System.out.println("address = " + address);
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {

}

@Override
public void setBeanName(String s) {
this.beanName = s;
}

@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}

AOP

aop简介

AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程。

AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

面向切面编程,就是在程序运行时,不改变程序源码的情况下,动态的增强方法的功能,常见的使用场景非常多:日志、事务、数据库操作等。

这些操作中,无一例外,都有很多模板化的代码,而解决模板化代码,消除臃肿就是 Aop 的强项。

在 Aop 中,有几个常见的概念:

概念 说明
Target(目标对象) 代理的目标对象
Proxy (代理) 一个类被 AOP 织入增强后,就产生一个结果代理类
Joinpoint(连接点) 指那些可以被拦截到的点。在spring中,这些点指的是方法,因为 spring只支持方法类型的连接点
Pointcut(切入点) 指我们要对哪些 Joinpoint 进行拦截
Advice(通知/ 增强) 通知是指拦截到 Joinpoint 之后所要做的事情,分为前置通知、后置通知、异常通知、最终通知、环绕通知
Aspect(切面) 是切入点和通知(引介)的结合
Weaving(织入) 是指把增强应用到目标对象来创建新的代理对象的程。spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入

在 Aop 实际上集基于 Java 动态代理来实现的,Java 中的动态代理有两种实现方式:

  • jdk
  • cglib

基于JDK的动态代理

不使用spring的aop来实现

  • 定义一个计算器的接口
1
2
3
4
5
public interface MyCalculator {

int add(int a, int b);

}
  • 定义计算器接口的实现
1
2
3
4
5
6
public class MyCalculatorImpl implements MyCalculator {
@Override
public int add(int a, int b) {
return a + b;
}
}

定义代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class CalculatorProxy {

public static Object getInstance(MyCalculatorImpl myCalculator) {

return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(), myCalculator.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println(method.getName() + "方法开始执行了");
Object invoke = method.invoke(myCalculator, args);
System.out.println(method.getName() + "方法执行结束了");

return invoke;
}
});

}

}

Proxy.newProxyInstance 方法接收三个参数,第一个是一个 classloader,第二个是代理多项实现的接口,第三个是代理对象方法的处理器,所有要额外添加的行为都在 invoke 方法中实现。

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Main {

public static void main(String[] args) {

MyCalculatorImpl myCalculator = new MyCalculatorImpl();
MyCalculator calculator = (MyCalculator) CalculatorProxy.getInstance(myCalculator);
int add = calculator.add(3, 4);
System.out.println("add = " + add);

test("a", "e", "aaa");

}

public static void test(String... x) {
for (String s : x) {
System.out.println("s = " + s);
}
}

}

/*输出结果:
add方法开始执行了
add方法执行结束了
add = 7
s = a
s = e
s = aaa
*/

AOP的五种通知

Spring 中的 Aop 的通知类型有 5 种:

  • 前置通知
  • 后置通知
  • 异常通知
  • 返回通知
  • 环绕通知

首先添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.6</version>
</dependency>
</dependencies>

接下来,定义切点,这里有两种切点的定义方式:

  • 使用自定义注解
  • 使用规则

其中,使用自定义注解标记切点,是侵入式的,所以这种方式在实际开发中不推荐,仅作为了解,另一种使用规则来定义切点的方式,无侵入,一般推荐使用这种方式。

自定义注解

首先自定义一个注解:

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}

计算器的接口:

1
2
3
4
5
6
7
public interface MyCalculator {

int add(int a, int b);

void min(int a, int b);

}

然后在需要拦截的方法上,添加该注解,在 add 方法上添加了 @Action 注解,表示该方法将会被 Aop 拦截,而其他未添加该注解的方法则不受影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.stereotype.Component;

@Component
public class MyCalculatorImpl implements MyCalculator {
@Action
@Override
public int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

@Override
public void min(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}

接下来,定义增强(通知、Advice):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect // 表示这是一个切面
public class LogAspect {

/**
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*
* @param joinPoint 包含了目标方法的关键信息
*/
@Before("@annotation(Action)")
public void before(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法开始执行了。。。");
}

/**
* @After 表示这是一个后置通知,即在目标方法执行之后执行
* @param joinPoint
*/
@After("@annotation(Action)")
public void after(JoinPoint joinPoint) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法执行结束了。。。");
}

/**
* @AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应
* 注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
* @param joinPoint
* @param r
*/
@AfterReturning(value = "@annotation(Action)", returning = "r")
public void returning(JoinPoint joinPoint, Integer r) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法返回通知:" + r);
}

/**
* 异常通知
*
* @param joinPoint
* @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
*/
@AfterThrowing(value = "@annotation(Action)", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
String name = joinPoint.getSignature().getName();
System.out.println(name + "方法返回通知:" + e.getMessage());
}


/**
* 环绕通知
* 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
*
* @param pjp
* @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
*/
@Around("@annotation(Action)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object proceed = pjp.proceed();
return proceed;
}

}

通知定义完成后,接下来在配置类中,开启包扫描和自动代理:

1
2
3
4
5
6
7
8
9
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class JavaConfig {
}

在 Main 方法中,开启调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
MyCalculator calculator = ctx.getBean(MyCalculator.class);
calculator.add(3, 4);
calculator.min(3, 4);

}

}

方法中统一定义切点

再来回顾 LogAspect 切面,我们发现,切点的定义不够灵活,之前的切点是直接写在注解里边的,这样,如果要修改切点,每个方法上都要修改,因此,我们可以将切点统一定义,然后统一调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Aspect//表示这是一个切面
public class LogAspect {

/**
* 可以统一定义切点
*/
@Pointcut("@annotation(Action)")
public void pointcut() {

}

/**
* @param joinPoint 包含了目标方法的关键信息
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}
}

非侵入定义切点

上面使用注解是侵入式的,我们还可以继续优化,改为非侵入式的。重新定义切点,新切点的定义就不在需要 @Action 注解了,要拦截的目标方法上也不用添加 @Action 注解。下面这种方式是更为通用的拦截方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
@Aspect//表示这是一个切面
public class LogAspect {
/**
* 可以统一定义切点
* 第一个 * 表示要拦截的目标方法返回值任意(也可以明确指定返回值类型)
* 第二个 * 表示包中的任意类(也可以明确指定类)
* 第三个 * 表示类中的任意方法
* 最后面的两个点表示方法参数任意,个数任意,类型任意
*/
@Pointcut("execution(* com.cwz.aop.commons.*.*(..))")
public void pointcut() {

}

/**
* @param joinPoint 包含了目标方法的关键信息
* @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}
}

XML配置AOP

定义通知/增强,但是单纯定义自己的行为即可,不再需要注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class LogAspect {

public void before(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法开始执行了...");
}

public void after(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法执行结束了...");
}

public void returing(JoinPoint joinPoint,Integer r) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法返回:"+r);
}

public void afterThrowing(JoinPoint joinPoint,Exception e) {
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println(name + "方法抛异常了:"+e.getMessage());
}

public Object around(ProceedingJoinPoint pjp) {
Object proceed = null;
try {
//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
proceed = pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return proceed;
}
}

在spring中添加xml配置:

1
2
3
4
5
6
7
8
9
10
11
<bean class="org.javaboy.aop.LogAspect" id="logAspect"/>
<aop:config>
<aop:pointcut id="pc1" expression="execution(* org.javaboy.aop.commons.*.*(..))"/>
<aop:aspect ref="logAspect">
<aop:before method="before" pointcut-ref="pc1"/>
<aop:after method="after" pointcut-ref="pc1"/>
<aop:after-returning method="returing" pointcut-ref="pc1" returning="r"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pc1" throwing="e"/>
<aop:around method="around" pointcut-ref="pc1"/>
</aop:aspect>
</aop:config>

在 Main 方法中加载配置文件:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);
myCalculator.add(3, 4);
myCalculator.min(5, 6);
}
}

JdbcTemplate

JdbcTemplate 是 Spring 利用 Aop 思想封装的 JDBC 操作工具。

比原生的jdbc方便,但不如mybatis这种。

准备工作

添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.6E</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
</dependencies>

准备数据:

1
2
3
4
5
6
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`address` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class User {
private Integer id;
private String username;
private String address;

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", address='" + address + '\'' +
'}';
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

Java配置

提供一个配置类,在配置类中配置 JdbcTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class JdbcConfig {
@Bean
DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql:///test01");
return dataSource;
}
@Bean
JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
}

提供两个 Bean,一个是 DataSource 的 Bean,另一个是 JdbcTemplate 的 Bean,JdbcTemplate 的配置非常容易,只需要 new 一个 Bean 出来,然后配置一下 DataSource 就可以。

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Main {

private JdbcTemplate jdbcTemplate;

@Before
public void before() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JdbcConfig.class);
jdbcTemplate = ctx.getBean(JdbcTemplate.class);
}

@Test
public void insert() {
jdbcTemplate.update("insert into user (username,address) values (?,?);", "javaboy", "www.javaboy.org");
}
@Test
public void update() {
jdbcTemplate.update("update user set username=? where id=?", "javaboy123", 1);

}
@Test
public void delete() {
jdbcTemplate.update("delete from user where id=?", 2);
}

@Test
public void select() {
User user = jdbcTemplate.queryForObject("select * from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);
System.out.println(user);
}
}

在查询时,如果使用了 BeanPropertyRowMapper,要求查出来的字段必须和 Bean 的属性名一一对应。如果不一样,则不要使用 BeanPropertyRowMapper,此时需要自定义 RowMapper 或者给查询的字段取别名。

  • 自定义 RowMapper:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void select2() {
User user = jdbcTemplate.queryForObject("select * from user where id=?", new RowMapper<User>() {
public User mapRow(ResultSet resultSet, int i) throws SQLException {
int id = resultSet.getInt("id");
String username = resultSet.getString("username");
String address = resultSet.getString("address");
User u = new User();
u.setId(id);
u.setName(username);
u.setAddress(address);
return u;
}
}, 1);
System.out.println(user);
}
  • 给查询的字段取别名
1
2
3
4
5
@Test
public void select3() {
User user = jdbcTemplate.queryForObject("select id,username as name,address from user where id=?", new BeanPropertyRowMapper<User>(User.class), 1);
System.out.println(user);
}

XML配置

1
2
3
4
5
6
7
8
9
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="url" value="jdbc:mysql:///test01?serverTimezone=Asia/Shanghai"/>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

事务

Spring 中的事务主要是利用 Aop 思想,简化事务的配置,可以通过 Java 配置也可以通过 XML 配置。

我们通过一个转账操作来看下 Spring 中的事务配置。

配置 JdbcTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class JdbcConfig {
@Bean
DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setUrl("jdbc:mysql:///test02");
return dataSource;
}
@Bean
JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
}

提供转账操作的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
public class UserDao {
@Autowired
JdbcTemplate jdbcTemplate;

public void addMoney(String username, Integer money) {
jdbcTemplate.update("update account set money=money+? where username=?", money, username);
}

public void minMoney(String username, Integer money) {
jdbcTemplate.update("update account set money=money-? where username=?", money, username);
}
}
@Service
public class UserService {
@Autowired
UserDao userDao;
public void updateMoney() {
userDao.addMoney("zhangsan", 200);
int i = 1 / 0;
userDao.minMoney("lisi", 200);
}
}

最后,在 XML 文件中,开启自动化扫描:

1
2
3
4
5
6
7
8
9
10
<context:component-scan base-package="org.javaboy"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="url" value="jdbc:mysql:///test02?serverTimezone=Asia/Shanghai"/>
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

XML配置

配置 TransactionManager

1
2
3
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

配置事务要处理的方法

1
2
3
4
5
6
7
8
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="update*"/>
<tx:method name="insert*"/>
<tx:method name="add*"/>
<tx:method name="delete*"/>
</tx:attributes>
</tx:advice>

配置aop

1
2
3
4
<aop:config>
<aop:pointcut id="pc1" expression="execution(* org.javaboy.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>

测试:

1
2
3
4
5
6
7
8
9
10
@Before
public void before() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = ctx.getBean(JdbcTemplate.class);
userService = ctx.getBean(UserService.class);
}
@Test
public void test1() {
userService.updateMoney();
}

Java配置

如果要开启 Java 注解配置,在 XML 配置中添加如下配置:

1
<tx:annotation-driven transaction-manager="transactionManager" />

上面的配置相当于:

1
2
3
4
5
6
7
8
9
10
11
12
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="update*"/>
<tx:method name="insert*"/>
<tx:method name="add*"/>
<tx:method name="delete*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pc1" expression="execution(* org.javaboy.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc1"/>
</aop:config>

在需要添加事务的方法上,添加 @Transactional 注解,表示该方法开启事务,当然,这个注解也可以放在类上,表示这个类中的所有方法都开启事务。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {
@Autowired
UserDao userDao;
@Transactional
public void updateMoney() {
userDao.addMoney("zhangsan", 200);
int i = 1 / 0;
userDao.minMoney("lisi", 200);
}
}

Spring MVC

MVC模式

MVC是软件工程中的一种软件架构模式,它是一种分离业务逻辑与显示界面的开发思想。

1
2
3
* M(model)模型:处理业务逻辑,封装实体
* V(view) 视图:展示内容
* C(controller)控制器:负责调度分发(1.接收请求、2.调用模型、3.转发到视图)

SpringMVC简介

Spring Web MVC 是一种基于 Java 的实现了 Web MVC 设计模式的轻量级 Web 框架,使用了 MVC 架构模式的思想,将 web 层进行职责解耦,基于请求驱动指的就是使用请求-响应模型。

在 传统的 Jsp/Servlet 技术体系中,如果要开发接口,一个接口对应一个 Servlet,会导致我们开发出许多 Servlet,使用 SpringMVC 可以有效的简化这一步骤。

SpringMVC的优势:

  • 能非常简单的设计出干净的 Web 层
  • 天生与 Spring 框架集成(如 IoC 容器、AOP 等)
  • 非常容易与其他视图技术集成,如 Velocity、FreeMarker 等等,因为模型数据不放在特定的 API 里,而是放在一个 Model 里。
  • 支持灵活的本地化、主题等解析
  • 支持 RESTful 风格

SpringMVC的框架就是封装了原来Servlet中的共有行为;例如:参数封装,视图转发等

SpringMVC实现一个helloword

创建一个Maven工程,添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
</dependency>

添加了 spring-webmvc 依赖之后,其他的 spring-web、spring-aop、spring-context 等等就全部都加入进来了。

这是一个Java SE工程,现在要把它改造成web工程

依赖加入:

1
<packaging>war</packaging>

右键打开Open Module Settings

这样就把webapp创建出来了

编写Controller类和视图页面

准备一个 Controller,即一个处理浏览器请求的接口

MyController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.cwz.springmvc01.controller;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class MyController implements Controller {

/**
* 这个方法用来处理请求
* @param httpServletRequest 这个是前端发送过来的请求
* @param httpServletResponse 这个是服务端给前端的响应
* @return 返回 ModelAndView Model就相当于数据模型,View 就是视图
* @throws Exception
*/
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mv = new ModelAndView("hello");
mv.addObject("name", "cwz");
return mv;
}
}

采用 jsp 作为视图,在 webapp 目录下创建 hello.jsp 文件,内容如下:

/WEB-INF/jsp/ hello.jsp

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
hello ${name} !
</body>
</html>

配置springmvc核心配置文件 spring-servlet.xml

在 resources 目录下,创建一个名为 spring-servlet.xml 的 springmvc 的配置文件,这里,我们先写一个简单的 demo ,因此可以先不用添加 spring 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--首先配置一个MyController的实例-->
<bean class="com.cwz.springmvc01.controller.MyController" name="/hello"/>

<!--配置处理器映射器,将前端请求和后端的接口关联起来-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="beanNameUrlHandlerMapping">
<property name="beanName" value="/hello"/>
</bean>

<!--处理器适配器-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>

<!--试图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>

</beans>

加载 springmvc 配置文件

在 web 项目启动时,加载 springmvc 配置文件,这个配置是在 web.xml 中完成的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

所有请求都将自动拦截下来,拦截下来后,请求交给 DispatcherServlet 去处理,在加载 DispatcherServlet 时,还需要指定配置文件路径。

这里有一个默认的规则,如果配置文件放在 webapp/WEB-INF/ 目录下,并且配置文件的名字等于 DispatcherServlet 的名字+ -servlet(即这里的配置文件路径是 webapp/WEB-INF/springmvc-servlet.xml),如果是这样的话,可以不用添加 init-param 参数,即不用手动配置 springmvc 的配置文件,框架会自动加载。

最后项目启动,进行检验

SpringMVC的执行流程

  • 用户发送请求至前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping处理器映射器
  • 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  • DispatcherServlet调用HandlerAdapter处理器适配器
  • HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
  • Controller执行完成返回ModelAndView
  • HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
  • DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  • ViewReslover解析后返回具体View
  • DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
  • DispatcherServlet将渲染后的视图响应响应用户

SpringMVC中组件

  • DispatcherServlet:前端控制器

用户请求到达前端控制器,它就相当于 mvc 模式中的c,DispatcherServlet 是整个流程控制的中心,相当于是 SpringMVC 的大脑,由它调用其它组件处理用户的请求,DispatcherServlet 的存在降低了组件之间的耦合性。

  • HandlerMapping:处理器映射器

HandlerMapping 负责根据用户请求找到 Handler 即处理器(也就是我们所说的 Controller),SpringMVC 提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等,在实际开发中,我们常用的方式是注解方式。

  • Handler:处理器

Handler 是继 DispatcherServlet 前端控制器的后端控制器,在DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。由于 Handler 涉及到具体的用户业务请求,所以一般情况需要程序员根据业务需求开发 Handler。(这里所说的 Handler 就是指我们的 Controller)

  • HandlAdapter:处理器适配器

通过 HandlerAdapter 对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行

  • ViewResolver:视图解析器

ViewResolver 负责将处理结果生成 View 视图,ViewResolver 首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成 View 视图对象,最后对 View 进行渲染将处理结果通过页面展示给用户

SpringMVC 中央控制器DispatcherServlet

DispatcherServlet 是前端控制器设计模式的实现,提供 Spring Web MVC 的集中访问点,而且负责职责的分派,而且与 Spring IOC 容器无缝集成,从而可以获得 Spring 的所有好处。DispatcherServlet 主要用作职责调度工作,本身主要用于控制流程,主要职责如下:

  • 通过 HandlerMapping,将请求映射到处理器(返回一个 HandlerExecutionChain,它包括一个处理器、多个 HandlerInterceptor 拦截器)
  • 通过 HandlerAdapter 支持多种类型的处理器(HandlerExecutionChain 中的处理器)
  • 通过 ViewResolver 解析逻辑视图名到具体视图实现
  • 本地化解析,渲染具体的试图等
  • 如果执行过程中遇到异常将交给 HandlerExceptionResolver 来解析

DispathcherServlet配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
  • load-on-startup:表示启动容器时初始化该 Servlet
  • contextConfigLocation:表示 SpringMVC 配置文件的路径
  • url-pattern:表示哪些请求交给 Spring Web MVC 处理, / 是用来定义默认 servlet 映射的。也可以如 *.html 表示拦截所有以 html 为扩展名的请求
  • contextClass:实现WebApplicationContext接口的类,当前的servlet用它来创建上下文。如果这个参数没有指定, 默认使用XmlWebApplicationContext
  • contextConfigLocation:传给上下文实例(由contextClass指定)的字符串,用来指定上下文的位置。这个字符串可以被分成多个字符串(使用逗号作为分隔符) 来支持多个上下文(在多上下文的情况下,如果同一个bean被定义两次,后面一个优先)
  • namespace:WebApplicationContext命名空间。默认值是[server-name]-servlet

Spring和SpringMVC分开配置

之前的hello案例中,只有 SpringMVC,没有 Spring,Web 项目也是可以运行的。在实际开发中,Spring 和 SpringMVC 是分开配置的,所以对上面的项目继续进行完善,添加 Spring 相关配置。

项目添加一个service包,提供HelloService类:

1
2
3
4
5
6
7
8
9
10
11
package com.cwz.springmvc01.service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
public String hello(String name) {
return "hello" + name;
}
}

将 HelloService 注入到 Spring 容器中并使用它,这个是属于 Spring 层的 Bean,所以我们一般将除了 Controller 之外的所有 Bean 注册到 Spring 容器中,而将 Controller 注册到 SpringMVC 容器中,在 resources 目录下添加 applicationContext.xml 作为 spring 的配置:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<!--这个包下除了controller都扫描-->
<context:component-scan base-package="com.cwz.springmvc01" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>

这个配置文件默认情况下不会被加载,需要在 web.xml 中对其进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<!--spring的配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!--springmvc的配置文件-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

通过 context-param 指定 Spring 配置文件的位置,这个配置文件也有一些默认规则,它的配置文件名默认就叫 applicationContext.xml 。

如果你将这个配置文件放在 WEB-INF 目录下,那么这里就可以不用指定配置文件位置了,只需要指定监听器就可以了。这段配置是 Spring 集成 Web 环境的通用配置;一般用于加载除 Web 层的 Bean(如DAO、Service 等),以便于与其他任何Web框架集成。

配置完成之后,还需要修改 MyController,在 MyController 中注入 HelloSerivce:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.cwz.springmvc01.controller;

import com.cwz.springmvc01.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@org.springframework.stereotype.Controller("/hello")
public class MyController implements Controller {

@Autowired
HelloService helloService;

/**
* 这个方法用来处理请求
* @param httpServletRequest 这个是前端发送过来的请求
* @param httpServletResponse 这个是服务端给前端的响应
* @return 返回 ModelAndView Model就相当于数据模型,View 就是视图
* @throws Exception
*/
@Override
public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
ModelAndView mv = new ModelAndView("hello");
mv.addObject("name", "cwz");
System.out.println("helloService = " + helloService);
return mv;
}
}

为了在 SpringMVC 容器中能够扫描到 MyController ,这里给 MyController 添加了 @Controller 注解,同时,由于我们目前采用的 HandlerMapping 是 BeanNameUrlHandlerMapping(意味着请求地址就是处理器 Bean 的名字),所以,还需要手动指定 MyController 的名字。

最后,修改 SpringMVC 的配置文件,将 Bean 配置为扫描形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<!--首先配置一个MyController的实例-->
<!-- <bean class="com.cwz.springmvc01.controller.MyController" name="/hello"/>-->

<!--除了controller其他的都不扫-->
<context:component-scan base-package="com.cwz.springmvc01" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!--配置处理器映射器,将前端请求和后端的接口关联起来-->
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="beanNameUrlHandlerMapping">
<property name="beanName" value="/hello"/>
</bean>

<!--处理器适配器-->
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" id="handlerAdapter"/>

<!--试图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>

</beans>

Spring容器和SpringMVC容器

当 Spring 和 SpringMVC 同时出现,我们的项目中将存在两个容器,一个是 Spring 容器,另一个是 SpringMVC 容器,Spring 容器通过 ContextLoaderListener 来加载,SpringMVC 容器则通过 DispatcherServlet 来加载,这两个容器不一样:

可以看出:

  • ContextLoaderListener 初始化的上下文加载的 Bean 是对于整个应用程序共享的,一般如 DAO 层、Service 层 Bean;
  • DispatcherServlet 初始化的上下文加载的 Bean 是只对 Spring Web MVC 有效的 Bean,如 Controller、HandlerMapping、HandlerAdapter 等等,该初始化上下文应该只加载 Web相关组件。
  • 子容器可以访问父容器,但父容器不能访问子容器,Spring是父容器,SpringMVC是子容器。

Spring容器中不扫描所有Bean:

  • 因为请求达到服务端后,找 DispatcherServlet 去处理,只会去 SpringMVC 容器中找,这就意味着 Controller 必须在 SpringMVC 容器中扫描

为什么不在 SpringMVC 容器中扫描所有 Bean:

  • 这个是可以的,可以在 SpringMVC 容器中扫描所有 Bean。不写在一起,是为了方便配置文件的管理

SpringMVC处理器详解

HandlerMapping

在 SpringMVC 中,系统提供了很多 HandlerMapping:

HandlerMapping 是负责根据 request 请求找到对应的 Handler 处理器及 Interceptor 拦截器,将它们封装在 HandlerExecutionChain 对象中返回给前端控制器。

BeanNameUrlHandlerMapping

BeanNameUrl 处理器映射器,根据请求的 url 与 Spring 容器中定义的 bean 的 name 进行匹配,从而从 Spring 容器中找到 bean 实例,就是说,请求的 url 地址就是处理器 Bean 的名字。但是不能配置多个请求路径。

配置如下:

1
2
3
4
<bean class="com.cwz.springmvc01.controller.MyController" name="/hello"/>
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" id="beanNameUrlHandlerMapping">
<property name="beanName" value="/hello"/>
</bean>

SimpleUrlHandlerMapping

SimpleUrlHandlerMapping 是 BeanNameUrlHandlerMapping 的增强版本,它可以将 url 和处理器 bean 的 id 进行统一映射配置:

1
2
3
4
5
6
7
8
9
10
11
12
<bean class="com.cwz.springmvc01.controller.MyController" name="myController"/>
<bean class="com.cwz.springmvc01.controller.MyController2" name="myController2"/>

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
<property name="mappings">
<props>
<prop key="/hello">myController</prop>
<prop key="/hello2">myController2</prop>
</props>
</property>
</bean>

注意,在 props 中,可以配置多个请求路径和处理器实例的映射关系。

HandlerAdapter

controller实现的方式很多,每一种执行方式肯定是不一样的。处理器适配器会根据执行的方式执行不同的代码。

HandlerAdapter 会根据适配器接口对后端控制器进行包装(适配),包装后即可对处理器进行执行,通过扩展处理器适配器可以执行多种类型的处理器,这里使用了适配器设计模式。

在 SpringMVC 中,HandlerAdapter 也有诸多实现类:

SimpleControllerHandlerAdapter

SimpleControllerHandlerAdapter简单控制器处理器适配器,所有实现了org.springframework.web.servlet.mvc.Controller接口的Bean通过此适配器进行适配。即 如果我们开发的接口是通过实现 Controller 接口来完成的(不是通过注解开发的接口),那么 HandlerAdapter 必须是 SimpleControllerHandlerAdapter。

1
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />

HttpRequestHandlerAdapter

http请求处理器适配器,所有实现了org.springframework.web.HttpRequestHandler 接口的 Bean 通过此适配器进行适配

例如:

1
2
3
4
5
6
@Controller
public class MyController3 implements HttpRequestHandler {
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("-----MyController3-----");
}
}
1
2
3
4
5
6
7
8
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" id="handlerMapping">
<property name="mappings">
<props>
<prop key="/hello3">myController3</prop>
</props>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" id="handlerAdapter"/>

处理器适配器与处理器映射器的最佳实践

  • 组件自动扫描

web 开发中,我们基本上不再通过 XML 或者 Java 配置来创建一个 Bean 的实例,而是直接通过组件扫描来实现 Bean 的配置,如果要扫描多个包,多个包之间用, 隔开即可:

spring配置:

1
2
3
<context:component-scan base-package="com.cwz.springmvc02" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

springmvc配置:

1
2
3
<context:component-scan base-package="com.cwz.springmvc02" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
  • HandlerMapping

正常情况下,我们在项目中使用的是 RequestMappingHandlerMapping,这个是根据处理器中的注解,来匹配请求(即 @RequestMapping 注解中的 url 属性)。因为在上面我们都是通过实现类来开发接口的,相当于还是一个类一个接口,所以,我们可以通过 RequestMappingHandlerMapping 来做处理器映射器,这样我们可以在一个类中开发出多个接口。

  • HandlerAdapter

对于上面提到的通过 @RequestMapping 注解所定义出来的接口方法,这些方法的调用都是要通过 RequestMappingHandlerAdapter 这个适配器来实现

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MyController {
@RequestMapping("/hello")
public ModelAndView hello(){
ModelAndView mv = new ModelAndView("hello");
mv.addObject("name", "cwz");
return mv;
}
}

要能够访问到这个接口,我们需要 RequestMappingHandlerMapping 才能定位到需要执行的方法,需要 RequestMappingHandlerAdapter,才能执行定位到的方法,修改 springmvc 的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<context:component-scan base-package="com.cwz.springmvc02" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<!-- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" id="requestMappingHandlerMapping"/>-->
<!-- <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" id="requestMappingHandlerAdapter"/>-->

<!--可以代替上面两个-->
<mvc:annotation-driven/>

<!--视图解析-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="viewResolver">
<property name="prefix" value="/"/>
<property name="suffix" value=".jsp"/>
</bean>

</beans>

SpringMVC中Controller的细节问题

@RequestMapping

这个注解用来标记一个接口,在接口开发中,使用最多的注解之一

  • 请求URL

标记请求 URL 很简单,只需要在相应的方法上添加该注解即可:

1
2
3
4
5
6
7
@Controller
public class HelloController {
@RequestMapping("/hello")
public ModelAndView hello() {
return new ModelAndView("hello");
}
}

这里 @RequestMapping(“/hello”) 表示当请求地址为 /hello 的时候,这个方法会被触发。其中,地址可以是多个,就是可以多个地址映射到同一个方法。

1
2
3
4
5
6
7
@Controller
public class HelloController {
@RequestMapping({"/hello","/hello2"})
public ModelAndView hello() {
return new ModelAndView("hello");
}
}

这个配置,表示 /hello 和 /hello2 都可以访问到该方法。

请求窄化

同一个项目中,会存在多个接口,例如订单相关的接口都是 /order/xxx 格式的,用户相关的接口都是 /user/xxx格式的。为了方便处理,这里的前缀(就是 /order、/user)可以统一在 Controller 上面处理。

1
2
3
4
5
6
7
8
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping("/hello")
public ModelAndView hello() {
return new ModelAndView("hello");
}
}

当类上加了 @RequestMapping 注解之后,此时,要想访问到 hello ,地址就应该是/user/hello

请求方法限定

默认情况下,使用 @RequestMapping 注解定义好的方法,可以被 GET 请求访问到,也可以被 POST 请求访问到,但是 DELETE 请求以及 PUT 请求不可以访问到。

1
2
3
4
5
6
7
8
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping(value = "/hello",method = RequestMethod.GET)
public ModelAndView hello() {
return new ModelAndView("hello");
}
}

通过 @RequestMapping 注解,指定了该接口只能被 GET 请求访问到,此时,该接口就不可以被 POST 以及请求请求访问到了。强行访问会报405错误

Controller的返回值

返回ModelAndView

如果是前后端不分的开发,大部分情况下,我们返回 ModelAndView,即数据模型+视图:

1
2
3
4
5
6
7
8
9
10
@Controller
@RequestMapping("/user")
public class HelloController {
@RequestMapping("/hello")
public ModelAndView hello() {
ModelAndView mv = new ModelAndView("hello");
mv.addObject("name", "cwz");
return mv;
}
}

Model 中,放我们的数据,然后在 ModelAndView 中指定视图名称。

hello.jsp

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>hello ${name}</h1>
</body>
</html>

返回void

没有返回值,并不一定真的没有返回值,只是方法的返回值为 void,我们可以通过其他方式给前端返回。

这种方式也可以理解为 Servlet 中的那一套方案

  • 通过 HttpServletRequest 做服务端跳转
1
2
3
4
@RequestMapping("/hello2")
public void hello2(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/hello.jsp").forward(req,resp);//服务器端跳转
}
  • 通过 HttpServletResponse 做重定向
1
2
3
4
@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.sendRedirect("/hello.jsp");
}

也可以自己手动指定响应头去实现重定向:

1
2
3
4
5
@RequestMapping("/hello3")
public void hello3(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setStatus(302);
resp.addHeader("Location", "/hello.jsp");
}
  • 通过 HttpServletResponse 给出响应
1
2
3
4
5
@RequestMapping("/hello4")
public void hello4(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("hello world");
}

这种方式,既可以返回 JSON,也可以返回普通字符串。

返回字符串

  • 返回逻辑视图名

前面的 ModelAndView 可以拆分为两部分,Model 和 View,在 SpringMVC 中,Model 我们可以直接在参数中指定,然后返回值是逻辑视图名

1
2
3
4
5
@RequestMapping("/hello5")
public String hello5(Model model) {
model.addAttribute("name", "cwz");//这是数据模型
return "hello";//表示去查找一个名为 hello 的视图
}
  • 服务端跳转
1
2
3
4
@RequestMapping("/hello5")
public String hello5() {
return "forward:/hello.jsp"; // forward 后面跟上跳转的路径
}
  • 客户端跳转
1
2
3
4
@RequestMapping("/hello5")
public String hello5() {
return "redirect:/user/hello";
}

本质上就是浏览器重定向

上面几种情况返回的字符串是有特殊的条件的,如需要forward等跳转。

如果一定要返回一个字符串,需要额外添加一个注意:@ResponseBody ,这个注解表示当前方法的返回值就是要展示出来返回值,没有特殊含义

1
2
3
4
5
@RequestMapping(value = "/hello5",produces = "text/html;charset=utf-8")
@ResponseBody
public String hello5() {
return "hello world!";
}

参数绑定

默认支持的参数类型

就是可以直接写在 @RequestMapping 所注解的方法中的参数类型,一共有四类:

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • Model/ModelMap
1
2
public void hello2(HttpServletRequest req, HttpServletResponse resp, HttpSession session, Model model, ModelMap modelMap) throws ServletException, IOException {
}

在请求的方法中,默认的参数就是这几个,如果在方法中,刚好需要这几个参数,那么就可以把这几个参数加入到方法中。

简单数据类型

Integer、Boolean、Double 等等简单数据类型也都是支持的

在 /webapp/ 目录下创建 addbook.jsp 作为图书添加页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>添加图书</h1>
<form action="/book/addbook2" method="post">
<table>
<tr>
<td>图书名称</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>图书作者</td>
<td><input type="text" name="author"></td>
</tr>
<tr>
<td>图书价格</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td><input type="submit" value="添加"></td>
</tr>
</table>
</form>
</body>
</html>

创建控制器,控制器提供两个功能,一个是访问 jsp 页面,另一个是提供添加接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@RequestMapping("book")
public class BookController {

@GetMapping("/book")
public String book() {
return "addbook";
}

@PostMapping(value = "/addbook", produces = "text/html;charset=utf-8")
@ResponseBody
public String addBook(String name, String author, Double price) {
System.out.println("name = " + name);
return name + ">>>" + author + ">>>" + price;
}

由于 addBook方法只想返回字符串,所以需要给该方法添加 @ResponseBody 注解,表示这个方法到此为止,不用再去查找相关视图了。另外, POST 请求传上来的中文会乱码,所以,我们在 web.xml 中再额外添加一个编码过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<filter>
<filter-name>encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

最后,浏览器中输入 http://localhost:8080/book/book ,就可以执行添加操作,服务端会打印出来相应的日志。

在上面的绑定中,有一个要求,表单中字段的 name 属性要和接口中的变量名一一对应,才能映射成功,否则服务端接收不到前端传来的数据。有一些特殊情况,我们的服务端的接口变量名可能和前端不一致,这个时候我们可以通过 @RequestParam 注解来解决。

1
2
3
4
5
6
@PostMapping(value = "/addbook", produces = "text/html;charset=utf-8")
@ResponseBody
public String addBook(@RequestParam("bookname") String name, String author, @RequestParam(defaultValue = "99.0") Double price) {
System.out.println("name = " + name);
return name + ">>>" + author + ">>>" + price;
}

注解中的bookname表示给 name这个变量取的别名,也就是说,name 将接收前端传来的 bookname 这个变量的值。

实体类

参数除了是简单数据类型之外,也可以是实体类。实际上,在开发中,大部分情况下,都是实体类。

当然对象中可能还有对象,图书作者是一个对象,有name、age属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Book {
private String name;
private Double price;
private Author author;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}


public Double getPrice() {
return price;
}

public void setPrice(Double price) {
this.price = price;
}

public Author getAuthor() {
return author;
}

public void setAuthor(Author author) {
this.author = author;
}
}

服务端接收数据方式如下:

1
2
3
4
5
@RequestMapping(value = "/addbook2",method = RequestMethod.POST)
@ResponseBody
public String addBook2(Book book) {
return book.toString();
}

Book 对象中,有一个 Author 属性,如何给 Author 属性传值呢?前端写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>添加图书</h1>
<form action="/book/addbook2" method="post">
<table>
<tr>
<td>图书名称</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>图书作者</td>
<td><input type="text" name="author.name"></td>
</tr>
<tr>
<td>作者年龄</td>
<td><input type="text" name="author.age"></td>
</tr>
<tr>
<td>图书价格</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td><input type="submit" value="添加"></td>
</tr>
</table>
</form>
</body>
</html>

自定义参数绑定

前面的转换,都是系统自动转换的,这种转换仅限于基本数据类型

特殊的数据类型,系统无法自动转换,例如日期是需要自己手动转换。

前端传一个日期到后端,后端不是用字符串接收,而是使用一个 Date 对象接收,这个时候就会出现参数类型转换失败。这个时候,需要我们手动定义参数类型转换器,将日期字符串手动转为一个 Date 对象。

自定义日期转换器:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class MyDateConverter implements Converter<String, Date> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public Date convert(String source) {
try {
return sdf.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}

在自定义的参数类型转换器中,将一个 String 转为 Date 对象,同时,将这个转换器注册为一个 Bean。

在SpringMVC中配置:

1
2
3
4
5
6
7
8
<mvc:annotation-driven conversion-service="conversionService"/>
<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="conversionService">
<property name="converters">
<set>
<ref bean="myDateConverter"/>
</set>
</property>
</bean>

集合类的参数

  • String数组

String 数组可以直接用数组去接收,前端传递的时候,数组的传递其实就多相同的 key,这种一般用在 checkbox 中较多

例如添加爱好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>添加图书</h1>
<form action="/book/addbook2" method="post">
<table>
<tr>
<td>图书名称</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>图书作者</td>
<td><input type="text" name="author.name"></td>
</tr>
<tr>
<td>作者年龄</td>
<td><input type="text" name="author.age"></td>
</tr>
<tr>
<td>作者兴趣爱好</td>
<td>
<input type="checkbox" value="足球" name="author.favorites">足球
<input type="checkbox" value="篮球" name="author.favorites">篮球
<input type="checkbox" value="乒乓球" name="author.favorites">乒乓球
</td>
</tr>
<tr>
<td>出版时间</td>
<td><input type="date" name="publishDate"></td>
</tr>
<tr>
<td>图书价格</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td><input type="submit" value="添加"></td>
</tr>
</table>
</form>
</body>
</html>

在服务端用一个数组去接收 favorites 对象:

1
2
3
4
5
@PostMapping(value = "/addbook2", produces = "text/html;charset=utf-8")
@ResponseBody
public String addBook2(Book book, String[] favorites){
return book.toString();
}

注意,前端传来的数组对象,服务端不可以使用 List 集合去接收

  • list集合

如果需要使用 List 集合接收前端传来的数据,List 集合本身需要放在一个封装对象中,这个时候,List 中,可以是基本数据类型,也可以是对象

例如作者对象中有角色属性:

1
2
3
4
5
6
7
// Author.java
public class Author {
private String name;
private Integer age;
private List<String> favorites;
private List<Role> roles;
}

添加作者时可以有多个角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>添加图书</h1>
<form action="/book/addbook2" method="post">
<table>
<tr>
<td>图书名称</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>图书作者</td>
<td><input type="text" name="author.name"></td>
</tr>
<tr>
<td>作者年龄</td>
<td><input type="text" name="author.age"></td>
</tr>
<tr>
<td>作者兴趣爱好</td>
<td>
<input type="checkbox" value="足球" name="author.favorites">足球
<input type="checkbox" value="篮球" name="author.favorites">篮球
<input type="checkbox" value="乒乓球" name="author.favorites">乒乓球
</td>
</tr>
<tr>
<td>作者角色</td>
<td>
<input type="checkbox" value="管理员" name="author.roles[0].name">管理员
<input type="hidden" value="1" name="author.roles[0].id">
<input type="checkbox" value="用户" name="author.roles[1].name">用户
<input type="hidden" value="2" name="author.roles[1].id">
</td>
</tr>
<tr>
<td>出版时间</td>
<td><input type="date" name="publishDate"></td>
</tr>
<tr>
<td>图书价格</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td><input type="submit" value="添加"></td>
</tr>
</table>
</form>
</body>
</html>

服务端直接接收数据即可:

1
2
3
4
5
@PostMapping(value = "/addbook2", produces = "text/html;charset=utf-8")
@ResponseBody
public String addBook2(Book book, String[] favorites){
return book.toString();
}
  • Map

相对于实体类而言,Map 是一种比较灵活的方案,但是,Map 可维护性比较差,因此一般不推荐使用。

给上述的Book实体类添加信息:

1
2
3
4
5
6
7
public class Book {
private String name;
private Double price;
private Author author;
private Date publishDate;
private Map<String,Object> info;
}

在前端,通过如下方式给 info 这个 Map 赋值:添加出版社和责任编辑属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h1>添加图书</h1>
<form action="/book/addbook2" method="post">
<table>
<tr>
<td>图书名称</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>图书作者</td>
<td><input type="text" name="author.name"></td>
</tr>
<tr>
<td>作者年龄</td>
<td><input type="text" name="author.age"></td>
</tr>
<tr>
<td>作者兴趣爱好</td>
<td>
<input type="checkbox" value="足球" name="author.favorites">足球
<input type="checkbox" value="篮球" name="author.favorites">篮球
<input type="checkbox" value="乒乓球" name="author.favorites">乒乓球
</td>
</tr>
<tr>
<td>作者角色</td>
<td>
<input type="checkbox" value="管理员" name="author.roles[0].name">管理员
<input type="hidden" value="1" name="author.roles[0].id">
<input type="checkbox" value="用户" name="author.roles[1].name">用户
<input type="hidden" value="2" name="author.roles[1].id">
</td>
</tr>
<tr>
<td>出版时间</td>
<td><input type="date" name="publishDate"></td>
</tr>
<tr>
<td>出版社</td>
<td><input type="text" name="info['publish']"></td>
</tr>
<tr>
<td>责任编辑</td>
<td><input type="text" name="info['editor']"></td>
</tr>
<tr>
<td>图书价格</td>
<td><input type="text" name="price"></td>
</tr>
<tr>
<td><input type="submit" value="添加"></td>
</tr>
</table>
</form>
</body>
</html>

文件上传

SpringMVC 中对文件上传做了封装,我们可以更加方便的实现文件上传。从 Spring3.1 开始,对于文件上传,提供了两个处理器:

  • CommonsMultipartResolver
  • StandardServletMultipartResolver

第一种处理器兼容性好,可以兼容 Servlet3.0 之前的版本,但是它依赖了 commons-fileupload 这个第三方工具,所以如果使用这个,一定要添加 commons-fileupload 依赖。

第二个处理器兼容性较差,它适用于 Servlet3.0 之后的版本,它不依赖第三方工具,使用它,可以直接做文件上传。

CommonsMultipartResolver

首先添加依赖:

1
2
3
4
5
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>

在 SpringMVC 的配置文件中,配置 MultipartResolver:

1
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver"/>

创建 jsp 页面:

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上传">
</form>
</body>
</html>

注意文件上传请求是 POST 请求,enctype 一定是 multipart/form-data

文件上传接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Controller
public class FileUploadController {
SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");
@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}


@PostMapping("/upload")
@ResponseBody
public String fileUpload(MultipartFile file, HttpServletRequest req){

// 准备文件夹
String format = sdf.format(new Date());
String realPath = req.getServletContext().getRealPath("/") + format;
File folder = new File(realPath);
if (!folder.exists()){
folder.mkdirs();
}
// 准备文件名
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
// 存储
try {
file.transferTo(new File(folder, newName));
// 组装 url
String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName;
return url;
} catch (IOException e) {
e.printStackTrace();
}
return "error";

}
}

这里还有一个小问题,在 SpringMVC 中,静态资源默认都是被自动拦截的,无法访问,意味着上传成功的图片无法访问,因此,还需要我们在 SpringMVC 的配置文件中,再添加如下配置:

1
<mvc:resources mapping="/**" location="/"/>

我们还可以自己手动配置文件上传大小等:

1
2
3
4
5
6
7
8
9
10
11
12
<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">
<!--默认的编码-->
<property name="defaultEncoding" value="UTF-8"/>
<!--上传的总文件大小-->
<property name="maxUploadSize" value="1048576"/>
<!--上传的单个文件大小-->
<property name="maxUploadSizePerFile" value="1048576"/>
<!--内存中最大的数据量,超过这个数据量,数据就要开始往硬盘中写了-->
<property name="maxInMemorySize" value="4096"/>
<!--临时目录,超过 maxInMemorySize 配置的大小后,数据开始往临时目录写,等全部上传完成后,再将数据合并到正式的文件上传目录-->
<property name="uploadTempDir" value="file:///E:\\tmp"/>
</bean>

StandardServletMultipartResolver

这种方式不需要其他jar依赖

在SpringMVC 的配置文件中,配置这个 Bean:

1
<bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver"></bean>

配置完成后,注意,这个 Bean 无法直接配置上传文件大小等限制。需要在 web.xml 中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
<multipart-config>
<!--文件保存的临时目录,这个目录系统不会主动创建-->
<location>E:\\temp</location>
<!--上传的单个文件大小-->
<max-file-size>1048576</max-file-size>
<!--上传的总文件大小-->
<max-request-size>1048576</max-request-size>
<!--这个就是内存中保存的文件最大大小-->
<file-size-threshold>4096</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

多文件上传

多文件上传分为两种,一种是 key 相同的文件,另一种是 key 不同的文件

key相同的文件

这种上传,前端页面一般如下:

1
2
3
4
<form action="/upload2" method="post" enctype="multipart/form-data">
<input type="file" name="files" multiple>
<input type="submit" value="上传">
</form>

主要是 input 节点中多了 multiple 属性。后端用一个数组来接收文件即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("/upload2")
@ResponseBody
public void upload2(MultipartFile[] files, HttpServletRequest req) {
String format = sdf.format(new Date());
String realPath = req.getServletContext().getRealPath("/") + format;
File folder = new File(realPath);
if (!folder.exists()) {
folder.mkdirs();
}
try {
for (MultipartFile file : files) {
String oldName = file.getOriginalFilename();
String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
file.transferTo(new File(folder, newName));
String url = req.getScheme() + "://" + req.getServerName() + ":" + req.getServerPort() + format + newName;
System.out.println(url);
}
} catch (IOException e) {
e.printStackTrace();
}
}

key不同的文件

key 不同的,一般前端定义如下:

1
2
3
4
5
<form action="/upload3" method="post" enctype="multipart/form-data">
<input type="file" name="file1">
<input type="file" name="file2">
<input type="submit" value="上传">
</form>

在后端用不同的变量来接收就行了:

1
2
3
@RequestMapping("/upload3")
@ResponseBody
public void upload3(MultipartFile file1, MultipartFile file2, HttpServletRequest req) {}

异常处理

项目中,可能会抛出多个异常,我们不可以直接将异常的堆栈信息展示给用户,主要是用户体验不好、不安全。

所以,针对异常,我们可以自定义异常处理,SpringMVC 中,针对全局异常也提供了相应的解决方案,主要是通过 @ControllerAdvice 和 @ExceptionHandler 两个注解来处理的。

自定义异常,只需要提供一个异常处理类即可:

1
2
3
4
5
6
7
8
9
@ControllerAdvice//表示这是一个增强版的 Controller,主要用来做全局数据处理
public class MyException {
@ExceptionHandler(Exception.class)
public ModelAndView fileuploadException(Exception e) {
ModelAndView error = new ModelAndView("error");
error.addObject("error", e.getMessage());
return error;
}
}
  • @ControllerAdvice 表示这是一个增强版的 Controller,主要用来做全局数据处理
  • @ExceptionHandler 表示这是一个异常处理方法,这个注解的参数,表示需要拦截的异常,参数为 Exception 表示拦截所有异常,这里也可以具体到某一个异常,如果具体到某一个异常,那么发生了其他异常则不会被拦截到。
  • 异常方法的定义,和 Controller 中方法的定义一样,可以返回 ModelAndview,也可以返回 String 或者 void

服务端数据校验

普通校验

需要加入校验需要的依赖:

1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.0.Final</version>
</dependency>

在 SpringMVC 的配置文件中配置校验的 Bean:

1
2
3
4
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>

配置时,提供一个 LocalValidatorFactoryBean 的实例,然后 Bean 的校验使用 HibernateValidator

提供一个添加学生的页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<form action="/addstudent" method="post">
<table>
<tr>
<td>学生编号:</td>
<td><input type="text" name="id"></td>
</tr>
<tr>
<td>学生姓名:</td>
<td><input type="text" name="name"></td>
</tr>
<tr>
<td>学生邮箱:</td>
<td><input type="text" name="email"></td>
</tr>
<tr>
<td>学生年龄:</td>
<td><input type="text" name="age"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="提交">
</td>
</tr>
</table>
</form>

在这里需要提交的数据中,假设学生编号不能为空,学生姓名长度不能超过 10 且不能为空,邮箱地址要合法,年龄不能超过 150。那么在定义实体类的时候,就可以加入这个判断条件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
@NotNull
private Integer id;
@NotNull
@Size(min = 2,max = 10)
private String name;
@Email
private String email;
@Max(150)
private Integer age;

// 省略get、set
}
  • @NotNull 表示这个字段不能为空
  • @Size 中描述了这个字符串长度的限制
  • @Email 表示这个字段的值必须是一个邮箱地址
  • @Max 表示这个字段的最大值

接下来,在 Controller 中定义接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class StudentController {
@RequestMapping("/addstudent")
@ResponseBody
public void addStudent(@Validated Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
}
}
}
  • @Validated 表示 Student 中定义的校验规则将会生效
  • BindingResult 表示出错信息,如果这个变量不为空,表示有错误,否则校验通过

默认情况下,打印出来的错误信息时系统默认的错误信息,这个错误信息,我们也可以自定义

在 resources 目录下新建一个 MyMessage.properties 文件:

1
2
3
4
5
student.id.notnull=id 不能为空
student.name.notnull=name 不能为空
student.name.length=name 最小长度为 2 ,最大长度为 10
student.email.error=email 地址非法
student.age.error=年龄不能超过 150

在 SpringMVC 配置中,加载这个配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" id="validatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="bundleMessageSource"/>
</bean>
<bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="bundleMessageSource">
<property name="basenames">
<list>
<value>classpath:MyMessage</value>
</list>
</property>
<property name="defaultEncoding" value="UTF-8"/>
<property name="cacheSeconds" value="300"/>
</bean>
<mvc:annotation-driven validator="validatorFactoryBean"/>

最后,在实体类上的注解中,加上校验出错时的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
@NotNull(message = "{student.id.notnull}")
private Integer id;
@NotNull(message = "{student.name.notnull}")
@Size(min = 2,max = 10,message = "{student.name.length}")
private String name;
@Email(message = "{student.email.error}")
private String email;
@Max(value = 150,message = "{student.age.error}")
private Integer age;

// 省略get、set
}

配置完成后,如果校验再出错,就会展示我们自己的出错信息了。

分组校验

校验规则都是定义在实体类上面的,但是在不同的数据提交环境下,校验规则可能不一样。

例如,用户的 id 是自增长的,添加的时候,可以不用传递用户 id,但是修改的时候则必须传递用户 id,这种情况下,就需要使用分组校验。

分组校验,首先需要定义校验组,所谓的校验组,其实就是空接口:

1
2
3
4
5
public interface ValidationGroup1 {
}

public interface ValidationGroup2 {
}

在实体类中,指定每一个校验规则所属的组:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Student {
@NotNull(message = "{student.id.notnull}",groups = ValidationGroup1.class)
private Integer id;
@NotNull(message = "{student.name.notnull}",groups = {ValidationGroup1.class, ValidationGroup2.class})
@Size(min = 2,max = 10,message = "{student.name.length}",groups = {ValidationGroup1.class, ValidationGroup2.class})
private String name;
@Email(message = "{student.email.error}",groups = {ValidationGroup1.class, ValidationGroup2.class})
private String email;
@Max(value = 150,message = "{student.age.error}",groups = {ValidationGroup2.class})
private Integer age;

// 省略get、set
}

在 group 中指定每一个校验规则所属的组,一个规则可以属于一个组,也可以属于多个组

最后在接收参数的地方,指定校验组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class StudentController {
@RequestMapping("/addstudent")
@ResponseBody
public void addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
}
}
}

配置完成后,属于 ValidationGroup2 这个组的校验规则,才会生效。

数据回显

数据回显就是当用户数据提交失败时,自动填充好已经输入的数据。一般来说,如果使用 Ajax 来做数据提交,基本上是没有数据回显这个需求的,但是如果是通过表单做数据提交,那么数据回显就非常有必要了。

简单数据类型

看一下student.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<form action="/addstudent" method="post">
<table>
<tr>
<td>学生编号:</td>
<td><input type="text" name="id" value="${id}"></td>
</tr>
<tr>
<td>学生姓名:</td>
<td><input type="text" name="name" value="${name}"></td>
</tr>
<tr>
<td>学生邮箱:</td>
<td><input type="text" name="email" value="${email}"></td>
</tr>
<tr>
<td>学生年龄:</td>
<td><input type="text" name="age" value="${age}"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="提交">
</td>
</tr>
</table>
</form>

在接收数据时,使用简单数据类型去接收:

1
2
3
4
5
6
7
8
@RequestMapping("/addstudent")
public String addStudent2(Integer id, String name, String email, Integer age, Model model) {
model.addAttribute("id", id);
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("age", age);
return "student";
}

这种方式,相当于框架没有做任何工作,就是我们手动做数据回显的。此时访问页面,服务端会再次定位到该页面,而且数据已经预填好。

实体类

SpringMVC 在页面跳转时,会自动将对象填充进返回的数据中

student.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<form action="/addstudent" method="post">
<table>
<tr>
<td>学生编号:</td>
<td><input type="text" name="id" value="${student.id}"></td>
</tr>
<tr>
<td>学生姓名:</td>
<td><input type="text" name="name" value="${student.name}"></td>
</tr>
<tr>
<td>学生邮箱:</td>
<td><input type="text" name="email" value="${student.email}"></td>
</tr>
<tr>
<td>学生年龄:</td>
<td><input type="text" name="age" value="${student.age}"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="提交">
</td>
</tr>
</table>
</form>

这 student 就是服务端接收数据的变量名,服务端的变量名和这里的 student 要保持一直。服务端定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/addstudent")
public String addStudent(@Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
return "student";
}
return "hello";
}

student 这个变量会被自动填充到返回的 Model 中。变量名就是填充时候的 key。如果想自定义这个 key,可以在参数中写出来 Model,然后手动加入 Student 对象,就像简单数据类型回显那样。

@ModelAttribute

@ModelAttribute 这个注解,主要有两方面的功能:

  1. 在数据回显时,给变量定义别名
  2. 定义全局数据

定义别名

在数据回显时,给变量定义别名,非常容易,直接加这个注解即可:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/addstudent")
public String addStudent(@ModelAttribute("s") @Validated(ValidationGroup2.class) Student student, BindingResult result) {
if (result != null) {
//校验未通过,获取所有的异常信息并展示出来
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError allError : allErrors) {
System.out.println(allError.getObjectName()+":"+allError.getDefaultMessage());
}
return "student";
}
return "hello";
}

这样定义完成后,在前端再次访问回显的变量时,变量名称就不是 student 了,而是 s:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<form action="/addstudent" method="post">
<table>
<tr>
<td>学生编号:</td>
<td><input type="text" name="id" value="${s.id}"></td>
</tr>
<tr>
<td>学生姓名:</td>
<td><input type="text" name="name" value="${s.name}"></td>
</tr>
<tr>
<td>学生邮箱:</td>
<td><input type="text" name="email" value="${s.email}"></td>
</tr>
<tr>
<td>学生年龄:</td>
<td><input type="text" name="age" value="${s.age}"></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="提交">
</td>
</tr>
</table>
</form>

定义全局数据

假设有一个 Controller 中有很多方法,每个方法都会返回数据给前端,但是每个方法返回给前端的数据又不太一样,虽然不太一样,但是没有方法的返回值又有一些公共的部分。可以将这些公共的部分提取出来单独封装成一个方法,用 @ModelAttribute 注解来标记。

在一个controller中加入:

1
2
3
4
5
6
7
@ModelAttribute("info")
public Map<String,Object> info() {
Map<String, Object> map = new HashMap<>();
map.put("username", "setcreed");
map.put("address", "https://setcree.gitee.io");
return map;
}

当用户访问当前 Controller 中的任意一个方法,在返回数据时,都会将添加了 @ModelAttribute 注解的方法的返回值,一起返回给前端。@ModelAttribute 注解中的 info 表示返回数据的 key。

json处理

返回json

目前主流的 JSON 处理工具主要有三种:jackson、gson、fastjson

  • 在 SpringMVC 中,对 jackson 和 gson 都提供了相应的支持,就是如果使用这两个作为 JSON 转换器,只需要添加对应的依赖就可以了,返回的对象和返回的集合、Map 等都会自动转为 JSON。
  • 如果使用 fastjson,除了添加相应的依赖之外,还需要自己手动配置 HttpMessageConverter 转换器。
  • jackson、gson也是使用 HttpMessageConverter 转换器,是SpringMVC 自动提供的,但SpringMVC 没有给 fastjson 提供相应的转换器。

jackson

在 SpringMVC 中使用 jackson ,只需要添加 jackson 的依赖即可:

1
2
3
4
5
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>

依赖添加成功后,凡是在接口中直接返回的对象,集合等等,都会自动转为 JSON。如下:

1
2
3
4
5
6
7
public class Book {
private String name;
private String author;
private Integer id;

// 省略get、set
}

controller:

1
2
3
4
5
6
7
8
9
@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
Book book = new Book();
book.setId(1);
book.setName("三国演义");
book.setAuthor("罗贯中");
return book;
}

这里返回一个对象,但是在前端接收到的则是一个 JSON 字符串,这个对象会通过 HttpMessageConverter 自动转为 JSON 字符串。

如果想返回一个 JSON 数组,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/books")
@ResponseBody
public List<Book> getAllBooks() {
List<Book> list = new ArrayList<Book>();
for (int i = 0; i < 10; i++) {
Book book = new Book();
book.setId(i);
book.setName("三国演义:" + i);
book.setAuthor("罗贯中:" + i);
list.add(book);
}
return list;
}

添加了 jackson ,就能够自动返回 JSON,这个依赖于一个名为 HttpMessageConverter 的类,这本身是一个接口,从名字上就可以看出,它的作用是 Http 消息转换器,既然是消息转换器,它提供了两方面的功能:

  • 将返回的对象转为json
  • 将前端提交上来的json转为对象

但是,HttpMessageConverter 只是一个接口,由各个 JSON 工具提供相应的实现,在 jackson 中,实现的名字叫做MappingJackson2HttpMessageConverter,而这个东西的初始化,则由 SpringMVC 来完成。除非自己有一些自定义配置的需求,否则一般来说不需要自己提供MappingJackson2HttpMessageConverter

在上面的例子中,book对象有一个出版日期:

1
2
3
4
5
6
7
8
public class Book {
private Integer id;
private String name;
private String author;
private Date publish;

// 省略get、set
}

然后在构造 Book 时添加日期属性:

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/book")
@ResponseBody
public Book getBookById() {
Book book = new Book();
book.setId(1);
book.setName("三国演义");
book.setAuthor("罗贯中");
book.setPublish(new Date());
return book;
}

访问 /book 接口,返回的 json 格式如下:

如果我们想自己定制返回日期的格式,简单的办法,可以通过添加注解来实现:

1
2
3
4
5
6
7
public class Book {
private Integer id;
private String name;
private String author;
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "Asia/Shanghai")
private Date publish;
}

这样,就可以定制返回的日期格式了。

但是,这种方式有一个弊端,这个注解可以加在属性上,也可以加在类上,也就说,最大可以作用到一个类中的所有日期属性上。如果项目中有很多实体类都需要做日期格式化,使用这种方式就比较麻烦了,这个时候,我们可以自己提供一个 jackson 的 HttpMesageConverter 实例,在这个实例中,自己去配置相关属性,这里的配置将是一个全局配置。

在SpringMVC配置文件中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" id="httpMessageConverter">
<property name="objectMapper">
<bean class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="dateFormat">
<bean class="java.text.SimpleDateFormat">
<constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"/>
</bean>
</property>
<property name="timeZone" value="Asia/Shanghai"/>
</bean>
</property>
</bean>

添加完成后,去掉 Book 实体类中日期格式化的注解,再进行测试,结果如下:

gson

gson 是 Google 推出的一个 JSON 解析器,主要在 Android 开发中使用较多,不过,Web 开发中也是支持这个的,而且 SpringMVC 还针对 Gson 提供了相关的自动化配置,我们只要添加依赖就可以使用了

1
2
3
4
5
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>

如果项目中,同时存在 jackson 和 gson 的话,那么默认使用的是 jackson

org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter 类的构造方法中,加载顺序就是先加载 jackson 的 HttpMessageConverter,后加载 gson 的 HttpMessageConverter

加完依赖之后,就可以直接返回 JSON 字符串了。使用 Gson 时,如果想做自定义配置,则需要自定义 HttpMessageConverter。

1
2
3
4
5
6
7
8
9
10
11
12
13
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.http.converter.json.GsonHttpMessageConverter" id="httpMessageConverter">
<property name="gson">
<bean class="com.google.gson.Gson" factory-bean="gsonBuilder" factory-method="create"/>
</property>
</bean>
<bean class="com.google.gson.GsonBuilder" id="gsonBuilder">
<property name="dateFormat" value="yyyy-MM-dd"/>
</bean>

fastjson

fastjson 号称最快的 JSON 解析器,但是也是这三个中 BUG 最多的一个。

SpringMVC 并没针对 fastjson 提供相应的 HttpMessageConverter,所以,fastjson 在使用时,一定要自己手动配置 HttpMessageConverter。

添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>

然后在 SpringMVC 的配置文件中配置 HttpMessageConverter:

1
2
3
4
5
6
7
8
9
10
11
12
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
<property name="fastJsonConfig">
<bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
<property name="dateFormat" value="yyyy-MM-dd"/>
</bean>
</property>
</bean>

fastjson 默认中文乱码,添加如下配置解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mvc:annotation-driven>
<mvc:message-converters>
<ref bean="httpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter" id="httpMessageConverter">
<property name="fastJsonConfig">
<bean class="com.alibaba.fastjson.support.config.FastJsonConfig">
<property name="dateFormat" value="yyyy-MM-dd"/>
</bean>
</property>
<property name="supportedMediaTypes">
<list>
<value>application/json;charset=utf-8</value>
</list>
</property>
</bean>

接收json

浏览器传来的参数,可以是 key/value 形式的,也可以是一个 JSON 字符串。在 Jsp/Servlet 中,我们接收 key/value 形式的参数,一般是通过 getParameter 方法。如果客户端是 JSON 数据,我们可以通过如下格式进行解析:

1
2
3
4
5
6
7
@RequestMapping("/addbook2")
@ResponseBody
public void addBook2(HttpServletRequest req) throws IOException {
ObjectMapper om = new ObjectMapper();
Book book = om.readValue(req.getInputStream(), Book.class);
System.out.println(book);
}

但是这种解析方式有点麻烦,在 SpringMVC 中,我们可以通过一个注解来快速的将一个 JSON 字符串转为一个对象:

1
2
3
4
5
@RequestMapping("/addbook3")
@ResponseBody
public void addBook3(@RequestBody Book book) {
System.out.println(book);
}

这样就可以直接收到前端传来的 JSON 字符串了

SpringMVC对RESTful的支持

SpringMVC 对 RESTful 提供了非常全面的支持,主要有如下几个注解:

  • @RestController

这个注解是一个组合注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {

/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
* @since 4.0.1
*/
@AliasFor(annotation = Controller.class)
String value() default "";

}

一般,直接用 @RestController 来标记 Controller,可以不使用 @Controller。

请求方法中,提供了常见的请求方法:

  • @PostMapping
  • @GetMapping
  • @PutMapping
  • @DeleteMapping

另外还有一个提取请求地址中的参数的注解 @PathVariable:

1
2
3
4
5
6
@GetMapping("/book/{id}")  //http://localhost:8080/book/2
public Book getBookById(@PathVariable Integer id) {
Book book = new Book();
book.setId(id);
return book;
}

参数 2 将被传递到 id 这个变量上

静态资源访问

在 SpringMVC 中,静态资源,默认都是被拦截的,例如 html、js、css、jpg等等,都是无法直接访问的。因为所有请求都被拦截了,所以,针对静态资源,我们要做额外处理,处理方式很简单,直接在 SpringMVC 的配置文件中,添加如下内容:

1
<mvc:resources mapping="/static/html/**" location="/static/html/"/>

mapping 表示映射规则,也是拦截规则,就是说,如果请求地址是 /static/html 这样的格式的话,那么对应的资源就去 /static/html/ 这个目录下查找。

**通配符表示匹配多层路径

为了省事,一般这样配:

1
<mvc:resources mapping="/**" location="/"/>

拦截器

SpringMVC 中的拦截器,相当于 Jsp/Servlet 中的过滤器,只不过拦截器的功能更为强大。

拦截器的定义非常容易:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Component
public class MyInterceptor1 implements HandlerInterceptor {
/**
* 这个是请求预处理的方法,只有当这个方法返回值为 true 的时候,后面的方法才会执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor1:preHandle");
return true;
}

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor1:postHandle");

}

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor1:afterCompletion");

}
}



@Component
public class MyInterceptor2 implements HandlerInterceptor {
/**
* 这个是请求预处理的方法,只有当这个方法返回值为 true 的时候,后面的方法才会执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor2:preHandle");
return true;
}

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor2:postHandle");

}

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor2:afterCompletion");

}
}

拦截器定义好之后,需要在 SpringMVC 的配置文件中进行配置:

1
2
3
4
5
6
7
8
9
10
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<ref bean="myInterceptor1"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<ref bean="myInterceptor2"/>
</mvc:interceptor>
</mvc:interceptors>

如果存在多个拦截器,拦截规则如下:

  • preHandle 按拦截器定义顺序调用
  • postHandler 按拦截器定义逆序调用
  • afterCompletion 按拦截器定义逆序调用
  • postHandler 在拦截器链内所有拦截器返成功调用
  • afterCompletion 只有 preHandle 返回 true 才调用

MyBastis

MyBatis简介

JDBC存在的问题

  • 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题
  • Sql 语句在代码中硬编码,造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java 代码
  • ……

Sql 语句在代码中硬编码,造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java 代码

MyBatis介绍

MyBatis 官网:https://mybatis.org/mybatis-3/zh/index.html

MyBatis 是一个优秀的持久层框架,它对 jdbc 的操作数据库的过程进行封装,使开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建 connection、创建 statement、手动设置参数、结果集检索等 jdbc 繁杂的过程代码。Mybatis 通过 xml 或注解的方式将要执行的各种 statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过 java 对象和 statement 中的 sql 进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射成 java 对象并返回。

与其他的对象关系映射框架不同,MyBatis 并没有将 Java 对象与数据库表关联起来,而是将 Java 方法与 SQL 语句关联。MyBatis 允许用户充分利用数据库的各种功能,例如存储过程、视图、各种复杂的查询以及某数据库的专有特性。如果要对遗留数据库、不规范的数据库进行操作,或者要完全控制 SQL 的执行,MyBatis 是一个不错的选择。

Mybatis简单使用

首先来准备一个数据库:

1
2
3
4
5
6
7
8
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`address` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

insert into `user`(`id`,`username`,`address`) values (1,'cwz','上海'),(4,'张三','深圳'),(5,'李四','广州'),(6,'王五','北京');

创建Maven工程,添加MyBatis依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

接下来,准备一个 Mapper 文件,Mapper 是用来在 MyBatis 中定义 SQL 的 XML 配置文件,由于在实际开发中,我们经常需要使用到 Mapper,经常需要自己创建 Mapper 文件,因此,我们可以将 Mapper 文件做成一个模板。具体操作如下:

在 IDEA 中,选择 resources 目录,右键单击,New–>Edit File Templates:

然后添加一个新的模板进来,给模板取名,同时设置扩展名,并将如下内容拷贝到模板中:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="#[[$namespace$]]#">
</mapper>

这样创建Mapper文件就很方便了

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis01.mapper.UserMapper">

</mapper>

创建一个新的 mapper ,需要首先给它取一个 namespace,这相当于是一个分隔符,因为我们在项目中,会存在很多个 Mapper,每一个 Mapper 中都会定义相应的增删改查方法,为了避免方法冲突,也为了便于管理,每一个 Mapper 都有自己的 namespace,而且这个 namespace 不可以重复。

接下来,在 Mapper 中,定义一个简单的查询方法,根据 所有用户:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis01.mapper.UserMapper">
<select id="getAllUser" resultType="com.cwz.mybatis01.model.User">
select * from user;
</select>
</mapper>

id 表示查询方法的唯一标识符,resultType 定义了返回值的类型

定义的User实体类:

1
2
3
4
5
6
7
public class User {
private Integer id;
private String username;
private String address;

// 省略get、set、toString
}

接下来,创建 MyBatis 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test1?useSSL=false&amp;serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/cwz/mybatis01/mapper/UserMapper.xml"/>
</mappers>
</configuration>

在这个配置文件中,我们只需要配置 environments 和 mapper 即可,environment 就是 MyBatis 所连接的数据库的环境信息,它放在一个 environments 节点中,意味着 environments 中可以有多个 environment,为社么需要多个呢?开发、测试、生产,不同环境各一个 environment,每一个 environment 都有一个 id,也就是它的名字,然后,在 environments 中,通过 default 属性,指定你需要的 environment。每一个 environment 中,定义一个数据的基本连接信息。

在 mappers 节点中,定义 Mapper,也就是指定我们上一步所写的 Mapper 的路径。

注意:一般情况下我们把mapper文件与接口放在一起,但如果mapper文件不放在资源文件夹resources下,可能会报错,这是maven的问题。解决方案就是在pom.xml中加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>

最后测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {

public static void main(String[] args) throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();

List<User> list = sqlSession.selectList("com.cwz.mybatis01.mapper.UserMapper.getAllUser");
for (User user : list) {
System.out.println("user = " + user);
}

sqlSession.close();
}
}
  • 首先,我们加载主配置文件,生成一个 SqlSessionFactory,再由 SqlSessionFactory 生成一个 SqlSession,一个 SqlSession 就相当于是我们的一个会话,类似于 JDBC 中的一个连接,在 SQL 操作完成后,这个会话是可以关闭的。
  • 在这里,SqlSessionFactoryBuilder 用于创建 SqlSessionFacoty,SqlSessionFacoty 一旦创建完成就不需要 SqlSessionFactoryBuilder 了,因为 SqlSession 是通过 SqlSessionFactory 生产,所以可以将 SqlSessionFactoryBuilder 当成一个工具类使用,最佳使用范围是方法范围即方法体内局部变量。
  • SqlSessionFactory 是一个接口,接口中定义了 openSession 的不同重载方法,SqlSessionFactory 的最佳使用范围是整个应用运行期间,一旦创建后可以重复使用,通常以单例模式管理 SqlSessionFactory。
  • SqlSession 中封装了对数据库的操作

对SqlSessionFactory进行封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SqlSessionFactoryUtils {
private static SqlSessionFactory sqlSessionFactory = null;

public static SqlSessionFactory getInstance() {
if (sqlSessionFactory == null) {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
return sqlSessionFactory;
}
}

使用log4j日志

使用log4j需要添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.3</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>

在resources下新建日志文件log4j.properties:

1
2
3
4
5
log4j.rootLogger=debug, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd} method:%l%n%m%n

MyBatis增删改查

添加记录,id 有两种不同的处理方式,一种就是自增长,另一种则是 Java 代码传一个 ID 进来。这个 ID可以是一个UUID,也可以是其他的自定义的ID。在 MyBatis 中,对这两种方式都提供了相应的支持。

主键自增长

首先我们在 Mapper 中,添加 SQL 插入语句:

1
2
3
<insert id="addUser" parameterType="com.cwz.mybatis.model.User">
insert into user (username,address) values (#{username},#{address});
</insert>

这里有一个 parameterType 表示传入的参数类型。参数都是通过 # 来引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = factory.openSession();
User user = new User();
user.setUsername("赵六");
user.setAddress("北京");
int insert = sqlSession.insert("com.cwz.mybatis.mapper.addUser", user);
System.out.println(insert);
sqlSession.commit();
sqlSession.close();
}
}

注意,SQL 插入完成后,一定要提交,即 sqlSession.commit()

使用UUID做主键

使用 UUID 做主键,又有两种不同的思路,第一种思路,就是在 Java 代码中生成 UUID,直接作为参数传入到 SQL 中,这种方式就和传递普通参数一样,另一种方式,就是使用 MySQL 自带的 UUID 函数来生成 UUID。

使用 MySQL 自带的 UUID 函数,整体思路是这样:首先调用 MySQL 中的 UUID 函数,获取到一个 UUID,然后,将这个 UUID 赋值给 User 对象的 ID 属性,然后再去执行 SQL 插入操作,再插入时使用这个 UUID。

mapper文件:

1
2
3
4
5
6
<insert id="addUser2" parameterType="com.cwz.mybatis.model.User">
<selectKey resultType="java.lang.String" keyProperty="id" order="BEFORE">
select uuid();
</selectKey>
insert into user (id,username,address) values (#{id},#{username},#{address});
</insert>

上述xml文件中参数:

  • selectKey 表示查询 key
  • keyProperty 属性表示将查询的结果赋值给传递进来的 User 对象的 id 属性
  • resultType 表示查询结果的返回类型
  • order 表示这个查询操作的执行时机,BEFORE 表示这个查询操作在 insert 之前执行
  • 在 selectKey 节点的外面定义 insert 操作

最后调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = factory.openSession();
User user = new User();
user.setUsername("赵六");
user.setAddress("北京");
int insert = sqlSession.insert("com.cwz.mybatis.mapper.addUser", user);
System.out.println(insert);
sqlSession.commit();
sqlSession.close();
}
}

在 UserMapper 中定义删除 SQL:

1
2
3
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>

在 Java 代码中调用该方法:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = factory.openSession();
int delete = sqlSession.delete("com.cwz.mybatis.mapper.deleteUserById", 2);
System.out.println(delete);
sqlSession.commit();
sqlSession.close();
}
}

1
2
3
<update id="updateUser" parameterType="com.cwz.mybatis.model.User">
update user set username = #{username} where id=#{id};
</update>

查一条数据

1
2
3
<select id="getUserById" resultType="com.cwz.mybatis.model.User">
select * from user where id=#{id}
</select>

java调用

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) throws IOException {
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = factory.openSession();
User user = sqlSession.selectOne("com.cwz.mybatis.mapper.UserMapper.getUserById", 1);

sqlSession.close();
}
}

MyBatis 架构介绍

  • mybatis 配置:mybatis-config.xml,此文件作为 mybatis 的全局配置文件,配置了 mybatis 的运行环境等信息。另一个 mapper.xml 文件即 sql 映射文件,文件中配置了操作数据库的 sql 语句。此文件需要在 mybatis-config.xml 中加载
  • 通过 mybatis 环境等配置信息构造 SqlSessionFactory 即会话工厂
  • 由会话工厂创建 sqlSession 即会话,操作数据库需要通过 sqlSession 进行
  • mybatis 底层自定义了 Executor 执行器接口操作数据库,Executor 接口有两个实现,一个是基本执行器、一个是缓存执行器
  • Mapped Statement 也是 mybatis 一个底层封装对象,它包装了 mybatis 配置信息及 sql 映射信息等。mapper.xml 文件中一个 sql 对应一个 Mapped Statement 对象,sql 的 id 即是Mapped statement 的 id
  • Mapped Statement 对 sql 执行输入参数进行定义,包括 HashMap、基本类型、pojo,Executor 通过 Mapped Statement 在执行 sql 前将输入的 java 对象映射至 sql 中,输入参数映射就是 jdbc 编程中对 preparedStatement 设置参数
  • Mapped Statement 对 sql 执行输出结果进行定义,包括 HashMap、基本类型、pojo,Executor 通过 Mapped Statement 在执行 sql 后将输出结果映射至 java 对象中,输出结果映射过程相当于 jdbc 编程中对结果的解析处理过程

MyBatis中主键回填的两种实现方式

在数据添加的过程中,我们经常需要添加完数据之后,需要获取刚刚添加的数据 id

使用JDBC

JDBC 中实现主键回填,主要是在构造 PreparedStatement 时指定需要主键回填,然后在插入成功后,查询刚刚插入数据的 id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int insert(Person person) {
Connection con = null;
PreparedStatement ps = null;
ResultSet rs = null;
con = DBUtils.getConnection();
ps = con.prepareStatement("INSERT INTO person(username,password,money) VALUES(?,?,?)", PreparedStatement.RETURN_GENERATED_KEYS);
ps.setObject(1, person.getUsername());
ps.setObject(2, person.getPassword());
ps.setObject(3, person.getMoney());
int i = ps.executeUpdate();
rs = ps.getGeneratedKeys();
int id = -1;
if (rs.next()) {
id = rs.getInt(1);
}
return id;
}
  • 构造 PreparedStatement 时,多了一个参数,指定了需要主键回填
  • 在更新操作执行完成之后,调用 getGeneratedKeys ,然后又会获取到一个 ResultSet 对象,从而可以获取到刚刚插入数据的id

MyBatis的写法

方法1:

1
2
3
<insert id="addUser" useGeneratedKeys="true" keyProperty="id">
insert into user (username,address) values (#{username},#{address});
</insert>

这种方式比较简单,就是在插入节点上添加 useGeneratedKeys 属性,同时设置接收回传主键的属性。配置完成后,我们执行一个插入操作,插入时传入一个对象,插入完成后,这个对象的 id 就会被自动赋值,值就是刚刚插入成功的id。

方法2:

利用MySQL自带的 last_insert_id() 函数查询刚刚插入的id

1
2
3
4
5
6
<insert id="addUser">
<selectKey keyProperty="id" resultType="java.lang.Integer">
SELECT LAST_INSERT_ID()
</selectKey>
insert into user (username,address) values (#{username},#{address});
</insert>

这种方式是在 insert 节点中添加 selectKey 来实现主键回填

selectKey 节点中的 SQL可以在插入之前或者插入之后执行,这可以通过设置节点的 Order 属性为 AFTER 或者 BEFORE 来实现

MyBatis Mapper

mapper接口

之前简单写的增删改查 冗余代码太多。比如我想写一个UserDao

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class UserDao {
private SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getInstance();

public User getUserById(Integer id) {
SqlSession sqlSession = sqlSessionFactory.openSession();
User user = (User) sqlSession.selectOne("com.cwz.mybatis.mapper.UserDao.getUserById", id);
sqlSession.close();
return user;
}

public Integer addUser(User user) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int insert = sqlSession.insert("com.cwz.mybatis.mapper.UserDao.addUser", user);
sqlSession.commit();
sqlSession.close();
return insert;
}


public Integer deleteUserById(Integer id) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int delete = sqlSession.delete("com.cwz.mybatis.mapper.UserDao.deleteUserById", id);
sqlSession.commit();
sqlSession.close();
return delete;
}

public Integer updateUser(User user) {
SqlSession sqlSession = sqlSessionFactory.openSession();
int delete = sqlSession.delete("com.cwz.mybatis.mapper.UserDao.updateUser", user);
sqlSession.commit();
sqlSession.close();
return delete;
}

public List<User> getAllUser() {
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> users = sqlSession.selectList("com.cwz.mybatis.mapper.UserDao.getAllUser");
sqlSession.close();
return users;
}
}

与之对应的UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis.mapper.UserDao">

<select id="getUserById" resultType="com.cwz.mybatis.model.User">
select * from user where id=#{id};
</select>
<insert id="addUser" parameterType="com.cwz.mybatis.model.User">
insert into user (username,address) values (#{username},#{address});
</insert>
<delete id="deleteUserById" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>

<update id="updateUser" parameterType="com.cwz.mybatis.model.User">
update user set username = #{username} where id=#{id};
</update>

<select id="getAllUser" resultType="com.cwz.mybatis.model.User">
select * from user;
</select>
</mapper>

上面的代码有很多优化的地方,比如:每个方法中都要获取 SqlSession,然后要commit之类的操作。我们可以将当前方法简化成一个接口:

1
2
3
4
5
6
7
8
9
10
11
public interface UserMapper {
User getUserById(Integer id);

Integer addUser(User user);

Integer deleteUserById(Integer id);

Integer updateUser(User user);

List<User> getAllUser();
}

这个接口对应的mapper文件内容还是不变。

这个接口提供了 UserDao 所需要的最核心的东西,根据这个接口,就可以自动生成 UserDao:

  • UserDao 中定义了 SqlSessionFactory,这是一套固定的代码
  • UserDao 中定义了 SqlSessionFactory,这是一套固定的代码

在MyBatis配置文件中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///test1?useSSL=false&amp;serverTimezone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.cwz.mybatis.mapper"/>
</mappers>
</configuration>

值得注意的是:

默认情况下,Maven 要求我们将 XML 配置、properties 配置等,都放在 resources 目录下,如果我们强行放在 java 目录下,默认情况下,打包的时候这个配置文件会被自动忽略掉,我们可以这样解决:

  • 在pom.xml中添加配置,让 Maven 不要忽略我在 java 目录下的 XML 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>

  • 手动在 resources 目录下,创建一个和 UserMapper 接口相同的目录

Mapper映射文件

mapper映射文件有很多的参数使用细节

parameterType

这个表示输入的参数类型

$#的区别

在 MyBatis 中,我们在 mapper 引用变量时,默认使用的是 #,像下面这样:

1
2
3
<select id="getUserById" resultType="com.cwz.mybatis.model.User">
select * from user where id=#{id};
</select>

除了使用 # 之外,我们也可以使用 $ 来引用一个变量:

1
2
3
<select id="getUserById" resultType="com.cwz.mybatis.model.User">
select * from user where id=${id};
</select>

在旧的MyBatis版本中,这两者是有很大区别的:如果使用 $,变量需要通过 @Param 取别名;而新版本中,不用取别名:

1
2
3
public interface UserMapper {
User getUserById(Integer id);
}

#$ 符号的根本区别其实就是Statement 和 PreparedStatement 之间的区别

  • $ 使用时 SQL是直接参数拼接好的,一般来说这样有SQL注入的问题
  • # 使用了预编译的方式,通过占位符的方式传递参数

有些时候使用$进行SQL拼接可以使用数据库函数解决,如进行模糊查询

1
2
3
<select id="getUserByName" resultType="com.cwz.mybatis.model.User">
select * from user where username like concat('%',${name},'%');
</select>

但是有的 SQL 无法使用 # 来拼接,例如传入一个动态字段进来,假设我想查询所有数据,要排序查询,但是排序的字段不确定,需要通过参数传入,这种场景就只能使用 $,例如如下方法:

1
List<User> getAllUser(String orderBy);

XML 文件:

1
2
3
<select id="getUserOrderBy" resultType="com.cwz.mybatis02.model.User" parameterType="java.lang.String">
select * from user order by ${order} desc;
</select>
简单类型的传递

像前面根据id查询只需要传一个参数就行了。但是多个参数传递会怎么样?

比如根据id修改名字:

1
Integer updateUsernameById(String username, Integer id);

但是对应的mapper怎么传递参数?

其实可以这样传递:

1
2
3
<update id="updateUsernameById">
update user set username = #{arg0} where id=#{arg1};
</update>

或者:

1
2
3
<update id="updateUsernameById">
update user set username = #{param1} where id=#{param2};
</update>

默认就是:[arg0,arg1,…] 或者 [param1,param2,…]

但是参数很多,这样是不容易记住的,我们可以给参数添加@Param注解来指定参数名:

1
Integer updateUsernameById(@Param("username") String username, @Param("id") Integer id);

对应的mapper:

1
2
3
<update id="updateUsernameById">
update user set username=#{username} where id=#{id};
</update>
对象参数的传递

添加用户:

1
Integer addUser(User user);

对应的mapper:

1
2
3
<insert id="addUser" parameterType="com.cwz.mybatis02.model.User">
insert into user (username,address) values(#{username}, #{address});
</insert>

在引用的时候,直接使用属性名就能够定位到对象了。如果对象存在多个,我们也需要给对象添加 @Param 注解

1
Integer addUser(@Param("user") User user);

对应的mapper:

1
2
3
<insert id="addUser" parameterType="com.cwz.mybatis02.model.User">
insert into user (username,address) values(#{user.username}, #{user.address});
</insert>
Map参数传递

一般在项目中不适用Map参数,一般使用传对象。

1
Integer updateUsernameById(HashMap<String,Object> map);

mapper:

1
2
3
<update id="updateUsernameById">
update user set username = #{username} where id=#{id};
</update>

引用的变量名,就是 map 中的 key。基本上和实体类是一样的

resultType

resultType 是返回类型,在实际开发中,如果返回的数据类型比较复杂,一般我们使用 resultMap,但是,对于一些简单的返回,使用 resultType 就够用了。

resultType返回的可以是简单类型,也可以是对象、集合等

resultMap

在实际开发中,resultMap 是使用较多的返回数据类型配置。因为实际项目中,一般的返回数据类型比较丰富,字段和属性名对应不上。我们需要使用 resultMap,自定义映射的结果集

看一下book实体类:

1
2
3
4
5
public class Book {
private Integer id;
private String name;
private String author;
}

数据库中书名的字段名是b_name,和实体类的属性不对应

接口方法:

1
2
3
public interface BookMapper {
List<Book> getAllBooks();
}

对应的mapper:

1
2
3
4
5
6
7
8
9
<resultMap id="BookMap" type="com.cwz.mybatis02.model.Book">
<id property="id" column="id"/>
<result property="name" column="b_name"/>
<result property="author" column="author"/>

</resultMap>
<select id="getAllBooks" resultMap="BookMap">
select * from t_book;
</select>

在这个 resultMap 中,id 用来描述主键,column 是数据库查询出来的列名,property 则是对象中的属性名。

以前老版本的MyBatis要求实体类要有无参构造方法,新版的没这个要求了。一般有参和无参构造方法都有,会先调用无参的。

我们也可以在 resultMap 中,自己指定要调用的构造方法,指定方式如下:

1
2
3
4
5
6
7
<resultMap id="BookMap" type="com.cwz.mybatis02.model.Book">
<constructor>
<idArg column="id" name="id"/>
<arg column="b_name" name="name"/>
<arg column="author" name="author"/>
</constructor>
</resultMap>

这个就表示使用两个参数的构造方法取构造一个 Book实例。需要注意的是,name 属性表示构造方法中的变量名,默认情况下,变量名是 arg0、arg1或者param1、param2 如果需要自定义,我们可以在构造方法中,手动加上 @Param 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Book {
private Integer id;
private String name;
private String author;

public Book() {
System.out.println("Book-----init-----无参");
}

public Book(@Param("id") Integer id, @Param("name") String name, @Param("author") String author) {
System.out.println("Book-----init---有参");
this.id = id;
this.name = name;
this.author = author;
}
}

动态SQL

if

if 是一个判断节点,如果满足某个条件,节点中的 SQL 就会生效。比如分页查询:

1
List<Book> getBooksByPage(@Param("start") Integer start, @Param("size") Integer size);

在 XML 中定义 SQL:

1
2
3
4
5
6
<select id="getBooksByPage" resultMap="BookMap">
select * from t_book
<if test="start != null and size!=null">
limit #{start},#{size}
</if>
</select>

if 节点中,test 表示判断条件,如果判断结果为 true,则 if 节点的中的 SQL 会生效,否则不会生效。也就是说,在方法调用时,如果分页的两个参数都为 null,则表示查询所有数据

where

where 用来处理查询参数

1
List<Book> getBookByNameOrAuthor(Book book);

xml:

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getBookByNameOrAuthor" resultMap="BookMap" parameterType="com.cwz.mybatis02.model.Book">
select * from t_book
<where>
<if test="name!=null and name!=''">
and b_name=#{name}
</if>
<if test="author!=null and author!=''">
and author=#{author}
</if>
</where>

</select>

用 where 节点将所有的查询条件包起来,如果有满足的条件,where 节点会自动加上,如果没有,where 节点也将不存在,在有满足条件的情况下,where 还会自动处理 and 关键字。

set

set 关键字一般用在更新中。因为大部分情况下,更新的字段可能不确定,如果对象中存在该字段的值,就更新该字段,不存在,就不更新。

1
Integer updateBook(Book book);
1
2
3
4
5
6
7
8
9
10
11
12
13
<update id="updateBook" parameterType="com.cwz.mybatis02.model.Book">
update t_book
<set>
<if test="name!=null and name!=''">
b_name = #{name},
</if>
<if test="author!=null and author!=''">
author=#{author},
</if>
</set>

where id=#{id}
</update>

trim

可以代替where set

1
2
3
4
5
6
7
8
9
10
11
12
<select id="getBookByNameOrAuthor2" resultMap="BookMap" parameterType="com.cwz.mybatis02.model.Book">
select * from t_book
<trim prefix="where" prefixOverrides="and ">
<if test="name!=null and name!=''">
and b_name=#{name}
</if>
<if test="author!=null and author!=''">
and author=#{author}
</if>
</trim>

</select>

prefixOverrides指定条件关系

1
2
3
4
5
6
7
8
9
10
11
12
13
<update id="updateBook2" parameterType="com.cwz.mybatis02.model.Book">
update t_book
<trim prefix="set" suffixOverrides=",">
<if test="name!=null and name!=''">
b_name = #{name},
</if>
<if test="author!=null and author!=''">
author=#{author},
</if>
</trim>

where id=#{id}
</update>

foreach

foreach 用来处理数组/集合参数

如批量查询:

1
List<Book> getBooksByIds(@Param("ids") List<Integer> ids);
1
2
3
4
5
6
<select id="getBooksByIds" resultMap="BookMap">
select * from t_book where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</select>

在 mapper 中,通过 foreach 节点来遍历数组,collection 表示数组变量,open 表示循环结束后,左边的符号,close 表示循环结束后,右边的符号,item 表示循环时候的单个变量,separator 表示循环的元素之间的分隔符。

批量插入:

1
Integer batchAddBooks(@Param("books") List<Book> books);
1
2
3
4
5
6
<insert id="batchAddBooks">
insert into t_book (b_name,author) values
<foreach collection="books" separator="," item="book">
(#{book.name},#{book.author})
</foreach>
</insert>

bind

bind 相当于在mapper文件中定义变量然后引用这个变量

比如模糊查询,根据书的作者的姓氏查询:

1
List<Book> getBooksByAuthorFirstName(String author);
1
2
3
4
<select id="getBooksByAuthorFirstName" resultMap="BookMap">
<bind name="authorLike" value="author+'%'"/>
select * from t_book where author like #{authorLike};
</select>

SQL片段

在 SQL 查询中,一般不建议写 *,因为 select * 会降低查询效率。但是,每次查询都要把字段名列出来,太麻烦。这种使用,我们可以利用 SQL 片段来解决这个问题。

先在 mapper 中定义一个 SQL 片段:

1
2
3
4
<sql id="Base_Column">
id,usename,address
</sql>

然后,在其他 SQL 中,就可以引用这个变量:

1
2
3
4
5
6
<select id="getUserByIds" resultType="com.cwz.mybatis.model.User">
select <include refid="Base_Column" /> from user where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</select>

MyBatis进阶查询

一对一查询

比如每篇博客有一个作者,作者有自己的属性。

数据库两张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) DEFAULT NULL,
`content` text,
`aid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;


CREATE TABLE `author` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

先定义两个实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Article {
private Integer id;
private String title;
private String content;
private Author author;
// 省略get、set
}

public class Author {
private Integer id;
private String name;
private Integer age;
// 省略get、set
}

新建一个ArticleMapper.java:

1
2
3
public interface ArticleMapper {
Article getArticleById(Integer id);
}

ArticleMapper中定义了一个查询Article的方法,希望查出Article的同时也能查出Author。

ArticleMapper.xml:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis02.mapper.ArticleMapper">
<select id="getArticleById" resultType="com.cwz.mybatis02.model.Article">
select a.*,au.id as 'author.id',au.name as 'author.name',au.age as 'author.age' from article a,author au where a.aid=au.id and a.id=#{id};
</select>
</mapper>

这样写太low了,而且很容易写错。

优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis02.mapper.ArticleMapper">

<resultMap id="ArticleMap2" type="com.cwz.mybatis02.model.Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
<association property="author" javaType="com.cwz.mybatis02.model.Author" columnPrefix="author_">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
</association>
</resultMap>

<select id="getArticleById2" resultMap="ArticleMap2">
select a.*,au.id as author_id,au.name as author_name,au.age as author_age from article a,author au where a.aid=au.id and a.id=#{id};
</select>
</mapper>

在这个查询 SQL 中,首先应该做好一对一查询,返回值一定要定义成 resultMap,注意,这里千万不能写错。然后,在 resultMap 中,来定义查询结果的映射关系。其中,association 节点用来描述一对一的关系。这个节点中的内容,和 resultMap 一样,也是 id,result 等,在这个节点中,我们还可以继续描述一对一。columnPrefix表示的是查询SQL字段的前缀。

由于在实际项目中,每次返回的数据类型可能都会有差异,这就需要定义多个 resultMap,而这多个 resultMap 中,又有一部份属性是相同的,所以,我们可以将相同的部分抽出来,做成一个公共的模板,然后被其他 resultMap 继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<resultMap id="BaseArticleMap" type="com.cwz.mybatis02.model.Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
</resultMap>

<resultMap id="ArticleMap2" type="com.cwz.mybatis02.model.Article" extends="BaseArticleMap">
<association property="author" javaType="com.cwz.mybatis02.model.Author" columnPrefix="author_" resultMap="com.cwz.mybatis02.mapper.AuthorMapper.AuthorMap">
<!-- <id property="id" column="id"/>-->
<!-- <result property="name" column="name"/>-->
<!-- <result property="age" column="age"/>-->
</association>
</resultMap>

<select id="getArticleById3" resultMap="ArticleMap3">
select a.*,au.id as author_id,au.name as author_name,au.age as author_age from article a,author au where a.aid=au.id and a.id=#{id};
</select>

AuthorMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis02.mapper.AuthorMapper">
<resultMap id="AuthorMap" type="com.cwz.mybatis02.model.Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
</resultMap>
</mapper>

在association节点中,还有有一个resultMap属性,可以用来指定其他查询的mapper

懒加载

上面这种加载方式,是一次性的读取到所有数据。然后在 resultMap 中做映射。如果一对一的属性使用不是很频繁,可能偶尔用一下,这种情况下,我们也可以启用懒加载。

懒加载,就是先查询 article,查询 article的过程中,不去查询 author,当用户第一次调用了 article 中的 author 属性后,再去查询 author。

定义一个 Article的查询方法:

1
Article getArticleById4(Integer id);

ArticleMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap id="BaseArticleMap" type="com.cwz.mybatis02.model.Article">
<id property="id" column="id"/>
<result property="title" column="title"/>
<result property="content" column="content"/>
</resultMap>

<resultMap id="ArticleMap4" type="com.cwz.mybatis02.model.Article" extends="BaseArticleMap">
<association property="author" javaType="com.cwz.mybatis02.model.Author" select="com.cwz.mybatis02.mapper.AuthorMapper.getAuthorById" column="{id=aid}" fetchType="lazy"></association>
</resultMap>

<select id="getArticleById4" resultMap="ArticleMap4">
select * from article where id=#{id};
</select>

这里定义 association 的时候,不直接指定映射的字段,而是指定要执行的方法,通过 select 字段来指定查询author的方法,column 表示执行方法时传递的参数字段,最后的 fetchType 表示开启懒加载。

AuthorMapper.java:

1
2
3
public interface AuthorMapper {
Author getAuthorById(Integer id);
}

AuthorMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis02.mapper.AuthorMapper">
<resultMap id="AuthorMap" type="com.cwz.mybatis02.model.Author">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="age" column="age"/>
</resultMap>

<select id="getAuthorById" resultMap="AuthorMap">
select * from author where id=#{id};
</select>

</mapper>

一对多查询

比如,用户和角色,一个用户可以有多个角色

先准备三张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 用户表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

-- 角色表
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- 连接表
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`rid` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

创建实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 角色
public class Role {
private Integer id;
private String name;
}

// 用户
public class User {

private Integer id;
private String username;
private String address;
private List<Role> roles;
}

定义一个根据 id 查询用户的方法:

UserMapper.java

1
List<User> getAllUsersWithRole();

对应的xml UserMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resultMap id="BaseUserMap" type="com.cwz.mybatis02.model.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="address" column="address"/>
</resultMap>

<resultMap id="UserMapWithRole" type="com.cwz.mybatis02.model.User" extends="BaseUserMap">
<collection property="roles" ofType="com.cwz.mybatis02.model.Role" columnPrefix="role_" resultMap="com.cwz.mybatis02.mapper.RoleMapper.BaseRoleMap">
</collection>
</resultMap>

<select id="getAllUsersWithRole" resultMap="UserMapWithRole">
select u.*,r.id as role_id,r.name as role_name from user u left join user_role ur on u.id=ur.uid left join role r on ur.rid=r.id
</select>

RoleMapper.xml:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis02.mapper.RoleMapper">
<resultMap id="BaseRoleMap" type="com.cwz.mybatis02.model.Role">
<id column="id" property="id"/>
<result column="name" property="name"/>
</resultMap>
</mapper>

在 resultMap 中,通过 collection 节点来描述集合的映射关系。在映射时,会自动将一的一方数据集合并,然后将多的一方放到集合中,能实现这一点,靠的就是 id 属性。

当然,这个一对多,也可以做成懒加载的形式:

查询的方法:

1
List<User> getAllUsersWithRole2();

对应的mapper:

1
2
3
4
5
6
7
8
<resultMap id="UserMapWithRole2" type="com.cwz.mybatis02.model.User" extends="BaseUserMap">
<collection property="roles" ofType="com.cwz.mybatis02.model.Role" select="com.cwz.mybatis02.mapper.RoleMapper.getRoleByUid" column="{uid=id}" fetchType="lazy">
</collection>
</resultMap>

<select id="getAllUsersWithRole2" resultMap="UserMapWithRole2">
select * from user;
</select>

RoleMapper.java:

1
2
3
public interface RoleMapper {
List<Role> getRoleByUid(Integer uid);
}

RoleMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis02.mapper.RoleMapper">
<resultMap id="BaseRoleMap" type="com.cwz.mybatis02.model.Role">
<id column="id" property="id"/>
<result column="name" property="name"/>
</resultMap>
<select id="getRoleByUid" resultMap="BaseRoleMap">
select r.* from role r,user_role ur where r.id=ur.rid and ur.uid=#{uid}
</select>
</mapper>

鉴别映射器

将上面的user表改一下:

1
2
3
4
5
6
7
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`address` varchar(255) DEFAULT NULL,
`enabled` tinyint(4) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

加了一个enabled字段,表示这个角色是可用的,就把它映射进来。如果不可用,就不把角色的映射进来,直接返回user本身。

先定义一个查询方法:

1
List<User> getAllUsersWithRole3();

对应的mapper:

UserMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<resultMap id="BaseUserMap" type="com.cwz.mybatis02.model.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="address" column="address"/>
<result property="enabled" column="enabled"/>
</resultMap>

<resultMap id="UserMapWithRole" type="com.cwz.mybatis02.model.User" extends="BaseUserMap">
<collection property="roles" ofType="com.cwz.mybatis02.model.Role" columnPrefix="role_" resultMap="com.cwz.mybatis02.mapper.RoleMapper.BaseRoleMap">
</collection>
</resultMap>

<resultMap id="UserMapWithRole3" type="com.cwz.mybatis02.model.User">
<discriminator javaType="int" column="enabled">
<case value="1" resultMap="UserMapWithRole"></case>
<case value="0" resultMap="BaseUserMap"></case>
</discriminator>
</resultMap>

<select id="getAllUsersWithRole3" resultMap="UserMapWithRole3">
select u.*,r.id as role_id,r.name as role_name from user u left join user_role ur on u.id=ur.uid left join role r on ur.rid=r.id
</select>

当enabled为0代表角色不可用,直接使用BaseUserMap映射,不返回role,只返回user本身

自定义类型处理器

比如在上面的user表中新增一个字段:

新增了一个字段favorites,类型是varchar

实体类修改:

1
2
3
4
5
6
7
8
9
public class User {

private Integer id;
private String username;
private String address;
private List<Role> roles;
private boolean enabled;
private List<String> favorites;
}

这里favorites存的是list,爱好可能不只一个。

MyBatis有默认的类型处理器,但是没有把list转为varchar的,所以需要自定义类型处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.cwz.mybatis02.typehandler;

import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

@MappedJdbcTypes(JdbcType.VARCHAR) // jdbc类型是varchar
@MappedTypes(List.class) // java是list
public class List2Varchar implements TypeHandler<List<String>> {

// 存到数据库的格式:跑步,跳绳
@Override
public void setParameter(PreparedStatement ps, int i, List<String> parameter, JdbcType jdbcType) throws SQLException {
StringBuffer sb = new StringBuffer();
for (String s : parameter) {
sb.append(s).append(",");
}
ps.setString(i, sb.toString());
}

@Override
public List<String> getResult(ResultSet rs, String columnName) throws SQLException {
String s = rs.getString(columnName);
if (s != null){
return Arrays.asList(s.split(","));
}
return null;
}

@Override
public List<String> getResult(ResultSet rs, int columnIndex) throws SQLException {
String s = rs.getString(columnIndex);
if (s != null){
return Arrays.asList(s.split(","));
}
return null;
}

@Override
public List<String> getResult(CallableStatement cs, int columnIndex) throws SQLException {
String s = cs.getString(columnIndex);
if (s != null){
return Arrays.asList(s.split(","));
}
return null;
}
}

定义一个添加用户的方法:

1
Integer addUser3(User user);

对应的sql:

1
2
3
<insert id="addUser3" parameterType="com.cwz.mybatis02.model.User">
insert into user (username,address,favorites) values(#{username},#{address},#{favorites,typeHandler=com.cwz.mybatis02.typehandler.List2Varchar})
</insert>

测试方法:

1
2
3
4
5
6
7
8
9
@Test
public void addUser3() {
User user = new User();
user.setUsername("赵子龙");
user.setFavorites(Arrays.asList("足球", "篮球"));
Integer result = userMapper.addUser3(user);
System.out.println("result = " + result);
sqlSession.commit();
}

这样就实现了类型转换插入数据

但是查询的时候也要指定类型处理器:

1
2
3
4
5
6
7
<resultMap id="BaseUserMap" type="com.cwz.mybatis02.model.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="address" column="address"/>
<result property="enabled" column="enabled"/>
<result property="favorites" column="favorites" typeHandler="com.cwz.mybatis02.typehandler.List2Varchar"/>
</resultMap>

MyBatis查询缓存

一级缓存

Mybatis 一级缓存的作用域是同一个 SqlSession,在同一个 sqlSession 中两次执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个 sqlSession 结束后该 sqlSession 中的一级缓存也就不存在了。

Mybatis 默认开启一级缓存。

如果开启了一个新的 SqlSession,则新的 SqlSession 无法就是之前的缓存,必须是同一个 SqlSession 中,缓存才有效

二级缓存

  • Mybatis 二级缓存是多个 SqlSession 共享的,其作用域是 mapper 的同一个 namespace,不同的 sqlSession 两次执行相同 namespace 下的 sql 语句且向 sql 中传递参数也相同即最终执行相同的 sql 语句
  • 第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率
  • Mybatis 默认没有开启二级缓存,需要在 setting 全局参数中配置开启二级缓存

在mybatis-config.xml中:

1
2
3
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

然后在对应的mapper加上<cache/>即可:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.mybatis03.mapper.UserMapper">

<cache/>

<select id="getUserById" resultType="com.cwz.mybatis03.model.User">
select * from user where id=#{id};
</select>
</mapper>

cache节点有一些参数:

  • <cache eviction="LRU"/> 缓存默认使用的策略是LRU
  • <cache flushInterval="60000"/> flushInterval 配置刷新间隔,一般不用配置
  • <cache size="2048"/> size 缓存对象的数目,最多可以缓存多少个对象,默认1024
  • <cache readOnly="true"/> readOnly 为true,缓存会给调用者返回缓存对象的相同实例,这相同实例不可被修改

验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test2() {
SqlSession session1 = sqlSessionFactory.openSession();
UserMapper um1 = session1.getMapper(UserMapper.class);
User user1 = um1.getUserById(1);
System.out.println("user1 = " + user1);
user1.setUsername("666666666");
session1.close();
SqlSession session2 = sqlSessionFactory.openSession();
UserMapper um2 = session2.getMapper(UserMapper.class);
User user2 = um2.getUserById(1);
System.out.println("user2 = " + user2);
}

自定义MyBatis插件

内存分页

在UserMapper.java中新增方法:

1
List<User> getAllUsersByPage(RowBounds rowBounds);

对应的mapper:

1
2
3
<select id="getAllUsersByPage" resultType="com.cwz.mybatis03.model.User">
select * from user;
</select>

测试:

1
2
3
4
5
6
7
8
@Test
public void test3() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
List<User> list = userMapper.getAllUsersByPage(new RowBounds(1, 2));
for (User user : list) {
System.out.println("user = " + user);
}
}

在RowBounds中传递分页参数,SQL中不用写就可以内存分页。但是数据量大这种分页没有意义,需要的是物理分页。

MyBatis 插件接口

MyBatis 插件是通过拦截器来起作用的,MyBatis 框架在设计的时候,就已经为插件的开发预留了相关接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

default Object plugin(Object target) {
return Plugin.wrap(target, this);
}

default void setProperties(Properties properties) {
// NOP
}

}

这个接口中就三个方法,第一个方法必须实现,后面两个方法都是可选的。三个方法作用分别如下:

  • intercept:具体的拦截方法,自定义 MyBatis 插件时,一般都需要重写该方法
  • plugin:这个方法的参数 target 就是拦截器要拦截的对象,一般来说我们不需要重写该方法。Plugin.wrap 方法会自动判断拦截器的签名和被拦截对象的接口是否匹配,如果匹配,才会通过动态代理拦截目标对象
  • setProperties:这个方法用来传递插件的参数,可以通过参数来改变插件的行为。我们定义好插件之后,需要对插件进行配置,在配置的时候,可以给插件设置相关属性,设置的属性可以通过该方法获取到

MyBatis拦截器签名

拦截器签名是一个名为 @Intercepts 的注解,该注解中可以通过 @Signature 配置多个签名。@Signature 注解中则包含三个属性:

  • type: 拦截器需要拦截的接口,有 4 个可选项,分别是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
  • method: 拦截器所拦截接口中的方法名,也就是前面四个接口中的方法名,接口和方法要对应上。
  • args: 拦截器所拦截方法的参数类型,通过方法名和参数类型可以锁定唯一一个方法。

被拦截的对象

根据前面的介绍,被拦截的对象主要有如下四个:

Executor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface Executor {

ResultHandler NO_RESULT_HANDLER = null;

int update(MappedStatement ms, Object parameter) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

List<BatchResult> flushStatements() throws SQLException;

void commit(boolean required) throws SQLException;

void rollback(boolean required) throws SQLException;

CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

boolean isCached(MappedStatement ms, CacheKey key);

void clearLocalCache();

void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

Transaction getTransaction();

void close(boolean forceRollback);

boolean isClosed();

void setExecutorWrapper(Executor executor);

}

各方法含义分别如下:

  • update:该方法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦截这些操作,可以通过该方法实现。
  • query:该方法会在 SELECT 查询方法执行时被调用,方法参数携带了很多有用的信息,如果需要获取,可以通过该方法实现。
  • queryCursor:当 SELECT 的返回类型是 Cursor 时,该方法会被调用。
  • flushStatements:当 SqlSession 方法调用 flushStatements 方法或执行的接口方法中带有 @Flush 注解时该方法会被触发。
  • commit:当 SqlSession 方法调用 commit 方法时该方法会被触发。
  • rollback:当 SqlSession 方法调用 rollback 方法时该方法会被触发。
  • getTransaction:当 SqlSession 方法获取数据库连接时该方法会被触发。
  • close:该方法在懒加载获取新的 Executor 后会被触发。
  • isClosed:该方法在懒加载执行查询前会被触发。

ParameterHandler

1
2
3
4
5
6
7
public interface ParameterHandler {

Object getParameterObject();

void setParameters(PreparedStatement ps) throws SQLException;

}

各方法含义分别如下:

  • getParameterObject:在执行存储过程处理出参的时候该方法会被触发。
  • setParameters:设置 SQL 参数时该方法会被触发。

ResultSetHandler

1
2
3
4
5
6
7
8
9
public interface ResultSetHandler {

<E> List<E> handleResultSets(Statement stmt) throws SQLException;

<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

void handleOutputParameters(CallableStatement cs) throws SQLException;

}
  • handleResultSets:该方法会在所有的查询方法中被触发(除去返回值类型为 Cursor 的查询方法),一般来说,如果我们想对查询结果进行二次处理,可以通过拦截该方法实现。
  • handleCursorResultSets:当查询方法的返回值类型为 Cursor 时,该方法会被触发。
  • handleOutputParameters:使用存储过程处理出参的时候该方法会被调用。

StatementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface StatementHandler {

Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;

void parameterize(Statement statement)
throws SQLException;

void batch(Statement statement)
throws SQLException;

int update(Statement statement)
throws SQLException;

<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;

<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;

BoundSql getBoundSql();

ParameterHandler getParameterHandler();

}
  • prepare:该方法在数据库执行前被触发。
  • parameterize:该方法在 prepare 方法之后执行,用来处理参数信息。
  • batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”,执行数据操作时该方法会被调用。
  • update:更新操作时该方法会被触发。
  • query:该方法在 SELECT 方法执行时会被触发。
  • queryCursor:该方法在 SELECT 方法执行时,并且返回值为 Cursor 时会被触发。

在开发一个具体的插件时,我们应当根据自己的需求来决定到底拦截哪个方法。

自定义分页插件

首先我们需要自定义一个 RowBounds,因为 MyBatis 原生的 RowBounds 是内存分页,并且没有办法获取到总记录数(一般分页查询的时候我们还需要获取到总记录数),所以我们自定义 PageRowBounds,对原生的 RowBounds 功能进行增强,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PageRowBounds extends RowBounds {
private Integer total;

public PageRowBounds(int offset, int limit) {
super(offset, limit);
}

public PageRowBounds() {
}

public Long getTotal() {
return total;
}

public void setTotal(Long total) {
this.total = total;
}
}

自定义的 PageRowBounds 中增加了 total 字段,用来保存查询的总记录数。

接下来我们自定义拦截器 PageInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.cwz.mybatis03.plugin;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.lang.reflect.Field;
import java.util.*;

@Intercepts(@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0]; // 拦截方法的MappedStatement xml中mapper的节点(select、insert等)会封装成MappedStatement对象
Object parameterObject = args[1]; // 拦截方法的参数
RowBounds rowBounds = (RowBounds) args[2];
// 需要分页
if (rowBounds != RowBounds.DEFAULT) {
Executor executor = (Executor) invocation.getTarget();
BoundSql boundSql = ms.getBoundSql(parameterObject);
Field additionalParametersFields = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersFields.setAccessible(true);
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersFields.get(boundSql);


if (rowBounds instanceof PageRowBounds) {
MappedStatement countMs = newMappedStatement(ms, Integer.class);
CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
Set<String> keySet = additionalParameters.keySet();
for (String key : keySet) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, ((ResultHandler) args[3]), countKey, countBoundSql);
Integer total = (Integer) countQueryResult.get(0);
((PageRowBounds) rowBounds).setTotal(total);

}


CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
pageKey.update("RowBounds");
String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
Set<String> keySet = additionalParameters.keySet();
for (String key : keySet) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
List<Object> list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
return list;
}
//不需要分页,直接返回结果
return invocation.proceed();
}

private MappedStatement newMappedStatement(MappedStatement ms, Class<Integer> typeClass) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType());
ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), typeClass, new ArrayList<>(0)).build();
builder.resource(ms.getResource())
.fetchSize(ms.getFetchSize())
.statementType(ms.getStatementType())
.timeout(ms.getTimeout())
.parameterMap(ms.getParameterMap())
.resultSetType(ms.getResultSetType())
.cache(ms.getCache())
.flushCacheRequired(ms.isFlushCacheRequired())
.useCache(ms.isUseCache())
.resultMaps(Arrays.asList(resultMap));
return builder.build();
}
}

  • 首先通过 @Intercepts 注解配置拦截器签名,从 @Signature 的定义中我们可以看到,拦截的是 Executor的query 方法,该方法有一个重载方法,通过 args 指定了方法参数,进而锁定了重载方法
  • 将查询操作拦截下来之后,接下来我们的操作主要在 PageInterceptor的intercept 方法中完成,该方法的参数重包含了拦截对象的诸多信息
  • 通过 invocation.getArgs() 获取拦截方法的参数,获取到的是一个数组,正常来说这个数组的长度为 4。
    • 数组第一项是一个 MappedStatement,我们在 Mapper.xml 中定义的各种操作节点和 SQL,都被封装成一个个的 MappedStatement 对象了
    • 数组第二项就是所拦截方法的具体参数,也就是你在 Mapper 接口中定义的方法参数
    • 数组的第三项是一个 RowBounds 对象,我们在 Mapper 接口中定义方法时不一定使用了 RowBounds 对象,如果我们没有定义 RowBounds 对象,系统会给我们提供一个默认的 RowBounds.DEFAULT
    • 数组第四项则是一个处理返回值的 ResultHandler
  • 接下来判断上一步提取到的 rowBounds 对象是否不为 RowBounds.DEFAULT,如果为RowBounds.DEFAULT,说明用户不想分页;如果不为 RowBounds.DEFAULT,则说明用户想要分页,如果用户不想分页,则直接执行最后的 return invocation.proceed();,让方法继续往下走就行了。
  • 如果需要进行分页,则先从 invocation 对象中取出执行器 Executor、BoundSql 以及通过反射拿出来 BoundSql 中保存的额外参数(如果我们使用了动态 SQL,可能会存在该参数)。BoundSql 中封装了我们执行的 Sql 以及相关的参数
  • 接下来判断 rowBounds 是否是 PageRowBounds 的实例,如果是,说明除了分页查询,还想要查询总记录数,如果不是,则说明 rowBounds 可能是 RowBounds 实例,此时只要分页即可,不用查询总记录数
  • 如果需要查询总记录数,则首先调用 newMappedStatement 方法构造出一个新的 MappedStatement 对象出来,这个新的 MappedStatement 对象的返回值是 Long 类型的。然后分别创建查询的 CacheKey、拼接查询的 countSql,再根据 countSql 构建出 countBoundSql,再将额外参数添加进 countBoundSql 中。最后通过 executor.query 方法完成查询操作,并将查询结果赋值给 PageRowBounds 中的 total 属性。
  • 接下来进行分页查询,需要强调的是,当我们启动了这个分页插件之后,MyBatis 原生的 RowBounds 内存分页会变成物理分页,原因就在这里我们修改了查询 SQL
  • 将查询结果返回

测试插件

在全局配置中配置分页插件:

1
2
3
<plugins>
<plugin interceptor="com.cwz.mybatis03.plugin.PageInterceptor"></plugin>
</plugins>

接下来我们在 Mapper 中定义查询接口:

1
2
3
public interface UserMapper {
List<User> getAllUsersByPage(RowBounds rowBounds);
}

定义 UserMapper.xml:

1
2
3
<select id="getAllUsersByPage" resultType="com.cwz.mybatis03.model.User">
select * from user
</select>

测试:

1
2
3
4
5
6
7
8
9
10
@Test
public void test3() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
PageRowBounds pageRowBounds = new PageRowBounds(1, 2);
List<User> list = userMapper.getAllUsersByPage(pageRowBounds);
for (User user : list) {
System.out.println("user = " + user);
}
System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
}

ssm整合

先创建一个maven工程,添加依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.6</version>
</dependency>
</dependencies>

创建java web工程:

配置web.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceRequestEncoding</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>forceResponseEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

</web-app>

数据库资源文件db.properties:

1
2
3
4
db.username=root
db.password=123456
db.url=jdbc:mysql:///test1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
db.driverClass=com.mysql.cj.jdbc.Driver

配置spring配置文件applicationContext.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">


<context:component-scan base-package="com.cwz.ssm" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<context:property-placeholder location="classpath:db.properties"/>

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource" id="dataSource">
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
<property name="url" value="${db.url}"/>
<property name="driverClassName" value="${db.driverClass}"/>
</bean>

<bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.cwz.ssm.model"/>
<property name="mapperLocations">
<list>
<value>classpath*:com/cwz/ssm/mapper/*.xml</value>
</list>
</property>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" id="mapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<property name="basePackage" value="com.cwz.ssm.mapper"/>
</bean>

</beans>

配置springmvc spring-servlet.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

<context:component-scan base-package="com.cwz.ssm" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

<mvc:annotation-driven/>

</beans>

实体类User.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.cwz.ssm.model;

public class User {
private Integer id;
private String username;
private String address;

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", address='" + address + '\'' +
'}';
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

定义查询方法UserMapper.java

1
2
3
4
5
6
7
8
9
package com.cwz.ssm.mapper;

import com.cwz.ssm.model.User;

import java.util.List;

public interface UserMapper {
List<User> getAllUsers();
}

对应的mapper文件UserMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwz.ssm.mapper.UserMapper">

<select id="getAllUsers" resultType="User">
select * from user
</select>

</mapper>

由于UseMapper.xml和UserMapper.java放在一起,需要在maven中build配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>

UserService.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.cwz.ssm.service;

import com.cwz.ssm.mapper.UserMapper;
import com.cwz.ssm.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {
@Autowired
UserMapper userMapper;
public List<User> getAllUsers() {
return userMapper.getAllUsers();
}
}

UserController.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.cwz.ssm.comtroller;

import com.cwz.ssm.model.User;
import com.cwz.ssm.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

@Autowired
UserService userService;

@GetMapping("/users")
public List<User> getAllUsers() {
return userService.getAllUsers();
}
}