SSM三大框架:Spring,SpringMVC,Mybatis
一、Spring
1. 关于Spring
Spring框架主要解决了创建对象、管理对象的问题。
在传统的开发中,当需要某个对象时,使用new
关键字及类型的构造方法即可创建对象,例如:
Random random = new Random();
如果以上代码存在于某个方法中。则random
就只是个局部变量,当方法运行结束,此变量就会被销毁!
在实际项目开发,许多对象被创建出来之后,应该长期存在于内存中,而不应该销毁,当需要使用这些对象时,通过某种方式获取对象即可,而不应该重新创建对象!
除了对象存在的时间(时长)以外,在实际项目开发中,还需要关注各个类型之间的依赖关系!例如:
public class UserMapper { public void insert() { // 向数据表中的“用户表”中插入数据 } }
public class UserController { public UserMapper userMapper; public void reg() { userMapper.insert(); } }
在以上示例代码中,可视为“UserController
是依赖于UserMapper
的”,也可以把UserMapper
称之为“UserController
的依赖项”。
当需要创建以上类型时,如果只是单纯的把UserController
创建出来了,却没有关注其内部userMapper
属性的值,甚至该属性没有值,则是错误的做法!
如果要使得UserController
中的UserMapper
属性是有值的,也非常简单,例如:
public class UserController { public UserMapper userMapper = new UserMapper(); public void reg() { userMapper.insert(); } }
但是,在实际项目中,除了UserController
以外,还会有其它的组件也可能需要使用到UserMapper
,如果每个组件中都使用new UserMapper()
的语法来创建对象,就不能保证UserMapper
对象的唯一性,就违背了设计初衷。
为了更好的创建对象和管理对象,应该使用Spring框架!
2. 创建基于Spring的工程
当某个项目需要使用Spring框架时,推荐使用Maven工程。
创建名为spring01
的Maven工程,创建完成后,会自动打开pom.xml
文件,首先,应该在此文件中添加节点,此节点是用于添加依赖项的:
<dependencies></dependencies>
然后,Spring框架的依赖项的代码需要编写在以上节点的子级,而依赖项的代码推荐从 https://mvnrepository.com/ 网站查询得到,Spring的依赖项名称是spring-context
,则在此网站搜索该名称,以找到依赖项的代码,代码示例:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.14</version></dependency>
将以上依赖项的代码复制到节点之下即可。
然后,点击IDEA中悬浮的刷新Maven的图标,或展开右侧的Maven面板点击刷新按钮,即可自动下载所需要的jar包文件(这些文件会被下载到本机的Maven仓库中,同一个依赖项的同一个版本只会下载一次)。
3. 通过Spring创建对象–通过@Bean方法
演示案例:spring01
操作步骤:
- 创建
cn.tedu.spring.SpringBeanFactory
类 - 在类中添加方法,方法的返回值类型就是你希望Spring创建并管理的对象的类型,并在此方法中自行编写返回有效对象的代码
- 在此类上添加
@Configuration
注解 - 在此方法上添加
@Bean
注解
以上步骤的示例代码:
package cn.tedu.spring;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.Random;@Configurationpublic class SpringBeanFactory { @Bean public Random random() { return new Random(); }}
接下来,创建某个类用于执行:
public class SpringRunner { public static void main(String[] args) { // 1. 加载Spring AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringBeanFactory.class); // 2. 从Spring中获取对象 Random random = (Random) ac.getBean("random"); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 System.out.println("random > " + random); System.out.println("random.nextInt() > " + random.nextInt()); // 4. 关闭 ac.close(); }}
接下来,运行SpringRunner
类的main()
方法即可看到执行效果。
关于以上代码:
- 在
AnnotationConfigApplicationContext
的构造方法中,应该将SpringBeanFactory.class
作为参数传入,否则就不会加载SpringBeanFactory
类中内容- 其实,在以上案例中,
SpringBeanFactory
类上的@Configuration
注解并不是必须的
- 其实,在以上案例中,
- 在
getBean()
时,传入的字符串参数"random"
是SpringBeanFactory
类中的方法的名称 - 在
SpringBeanFactory
类中的方法必须添加@Bean
注解,其作用是使得Spring框架自动调用此方法,并管理此方法返回的结果 - 关于
getBean()
方法,此方法被重载了多次,典型的有:Object getBean(String beanName)
- 通过此方法,传入的
beanName
必须是有效的,否则将导致NoSuchBeanDefinitionException
- 通过此方法,传入的
T getBean(Class beanClass)
;- 使用此方法时,传入的类型在Spring中必须有且仅有1个对象,如果没有匹配类型的对象,将导致
NoSuchBeanDefinitionException
,如果有2个,将导致NoUniqueBeanDefinitionException
- 使用此方法时,传入的类型在Spring中必须有且仅有1个对象,如果没有匹配类型的对象,将导致
T getBean(String beanName, Class beanClass)
- 此方法仍是根据传入的
beanName
获取对象,并且根据传入的beanClass
进行类型转换
- 此方法仍是根据传入的
- 使用的
@Bean
注解可以传入String
类型的参数,如果传入,则此注解对应的方法的返回结果的beanName
就是@Bean
注解中传入的String
参数值
4. 通过Spring创建对象–组件扫描
演示案例:spring02
先使用同样的步骤创建spring02
工程。
操作步骤:
-
在
pom.xml
中添加spring-context
的依赖项 -
自行创建某个类,例如创建
cn.tedu.spring.UserMapper
类,并在类的声明之前添加@Component
注解 -
与前次案例相似,创建可执行的类,与前次案例的区别在于:
-
在
AnnotationConfigApplicationContext
的构造方法中传入的是UserMapper
类的包名,即:AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext("cn.tedu.spring");
-
调用
getBean()
时,传入的名称是将UserMapper
类的名称的首字母改为小写,即:UserMapper userMapper = ac.getBean("userMapper", UserMapper.class);
-
关于以上代码:
- 在创建
AnnotationConfigApplicationContext
时传入的参数是一个basePackages
,即多个“根包”,它会使得Spring框架扫描这个包及其子孙包中的所有类,并尝试创建这些包中的组件的对象AnnotationConfigApplicationContext
的构造方法设计的是String...
类型的参数,即可变参数,当需要输入多个包名时,各包名使用逗号隔开即可- 推荐传入的包名是更加具体的,但不需要特别精准,只需要保证不会扫描到非自定义的包即可,例如包名肯定不会包含项目的依赖项的包
- 即使有了组件扫描,Spring也不会直接创建包下所有类的对象,仅当类上添加了组件注解,才会被Spring视为“组件”,Spring才会创建对应的类的对象
- 当
getBean()
时,由Spring创建的组件类的对象,默认的名称都是将首字母改为小写- 以上规则仅适用于:类名中的第1个字母是大写,且第2个字母是小写的情况,如果类名不符合这种情况,则
getBean()
时传入的名称就是类名(与类名完全相同的字符串)
- 以上规则仅适用于:类名中的第1个字母是大写,且第2个字母是小写的情况,如果类名不符合这种情况,则
关于组件:
- 在Spring框架中,可用的组件注解有:
@Component
:通用组件注解@Controller
:应该添加在“控制器类”上@Service
:应该添加在“业务类”上@Repository
:应该添加在“数据存取类”上
- 另外,
@Configuration
是一种特殊的组件,应该添加在“配置类”上,当执行组件扫描时,添加了@Configuration
注解的类也会被创建对象
其它:
-
可以在
@Component
等组件注解(不包含@Configuration
)中配置字符串参数,以显式的指定Bean的名称 -
可以使用一个配置类,在配置类上通过
@ComponentScan
来指定组件扫描的包,并在加载Spring时,传入此配置类即可,例如:package cn.tedu.spring;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan("cn.tedu.spring")public class SpringConfig {}
// 以下是加载Spring的代码片段AnnotationConfigApplicationContext ac= new AnnotationConfigApplicationContext(SpringConfig.class);
在使用
@ComponentScan
时,也可以传入多个包名,例如:@ComponentScan({"cn.tedu.spring.controller", "cn.tedu.spring.service"})
5. 关于2种通过Spring创建对象的做法
以上分别介绍了使用@Bean
方法和使用组件扫描的方式使得Spring创建对象的做法,在实际应用中:
- 使用
@Bean
方法可用在所有场景,但是使用过程相对繁琐 - 使用组件扫描的做法只适用于自定义的类型(这些类是你自己编写出来的),使用过程非常便捷
所以,当需要被Spring创建对象的类型是自定义的,应该使用组件扫描的做法,如果不是自定义的,只能使用@Bean
方法。这2种做法在实际的项目开发中都会被使用到!
6. Spring管理的对象的作用域
由Spring管理的对象的作用域默认是单例的(并不是单例模式),对于同一个Bean,无论获取多少次,得到的都是同一个对象!如果希望某个被Spring管理的对象不是单例的,可以在类上添加@Scope("prototype")
注解。
并且,在单例的情况下,默认不是懒加载的,还可以通过@Lazy
注解控制它是否为懒加载模式!所谓的懒加载,就是“不要逼不得已不创建对象”。
7. Spring管理的对象的生命周期
由Spring创建并管理对象,则开发人员就没有了对象的控制权,无法对此对象的历程进行干预,而Spring允许在类中自定义最多2个方法,分别表示初始化方法和销毁方法,并且,Spring会在创建对象之后就执行初始化方法,在销毁对象之前执行销毁方法。
关于这2个方法,你可以按需添加,例如,当你只需要干预销毁过程时,你可以只定义销毁的方法,不需要定义初始化方法。
关于这2个方法的声明:
- 访问权限:推荐使用
public
- 返回值类型:推荐使用
void
- 方法名称:自定义
- 参数列表:推荐为空
然后,需要在初始化方法的声明之前添加@PostConstruct
注解,在销毁方法的声明之前添加@PreDestroy
注解。
例如:
package cn.tedu.spring;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Lazy;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Component;import org.springframework.stereotype.Controller;import org.springframework.stereotype.Repository;import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;import javax.annotation.PreDestroy;@Componentpublic class UserMapper { public UserMapper() { System.out.println("\tUserMapper.UserMapper()"); } @PostConstruct public void init() { System.out.println("\tUserMapper.init()"); } @PreDestroy public void destroy() { System.out.println("\tUserMapper.destroy()"); }}
注意:仅当类的对象被Spring管理且是单例的,才有讨论生命周期的价值,否则,不讨论生命周期。
如果某个类的对象是通过@Bean
方法被Spring管理的,并且这个类不是自定义的,可以在@Bean
注解中配置initMethod
和destroyMethod
这2个属性(它们的值就是方法名称,例如destroyMethod="close"
),将这个类中的方法指定为生命周期方法。
8. Spring的自动装配机制
Spring的自动装配机制表现为:当你需要某个对象时,可以使用特定的语法,而Spring就会尝试从容器找到合适的值,并赋值到对应的位置!
最典型的表现就是在类的属性上添加@Autowired
注解,Spring就会尝试从容器中找到合适的值为这个属性赋值!
例如有如下代码:
SpringConfig.java
package cn.tedu.spring;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration@ComponentScan("cn.tedu.spring")public class SpringConfig {}
UserMapper.java
package cn.tedu.spring;import org.springframework.stereotype.Repository;@Repositorypublic class UserMapper { public void insert() { System.out.println("UserMapper.insert() >> 将用户数据写入到数据库中……"); }}
UserController.java
package cn.tedu.spring;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;@Controllerpublic class UserController { @Autowired // 注意:此处使用了自动装配的注解 private UserMapper userMapper; public void reg() { System.out.println("UserController.reg() >> 控制器即将执行用户注册……"); userMapper.insert(); }}
SpringRunner.java
package cn.tedu.spring;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringRunner { public static void main(String[] args) { // 1. 加载Spring AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); // 2. 从Spring中获取对象 UserController userController = ac.getBean("userController", UserController.class); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 userController.reg(); // 4. 关闭 ac.close(); }}
关于@Autowired
的装配机制:
首先,会根据需要装配的数据的类型在Spring容器中查找匹配的Bean(对象)的数量,当数量为:
- 0个:判断
@Autowired
注解的required
属性的值- 当
required=true
时:装配失败,启动项目时即报告异常 - 当
required=false
时:放弃自动装配,不会报告异常- 后续当使用到此属性时,会出现
NullPointerException
- 后续当使用到此属性时,会出现
- 当
- 1个:直接装配,且装配成功
- 多个:自动尝试按照名称实现装配(属性的名称与Spring Bean的名称)
- 存在与属性名称匹配的Spring Bean:装配成功
- 不存在与属性名称匹配的Spring Bean:装配失败,启动项目时即报告异常
另外,使用@Resource
注解也可以实现自动装配(此注解是javax
包中的),其装配机制是先尝试根据名称来装配,如果失败,再尝试根据类型装配!
除了对属性装配以外,Spring的自动装配机制还可以表现出:如果某个方法是由Spring框架自动调用的(通常是构造方法,或@Bean
方法,其它的方法中,如果参数有限制则专门说明),当这个方法被声明了参数时,Spring框架也会自动的尝试从容器找到匹配的对象,用于调用此方法!
9. 读取properties配置文件中的信息
操作步骤:
-
创建新的工程
spring04
,创建步骤参考前序案例 -
在
src/main/resources
文件夹下创建jdbc.properties
,内容为:spring.jdbc.url=jdbc:mysql://localhost:3306/teduspring.jdbc.driver=com.mysql.jdbc.Driverspring.jdbc.username=rootspring.jdbc.password=1234spring.jdbc.init-size=5spring.jdbc.max-active=20
注意:自定义的属性名称建议添加一些前缀,避免与系统属性和Java属性冲突。
-
在
src/main/java
下创建Java类,使用@PropertySource
注解读取以上配置文件中的信息,则创建cn.tedu.spring.SpringConfig
类:package cn.tedu.spring;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;@Configuration@ComponentScan("cn.tedu.spring")@PropertySource("classpath:jdbc.properties")public class SpringConfig {}
提示:当Spring框架读取了配置文件中的信息后,会将这些读取到的数据封装在内置的
Environment
对象中,后续,任何需要这些配置信息的组件都可以从Environment
中读取到配置的数据。 -
接下来,可以创建某个Java类,从
Environment
中读取配置的数据,例如创建JdbcConfig
类:package cn.tedu.spring;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Componentpublic class JdbcConfig { @Value("${spring.jdbc.url}") private String url; @Value("${spring.jdbc.driver}") private String driver; @Value("${spring.jdbc.username}") private String username; @Value("${spring.jdbc.password}") private String password; @Value("${spring.jdbc.init-size}") private int initSize; @Value("${spring.jdbc.max-active}") private int maxActive; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getDriver() { return driver; } public void setDriver(String driver) { this.driver = driver; } 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; } public int getInitSize() { return initSize; } public void setInitSize(int initSize) { this.initSize = initSize; } public int getMaxActive() { return maxActive; } public void setMaxActive(int maxActive) { this.maxActive = maxActive; }}
提示:前序的操作中,在
SpringConfig
中已经配置了组件扫描,这个JdbcConfig
类必须在组件扫描的范围内,并添加组件注解,这样Spring框架才会创建JdbcConfig
类的对象,进而根据各@Value
注解将Environment
中的配置数据注入到属性中 -
最后,可以执行本案例:
package cn.tedu.spring;import org.springframework.context.annotation.AnnotationConfigApplicationContext;public class SpringRunner { public static void main(String[] args) { // 1. 加载Spring System.out.println("1. 加载Spring,开始……"); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); System.out.println("1. 加载Spring,完成!"); System.out.println(); // 2. 从Spring中获取对象 System.out.println("2. 从Spring中获取对象,开始……"); JdbcConfig jdbcConfig = ac.getBean("jdbcConfig", JdbcConfig.class); System.out.println("2. 从Spring中获取对象,完成!"); System.out.println(); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 System.out.println("3. 测试使用对象,开始……"); System.out.println("\turl >> " + jdbcConfig.getUrl()); System.out.println("\tdriver >> " + jdbcConfig.getDriver()); System.out.println("\tusername >> " + jdbcConfig.getUsername()); System.out.println("\tpassword >> " + jdbcConfig.getPassword()); System.out.println("\tinit-size >> " + jdbcConfig.getInitSize()); System.out.println("\tmax-active >> " + jdbcConfig.getMaxActive()); System.out.println("3. 测试使用对象,完成!"); System.out.println(); // 4. 关闭 System.out.println("4. 关闭,开始……"); ac.close(); System.out.println("4. 关闭,完成!"); }}
另外,还可以直接装配一个Environment
对象,并在需要的时候通过Environment
对象读取配置的数据,例如:
package cn.tedu.spring;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.env.Environment;import org.springframework.stereotype.Component;@Componentpublic class EnvironmentData { @Autowired private Environment environment; public Environment getEnvironment() { return environment; }}
package cn.tedu.spring;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.core.env.Environment;public class SpringRunner { public static void main(String[] args) { // 1. 加载Spring System.out.println("1. 加载Spring,开始……"); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); System.out.println("1. 加载Spring,完成!"); System.out.println(); // 2. 从Spring中获取对象 System.out.println("2. 从Spring中获取对象,开始……"); EnvironmentData environmentData = ac.getBean("environmentData", EnvironmentData.class); System.out.println("2. 从Spring中获取对象,完成!"); System.out.println(); // 3. 测试使用对象,以便于观察是否获取到了有效的对象 System.out.println("3. 测试使用对象,开始……"); System.out.println("---------------------------------------"); System.out.println("通过自动装配Environment对象获取的值:"); Environment env = environmentData.getEnvironment(); System.out.println("\tEnvironment >> " + env); System.out.println("\turl >> " + env.getProperty("spring.jdbc.url")); System.out.println("\tdriver >> " + env.getProperty("spring.jdbc.driver")); System.out.println("\tusername >> " + env.getProperty("spring.jdbc.username")); System.out.println("\tpassword >> " + env.getProperty("spring.jdbc.password")); System.out.println("\tinit-size >> " + env.getProperty("spring.jdbc.init-size")); System.out.println("\tmax-active >> " + env.getProperty("spring.jdbc.max-active")); System.out.println("3. 测试使用对象,完成!"); System.out.println(); // 4. 关闭 System.out.println("4. 关闭,开始……"); ac.close(); System.out.println("4. 关闭,完成!"); }}
10. 关于Spring框架的小结
关于Spring框架,你应该:
- 了解Spring框架的作用:创建对象,管理对象
- 掌握通过Spring创建对象的2种方式:
- 在配置类(带
@Configuration
注解的类)中使用@Bean
方法 - 使用组件扫描,并在类上添加组件注解
- 组件注解有:
@Component
、@Controller
、@Service
、@Repository
- 组件注解有:
- 如果是自定义的类,应该使用组件扫描+组件注解的方式,如果不是自定义的类,必须使用配置类中的
@Bean
方法
- 在配置类(带
- 了解Spring Bean的作用域与生命周期
- 掌握
@Autowired
自动装配,理解其装配机制- 建议背下来:
@Autowired
与@Resource
的区别
- 建议背下来:
- 掌握读取
.properties
配置文件中的数据- 先使用
@PropertySource
注解指定需要读取的文件 - 读取配置的数据时,可以:
- 使用
@Value
注解将值注入到属性中 - 自动装配
Environment
对象,并调用此对象的getProperty()
方法以获取配置值
- 使用
- 先使用
- 了解Spring的IoC(Inversion of Controll:控制反转)和DI(Dependency Injection:依赖注入)
- Spring框架基于DI实现了IoC,DI是一种实现手段,IoC是最终实现的目标/效果
- Spring AOP后续再讲
二、SpringMVC
1. 关于Spring MVC
Spring MVC是基于Spring框架基础之上的,主要解决了后端服务器接收客户端提交的请求,并给予响应的相关问题。
MVC = Model + View + Controller,它们分别是:
- Model:数据模型,通常由业务逻辑层(Service Layer)和数据访问层(Data Access Object Layer)共同构成
- View:视图
- Controller:控制器
MVC为项目中代码的职责划分提供了参考。
需要注意:Spring MVC框架只关心V - C之间的交互,与M其实没有任何关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ojp9GhlC-1649608506966)(images/springmvc.png)]
2. 创建Spring MVC工程
请参考 http://doc.canglaoshi.org/doc/idea_tomcat/index.html 创建项目,首次练习的项目名称请使用springmvc01
。
3. 使用Spring MVC工程接收客户端的请求
【操作步骤】
-
在
pom.xml
中添加spring-webmvc
依赖项:<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.14</version></dependency>
提示:如果后续运行时提示不可识别Servlet相关类,则补充添加以下依赖项:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope></dependency>
-
接下来,准备2个配置类,一个是Spring框架的配置类,一个是Spring MVC框架的配置类:
cn.tedu.springmvc.config.SpringConfig.java
package cn.tedu.springmvc.config;import org.springframework.context.annotation.Configuration;@Configuration // 此注解不是必须的public class SpringConfig {}
cn.tedu.springmvc.config.SpringMvcConfig.java
package cn.tedu.springmvc.config;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration // 此注解不是必须的@ComponentScan("cn.tedu.springmvc") // 必须配置在当前配置类,不可配置在Spring的配置类public class SpringMvcConfig implements WebMvcConfigurer {}
-
接下来,需要创建项目的初始化类,此类必须继承自
AbstractAnnotationConfigDispatcherServletInitializer
,并在此类中重写父类的3个抽象方法,返回正确的值(各方法的意义请参见以下代码中的注释):cn.tedu.springmvc.SpringMvcInitializer
package cn.tedu.springmvc;import cn.tedu.springmvc.config.SpringConfig;import cn.tedu.springmvc.config.SpringMvcConfig;import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;/** * Spring MVC项目的初始化类 */public class SpringMvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { // 返回自行配置的Spring相关内容的类 return new Class[] { SpringConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { // 返回自行配置的Spring MVC相关内容的类 return new Class[] { SpringMvcConfig.class }; } @Override protected String[] getServletMappings() { // 返回哪些路径是由Spring MVC框架处理的 return new String[] { "*.do" }; }}
-
最后,创建控制器类,用于接收客户端的某个请求,并简单的响应结果:
cn.tedu.springmvc.controller.UserController
package cn.tedu.springmvc.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;@Controller // 必须是@Controller,不可以是其它组件注解public class UserController { public UserController() { System.out.println("UserController.UserController()"); } // http://localhost:8080/springmvc01_war_exploded/login.do @RequestMapping("/login.do") @ResponseBody public String login() { return "UserController.login()"; }}
-
全部完成后,启动项目,会自动打开浏览器并显示主页,在主页的地址栏URL上补充
/login.do
即可实现访问,并看到结果。
关于以上案例:
- 当启动Tomcat时,会自动将项目打包并部署到Tomcat,通过自动打开的浏览器中的URL即可访问主页,在URL中有很长一段是例如
springmvc01_war_explored
这一段是不可以删除的,其它的各路径必须补充在其之后,例如/login.do
就必须在此之后 - 当启动Tomcat时,项目一旦部署成功,就会自动创建并加载
AbstractAnnotationConfigDispatcherServletInitializer
的子类,即当前项目中自定义的SpringMvcInitialier
,无论这个类放在哪个包中,都会自动创建并加载,由于会自动调用这个类中所有方法,所以会将Spring MVC框架处理的请求路径设置为*.do
,并执行对cn.tedu.springmvc
的组件扫描,进而会创建UserController
的对象,由于在UserController
中配置的方法使用了@RequestMapping("/login.do")
,则此时还将此方法与/login.do
进行了绑定,以至于后续随时访问/login.do
时都会执行此方法 - 注意:组件扫描必须配置在Spring MVC的配置类中
- 注意:控制器类上的注解必须是
@Controller
,不可以是@Component
、@Service
、@Repository
4. 关于@RequestMapping
注解
@RequestMapping
注解的主要作用是配置请求路径与处理请求的方法的映射关系,例如将此注解添加在控制器中某个方法之前:
// http://localhost:8080/springmvc01_war_exploded/login.do@RequestMapping("/login.do")@ResponseBodypublic String login() { return "UserController.login()";}
就会将注解中配置的路径与注解所在的方法对应上!
除了方法之前,此注解还可以添加在控制器类之前,例如:
@Controller@RequestMapping("/user")public class UserController {}
一旦在类上添加了此注解并配置路径,则每个方法实际映射到的请求路径都是“类上的@RequestMapping
配置的路径 + 方法上的@RequestMapping
配置的路径”。
通常,在项目中,推荐为每一个控制器类都配置此注解,以指定某个URL前缀。
在使用@RequestMapping
配置路径时,并不要求各路径使用 /
作为第1个字符!
另外,在@RequestMapping
还可以配置:
- 请求方式
- 请求头
- 响应头
- 等等
所以,在@RequestMapping
注解中,增加配置method
属性,可以限制客户端的请求方式,例如可以配置为:
@RequestMapping(value = "/login.do", method = RequestMethod.POST)@ResponseBodypublic String login() { return "UserController.login()";}
如果按照以上代码,则/login.do
路径只能通过POST
方式发起请求才可以被正确的处理,如果使用其它请求方式(例如GET
),则会导致HTTP的405错误。
如果没有配置method
属性,则表示可以使用任何请求方式,包括:
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
另外,Spring MVC框架还提供了@RequestMapping
的相关注解,例如:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
- 等等
这些注解就是已经限制了请求方式的注解!以@GetMapping
为例,就限制了请求方式必须是GET
,除此以外,使用方式与@RequestMapping
完全相同!
所以,在实际应用中,在类的上方肯定使用@RequestMapping
(其它的@XxxMapping
不可以加在类上),方法上一般都使用@GetMapping
、@PostMapping
等注解,除非在极特殊的情况下,某些请求同时允许多种请求方式,才会在方法上使用@RequestMapping
。
5. 关于@ResponseBody
注解
@ResponseBody
注解表示:响应正文。
一旦配置为“响应正文”,则处理请求的方法的返回值就会直接响应到客户端去!
如果没有配置为“响应正文”,则处理请求的方法的返回值表示“视图组件的名称”,当方法返回后,服务器端并不会直接响应,而是根据“视图组件的名称”在服务器端找到对应的视图组件,并处理,最后,将处理后的视图响应到客户端去,这不是前后端分离的做法!
可以在需要正文的方法上添加@ResponseBody
注解,由于开发模式一般相对统一,所以,一般会将@ResponseBody
添加在控制器类上,表示此控制器类中所有处理请求的方法都将响应正文!
在Spring MVC框架中,还提供了@RestController
注解,它同时具有@Controller
和@ResponseBody
注解的效果,所以,在响应正文的控制器上,只需要使用@RestController
即可,不必再添加@Controller
和@ResponseBody
注解。
关于响应正文,Spring MVC内置了一系列的转换器(Converter),用于将方法的返回值转换为响应到客户端的数据(并根据HTTP协议补充了必要的数据),并且,Spring MVC会根据方法的返回值不同,自动选取某个转换器,例如,当方法的返回值是String
时,会自动使用StringHttpMessageConverter
这个转换器,这个转换器的特点就是直接将方法返回的字符串作为响应的正文,并且,其默认的响应文档的字符集是ISO-8859-1,所以在默认情况并不支持非ASCII字符(例如中文)。
在实际应用中,不会使用String
作为处理请求的方法的返回值类型,主要是因为普通的字符串不足以清楚的表现多项数据,如果自行组织成JSON或其它某种格式的字符串成本太高!
通常,建议向客户端响应JSON格式的字符串,应该在项目中添加jackson-databind
的依赖项:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.12.3</version></dependency>
以上jackson-databind
依赖项中也有一个转换器,当Spring MVC调用的处理请求的方法的返回值是Spring MVC没有匹配的默认转换器时,会自动使用jackson-databind
的转换器,而jackson-databind
转换器就会解析方法的返回值,并将其处理为JSON格式的字符串,在响应头中将Content-Type
设置为application/json
。
注意:在Spring MVC项目中,还需要在Spring MVC的配置类(SpringMvcConfig
)上添加@EnableWebMvc
注解,否则响应时将导致出现HTTP的406错误。
【示例代码】
cn.tedu.springmvc.vo.UserVO
package cn.tedu.springmvc.vo;public class UserVO { private String username; private String password; private String email; // 请自行补充以上3个属性的Setter & Getter}
UserController
的代码片段:
// http://localhost:8080/springmvc01_war_exploded/user/info.do@GetMapping("/info.do")public UserVO info() { UserVO userVO = new UserVO(); userVO.setUsername("chengheng"); userVO.setPassword("1234567890"); userVO.setEmail("chengheng@qq.com"); return userVO;}
SpringMvcConfig(补充@EnableWebMvc
注解)
@Configuration // 此注解不是必须的@EnableWebMvc@ComponentScan("cn.tedu.springmvc") // 必须配置在当前配置类,不可配置在Spring的配置类public class SpringMvcConfig implements WebMvcConfigurer {}
6. 接收请求参数
在Spring MVC中,当需要接收客户端的请求参数时,只需要将各参数直接声明为处理请求的方法的参数即可,例如:
// http://localhost:8080/springmvc01_war_exploded/user/reg.do?username=root&password=123456&age=25@RequestMapping("/reg.do")public String reg(String username, String password, Integer age) { System.out.println("username = " + username + ", password = " + password + ", age = " + age); return "OK";}
需要注意:
- 如果客户端提交的请求中根本没有匹配名称的参数,则以上获取到的值将是
null
- 例如:http://localhost/user/login.do
- 如果客户端仅提交了参数名称,却没有值,则以上获取到的值将是
""
(长度为0的字符串)- 例如:http://localhost/user/login.do?username=&password=&age=
- 如果客户端提交了匹配名称的参数,并且值是有效的,则可以获取到值
- 例如:http://localhost/user/login.do?username=admin&password=1234&age=27
- 以上名称应该是由服务器端决定的,客户端需要根据以上名称来提交请求参数
- 声明参数时,可以按需将参数声明成期望的类型,例如以上将
age
声明为Integer
类型- 注意:声明成
String
以外的类型时,应该考虑是否可以成功转换类型
- 注意:声明成
当有必要的情况下,可以在以上各参数的声明之前添加@RequestParam
注解,其作用主要有:
- 配置
name
属性:客户端将按照此配置的值提交请求参数,而不再是根据方法的参数名称来提交请求参数 - 配置
required
属性:是否要求客户端必须提交此请求参数,默认为true
,如果不提交,则出现400错误,当设置为false
时,如果不提交,则服务器端将此参数值视为null
- 配置
defaultValue
属性:配置此请求参数的默认值,当客户端没有提交此请求参数时,视为此值
另外,如果需要客户端提交的请求参数较多,可以将这些参数封装为自定义的数据类型,并将自定义的数据类型作为处理方法的参数即可,例如:
cn.tedu.springmvc.dto.UserRegDTO
package cn.tedu.springmvc.dto;public class UserRegDTO { private String username; private String password; private Integer age; // 生成Setters & Getters // 生成toString() }
UserController(代码片段)
// http://localhost:8080/springmvc01_war_exploded/user/reg.do?username=root&password=123456&age=25@RequestMapping("/reg.do")public String reg(UserRegDTO userRegDTO) { System.out.println(userRegDTO); return "OK";}
需要注意,不要将@RequestParam
添加在封装的类型之前。
另外,你也可以将多个请求参数区分开来,一部分直接声明为处理请求的方法的参数,另一部分封装起来。
200成功 301/302重定向
400请求参数错误 404找不到资源 405请求方式错误 406请求头不可接受 500服务器内部错误
7. 关于RESTful
百科资料:RESTFUL是一种网络应用程序的设计风格和开发方式,基于HTTP,可以使用XML格式定义或JSON格式定义。RESTFUL适用于移动互联网厂商作为业务接口的场景,实现第三方OTT调用移动网络资源的功能,动作类型为新增、变更、删除所调用资源。
RESTful的设计风格的典型表现就是:将某些唯一的请求参数的值放在URL中,使之成为URL的一部分,例如https://www.zhihu.com/question/28557115这个URL的最后一部分28557115
应该就是这篇贴子的id值,而不是使用例如?id=28557115
这样的方式放在URL参数中。
注意:RESTful只是一种设计风格,并不是一种规定,也没有明确的或统一的执行方式!
如果没有明确的要求,以处理用户数据为例,可以将URL设计为:
/users
:查看用户列表/users/9527
:查询id=9527的用户的数据/users/9527/delete
:删除id=9527的用户的数据
在RESTful风格的URL中,大多是包含了某些请求参数的值,在使用Spring MVC框架时,当需要设计这类URL时,可以使用{名称}
进行占位,并在处理请求的方法的参数列表中,使用@PathVariable
注解请求参数,即可将占位符的实际值注入到请求参数中!
例如:
// http://localhost:8080/springmvc01_war_exploded/user/3/info.do@GetMapping("/{id}/info.do")public UserVO info(@PathVariable Long id) { System.out.println("即将查询 id = " + id + " 的用户的信息……"); UserVO userVO = new UserVO(); userVO.setUsername("chengheng"); userVO.setPassword("1234567890"); userVO.setEmail("chengheng@qq.com"); return userVO;}
提示:在以上代码中,URL中使用的占位符是{id}
,则方法的参数名称也应该是id
,就可以直接匹配上!如果无法保证这2处的名称一致,则需要在@PathVariable
注解中配置占位符中的名称,例如:
@GetMapping("/{userId}/info.do")public UserVO info(@PathVariable("userId") Long id) { // ...}
在使用{}
格式的占位符时,还可以结合正则表达式进行匹配,其基本语法是:
{占位符名称:正则表达式}
例如:
@GetMapping("/{id:[0-9]+}/info.do")
当设计成以上URL时,仅当占位符位置的是纯数字的URL才会被匹配上,如果不是纯数字的刚出现404错误页面。
并且,以上模式的多种不冲突的正则表达式是可以同时存在的,例如:
@GetMapping("/{id:[0-9]+}/info.do")public UserVO info(@PathVariable Long id) { System.out.println("即将查询 id = " + id + " 的用户的信息……"); // ...}@GetMapping("/{username:[a-zA-Z]+}/info.do")public UserVO info(@PathVariable String username) { System.out.println("即将查询 用户名 = " + username + " 的用户的信息……"); // ...}
甚至,还可以存在不使用正则表达式,但是URL格式几乎一样的配置:
@GetMapping("/{id:[0-9]+}/info.do")public UserVO info(@PathVariable Long id) { System.out.println("即将查询 id = " + id + " 的用户的信息……"); // ...}@GetMapping("/{username:[a-zA-Z]+}/info.do")public UserVO info(@PathVariable String username) { System.out.println("即将查询 用户名 = " + username + " 的用户的信息……"); // ...}// 【以下是新增的】// http://localhost:8080/springmvc01_war_exploded/user/list/info.do@GetMapping("/list/info.do")public UserVO list() { System.out.println("即将查询 用户的列表 的信息……"); // ...}
最终执行时,如果使用/user/list/info.do
,则会匹配到以上代码中的最后一个方法,并不会因为这个URL还能匹配第2个方法配置的{username:[a-zA-Z]+}
而产生冲突。所以,使用了占位符的做法并不影响精准匹配的路径。优先级问题.
8. 关于响应正文时的结果类型
当响应正文时,只要方法的返回值是自定义的数据类型,则Spring MVC框架就一定会调用jackson-databind
中的转换器,就可以将结果转换为JSON格式的字符串!
通常,在项目开发中,会定义一个“通用”的数据类型,无论是哪个控制器的哪个处理请求的方法,最终都将返回此类型!
显示的通用返回类型如下:
public class JsonResult<T> { private Integer state; // 业务返回码 private String message; // 消息 private T data; // 数据 private JsonResult() { } public static JsonResult<Void> ok() { return ok(null); } public static <T> JsonResult<T> ok(T data) { JsonResult<T> jsonResult = new JsonResult<>(); jsonResult.state = State.OK.getValue(); jsonResult.data = data; return jsonResult; } public static JsonResult<Void> fail(State state, String message) { JsonResult<Void> jsonResult = new JsonResult<>(); jsonResult.state = state.getValue(); jsonResult.message = message; return jsonResult; } public enum State {OK(20000),ERR_USERNAME(40400),ERR_PASSWORD(40600);Integer value;State(Integer value) { this.value = value;}public Integer getValue() { return value;} } // Setters & Getters}
9. 统一处理异常
Spring MVC框架提供了统一处理异常的机制,使得特定种类的异常对应一段特定的代码,后续,当编写代码时,无论在任何位置,都可以将异常直接抛出,由统一处理异常的代码进行处理即可!
关于统一处理异常,需要自定义方法对异常进行处理,关于此方法:
- 注解:需要添加
@ExceptionHandler
注解 - 访问权限:应该是公有的
- 返回值类型:可参考处理请求的方法的返回值类型
- 方法名称:自定义
- 参数列表:必须包含1个异常类型的参数,并且可按需添加
HttpServletRequest
、HttpServletResponse
等少量特定的类型的参数,不可以随意添加参数
例如:
@ExceptionHandlerpublic String handleException(NullPointerException e) { return "Error, NullPointerException!";}
需要注意:以上处理异常的代码,只能作用于当前控制器类中各个处理请求的方法,对其它控制器类的中代码并不产生任何影响,也就无法处理其它控制类中处理请求时出现的异常!
为保证更合理的处理异常,应该:
- 将处理异常的代码放在专门的类中
- 在此类上添加
@ControllerAdvice
注解- 由于目前主流的响应方式都是“响应正文”的,则可以将
@ControllerAdvice
替换为@RestControllerAdvice
- 由于目前主流的响应方式都是“响应正文”的,则可以将
所以,可以创建GlobalExceptionHandler
类,代码如下:
@RestControllerAdvicepublic class GlobalExceptionHandler { @ExceptionHandler public String handleException(NullPointerException e) { return "Error, NullPointerException!"; }}
另外,可以将处理异常的代码放在所有控制器类公共的父类中,则各控制器类都相当于有此代码,则处理异常的代码可以作用于所有控制器中处理请求的方法!但不推荐此做法。
在以上处理异常的过程中,Spring MVC的处理模式大致如下:
try {userController.npe();} catch (NullPointerException e) {globalExceptionHandler.handleException(e);}
关于以上处理的方法的参数中的异常类型,将对应Spring MVC框架能够统一处理的异常类型,例如将其声明为Throwable
时,所有异常都可被此方法进行处理!但是,在处理过程中,应该判断当前异常对象所归属的类型,以针对不同类型的异常进行不同的处理!
需要注意:允许存在多个统一处理异常的方法,例如:
@ExceptionHandlerpublic String handleNullPointerException(NullPointerException e) { return "Error, NullPointerException!";}@ExceptionHandlerpublic String handleNumberFormatException(NumberFormatException e) { return "Error, NumberFormatException!";}@ExceptionHandlerpublic String handleThrowable(Throwable e) { e.printStackTrace(); return "Error, Throwable!";}
并且,如果某个异常能够被多个方法处理(异常类型符合多个处理异常的方法的参数类型),则优先执行最能精准匹配的处理异常的方法,例如,当出现NullPointerException
时,将执行handleNullPointerException()
而不会执行handleThrowable()
!
在开发实践中,通常都会有handleThrowable()
方法,以避免某个异常没有被处理而导致500错误!
关于@ExceptionHandler
注解,可用于表示被注解的方法是用于统一处理异常的,而且,可用于配置被注解的方法能够处理的异常的类型,其效力的优先级高于在方法的参数上指定异常类型。
在开发实践中,建议为每一个@ExceptionHandler
配置注解参数,在注解参数中指定需要处理异常的类型,而处理异常的方法的参数直接使用Throwable
即可。
例如:
@ExceptionHandler({ NullPointerException.class, ClassCastException.class})public String handleNullPointerException(Throwable e) { return "Error, NullPointerException or ClassCastException!";}@ExceptionHandler(NumberFormatException.class)public String handleNumberFormatException(Throwable e) { return "Error, NumberFormatException!";}@ExceptionHandler(Throwable.class)public String handleThrowable(Throwable e) { return "Error, Throwable!";}
10. 拦截器(Interceptor)
在Spring MVC框架中,拦截器是可以运行在所有控制器处理请求之前和之后的一种组件,并且,如果拦截器运行在控制器处理请求之前,还可以选择对当前请求进行阻止或放行。
注意:拦截器的目的并不是“拦截下来后阻止运行”,更多的是“拦截下来后执行某些代码”,其优势在于可作用于若干种不同请求的处理过程,即写一个拦截器,就可以在很多种请求的处理过程中被执行。
只要是若干种不同的请求过程中都需要执行同样的或高度相似的代码,都可以使用拦截器解决,典型的例如验证用户是否已经登录等等。
当需要使用拦截器时,首先,需要自定义类,实现HandlerInterceptor
接口,例如:
package cn.tedu.springmvc.interceptor;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("LoginInterceptor.preHandle()"); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("LoginInterceptor.postHandle()"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("LoginInterceptor.afterCompletion()"); }}
每个拦截器都必须注册才会被启用,注册过程通过重写WebMvcConfigure
接口中的addInterceptors()
方法即可,例如:
@Configuration // 此注解不是必须的@EnableWebMvc@ComponentScan("cn.tedu.springmvc") // 必须配置在当前配置类,不可配置在Spring的配置类public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/user/login.do"); }}
当进行访问时,在浏览器窗口中将看到一片空白,在Tomcat控制台可以看到preHandle()
方法已经执行。当把拦截器中preHandle()
方法的返回值改为true
时,在Tomcat控制台可以看到依次执行了preHandle()
> 控制器中处理请求的方法 > postHandle()
> afterCompletion()
。
其实,preHandle()
方法的返回值为true
时,表示“放行”,为false
时,表示“阻止”。
关于注册拦截器时的配置,使用链式语法可以先调用addInterceptor()
方法添加拦截器,然后调用addPathPatter()
方法添加哪些路径需要被拦截,此方法的参数可以是String...
,也可以是List
,在编写路径值时,可以使用*
作为通配符,例如配置为/user/*
,则可以匹配/user/login.do
、/user/reg.do
等所有直接在/user
下的路径,但不能匹配/user/1/info.do
,如果需要匹配若干层级,必须使用2个连续的星号,例如配置为/user/**
。一旦使用通配符,就有可能导致匹配的范围过大,例如配置为/user/**
时,还可以匹配到/user/reg.do
(注册)和/user/login.do
(登录),如果此拦截器是用于“验证用户是否登录”的,则不应该对这2个路径进行处理,那么,配置拦截器时,还可以在链式语法中调用excludePathPattern()
方法,以添加“排除路径”(例外)。
配置示例:
@Overridepublic void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/user/**") .excludePathPatterns("/user/reg.do", "/user/login.do");}
11. 关于Spring MVC的小结
关于Spring MVC框架,你应该:
- 理解Spring MVC框架的作用
- 接收请求,响应结果,处理异常……
- 掌握创建基于Maven的运行在Tomcat的Webapp
- 认识基础的依赖项
spring-webmvc
、javax.servlet-api
、jackson-databind
- 掌握配置Spring MVC的运行环境(使得控制器能接收到请求)
- 掌握以下注解的使用:
@Controller
/@RestController
@ResponseBody
@RequestMapping
/@GetMapping
/@PostMapping
…@RequestParam
/@PathVariable
@ExceptionHandler
/@ControllerAdvice
/@RestControllerAdvice
@EnableWebMvc
- 掌握接收请求参数的方式
- 将请求参数直接声明在处理请求的方法的参数列表中
- 将若干个请求参数进行封装,并将封装的类型声明在处理请求的方法的参数列表中
- 如果是URL中的路径,则需要使用
@PathVariable
- 掌握响应JSON格式的正文的做法
- 处理请求的方法必须添加
@ResponseBody
,或当前控制器类添加@ResponseBody
,或当前控制器类添加@RestController
- 在Spring MVC配置类上添加
@EnableWebMvc
- 在项目的
pom.xml
中添加了jackson-databind
- 处理请求的方法返回自定义的数据类型
- 处理请求的方法必须添加
- 掌握响应JSON格式的正文时,统一的响应类型的类的设计
- 了解RESTful风格
- 掌握统一处理异常
- 掌握拦截器的创建与配置
附:关于POJO
所有用于封装属性的类型都可以统称为POJO。
常见的POJO后缀有:BO、DO、VO、DTO等,不同的后缀表示不同的意义,例如:VO = Value Object / View Object,DTO = Data Transfer Object ……
在一个项目中,哪些情景下使用哪种后缀并没有统一的规定,通常是各项目内部决定。
注意:在为封装属性的类进行命名时,以上BO、DO、VO、DTO等这些后缀的每一个字母都应该是大写的!
附:关于数据库
1. 关于创建数据库
创建数据库的语法是:
CREATE DATABASE 数据库名称;
当某个项目规模特别大时,应该根据数据之间的关系,尽可能的拆为多个数据库。
2. 关于使用数据库
使用数据库的语法是:
USE 数据库名称;
提示:以上语法中的分号是可选的。
3. 创建数据表
创建数据表的基本语法是:
CREATE TABLE 数据表名称 (字段设计列表) CHARSET 字符编码 COMMENT 注释;
注意:主流的设计中,数据表的编码强烈建议配置为utf8mb4
(过低版本的MySQL不支持)。
在设计字段时,基本语法是:
字段名 字段类型 字段约束 comment 注释
简单示例:
create table ams_admin ( id bigint unsigned auto_increment, username varchar(50) default null unique comment '用户名', password char(64) default null comment '密码(密文)', nickname varchar(50) default null comment '昵称', avatar varchar(255) default null comment '头像URL', phone varchar(50) default null unique comment '手机号码', email varchar(50) default null unique comment '电子邮箱', description varchar(255) default null comment '描述', is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用', last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)', login_count int unsigned default 0 comment '累计登录次数(冗余)', gmt_last_login datetime default null comment '最后登录时间(冗余)', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '管理员表' charset utf8mb4;
关于以上设计:
- 自动编号的
id
应该是bigint unsigned
类型的,以确保id够用- MySQL中的整形类型:
tinyint
、smallint
、int
、bigint
- MySQL中的整形类型:
- 关于
varchar
的使用,必须设置长度,建议设置为比合理的最大值更大一些的值,例如规则为“用户名最多16字符”,则varchar
中设置的字符长度最少20
- 永远不要使用
not null
约束,任何你认为必须的字段,以后都可能不是必须的 - 相对固定长度的字符串类型应该使用
char
,而不是使用varchar
- 并不要求每个数据的长度完全一致
char
的读取效率略高于varchar
,占用的空间可能比varchar
少1~2个字节
附:关于密码加密
在基于Maven的项目中添加以下依赖:
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.15</version></dependency>
public class DigestTests { @Test public void digest() { String rawPassword = "123456" + "fdh98geljgdfskj"; String encodedPassword = DigestUtils.md5Hex(rawPassword); System.out.println("原密码:" + rawPassword); System.out.println("加密后的密码:" + encodedPassword); }}
三、Mybatis
1. 关于Mybatis
Mybatis的主要作用是快速实现对关系型数据库中的数据进行访问的框架。
2. 创建整合了Spring与Mybatis的工程
Mybatis可以不依赖于Spring等框架直接使用的,但是,就需要进行大量的配置,前期配置工作量较大,基于Spring框架目前是业内使用的标准之一,所以,通常会整合Spring与Mybatis,以减少配置。
在创建工程时,创建普通的Maven工程即可(不需要选择特定的骨架)。
在pom.xml
中,需要添加几个依赖项,分别是:
Mybatis的依赖项:mybatis
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version></dependency>
Mybatis整合Spring的依赖项:mybatis-spring
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version></dependency>
Spring的依赖项:spring-context
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.14</version></dependency>
Spring JDBC的依赖项:spring-jdbc
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.14</version></dependency>
MySQL连接的依赖项:mysql-connector-java
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version></dependency>
数据库连接池的依赖项:commons-dbcp2
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.8.0</version></dependency>
JUnit测试的依赖项:junit-jupiter-api
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.7.0</version> <scope>test</scope></dependency>
创建完成后,可以在src/test/java
下创建测试类,并编写测试方法,例如:
package cn.tedu.mybatis;import org.junit.jupiter.api.Test;public class MybatisTests { @Test public void contextLoads() { System.out.println("MybatisTests.contextLoads()"); }}
由于目前尚未编写实质的代码,以上测试代码也非常简单,应该是可以成功通过测试的,如果不能通过测试,必然是开发工具、开发环境、依赖项、项目创建步骤等问题。
3. 配置Mybatis的开发环境
配置java中Teminal环境变量
此电脑点右键的属性
首先,登录MySQL控制台,创建名为mall_ams
的数据库:
CREATE DATABASE mall_ams;
然后,在IntelliJ IDEA中配置数据库视图:http://doc.canglaoshi.org/doc/idea_database/index.html
然后,通过数据库视图的Console面板创建数据表:
create table ams_admin ( id bigint unsigned auto_increment, username varchar(50) default null unique comment '用户名', password char(64) default null comment '密码(密文)', nickname varchar(50) default null comment '昵称', avatar varchar(255) default null comment '头像URL', phone varchar(50) default null unique comment '手机号码', email varchar(50) default null unique comment '电子邮箱', description varchar(255) default null comment '描述', is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用', last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)', login_count int unsigned default 0 comment '累计登录次数(冗余)', gmt_last_login datetime default null comment '最后登录时间(冗余)', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '管理员表' charset utf8mb4;
至此,本案例所需的数据库与数据表已经准备完毕。
在src/main/resources
下创建datasource.properties
配置文件,用于配置连接数据库的参数,例如:
datasource.url=jdbc:mysql://localhost:3306/mall_ams?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaidatasource.driver=com.mysql.cj.jdbc.Driverdatasource.username=rootdatasource.password=root
并且,在cn.tedu.mybatis
包下(不存在,则创建)创建SpringConfig
类,读取以上配置文件:
@Configuration@PropertySource("classpath:datasource.properties")public class SpringConfig {}
完成后,在测试方法中补充测试代码:
@Testpublic void contextLoads() { System.out.println("MybatisTests.contextLoads()"); AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); ConfigurableEnvironment environment = ac.getEnvironment(); System.out.println(environment.getProperty("datasource.url")); System.out.println(environment.getProperty("datasource.driver")); System.out.println(environment.getProperty("datasource.username")); System.out.println(environment.getProperty("datasource.password")); ac.close();}
接下来,在SpringConfig
中配置一个DataSource
对象:
package cn.tedu.mybatis;import org.apache.commons.dbcp.BasicDataSource;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.PropertySource;import org.springframework.core.env.Environment;import javax.sql.DataSource;@Configuration@PropertySource("classpath:datasource.properties")public class SpringConfig { @Bean public DataSource dataSource(Environment env) { BasicDataSource dataSource = new BasicDataSource(); dataSource.setUrl(env.getProperty("datasource.url")); dataSource.setDriverClassName(env.getProperty("datasource.driver")); dataSource.setUsername(env.getProperty("datasource.username")); dataSource.setPassword(env.getProperty("datasource.password")); return dataSource; }}
并在测试类中添加新的测试方法,以尝试获取数据库的连接对象,检测是否可以正确连接到数据库:
@Testpublic void testConnection() throws Exception { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); DataSource dataSource = ac.getBean("dataSource", DataSource.class); Connection connection = dataSource.getConnection(); System.out.println(connection); ac.close();}
至此,项目的数据库编程的准备完毕。
4. Mybatis的基本使用
当使用Mybatis实现数据访问时,主要:
- 编写数据访问的抽象方法
- 配置抽象方法对应的SQL语句
关于抽象方法:
- 必须定义在某个接口中,这样的接口通常使用
Mapper
作为名称的后缀,例如AdminMapper
- Mybatis框架底层将通过接口代理模式来实现
- 方法的返回值类型:如果要执行的数据操作是增、删、改类型的,统一使用
int
作为返回值类型,表示“受影响的行数”,也可以使用void
,但是不推荐;如果要执行的是查询操作,返回值类型只需要能够装载所需的数据即可 - 方法的名称:自定义,不要重载,建议风格如下:
- 插入数据使用
insert
作为方法名称中的前缀或关键字 - 删除数据使用
delete
作为方法名称中的前缀或关键字 - 更新数据使用
update
作为方法名称中的前缀或关键字 - 查询数据时:
- 如果是统计,使用
count
作为方法名称中的前缀或关键字 - 如果是单个数据,使用
get
或find
作为方法名称中的前缀或关键字 - 如果是列表,使用
list
作为方法名称中的前缀或关键字
- 如果是统计,使用
- 如果操作数据时有条件,可在以上前缀或关键字右侧添加
by字段名
,例如deleteById
- 插入数据使用
- 方法的参数列表:取决于需要执行的SQL语句中有哪些参数,如果有多个参数,可将这些参数封装到同一个类型中,使用封装的类型作为方法的参数类型
假设当需要实现“插入一条管理员数据”,则需要执行的SQL语句大致是:
insert into ams_admin (username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified) values (?,?,? ... ?);
由于以上SQL语句中的参数数量较多,则应该将它们封装起来,则在cn.tedu.mybatis
包下创建Admin
类,声明一系列的属性,对应以上各参数值:
package cn.tedu.mybatis;import java.time.LocalDateTime;public class Admin { private String username; private String password; private String nickname; private String avatar; private String phone; private String email; private String description; private Integer isEnable; private String lastLoginIp; private Integer loginCount; private LocalDateTime gmtLastLogin; private LocalDateTime gmtCreate; private LocalDateTime gmtModified; // Setters & Getters // toString()}
接下来,在cn.tedu.mybatis
包下创建mapper.AdminMapper
接口,并在接口中添加“插入1条管理员数据”的抽象方法:
package cn.tedu.mybatis.mapper;import cn.tedu.mybatis.Admin;public interface AdminMapper { int insert(Admin admin);}
所有用于Mybatis处理数据的接口都必须被Mybatis识别,有2种做法:
- 在每个接口上添加
@Mapper
注解 - 推荐:在配置类上添加
@MapperScan
注解,指定接口所在的根包
例如,在SpringConfig
上添加配置@MapperScan
:
@Configuration@PropertySource("classpath:datasource.properties")@MapperScan("cn.tedu.mybatis.mapper")public class SpringConfig { // ... ... }
注意:因为Mybatis会扫描以上配置的包,并自动生成包中各接口中的代理对象,所以,千万不要放其它接口文件!
接下来,需要配置抽象方法对应的SQL语句,这些SQL语句推荐配置在XML文件中,可以从 http://doc.canglaoshi.org/config/Mapper.xml.zip 下载到XML文件。在项目的src/main/resources
下创建mapper
文件夹,并将下载得到的XML文件复制到此文件夹中,重命名为AdminMapper.xml
。
放到自己找得到的位置.
打开XML文件夹,进行配置:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="cn.tedu.mybatis.mapper.AdminMapper"> <insert id="insert"> insert into ams_admin ( username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified ) values ( #{username}, #{password}, #{nickname}, #{avatar}, #{phone}, #{email}, #{description}, #{isEnable}, #{lastLoginIp}, #{loginCount}, #{gmtLastLogin}, #{gmtCreate}, #{gmtModified} ) </insert></mapper>
最后,还需要将DataSource
配置给Mybatis框架,并且,为Mybatis配置这些XML文件的路径,这2项配置都将通过配置SqlSessionFactoryBean
来完成。
先在datasource.properties
中补充一条配置:
mybatis.mapper-locations=classpath:mapper/AdminMapper.xml
然后在配置类中创建SqlSessionFactoryBean
类型的对象:
@Beanpublic SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource, @Value("${mybatis.mapper-locations}") Resource mapperLocations) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(mapperLocations); return sqlSessionFactoryBean;}
最后,在测试类中补充测试方法,以检验是否可以通过调用AdminMapper
的insert()
方法插入数据:
@Testpublic void testInsert() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); Admin admin = new Admin(); admin.setUsername("admin001"); admin.setPassword("12345678"); adminMapper.insert(admin); ac.close();}
5. 获取新增的数据的自动编号的id
如果某数据的id是自动编号,当需要获取新增的数据的id时,需要先使得插入的数据类型中有id对应的属性,则在Admin
类中添加id
属性:
public class Admin { private Long id; // 原有其它属性及Setter & Getter // 补充id的Setter & Getter // 重新生成toString() }
接下来,在节点配置2个属性,分别是
useGeneratedKeys
和keyProperty
:
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> 原有代码</insert>
当配置完成后,Mybatis执行此插入数据的操作后,会将自动编号的id赋值到参数Admin admin
的id
属性中,以上keyProperty
指的就是将自动编号的值放回到参数对象的哪个属性中!
6. 删除数据
目标:根据id删除某一条数据
要实现此目标,需要执行的SQL语句大致是:
delete from ams_admin where id=?
然后,在AdminMapper
接口中添加抽象方法:
int deleteById(Long id);
接下来,在AdminMapper.xml
中配置以上抽象方法映射的SQL语句:
<delete id="deleteById"> delete from ams_admin where id=#{id}</delete>
最后,编写并执行测试:
@Testpublic void testDeleteById() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); Long id = 12L; int rows = adminMapper.deleteById(id); // System.out.println("删除完成,受影响的行数=" + rows); if (rows == 1) { System.out.println("删除成功"); } else { System.out.println("删除失败,尝试删除的数据(id=" + id + ")不存在!"); } ac.close();}
7. 修改数据
jvm在把java文件编译成class文件时局部变量会变量名会丢失,变成arg0,arg1等等。如上图的id随意写效果都一样。
这里有改进方法就是把id里改为arg0,arg1…或者param1,param2…第三种就是加@Param注解在AdminMapper里面
目标:根据id修改某一条数据的密码
要实现此目标,需要执行的SQL语句大致是:
update ams_admin set password=? where id=?
然后,在AdminMapper
接口中添加抽象方法:
int updatePasswordById(@Param("id") Long id, @Param("password") String password);
接下来,在AdminMapper.xml
中配置以上抽象方法映射的SQL语句:
<update id="updatePasswordById"> update ams_admin set password=#{password} where id=#{id}</update>
最后,编写并执行测试:
@Testpublic void testUpdatePasswordById() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); Long id = 12L; String password = "000000"; int rows = adminMapper.updatePasswordById(id, password); if (rows == 1) { System.out.println("修改密码成功"); } else { System.out.println("修改密码失败,尝试访问的数据(id=" + id + ")不存在!"); } ac.close();}
8. 查询数据-1
目标:统计当前表中有多少条数据
要实现此目标,需要执行的SQL语句大致是:
select count(*) from ams_admin
然后,在AdminMapper
接口中添加抽象方法:
int count();
接下来,在AdminMapper.xml
中配置以上抽象方法映射的SQL语句:
<select id="count" resultType="int"> select count(*) from ams_admin</select>
最后,编写并执行测试:
@Testpublic void testCount() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); int count = adminMapper.count(); System.out.println("当前表中有" + count + "条记录"); ac.close();}
9. 查询数据-2
目标:根据id查询管理员信息
要实现此目标,需要执行的SQL语句大致是:
select * from ams_admin where id=?
然后,在AdminMapper
接口中添加抽象方法:
Admin getById(Long id);
接下来,在AdminMapper.xml
中配置以上抽象方法映射的SQL语句:
<select id="getById" resultType="cn.tedu.mybatis.Admin"> select * from ams_admin where id=#{id}</select>
最后,编写并执行测试:
@Testpublic void testGetById() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); Long id = 3L; Admin admin = adminMapper.getById(id); System.out.println("查询结果:" + admin); ac.close();}
通过测试可以发现:当存在匹配的数据时,将可以查询到数据,当不存在匹配的数据时,将返回null
。
需要注意,如果查询结果集中的列名与类的属性名不匹配时,默认将放弃处理这些结果数据,则返回的对象中对应的属性值为null
,为了解决此问题,可以在查询时使用自定义的别名,使得名称保持一致,不过,更推荐配置以指导Mybatis封装查询结果,例如:
<select id="getById" resultMap="BaseResultMap"> select * from ams_admin where id=#{id}</select><resultMap id="BaseResultMap" type="cn.tedu.mybatis.Admin"> <result column="is_enable" property="isEnable" /> <result column="last_login_ip" property="lastLoginIp" /> <result column="login_count" property="loginCount" /> <result column="gmt_last_login" property="gmtLastLogin" /> <result column="gmt_create" property="gmtCreate" /> <result column="gmt_modified" property="gmtModified" /></resultMap>
注意:在后续的应用中,凡是可以通过resultMap
处理结果的,都不要使用resultType
。
10. 查询数据-3
目标:查询所有管理员的信息
要实现此目标,需要执行的SQL语句大致是:
select * from ams_admin order by id
注意:(1) 查询时,结果集中可能超过1条数据时,必须显式的使用ORDER BY
子句对结果集进行排序;(2) 查询时,结果集中可能超过1条数据时,应该考虑是否需要分页。
然后,在AdminMapper
接口中添加抽象方法:
List<Admin> list();
接下来,在AdminMapper.xml
中配置以上抽象方法映射的SQL语句:
<!-- List list(); --><select id="list" resultMap="BaseResultMap"> select * from ams_admin order by id</select>
最后,编写并执行测试:
@Testpublic void testList() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); List<Admin> list = adminMapper.list(); for (Admin admin : list) { System.out.println(admin); } ac.close();}
注:这里有快捷键list.iter可以快速写出增强for循环。
11. 动态SQL – foreach
Mybatis中的动态SQL表现为:根据参数不同,生成不同的SQL语句
目标:根据若干个id一次性删除若干条管理数据
要实现此目标,需要执行的SQL语句大致是:
delete from ams_admin where id in (?,?)
以上SQL语句中,id值的数量(以上?
的数量)对于开发人员而言是未知的!
然后,在AdminMapper
接口中添加抽象方法:
int deleteByIds(Long... ids);
或
int deleteByIds(Long[] ids);
或
int deleteByIds(List<Long> ids);
接下来,在AdminMapper.xml
中配置以上抽象方法映射的SQL语句:
<!-- int deleteByIds(List ids); --><delete id="deleteByIds"> delete from ams_admin where id in ( <foreach collection="list" item="id" separator=","> #{id} </foreach> )</delete>
以上代码中:
标签:用于遍历集合或数组类型的参数对象
collection
属性:被遍历的参数对象,当抽象方法的参数只有1个且没有添加@Param
注解时,如果参数是List
类型则此属性值为list
,如果参数是数组类型(包括可变参数)则此属性值为array
;当抽象方法的参数有多个或添加了@Param
注解时,则此属性值为@Param
注解中配置的值item
属性:自定义的名称,表示遍历过程中每个元素的变量名,可在子级使用
#{变量名}
表示数据separator
属性:分隔符号,会自动添加在遍历到的各元素之间
最后,编写并执行测试:
@Testpublic void testDeleteByIds() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfig.class); AdminMapper adminMapper = ac.getBean(AdminMapper.class); List<Long> ids = new ArrayList<>(); ids.add(16L); ids.add(18L); ids.add(19L); int rows = adminMapper.deleteByIds(ids); System.out.println("受影响的行数为:" + rows); ac.close();}
12. 动态SQL – 其它
在Mybatis中动态SQL还有其它节点,例如: /
+
+
等,将在项目中补充。
13. 关于查询时的字段列表
在阿里巴巴的《Java开发手册》中指出:
【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
通常,建议将字段列表使用节点进行封装,例如:
<sql id="BaseQueryFields"> <if test="true"> id, username, password, nickname, avatar, phone, email, description, is_enable, last_login_ip, login_count, gmt_last_login, gmt_create, gmt_modified </if></sql>
提示:为避免IntelliJ IDEA误以为以上代码片段是错误的而提示红色的波浪线,所以使用框住了字段列表的代码片段,但这个
并不是必须的,即使提示了红色的波浪线,也不影响运行。快捷键:alt+shift+向上或向下箭头可以移动一行数据。和效果一样,推荐如果之间如果没数据时用。
注意:以上节点可以用于封装任何SQL语句的任何片段,不仅仅只是字段列表。
封装后,当需要引用以上代码片段时,可以使用节点进行引用,例如:
<select id="getById" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin where id=#{id}</select><!-- List list(); --><select id="list" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin order by id</select>
14. 基于Spring的测试
在pom.xml
中添加spring-test
依赖项:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.14</version></dependency>
注意:与其它的spring-????
使用完全相同的版本!
接下来,在编写测试时,就可以在测试类上添加@SpringJUnitConfig
注解,并在注解中配置Spring的配置类作为参数,则执行此类的任何测试方法之前,都会加载这些Spring配置类,并且,在编写测试时,只要是在Spring容器中存在的对象,都可以自动装配,例如:
@SpringJUnitConfig(SpringConfig.class)public class MybatisSpringTests { @Autowired Environment env; @Test public void contextLoads() { System.out.println(env.getProperty("datasource.url")); System.out.println(env.getProperty("datasource.driver")); System.out.println(env.getProperty("datasource.username")); System.out.println(env.getProperty("datasource.password")); } }
15. 关于@Sql
注解
当添加了spring-test
依赖后,可以在测试时使用@Sql
注解,以加载某些.sql
脚本,使得测试之前或之后将执行这些脚本!
使用此注解主要是为了保障可以反复测试,并且得到预期的结果!例如执行删除的测试时,假设数据是存在的,第1次删除可以成功,但是在这之后的测试将不会成功,因为数据在第1次测试时就已经被删除!则可以编写一个.sql
脚本,通过脚本向数据表中插入数据,并在每次测试之前执行此脚本,即可保证每次测试都是成功的!
此注解可以添加在测试类上,则对当前测试类的每个测试方法都是有效的。
此注解也可以添加在测试方法上,则只对当前测试方法是有效的。
如果测试类和测试方法上都添加了此注解,则仅测试方法上的注解会生效。
此注解除了配置需要执行的.sql
脚本以外,还可以通过executionPhase
属性配置其执行阶段,例如取值为Sql.ExecutionPhase.AFTER_TEST_METHOD
时将使得.sql
脚本会在测试方法之后被执行。
每个测试方法可以添加多个@Sql
注解。
例如:
package cn.tedu.mybatis;import cn.tedu.mybatis.mapper.AdminMapper;import org.junit.jupiter.api.Assertions;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.core.env.Environment;import org.springframework.test.context.jdbc.Sql;import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;import javax.annotation.Resource;import javax.sql.DataSource;import java.sql.Connection;import java.sql.SQLException;@SpringJUnitConfig(SpringConfig.class)public class MybatisSpringTests { @Autowired Environment env; @Test public void contextLoads() { System.out.println(env.getProperty("datasource.url")); System.out.println(env.getProperty("datasource.driver")); System.out.println(env.getProperty("datasource.username")); System.out.println(env.getProperty("datasource.password")); } @Autowired DataSource dataSource; @Test public void testGetConnection() throws SQLException { Connection connection = dataSource.getConnection(); System.out.println(connection); } @Autowired AdminMapper adminMapper; @Test @Sql("classpath:insert_data.sql") public void testDeleteByIdSuccessfully(){ Long id = 1L; int rows = adminMapper.deleteById(id); //断言 Assertions.assertEquals(1,rows); } @Test @Sql("classpath:truncate.sql") public void testDeleteByIdFailBecauseNotExist(){ Long id = 1L; int rows = adminMapper.deleteById(id); Assertions.assertEquals(0,rows); }}
insert_data.sql
脚本示例:
truncate ams_admin;insert into ams_admin (username, password) values ('admin001', '123456');insert into ams_admin (username, password) values ('admin002', '123456');insert into ams_admin (username, password) values ('admin003', '123456');insert into ams_admin (username, password) values ('admin004', '123456');insert into ams_admin (username, password) values ('admin005', '123456');
truncate.sql
脚本示例:
truncate ams_admin;
16. 关于测试中的断言
在执行测试时,应该使用断言对测试结果进行预判,而不是使用输出语句结合肉眼观察结果,这样才更符合自动化测试的标准(在自动化测试中,可以一键执行项目中的所有测试方法,并将测试结果汇总到专门的测试报告文件中)。
通过调用Assertions
类中的静态方法可以对结果进行断言,常用方法有:
assertEquals()
:断言匹配(相等)assertNotEquals()
:断言不匹配(不相等)assertTrue()
:断言为“真”assertFalse()
:断言为“假”assertNull()
:断言为null
assertNotNull()
:断言不为null
assertThrows()
:断言将抛出异常assertDoesNotThrow()
:断言不会抛出异常- 其它
17. 关于#{}
和${}
格式的占位符
在Mybatis中,配置SQL语句时,参数可以使用#{}
或${}
格式的占位符。
例如存在需求:分页查询表中的所有数据。
需要执行的SQL语句大致是:
select * from ams_admin order by id limit ?, ?
则此功能的抽象方法应该是:
List<Admin> listPage(@Param("offset") Integer offset, @Param("size") Integer size);
配置SQL语句:
<!-- List listPage(@Param("offset") Integer offset, @Param("size") Integer size); --><select id="listPage" resultMap="BaseResultMap"> select <include refid="BaseQueryFields" /> from ams_admin order by id limit #{offset}, #{size}</select>
最后,执行测试:
@Test@Sql(scripts = {"classpath:truncate.sql", "classpath:insert_data.sql"})@Sql(scripts = {"classpath:truncate.sql"}, executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)public void testListPage() { // 准备测试数据 Integer offset = 0; Integer size = 3; // 断言不会抛出异常 Assertions.assertDoesNotThrow(() -> { // 执行测试 List<Admin> adminList = adminMapper.listPage(offset, size); // 观察结果(通过输出语句) System.out.println("查询到的记录数:" + adminList.size()); for (Admin admin : adminList) { System.out.println(admin); } });}
如果确保文件存在且文件路径没有写错却加载不出来,可以用下面的方法。
或者删除target,再或者rebuild project。
以上代码可以正常通过测试,并且观察结果也都是符合预期的,即使把SQL语句中的#{}
换成${}
格式,也是完全没有问题的!
例如还存在需求:根据用户名查询此用户的详情。
在“根据用户名查询用户详情”时,如果将username=#{username}
换成username=${username}
会出现错误!
其实,使用#{}
格式的占位符时,Mybatis在处理时会使用预编译的做法,所以,在编写SQL语句时不必关心数据类型的问题(例如字符串值不需要添加单引号),也不存在SQL注入的风险!这种占位符只能用于表示某个值,而不能表示SQL语句片段!
当使用${}
格式的占位符时,Mybatis在处理时会先将参数值代入到SQL语句中,然后再执行编译相关过程,所以需要关心某些值的数据类型问题(例如涉及字符串值时,需要在编写SQL语句时添加一对单引号框住字符串),并且,存在SQL注入的风险!其优点是可以表示SQL语句中的任何片段!
要满足sql注入至少4的倍数以上单引号。
$符非常灵活。
在一般情况下,应该尽可能的使用#{}
格式的占位符,并不推荐使用${}
格式的占位符,即使它可以实现“泛用”的效果!在一些特殊的情况下,如果一定要使用${}
格式的占位符,必须考虑SQL注入的风险,应该使用正则表达式或其它做法避免出现SQL注入问题!
18. 关于RBAC
RBAC = Role Based Access Control(基于角色的访问控制)
RBAC是经典的用户权限管理的设计思路。在这样的设计中,会存在3种类型:用户、角色、权限,权限将分配到各种角色上,用户可以关联某种角色,进而实现用户与权限相关。使用这样的设计,更加利于统一管理若干个用户的权限。
在RBAC的设计思路中,用户与角色一般是多对多的关系,而在数据库中,仅仅只是使用“用户”和“角色”这2张表是不利于维护多对多关系的,通常会增加一张中间表,专门记录对应关系,同理,角色和权限也是多对多的关系,也需要使用中间表来记录对应关系!
关于这些表的设计参考如下:
ams_admin:管理员表
-- 管理员表:创建数据表drop table if exists ams_admin;create table ams_admin ( id bigint unsigned auto_increment, username varchar(50) default null unique comment '用户名', password char(64) default null comment '密码(密文)', nickname varchar(50) default null comment '昵称', avatar varchar(255) default null comment '头像URL', phone varchar(50) default null unique comment '手机号码', email varchar(50) default null unique comment '电子邮箱', description varchar(255) default null comment '描述', is_enable tinyint unsigned default 0 comment '是否启用,1=启用,0=未启用', last_login_ip varchar(50) default null comment '最后登录IP地址(冗余)', login_count int unsigned default 0 comment '累计登录次数(冗余)', gmt_last_login datetime default null comment '最后登录时间(冗余)', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '管理员表' charset utf8mb4;-- 管理员表:插入测试数据insert into ams_admin (username, password, nickname, email, description, is_enable) values ('root', '1234', 'root', 'root@tedu.cn', '最高管理员', 1), ('super_admin', '1234', 'administrator', 'admin@tedu.cn', '超级管理员', 1), ('nobody', '1234', '无名', 'liucs@tedu.cn', null, 0);
ams_role:角色表
-- 角色表:创建数据表drop table if exists ams_role;create table ams_role ( id bigint unsigned auto_increment, name varchar(50) default null comment '名称', description varchar(255) default null comment '描述', sort tinyint unsigned default 0 comment '自定义排序序号', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '角色表' charset utf8mb4;-- 角色表:插入测试数据insert into ams_role (name) values ('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');
ams_admin_role:管理员与角色的关联表
-- 管理员角色关联表:创建数据表drop table if exists ams_admin_role;create table ams_admin_role ( id bigint unsigned auto_increment, admin_id bigint unsigned default null comment '管理员id', role_id bigint unsigned default null comment '角色id', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '管理员角色关联表' charset utf8mb4;-- 管理员角色关联表:插入测试数据insert into ams_admin_role (admin_id, role_id) values (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (3, 3);
ams_permission:权限表
-- 权限表:创建数据表drop table if exists ams_permission;create table ams_permission ( id bigint unsigned auto_increment, name varchar(50) default null comment '名称', value varchar(255) default null comment '值', description varchar(255) default null comment '描述', sort tinyint unsigned default 0 comment '自定义排序序号', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '权限' charset utf8mb4;-- 权限表:插入测试数据insert into ams_permission (name, value, description) values('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');
ams_role_permission:角色与权限的关联表
-- 角色权限关联表:创建数据表drop table if exists ams_role_permission;create table ams_role_permission ( id bigint unsigned auto_increment, role_id bigint unsigned default null comment '角色id', permission_id bigint unsigned default null comment '权限id', gmt_create datetime default null comment '数据创建时间', gmt_modified datetime default null comment '数据最后修改时间', primary key (id)) comment '角色权限关联表' charset utf8mb4;-- 角色权限关联表:插入测试数据insert into ams_role_permission (role_id, permission_id) values (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (3, 1), (3, 2), (3, 3);
在mall_ams
数据库中创建以上数据表,并插入以上测试数据。
练习(单表数据访问):
- 向权限表中插入新的数据
- 根据id删除某个权限
- 查询权限表中的所有权限
提示:
- 需要新的数据类型,例如
Permission
类 ctrl+R替换 - 需要新的接口文件,用于定义以上2个数据访问功能的抽象方法
- 需要新的XML文件,用于配置抽象方法对应的SQL语句
- 需要修改配置信息,将此前指定的XML文件由
AdminMapper.xml
改为*.xml
,并把SpringConfig
类中sqlSessionFactoryBean()
方法的第2个参数由Resource
类型改为Resource...
类型 - 当需要测试时,使用新的测试类
(setting里面搜dialect可以修改方言)
19. 关于关联查询
首先,请准备一些测试数据,使得:存在若干条用户数据,存在若干条角色数据,某个用户存在与角色的关联,最好有些用户有多个关联,又有些用户只有1个关联,还有些用户没有关联。
假设存在需求:根据id查询某用户信息时,也查出该用户归属于哪几种角色。
测试数据参考:
truncate ams_admin;truncate ams_admin_role;truncate ams_role;truncate ams_permission;insert into ams_admin (username, password) values ('admin001', '123456');insert into ams_admin (username, password) values ('admin002', '123456');insert into ams_admin (username, password) values ('admin003', '123456');insert into ams_admin (username, password) values ('admin004', '123456');insert into ams_admin (username, password) values ('admin005', '123456');insert into ams_admin (username, password) values ('admin006', '123456');insert into ams_admin (username, password) values ('admin007', '123456');insert into ams_admin (username, password) values ('admin008', '123456');insert into ams_admin (username, password) values ('admin009', '123456');insert into ams_admin (username, password) values ('admin010', '123456');insert into ams_admin (username, password) values ('admin011', '123456');insert into ams_admin (username, password) values ('admin012', '123456');insert into ams_admin (username, password) values ('admin013', '123456');insert into ams_admin (username, password) values ('admin014', '123456');insert into ams_admin (username, password) values ('admin015', '123456');insert into ams_admin (username, password) values ('admin016', '123456');insert into ams_admin (username, password) values ('admin017', '123456');insert into ams_admin (username, password) values ('admin018', '123456');insert into ams_admin (username, password) values ('admin019', '123456');insert into ams_admin (username, password) values ('admin020', '123456');insert into ams_permission (name, value, description) values('商品-商品管理-读取', '/pms/product/read', '读取商品数据,含列表、详情、查询等'),('商品-商品管理-编辑', '/pms/product/update', '修改商品数据'),('商品-商品管理-删除', '/pms/product/delete', '删除商品数据'),('后台管理-管理员-读取', '/ams/admin/read', '读取管理员数据,含列表、详情、查询等'),('后台管理-管理员-编辑', '/ams/admin/update', '编辑管理员数据'),('后台管理-管理员-删除', '/ams/admin/delete', '删除管理员数据');insert into ams_role (name) values('超级管理员'), ('系统管理员'), ('商品管理员'), ('订单管理员');insert into ams_admin_role (admin_id, role_id) values(1, 1), (1, 2), (1, 3), (1, 4),(2, 1), (2, 2), (2, 3),(3, 1), (3, 2),(4, 1);
本次查询需要执行的SQL语句大致是:
select *from ams_adminleft join ams_admin_role on ams_admin.id=ams_admin_role.admin_idleft join ams_role on ams_admin_role.role_id=ams_role.idwhere ams_admin.id=?
通过测试运行,可以发现(必须基于以上测试数据):
- 当使用的id值为1时,共查询到4条记录,并且用户的基本信息是相同的,只是与角色关联的数据不同
- 当使用的id值为2时,共查询到3条记录
- 当使用的id值为3时,共查询到2条记录
- 当使用其它有效用户的id时,共查询到1条记录
其实,这种查询期望的结果应该是:
public class xxx { // 用户基本信息的若干个属性,例如用户名、密码等 // 此用户的若干个角色数据,可以使用 List}
则可以先创建“角色”对应的数据类型:
public class Role { private Long id; private String name; private String description; private Integer sort; private LocalDateTime gmtCreate; private LocalDateTime gmtModified; // Setters & Getterss // toString()}
再创建用于封装此次查询结果的类型:
public class AdminDetailsVO { private Long id; private String username; private String password; private String nickname; private String avatar; private String phone; private String email; private String description; private Integer isEnable; private String lastLoginIp; private Integer loginCount; private LocalDateTime gmtLastLogin; private LocalDateTime gmtCreate; private LocalDateTime gmtModified; private List<Role> roles; // Setters & Getterss // toString()}
接下来,可以在AdminMapper
接口中添加抽象方法:
AdminDetailsVO getDetailsById(Long id);
需要注意,由于此次关联了3张表一起查询,结果集中必然出现某些列的名称是完全相同的,所以,在查询时,不可以使用星号表示字段列表(因为这样的结果集中的列名就是字段名,会出现相同的列名),而是应该至少为其中的一部分相同名称的列定义别名,例如:
select ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.nickname,ams_admin.avatar,ams_admin.phone,ams_admin.email,ams_admin.description,ams_admin.is_enable,ams_admin.last_login_ip,ams_admin.login_count,ams_admin.gmt_last_login,ams_admin.gmt_create,ams_admin.gmt_modified,ams_role.id AS role_id,ams_role.name AS role_name,ams_role.description AS role_description,ams_role.sort AS role_sort,ams_role.gmt_create AS role_gmt_create,ams_role.gmt_modified AS role_gmt_modifiedfrom ams_adminleft join ams_admin_role on ams_admin.id=ams_admin_role.admin_idleft join ams_role on ams_admin_role.role_id=ams_role.idwhere ams_admin.id=1;
在Mybatis处理中此查询时,并不会那么智能的完成结果集的封装,所以,必须自行配置用于指导Mybatis完成封装!
<resultMap id="DetailsResultMap" type="xx.xx.xx.xx.AdminDetailsVO"> <id column="id" property="id" /><result column="gmt_create" property="gmtCreate" /> <collection property="roles" ofType="xx.xx.xx.Role"> <id column="role_id" property="id" /> <result column="gmt_create" property="gmtCreate" /> </collection></resultMap>
最后,使用以上的查询SQL语句,并使用以上的封装结果即可!
<sql id="DetailsQueryFields"> <if test="true">ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.nickname,ams_admin.avatar,ams_admin.phone,ams_admin.email,ams_admin.description,ams_admin.is_enable,ams_admin.last_login_ip,ams_admin.login_count,ams_admin.gmt_last_login,ams_admin.gmt_create,ams_admin.gmt_modified,ams_role.id AS role_id,ams_role.name AS role_name,ams_role.description AS role_description,ams_role.sort AS role_sort,ams_role.gmt_create AS role_gmt_create,ams_role.gmt_modified AS role_gmt_modified </if></sql><resultMap id="DetailsResultMap" type="cn.tedu.mybatis.AdminDetailsVO"> <id column="id" property="id" /> <result column="username" property="username" /> <result column="password" property="password" /> <result column="nickname" property="nickname" /> <result column="avatar" property="avatar" /> <result column="phone" property="phone" /> <result column="email" property="email" /> <result column="description" property="description" /> <result column="is_enable" property="isEnable" /> <result column="last_login_ip" property="lastLoginIp" /> <result column="login_count" property="loginCount" /> <result column="gmt_last_login" property="gmtLastLogin" /> <result column="gmt_create" property="gmtCreate" /> <result column="gmt_modified" property="gmtModified" /> <collection property="roles" ofType="cn.tedu.mybatis.Role"> <id column="role_id" property="id" /> <result column="role_name" property="name" /> <result column="role_description" property="description" /> <result column="role_sort" property="sort" /> <result column="role_gmt_create" property="gmtCreate" /> <result column="role_gmt_modified" property="gmtModified" /> </collection></resultMap><select id="getDetailsById" resultMap="DetailsResultMap"> select <include refid="DetailsQueryFields" /> from ams_admin left join ams_admin_role on ams_admin.id=ams_admin_role.admin_id left join ams_role on ams_admin_role.role_id=ams_role.id where ams_admin.id=#{id}</select>
20. Mybatis的缓存机制
缓存:通常是一个临时存储的数据,在未来的某个时间点可能会被删除,通常,存储缓存数据的位置通常是读写效率较高的,相比其它“非缓存”的数据有更高的处理效率。由于缓存的数据通常并不是必须的,则需要额外消耗一定的存储空间,同时由于从缓存获取数据的效率更高,所以是一种牺牲空间、换取时间的做法!另外,你必须知道,从数据库读取数据的效率是非常低下的!
Mybatis有2种缓存机制,分别称之一级缓存和二级缓存,其中,一级缓存是基于SqlSession
的缓存,也称之为“会话缓存”,仅当是同一个会话、同一个Mapper、同一个抽象方法(同一个SQL语句)、同样的参数值时有效,一级缓存在集成框架的应用中默认是开启的,且整个过程不由人为控制(如果是自行得到SqlSession
后的操作,可自行清理一级缓存),另外,二级缓存默认是全局开启的,它是基于namespace的,所以也称之为“namespace缓存”,需要在配置SQL语句的XML中添加节点,以表示当前XML中的所有查询都允许开通二级缓存,并且,在
节点上配置
useCache="true"
,则对应的节点的查询结果将被二级缓存处理,并且,此查询返回的结果的类型必须是实现了
Serializable
接口的,如果使用了配置如何封装查询结果,则必须使用
节点来封装主键的映射,满足以上条件后,二级缓存将可用,只要是当前namespace中查询出来的结果,都会根据所执行的SQL语句及参数进行结果的缓存。无论是一级缓存还是二级缓存,只要数据发生了写操作(增、删、改),缓存数据都将被自动清理。
由于Mybatis的缓存清理机制过于死板,所以,一般在开发实践中并不怎么使用!更多的是使用其它的缓存工具并自行制定缓存策略。
21. 关于Mybatis小结
关于Mybatis框架,你应该:
- 了解如何创建一个整合了Spring框架的Mybatis工程
- 了解整合了Spring框架的Mybatis工程的配置
- 掌握基于
spring-test
的测试- 在测试类上使用
@SpringJUnitConfig
注解加载Spring的配置文件,则在当前测试中可以自动装配Spring容器中的任何对象
- 在测试类上使用
- 掌握
@Sql
注解的使用,掌握断言的使用 - 掌握声明抽象方法的原则:
- 返回值类型:增删改类型的操作均返回
int
,表示“受影响的行数”,查询类型操作,根据操作得到的结果集来决定,只要能够放入所有所需的数据即可,通常,统计查询返回int
,查询最多1个结果时返回自定义的某个数据类型,查询多个结果时返回List
集合类型 - 方法名称:自定义,不要使用重载,其它命名建议参考此前的笔记
- 参数列表:根据需要执行的SQL语句中的参数来设计,并且,当需要执行的是插入数据操作时,必须将这些参数进行封装,并在封装的类中添加主键属性,以便于Mybatis能获取自动编号的值回填到此主键属性中,当需要执行的是其它类型的操作时,如果参数数量较多,可以封装,如果只有1个,则直接声明为方法参数,如果超过1个且数量不多,则每个参数之前添加
@Param
注解
- 返回值类型:增删改类型的操作均返回
- 了解使用注解配置SQL语句
- 掌握使用XML配置SQL语句
- 这类XML文件需要顶部特殊的声明,所以,通常是从网上下载或通过复制粘贴得到此类文件
- 根节点必须是
,且必须配置
namespace
,取值为对应的Java接口的全限定名 - 应该根据要执行的SQL语句不同来选择
、
、
、
节点,这些节点都必须配置
id
属性,取值为对应的抽象方法的名称- 其实,
节点和
节点可以随意替换使用,但不推荐
- 在不考虑“获取自动生成的主键值”的情况下,
和
、
也可以混为一谈,但不推荐
- 当插入数据时,当需要获取自动生成的主键值时,需要在
节点上配置
useGeneratedKeys
和keyProperty
属性 - 在
节点上,必须配置
resultMap
或resultType
属性中的其中1个
- 其实,
- 掌握使用
封装SQL语句片段,并使用
进行引用,以实现SQL语句的复用
- 掌握
的配置方式
- 主键列与属性的映射必须使用
节点配置
- 在1对多、多对多的查询中,集合类型的属性的映射必须使用
子节点配置
- 其它列与属性的映射使用
节点配置
- 在单表查询中,列与属性名一致时,可以不必显式的配置,但是,在关联查询中,即使列与属性名称一致,也必须显式的配置出来
- 主键列与属性的映射必须使用
- 理解
resultType
与resultMap
使用原则- 尽可能的全部使用
resultMap
,如果查询结果是单一某个数据类型(例如基本数据类型或字符串或某个时间等),则使用resultType
- 尽可能的全部使用
- 掌握动态SQL中的
的使用
- 大概了解动态SQL中的其它标签
- 在后续项目中补充
- 理解
#{}
和${}
的区别 - 了解Mybatis中的一级缓存和二级缓存
- 有专门的视频讲解