Spring 6
第 1 章 Spring 概述
Spring 是一款主流的 Java EE 轻量级开源框架,用于简化 Java 企业级应用的开发难度和开发周期。Spring 框架特点如下:
- 控制反转:IoC——Inversion of Control,容器负责创建、配置和管理 bean。容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率
- 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功能
- 事务管理:声明式事务管理,底层采用 AOP 技术
第 2 章 IoC 容器(⭐)
组件就是可以复用的 Java 对象,比如控制层组件 XxxController、业务层组件 XxxService、持久层组件 XxxDao。由 IoC 容器管理的组件对象称为 Bean。
Spring 通过 IoC 容器读取配置文件来管理所有 Bean 的实例化(IoC 功能),以及控制 Bean 之间的依赖关系(DI 功能)。
对于配置文件的配置方式,目前最常用的配置方式是注解 + 配置类方式。
2.1 IoC 容器功能 IoC 和 DI
IoC 是控制反转 Inversion of Control 的简写,用于 Bean 的实例化。IoC 容器读取配置文件,通过反射完成 Bean 的实例化。
DI 是依赖注入 Dependency Injection 的简写,用于实例化对象的依赖注入。 实例化后的对象所依赖的其它对象,通过 XML 或者注解配置方式进行注入,实现对象之间的解耦。常见的注入方式有两种:
- 构造方法注入:被依赖注入的对象在构造方法中声明依赖对象的参数列表,让容器外部知道它需要哪些依赖对象
- set 方法注入:被依赖对象中通过 set 方法设置依赖对象
2.2 IoC 容器接口
- BeanFactory:Spring 内部使用接口,不提供开发人员使用。加载配置文件时不创建 Bean 对象,使用时才创建
- ApplicationContext:BeanFactory 的子接口,提供更强大的功能,面向开发人员使用,加载配置文件时就创建 Bean 对象。ApplicationContext 接口的主要实现类有以下两个:
主要实现类 | 简介 |
---|---|
ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 |
AnnotationContigApplicationContext | 用于读取 Java 配置类(Java 类也可以做配置文件)创建 IOC 容器对象 |
2.3 Bean 的生命周期
- Bean 对象创建(调用无参构造器)
- Bean 属性设置(setter 方法)
- Bean 对象初始化(需在配置 Bean 时指定初始化方法 initMethod)
- Bean 对象就绪,可以使用
- Bean 对象销毁(需在配置 Bean 时指定销毁方法 destroyMethod)
- IOC 容器关闭(调用 close() 方法)
2.4 基于注解方式管理 Bean
基于注解方式管理 Bean 主要有以下几步:
- 在类上添加相关注解,定义为 Bean
- 使用配置类配置注解生效范围、引用外部配置文件、声明第三方 Bean
2.4.1 定义 Bean 的注解
以下注解可以直接标注在 Java 类上,将它们定义成 Bean:
注解 | 说明 |
---|---|
@Component | 将该类定义为 Bean,可以作用在应用的任何层次,使用时将该注解标注在相应类上 |
@Repository | 作用在数据访问层(Dao 层),将该层的类定义为 Bean,其功能与 @Component 相同 |
@Service | 作用在业务层(Service 层),将该层的类定义为 Bean,其功能与 @Component 相同 |
@Controller | 作用在控制层(Controller 层),将该层的类定义为 Bean,其功能与 @Component 相同 |
2.4.2 周期方法的注解
注解 | 说明 |
---|---|
@PostConstruct | 执行顺序:Constructor(构造方法)-> @Autowired(依赖注入)-> @PostConstruct(注解方法) |
@PreDestroy | 执行顺序:@PreDestroy(注解方法)-> IoC 容器销毁 Bean |
@Component
public class JavaBean {
@PostConstruct
public void init(){
// 初始化逻辑
System.out.println("JavaBean init");
}
@PreDestroy
public void destroy(){
// 释放资源逻辑
System.out.println("JavaBean destroy");
}
}
2.4.3 作用域注解(单例、多例)
@Scope 注解指定 Bean 的作用域范围。
属性 | 含义 | 创建对象时间 |
---|---|---|
singleton(默认) | Bean 在 IOC 容器中为单例 | IOC 容器初始化时 |
prototype | Bean 在 IOC 容器中为多例 | 获取 Bean 时 |
2.4.4 引用类型依赖注入之 @Autowired
单独使用 @Autowired 注解,默认根据类型装配。
- @Autowired 属性注入:
创建 UserController 类:
@Controller
public class UserController {
@Autowired
private UserService userService;
public void addUser() {
userService.addUser();
}
}
创建 UserService 接口,包含抽象方法 addUser():
public interface UserService {
void addUser();
}
创建 UserService 接口的实现类 UserServiceImpl:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void addUser() {
userDao.addUser();
}
}
创建 UserDao 接口,包含抽象方法 addUser():
public interface UserDao {
void addUser();
}
创建 UserDao 接口的实现类 UserDaoImpl:
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
}
}
- @Autowired + @Qualifier。
如果此时有另一个实现类 UserRedisDaoImpl 实现了 UserDao 接口,此时 UserDao 有不止一个 Bean,所以不能通过类型注入,而要根据名称注入:
@Service
public class UserServiceImpl implements UserService {
// @Qualifier 指定 Bean 的名字
@Autowired
@Qualifier("userDaoImpl")
private UserDao userDao;
@Override
public void addUser() {
userDao.addUser();
}
}
2.4.5 引用类型依赖注入之 @Resource
@Autowired | @Resource | |
---|---|---|
注解来源 | Spring | JDK 扩展包 |
注入方式 | @Autowired 默认根据类型注入,如果想根据名称注入,需要配合 @Qualifier 一起用 | @Resource 默认根据名称注入,通过名称找不到会通过类型注入 |
注解作用位置 | 属性上、setter方法上、构造器上、构造器形参上 | 属性上、setter 方法上 |
创建 UserController 类:
@Controller("myUserController")
public class UserController {
@Resource(name = "myUserService")
private UserService userService;
public void addUser() {
userService.addUser();
}
}
创建 UserService 接口,包含抽象方法 addUser():
public interface UserService {
void addUser();
}
创建 UserService 接口的实现类 UserServiceImpl:
@Service(value="myUserService")
public class UserServiceImpl implements UserService {
// 未指定名称时,默认使用属性名字 myUserDao
@Resource
private UserDao myUserDao;
@Override
public void addUser() {
myUserDao.addUser();
}
}
创建 UserDao 接口,包含抽象方法 addUser():
public interface UserDao {
void addUser();
}
创建 UserDao 接口的实现类 UserDaoImpl:
@Repository("myUserDao")
public class UserDaoImpl implements UserDao {
@Override
public void addUser() {
System.out.println("Dao执行成功");
}
}
2.4.6 基础类型依赖注入之 @Value
配置文件 db.properties:
druid.url=xxx
druid.driver=xxx
druid.username=root
druid.password=root
以配置类 DruidConfig 为例:
@Configuration
@PropertySource("classpath:db.properties")
public class DruidConfig {
@Value("${druid.username}")
private String username;
}
2.4.7 @Configuration、@ComponentScan 和 @Bean 实现配置
@Configuration 注解标识的类为注解类。注解类可以用于包扫描注解配置、引用外部配置文件、声明第三方依赖 Bean
- @ComponentScan 注解用于包扫描注解配置
- @PropertySource 注解引用外部配置文件
在 com/atguigu/spring 包下创建一个 config 包,在 config 包下创建配置类 SpringConfig:
@Configuration
@ComponentScan("com.atguigu.spring")
@PropertySource(value = "classpath:db.properties")
public class SpringConfig {
@Value("${druid.url}")
private String url;
@Value("${druid.driver}")
private String driver;
@Value("${druid.username}")
private String username;
@Value("${druid.password}")
private String password;
// 方法的名字为第三方 Bean 的 id
// 方法的返回值为 Bean 组件的类型
@Bean
public DruidDataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setDriverClassName(driver);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
// jdbcTemplate 组件引用 dataSource 组件
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
第 3 章 AOP
计算器接口 Calculator,包含加减两个抽象方法:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
}
创建接口 Calculator 带日志功能的实现类 CalculatorLogImpl:
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j); // 核心操作前的日志
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result); // 核心操作后的日志
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
}
现有代码缺陷:附加的日志功能对核心业务功能有干扰,且日志功能分散在各个业务功能方法中,代码重复且不利于统一维护。
因此提出了代理模式,它的原理是通过提供一个代理类,在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用,让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。
3.1 静态代理
创建静态代理类 CalculatorStaticProxy 实现 Calculator 接口:
public class CalculatorStaticProxy implements Calculator {
private Calculator calculator;
public CalculatorStaticProxy(Calculator calculator) {
this.calculator = calculator; // 依赖目标接口对象
}
@Override
public int add(int i, int j) {
// 增强日志功能由代理类中的代理方法来实现
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标接口对象 calculator 来实现核心业务逻辑
int addResult = calculator.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
}
静态代理的缺点:
- 静态代理实现了解耦,但是代码完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复代码
- 静态代理中一个代理对象只能代理一个被代理对象,不能同时代理多个被代理对象。静态代理类 CalculatorStaticProxy 和被代理类 CalculatorLogImpl,必须实现同一个接口 Calculator
3.2 动态代理
动态代理分为 JDK 动态代理和 CGLib 动态代理。
JDK 动态代理 | CGLib 动态代理 | |
---|---|---|
来源 | JDK | 第三方工具库 CGLib |
接口要求 | 代理类和目标类必须实现同一个接口 | 目标类无接口时只能使用 CGLib 动态代理 |
生成代理类 | 在 com.sun.proxy 包下 | 继承目标类,和目标类在相同的包下 |
实现 | 目标对象和代理对象实现同样的接口 | 继承目标类并创建它的子类,在子类中重写父类的方法, 实现方法增强 |
3.2.1 JDK 代理
接口和其实现类:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
}
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
}
生产代理对象的工厂类 ProxyFactory:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target; // 目标对象
}
public Object getProxy() {
// 参数 1:加载动态生成的代理类的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
// 参数 2:目标对象实现的所有接口
Class<?>[] interfaces = target.getClass().getInterfaces();
// 参数 3:设置代理对象实现目标对象方法的过程
InvocationHandler invocationHandler = new InvocationHandler() {
/**
* invoke() 代理类中重写接口中的抽象方法
* @param proxy 代理对象
* @param method 代理对象需要实现的方法,即需要重写的方法
* @param args 方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
// 方法调用前
System.out.println("[日志] " + method.getName() + ",参数:" + Arrays.toString(args));
// 调用目标方法
result = method.invoke(target, args);
// 方法调用后
System.out.println("[日志] " + method.getName() + ",结果:" + result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("[日志] " + method.getName() + ",异常:" + e.getMessage());
} finally {
System.out.println("[日志] " + method.getName() + ",方法执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
测试:
public class TestCalculator {
public static void main(String[] args) {
// 创建代理对象(动态):同一个代理对象可以代理多个被代理对象
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
// 返回代理对象
Calculator calculator = (Calculator) proxyFactory.getProxy();
calculator.add(1,2);
}
}
3.2.2 CGLib 代理
目标对象不需要实现接口,使用 CGLib 代理。引入 CGLib 的依赖:
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
目标对象:
public class CalculatorDao {
public void calculate(){
System.out.println("计算,使用CGLib代理");
}
}
生产代理对象的工厂类 ProxyFactory:
public class ProxyFactory implements MethodInterceptor {
private Object target;
public ProxyFactory(Object target) {
this.target = target; // 依赖目标对象
}
public Object getProxyInstance(){
Enhancer enhancer = new Enhancer(); // 创建工具类
enhancer.setSuperclass(User.class); // 设置 enhancer 的父类
enhancer.setCallback(new MyProxy()); // 设置 enhancer 的回调对象
return enhancer.create();
}
/**
* @param o cglib 生成的代理对象
* @param method 被代理对象的方法
* @param objects 传入方法的参数
* @param methodProxy 代理的方法
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CGLib代理开始");
Object returnVal = method.invoke(target, objects);
System.out.println("CGLib代理结束");
return returnVal ;
}
}
测试:
public class TestCalculator {
public static void main(String[] args) {
// 创建代理对象(动态):同一个代理对象可以代理多个被代理对象
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorDao());
// 返回代理对象
CalculatorDao proxyInstance = (CalculatorDao) proxyFactory.getProxy();
proxyInstance.calculate();
}
}
3.3 AOP 概念和相关术语(⭐)
AOP (Aspect Oriented Programming)是一种编程思想,是面向对象编程的延续:面向切面编程。它通过预编译方式和运行期动态代理方式实现,在不修改源代码的情况下,给程序动态统一添加额外功能的一种技术。
七个相关术语如下:
- 横切关注点:从每个业务中抽取出来的同一类非核心业务,如用户验证、日志管理、事务处理、数据缓存等都属于横切关注点。例如我们要额外实现某业务的十个附加功能,就有十个横切关注点
-
通知(增强):想要增强的功能,比如安全,事务,日志等。每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。通知方法有以下几种:
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行
- 异常通知:在被代理的目标方法异常结束后执行
- 后置通知:在被代理的目标方法最终结束后执行
- 环绕通知:使用 try…catch…finally 结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
- 切面:封装通知方法的类。由切入点和增强组成
- 目标:被代理的目标对象
- 代理:创建的代理对象
- 连接点:Spring 允许使用通知的地方。把方法排成一排,每一个横切位置看成 x 轴方向,把方法从上到下执行的顺序看成 y 轴,x 轴和 y 轴的交叉点就是连接点
- 切入点:Spring 的 AOP 技术可以通过切入点定位到的连接点。通俗说就是 Spring 实际去增强的方法
3.4 基于注解的 AOP(⭐)
3.4.1 引入依赖
<!-- spring aop -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.2</version>
</dependency>
<!-- spring aspects -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.2</version>
</dependency>
3.4.2 创建目标资源(接口+实现类)
Calculator 接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
CalculatorImpl 实现类:
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
3.4.3 创建切面类
创建切面类 LogAspect,@Aspect 标识该类为切面类:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.atguigu.spring6.aop.annotation.*.*(..))")
public void pointCut(){}
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("Logger-->前置通知,方法名:" + methodName + ",参数:" + args);
}
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名:" + methodName + ",结果:" + result);
}
@AfterThrowing(value = "pointCut()", throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名:" + methodName + ",异常:" + ex);
}
@After("pointCut()")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名:" + methodName);
}
@Around("pointCut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
Object result = null;
try {
System.out.println("环绕通知-->目标方法执行之前");
result = joinPoint.proceed();
System.out.println("环绕通知-->目标方法返回值之后");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知-->目标方法出现异常时");
} finally {
System.out.println("环绕通知-->目标方法执行完毕");
}
return result;
}
}
切入点表达式如下:
3.4.4 创建配置类
@EnableAspectJAutoProxy 注解开启 @AspectJ 注解:
@Configuration
@ComponentScan("com.atguigu.spring6")
@EnableAspectJAutoProxy
public class AopConfig(){
}
3.4.5 切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。使用 @Order 注解可以控制切面的优先级:@Order(较小的数):优先级高;@Order(较大的数):优先级低。
优先级高的前置先执行,后置后执行。
3.5 AOP 实现统计方法执行时间
@Aspect
@Component
public class MethodExecutionTimeAspect() {
@Pointcut("execution(* com.atguigu.spring.xxx)")
public void pointCut() {
}
@Around("pointCut")
public object timeAroundMethod(ProceedingJoinPoint pjp) {
Object obj = null;
Object[] args = pjp.getArgs();
long start = System.currentTimeMillis();
try {
obj = pjp.proceed(args);
}catch (Throwable e) {
log.error("aop error", e);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
第 4 章 事务 Transaction
4.1 编程式事务和声明式事务(⭐)
Spring 的事务其实就是数据库对事务的支持,Spring 事务支持编程式事务管理和声明式事务管理两种方式:
- 编程式事务:事务功能的相关操作全部通过自己编写代码来实现,使用 TransactionTemplate 管理
- 声明式事务:通过配置让框架实现功能。声明式事务管理其本质是通过 AOP 对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
4.2 基于注解的声明式事务(⭐)
4.2.1 建表
模拟用户购买图书的场景,用户可以查询图书价格,买图书后图书表库存变化,并且用户余额变化。
在 DataGrip 中创建用户表 t_user 和图书表 t_book:
use `spring`;
CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
4.2.2 添加配置类
配置类需要添加 @EnableTransactionManagement 注解开启事务注解的支持。
在配置类中将 Druid 连接池 DataSource、JDBC 封装类 JdbcTemplate 以及事务管理器 DataSourceTransactionManager 加入 IoC 容器。
@Configuration
@ComponentScan("com.atguigu.spring.tx")
@EnableTransactionManagement
public class SpringConfig {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("123");
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
4.2.3 创建组件
创建 BookController 类:
@Controller
public class BookController {
@Autowired
private BookService bookService;
public void buyBook(Integer bookId, Integer userId){
bookService.buyBook(bookId, userId);
}
}
创建接口 BookService:
public interface BookService {
void buyBook(Integer bookId, Integer userId);
}
创建 BookService 的实现类 BookServiceImpl:
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
Integer price = bookDao.getPriceByBookId(bookId);
bookDao.updateStock(bookId);
bookDao.updateBalance(userId, price);
}
}
创建接口 BookDao:
public interface BookDao {
Integer getPriceByBookId(Integer bookId);
void updateStock(Integer bookId);
void updateBalance(Integer userId, Integer price);
}
创建 BookDao 的实现类 BookDaoImpl:
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
}
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}
@Override
public void updateBalance(Integer userId, Integer price) {
String sql = "update t_user set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql, price, userId);
}
}
4.2.4 添加事务注解
处理事务一般在 Service 层处理。在 BookServiceImpl 类上添加注解 @Transactional:
@Transactional
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer bookId, Integer userId) {
Integer price = bookDao.getPriceByBookId(bookId);
bookDao.updateStock(bookId);
bookDao.updateBalance(userId, price);
}
}
4.2.5 测试
假设 id 为 1 的用户,购买 id 为 1 的图书,用户余额为 50,而图书价格为 80。购买图书之后,用户的余额为 -30,数据库中余额字段设置了无符号,因此无法将 -30 插入到余额字段。
public class TransactionManagerTest {
@Test
public void testTxAllAnnotation(){
ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookController bookController = context.getBean("bookController", BookController.class);
bookController.buyBook(1, 1);
}
}
此时执行 sql 语句会抛出异常:
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance = balance - ? where user_id = ?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`spring`.`t_user`.`balance` - 80)'
由于我们使用了 Spring 的声明式事务,更新库存和更新余额都没有执行,满足实际生产情况。
4.3 事务注解的属性
类中具体方法上的事务注解属性会覆盖类上的事务注解属性。
4.3.1 只读
对一个查询操作来说,如果我们把它设置成只读,数据库就能够针对查询操作来进行优化。注意注解为只读事务的方法只能进行查询,不能增删改。
@Transactional(readOnly = true)
4.3.2 超时
事务在执行过程中,有可能遇到 Java 程序或 MySQL 数据库超时或网络连接超时,从而长时间占用数据库资源。此时程序应该被回滚,把资源让出来,让其他正常程序可以执行。设置的超时时间单位为秒:
@Transactional(timeout = 3)
4.3.3 回滚策略
在一般情况下,如果发生运行时异常,事务才会回滚;如果发生 IO 异常,事务不会回滚。所以,我们希望发生 IO 异常时事务也进行回滚,可以使用 rollbackFor 属性。
- rollbackFor :指定需要回滚的异常
@Transactional(rollbackFor = Exception.class)
- noRollbackFor :指定不需要回滚的异常
@Transactional(noRollbackFor = IOException.class)
4.3.4 隔离级别
- ISOLATION_DEFAULT:默认隔离级别,使用数据库默认的事务隔离级别
- ISOLATION_READ_UNCOMMITTED:读未提交,允许另外一个事务可以看到这个事务未提交的数据
- ISOLATION_READ_COMMITTED:读已提交,保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新
- ISOLATION_REPEATABLE_READ:可重复读,保证一个事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新
- ISOLATION_SERIALIZABLE:一个事务在执行的过程中完全看不到其他事务对数据库所做的更新
@Transactional(isolation = Isolation.DEFAULT)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
4.3.5 传播行为
在 Service 类中有 a 方法和 b 方法,两个方法上都有事务,当 a 方法执行过程中调用了 b 方法,事务是如何传递的?合并到一个事务里还是开启一个新的事务?这就是事务传播行为。
- REQUIRED:如果当前有事务就加入,没有事务就新建一个事务(常用)
- SUPPORTS:有事务就加入,没有事务就以非事务方式执行
- MANDATORY:必须运行在一个事务中,没有事务将抛出一个异常
- REQUIRES_NEW:创建一个新事务,在新事务中执行,已存在的事务被挂起(常用)
- NOT_SUPPORTED:以非事务方式运行,已存在的事务被挂起
- NEVER:以非事务方式运行,如果有事务存在抛出异常
- NESTED:有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和 REQUIRED 一样
@Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
第 5 章 数据校验 Validation
在开发中,我们经常遇到参数校验的需求,比如用户注册的时候,要校验用户名不能为空、用户名长度不超过20个字符、手机号是合法的手机号格式等等。
Spring Validation 允许通过注解的方式来定义对象校验规则,把校验和业务逻辑分离开,让代码编写更加方便。在 Spring 中有多种校验的方式:通过 Validator 接口实现;Bean Validation 注解实现;基于方法实现校验;自定义校验。
5.1 Bean Validation 注解实现
Spring 默认有一个实现类 LocalValidatorFactoryBean,它实现了上面 Bean Validation 中的接口,并且也实现了 Validator 接口。
5.1.1 创建配置类,配置 LocalValidatorFactoryBean
@Configuration
@ComponentScan("com.sunyu.spring6.validation2")
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
5.1.2 创建实体类,使用注解定义校验规则
注解 | 规则 |
---|---|
@NotNull | 限制必须不为 null |
@NotEmpty | 只作用于字符串类型,字符串不为空,并且长度不为 0 |
@NotBlank | 只作用于字符串类型,字符串不为空,并且 trim() 后不为空串 |
@DecimalMax(value) / @Max(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) / @Min(value) | 限制必须为一个不小于指定值的数字 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在 min 到 max 之间 |
验证注解的元素值是 Email |
public class User {
@NotNull
private String name;
@Min(0)
@Max(120)
private int age;
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
}
5.1.3 使用两种校验器实现
(1)使用 jakarta.validation.Validator 校验
import jakarta.validation.Validator;
@Service
public class MyValidation1 {
@Autowired
private Validator validator;
public boolean validatorByUser1(User user){
Set<ConstraintViolation<User>> sets = validator.validate(user);
return sets.isEmpty();
}
}
(2)使用 org.springframework.validation.Validator 校验
import org.springframework.validation.Validator;
@Service
public class MyValidation2 {
@Autowired
private Validator validator;
public boolean validatorByUser2(User user) {
BindException bindException = new BindException(user, user.getName());
validator.validate(user, bindException);
return bindException.hasErrors();
}
}
5.1.4 测试
public class TestMethod2 {
@Test
public void testMyValidation1() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation1 validation1 = context.getBean(MyValidation1.class);
User user = new User();
user.setName("Tom");
boolean message = validation1.validatorByUser1(user);
System.out.println(message); // 输出 true 即为校验通过
}
@Test
public void testMyValidation2() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyValidation2 validation2 = context.getBean(MyValidation2.class);
User user = new User();
user.setName("Tom");
user.setAge(-1);
boolean message= validation2.validatorByUser2(user);
System.out.println(message); // 输出 true 为校验不通过,有错误
}
}
5.2 基于方法实现校验
5.2.1 创建配置类,配置 MethodValidationPostProcessor
@Configuration
@ComponentScan("com.sunyu.spring6.validation3")
public class ValidationConfig {
@Bean
public MethodValidationPostProcessor validationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
5.2.2 创建实体类,使用注解设置校验规则
public class User {
@NotNull
private String name;
@Min(0)
@Max(120)
private int age;
@Pattern(regexp = "^1(3|4|5|7|8)\d{9}$",message = "手机号码格式错误")
@NotBlank(message = "手机号码不能为空")
private String phone;
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public int getAge() {return age;}
public void setAge(int age) {this.age = age;}
public String getPhone() {return phone;}
public void setPhone(String phone) {this.phone = phone;}
}
5.2.3 定义 Service 类,通过注解操作对象
@Service
@Validated
public class MyService {
public String testParams(@NotNull @Valid User user) {
return user.toString();
}
}
5.2.4 测试
public class TestMethod3 {
@Test
public void test1() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyService myService = context.getBean(MyService.class);
User user = new User();
user.setName("Tom");
user.setAge(6);
user.setPhone("13560005555");
myService.testParams(user); // 没有报错说明校验通过
}
}
5.3 自定义校验
5.3.1 自定义校验注解
我们自定义了一个校验注解 @MyNotBlank:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {BlankValidator.class})
public @interface MyNotBlank {
String message() default "不能包含空格"; // 默认错误消息
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
MyNotBlank[] value();
}
}
5.3.2 编写自定义校验类
public class BlankValidator implements ConstraintValidator<MyNotBlank, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null && value.contains(" ")) {
// 自定义提示信息
String defaultMessage = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultMessage);
context.disableDefaultConstraintViolation(); // 禁用默认提示信息
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation(); // 设置提示语
return false;
}
return true;
}
}
5.3.3 创建实体类,使用自定义注解设置校验规则
public class User {
@MyNotBlank // 应用我们的自定义注解
private String phone;
public String getPhone() {return phone;}
public void setPhone(String phone) {this.phone = phone;}
}
5.3.4 测试
public class TestMethod4 {
@Test
public void test1() {
ApplicationContext context = new AnnotationConfigApplicationContext(ValidationConfig.class);
MyService1 myService = context.getBean(MyService.class);
User user = new User();
user.setPhone(" 13560002222 ");
myService.testParams(user);
}
}
SpringBoot 2
第 1 章 SpringBoot 简介
1.1 SpringBoot 的启动器以及自动配置原理(⭐)
SpringBoot 官方的启动器都是以 spring-boot-starter-XXX 命名:
启动器 | 功能 |
---|---|
spring-boot-starter-web | 使用 SpringMVC 构建 web 工程,默认使用 Tomcat 容器 |
spring-boot-starter-actuator | 提供生产环境特性,能监控管理应用 |
spring-boot-starter-json | 提供对 JSON 的读写支持 |
spring-boot-starter-logging | 日志启动器,默认使用 Logback 日志 |
- starter 启动器先加载所有的自动配置类 xxxxxAutoConfiguration
- 按照条件装配规则(@Conditional)按需在容器中配置组件
- 配置的组件默认都会绑定配置文件中的配置项,用户也可以手动修改配置文件 application.properties / application.yml
1.2 SpringBoot 启动原理
1.2.1 创建 SpringApplication
- 在 spring.factories 文件中找初始启动器 bootstrappers
- 在 spring.factories 文件中找应用容器初始化接口 ****ApplicationContextInitializer
- 在 spring.factories 文件中找应用监听器 ApplicationListener
1.2.2 运行 SpringApplication
- StopWatch 监听整个应用程序启动停止的监听器,记录应用的启动时间
- 获取之前所有的 bootstrappers 挨个执行 ****intitialize() 来创建引导上下文(Context环境)
- 让应用进入 headless(自力更生)模式
- 获取所有运行监听器 RunListener,方便所有监听器进行事件感知。SpringApplicationRunListener 调用 starting 方法,相当于通知所有感兴趣系统启动的人项目正在启动
- 保存命令行参数
- 准备环境
- 创建 IOC 容器,准备 IOC 容器的基本信息
- 刷新 IOC 容器,创建容器中的所有组件
- 所有的监听器开启
- 调用所有 runners 读取配置文件、或者把数据库的数据加载到缓存中
- 如果前面步骤出现异常,调用监听器的 failed 方法,如果没有异常,调用所有监听器的 running 方法
1.3 SpringBoot 配置文件加载顺序
当前 jar 包内部的配置文件 -> 当前 jar 包内部的 profile 多环境配置文件 -> 引用外部 jar 包的配置文件 -> 引用外部 jar 包的 profile 多环境配置文件
1.4 SpringBoot 的常用注解(⭐)
1.4.1 @Configuration
@Configuration 注解标识的类为配置类。配置类里面使用 @Bean 标注在方法上给容器注册组件,默认也是单实例的。其 proxyBeanMethods 属性表示代理容器的方法:
- @Configuration(proxyBeanMethods = true):保证每个方法被调用,返回组件都是单例的(默认)
- @Configuration(proxyBeanMethods = false):保证每个方法被调用,返回组件都是新创建的
1.4.2 @Conditional
满足 Conditional 指定条件,则进行组件注入。
1.4.3 @ImportResource
@ImportResource(“classpath:XXX.xml”) 可以导入外部 xml 配置文件。
1.4.4 @Value / @ConfigurationProperties
@Value 与 @ConfigurationProperties 都作用在实体类中,用于获取配置文件中的属性并绑定到实体类。
-
@Value 注解只有一个 value 属性,支持字面量,标识在 setter 属性上或者方法上
-
@ConfigurationProperties 注解有四个属性:
- prefix:配置文件 key 的前缀,给属性进行值的注入时会加上此前缀
- value:与 prefix 一样的作用,与 prefix 不能同时设置
- ignoreInvalidFields:是否忽略无效的字段,默认不忽略
- ignoreUnknownFields:是否忽略未知字段,默认忽略
@ConfigurationProperties 不支持字面量,标识在类和方法上
1.4.5 @EnableConfigurationProperties
@EnableConfigurationProperties(XXX.class)标识在配置类上,实现 XXX 实体类和 properties 文件的配置绑定,并将 XXX 组件自动注册到容器中,一般与 @ConfigurationProperties 搭配使用:
# application.properties
mycar.brand=BYD
mycar.price=100000
@ConfigurationProperties(prefix = "mycar") // 绑定配置文件中前缀为 mycar 的配置
public class Car {
private String brand;
private Integer price;
}
@EnableConfigurationProperties(Car.class) // 配置类 MyConfig
public class CarConfig {}
1.4.5 @SpringBootApplication
@SpringBootApplication 有以下三个注解组成:
- @SpringBootConfiguration:与 @Configuration 功能一样
- @ComponentScan:指定扫描哪些 Spring 注解
- @EnableAutoConfiguration:包含 @AutoConfigurationPackage 和 @Import。@AutoConfigurationPackage 自动配置包;@Import 将指定的组件 XXX 注入容器。所以 @EnableAutoConfiguration 注解的作用是将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的容器
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
第 2 章 SpringBoot 入门
2.1 创建 SpringBoot 工程
选择开发中用到的配置依赖包,也可以不选择,在用到时向 pom.xml 里面添加对应的依赖:
2.2 创建 Maven 模块
创建模块 boot-01-helloworld,在其 pom.xml 文件中引入父工程及依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
2.3 主程序
在 com/sunyu/boot 下默认创建了主程序,主程序是所有启动的入口:
@SpringBootApplication
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Main.class, args);
}
}
2.4 编写实体类
使用 Lombok 插件简化 JavaBean 开发:
@AllArgsConstructor // 自动生成全参构造器
@NoArgsConstructor // 自动生成无参构造器
@Data // 自动生成已有属性的 Getter、Setter 方法,toString 方法
@Component
public class Car {
private String brand;
private Integer price;
}
2.5 编写业务类
在 com/sunyu/boot/controller 下创建 HelloController.java:
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello, Spring Boot 2!";
}
}
2.6 运行主程序
启动运行主程序,在浏览器网址输入 http://localhost:8080/hello 即可看见我们写的 Hello, Spring Boot 2!
第 3 章 SpringBoot 核心技术 (⭐)
3.1 yaml 配置文件
SpringBoot 的配置文件有 properties 和 yaml 格式,当我们配置数据时通常选择使用 yaml 配置文件。yaml 格式中,相同层级的元素左对齐,key 和 value 之间用 [冒号 + 空格] 隔开:
user:
name: Tom
password: 123
3.1.1 键值对集合写法
k:
k1: v1
k2: v2
k3: v3
3.1.2 数组写法
k:
- v1
- v2
- v3
3.2 Web 开发
3.2.1 静态资源目录
请求进来,先去找 Controller 看能不能处理,Controller 不能处理的所有请求都交给静态资源处理器。只要静态资源放在类路径 recources/static 下,通过 localhost:8080/ + 静态资源名即可访问。
例如欢迎页 index.html 就可以放在 recources/static 下。
3.2.2 @RestController
@RestController 的作用等同于 @Controller + @ResponseBody。@ResponseBody 表示控制器方法的返回值直接以指定的格式写入 Http response body中,而不是解析为跳转路径。
3.2.3 @PathVariable
@PathVariable 注解可以使 URL 中的占位符绑定到控制器方法的入参中。一般与 @GetMapping 一起使用:
<!-- index.html -->
<body>
<ul>
<a href="car/3/owner/Tom">测试</a>
</ul>
</body>
@RestController
public class ParameterTestController {
// URL 中占位符参数 id 和 username 绑定到处理器方法的入参 id、username 中
@GetMapping("/car/{id}/owner/{username}")
public Map<String, Object> getCarAndUsername(@PathVariable("id") Integer id,
@PathVariable("username") String username,
@PathVariable Map<String, String> pv) {
Map<String, Object> map = new HashMap<>();
map.put("id", id);
map.put("username", username);
map.put("pv", pv);
return map;
// 浏览器中显示 {"pv":{"id":"3","username":"Tom"},"id":3,"username":"Tom"}
}
}
3.2.4 @RequestHeader
@RequestHeader 注解用于控制器方法获取请求头内容:
@RestController
public class ParameterTestController {
@RequestMapping("/testRequestHeader")
public Map<String, Object> getHeader(@RequestHeader("Connection") String connection,
@RequestHeader("Host") String host
) {
Map<String, Object> map = new HashMap<>();
map.put("Connection", connection);
map.put("Host", host);
return map;
// 浏览器中显示 {"Connection":"keep-alive","Host":"localhost:8080"}
}
}
3.2.5 @RequestParam
@RequestParam 注解用于控制器方法获取请求参数:
<!-- index.html -->
<body>
<ul>
<a href="/testRequestParam?age=18&interests=basketball&interests=game">测试</a><br>
</ul>
</body>
@RestController
public class ParameterTestController {
@GetMapping("/testRequestParam")
public Map<String, Object> getParam(@RequestParam("age") Integer age,
@RequestParam("interests") List<String> interests
) {
Map<String, Object> map = new HashMap<>();
map.put("age",age);
map.put("interests",interests);
return map;
// {"interests":["basketball","game"],"age":18}
}
}
3.2.6 @CookieValue
@CookieValue 注解用于控制器方法获取 Cookie:
@RestController
public class ParameterTestController {
@GetMapping("/testCookieValue")
public Map<String, Object> getCookie(@CookieValue("Idea-8296eef3") String cookie) {
Map<String, Object> map = new HashMap<>();
map.put("Idea-8296eef3",cookieValue);
return map;
// {"Idea-8296eef3":"d93df572-e50a-4cfc-8711-ad30d3f9f25b"}
}
}
3.2.7 视图模板引擎 Thymeleaf
语法 | 用途 |
---|---|
${…} | 获取请求域、session 域、对象等 |
@{…} | 生成链接 |
*{…} | 获取上下文对象值 |
#{…} | 获取国际化等值 |
’ ’ | 文本值 |
3.2.8 拦截器
- 编写一个拦截器实现 HandlerInterceptor 接口
package com.sunyu.admin.interceptor;
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
// 控制器方法执行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}", requestURI);
// 登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
return true;
}
// 未登录拦截住,跳转到登录页
request.setAttribute("msg", "请先登录");
request.getRequestDispatcher("/").forward(request, response);
return false;
}
// 控制器方法执行完成以后
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}", modelAndView);
}
// 页面渲染以后
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}", ex);
}
}
- 拦截器注册到容器中,并指定拦截规则
package com.sunyu.admin.config;
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/css/**", "/fonts/**", "/images/**", "/js/**");
}
}
3.2.9 文件上传
package com.sunyu.admin.controller;
@Slf4j
@Controller
public class FormController {
@GetMapping("/form_layouts")
public String form_layouts() {
return "form/form_layouts";
}
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getSize(), photos.length);
if (!headerImg.isEmpty()) {
// 保存到文件服务器,OSS 服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("文件路径" + originalFilename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("文件路径" + originalFilename));
}
}
}
return "main";
}
}
3.2.10 错误/异常处理
- Spring Boot 默认提供 /error 处理所有错误的映射
- 对于机器客户端,它将生成 JSON 响应,其中包含错误,HTTP 状态和异常消息的详细信息
- 对于浏览器客户端,响应一个 " whitelabel " 错误视图,以 HTML 格式呈现相同的数据
要完全替换默认行为,可以实现 ErrorController 并注册该类型的 Bean 定义,或添加 ErrorAttributes 类型的组件以使用现有机制但替换其内容。
SpringBoot 提供了一种ControlerAdvice
类,来处理控制器类抛出的所有异常。
@ControllerAdvice
配合@ExceptionHandler
实现全局异常处理@ControllerAdvice
配合@ModelAttribute
预设全局数据@ControllerAdvice
配合@InitBinder
实现对请求参数的预处理
3.2.11 Web 原生组件注入(Servlet、Filter、Listener)
- 使用 Servlet API
package com.sunyu.admin;
// 指定原生 Servlet 组件所在包
@ServletComponentScan(basePackages = "com.sunyu.admin")
@SpringBootApplication
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}
Servlet 注入:
package com.sunyu.admin.servlet;
@WebServlet(urlPatterns = "/my")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("666");
}
}
Filter 注入:
package com.sunyu.admin.servlet;
@Slf4j
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Filter初始化完成");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("Filter工作");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
log.info("Filter销毁");
}
}
Listener 注入:
package com.sunyu.admin.servlet;
@Slf4j
@WebListener()
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("监听到项目销毁完成");
}
}
- 使用 RegistrationBean(MyServlet、MyFilter、MyListener 类上不再使用 @WebXXX 注解):
package com.sunyu.admin.servlet;
@Configuration
public class MyRegisterConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MyListener myListener = new MyListener();
return new ServletListenerRegistrationBean(myListener);
}
}
3.2.12 定制化组件
自定义配置类用 @EnableWebMvc 注解标识,以及实现 WebMvcConfigurer 接口,可以重新配置 SpringMVC 的静态资源、欢迎页等。所有功能的定制都是这些实现了 WebMvcConfigurer 接口的配置类合起来一起生效。
3.3 数据访问
3.3.1 导入 JDBC
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
修改配置项导入数据库驱动:
spring:
datasource:
url: jdbc:mysql://localhost:3306/<数据库名字>?characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
测试:
package com.sunyu.admin;
@Slf4j
@SpringBootTest
class AdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long rows = jdbcTemplate.queryForObject("select count(*) from t_user", Long.class);
log.info("记录总数:{}", rows);
}
}
3.3.2 使用 Druid 数据源(官方配置)
在 pom.xml 中引入依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
在配置文件 application.yml 中添加:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: zxcvbnm5237
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.sunyu.admin.* # 监控 SpringBean
filters: stat,wall # 底层开启 stat(sql监控),wall(防火墙)
stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin # 监控页账户
login-password: 123 # 监控页密码
resetEnable: false
web-stat-filter: # 监控 web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat:
slow-sql-millis: 1000 # 慢查询时间(毫秒)
logSlowSql: true # 是否记录慢查询
enabled: true # stat 功能是否开启
wall:
enabled: true
config:
drop-table-allow: false # 不允许删表
两个学习文档:SpringBoot 配置 Druid,DruidDataSource 配置属性列表
3.3.3 整合 MyBatis (配置文件方式)
在 pom.xml 中引入依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
在配置文件 application.yml 中添加:
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml # sql 映射文件位置
configuration:
map-underscore-to-camel-case: true
编写 mapper 接口:
package com.sunyu.admin.mapper;
@Mapper
public interface UserMapper {
public User getUser(Integer id);
}
编写 sql 映射文件并绑定 mapper 接口:
<?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.sunyu.admin.mapper.UserMapper">
<select id="getUser" resultType="com.sunyu.admin.bean.User">
select * from t_user where id=#{id}
</select>
</mapper>
3.3.4 整合 MyBatis-Plus
在 pom.xml 中引入依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
3.3.5 整合 Redis (RedisTemplate)
在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
logging.level.root=info
logging.level.com.sunyu.redis7=info
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
spring.redis.host=192.168.239.128
spring.redis.port=6379
spring.redis.password=123
spring.redis.database=0
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
配置类 RedisConfig:
package com.sunyu.redis7.config;
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.setValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
测试:
package com.sunyu.redis7;
@SpringBootTest
public class RedisTemplateTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void set() {
stringRedisTemplate.opsForValue().set("a", "1");
}
@Test
public void get() {
String value = stringRedisTemplate.opsForValue().get("a");
System.out.println(value);
}
}
3.4 单元测试
Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
在 src/test/java/com/sunyu/admin/AdminApplicationTests.java 测试方法下,使用 @SpringBootTest 标识:
@DisplayName("测试Junit5")
class AdminApplicationTests {
@Test
void contextLoads() {
}
}
3.4.1 @DisplayName
@DisplayName 为测试类或者测试方法设置展示名称:
@DisplayName("测试Junit5")
class Junit5Tests {
@DisplayName("@DisplayName注解")
@Test
void testDisplayName() {
System.out.println(1);
}
}
3.4.2 @BeforeEach / @AfterEach
@BeforeEach 表示在每个单元测试之前执行,@AfterEach 表示在每个单元测试之后执行:
@DisplayName("测试Junit5")
class Junit5Tests {
@DisplayName("测试DisplayName")
@Test
void testDisplayName(){
System.out.println(1);
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试开始");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束");
}
}
3.4.3 @BeforeAll / @AfterAll
@BeforeAll 表示在所有单元测试之前执行,@AfterAll 表示在所有单元测试之后执行,两者标识的方法必须用 static 修饰:
@DisplayName("测试Junit5")
public class Junit5Test {
@DisplayName("测试DisplayName")
@Test
void testDisplayName(){
System.out.println(1);
}
@DisplayName("测试DisplayName2")
@Test
void testDisplayName2(){
System.out.println(2);
}
@BeforeEach
void testBeforeEach() {
System.out.println("测试开始");
}
@AfterEach
void testAfterEach() {
System.out.println("测试结束");
}
@BeforeAll
static void testBeforeAll(){
System.out.println("所有测试开始...");
}
@AfterAll
static void testAfterAll(){
System.out.println("所有测试结束...");
}
}
3.4.4 @Disabled
@Disabled 表示测试类或测试方法不执行。
3.4.5 @Timeout
@Timeout 表示测试方法运行如果超过了指定时间将会返回错误:
@DisplayName("测试Junit5")
public class Junit5Test {
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
public void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
}
3.4.6 @SpringBootTest
如果想要在自定义测试类中实现自动注入功能,需要加上 @SpringBootTest 注解:
@SpringBootTest
@DisplayName("测试Junit5")
public class Junit5Test {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void test1() {
System.out.println(jdbcTemplate);
}
}
3.4.7 @RepeatTest
@RepeatTest 实现重复测试:
@DisplayName("测试Junit5")
public class Junit5Test {
@RepeatedTest(3)
@Test
void test1() {
System.out.println(666);
}
}
3.4.8 断言机制
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。断言方法都是 org.junit.jupiter.api.Assertions 的静态方法:
静态方法 | 说明 |
---|---|
assertEquals / assertNotEquals | 判断两个对象或两个原始类型是否相等 / 不相等 |
assertSame / assertNotSame | 判断两个对象引用是否指向同一对象 / 不同对象 |
assertTrue / assertFalse | 判断给定的布尔值是否为 true / false |
assertNull / assertNotNull | 判断给定的对象引用是否为 null / 是否不为 null |
assertArrayEquals | 判断两个对象或原始类型的数组是否相等 |
assertAll | 组合断言,内部全部判断断言成功才成功 |
assertThrows() | 异常断言 |
assertTimeout() | 超时断言 |
@Test
@DisplayName("assertAll")
public void assertAllTest() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
ArithmeticException.class, () -> System.out.println(1 % 0));
}
@DisplayName("测试Junit5")
public class Junit5Test {
@Test
@DisplayName("超时测试")
public void timeoutTest() {
assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(1200));
}
}
3.4.9 前置条件
前置条件(assumptions)类似于断言,不同之处在于:不满足的断言会使得测试方法失败,而不满足的前置条件只会使测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("前置条件1")
public void assumeTest() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("前置条件2")
public void assumeThatTest() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
3.4.10 参数化测试
参数化测试 @ParameterizedTest 是 JUnit5 很重要的一个新特性,它利用注解指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
注解 | 说明 |
---|---|
@ValueSource | 为参数化测试指定入参来源,支持八大基础类以及 String 类型, Class 类型 |
@NullSource | 为参数化测试提供一个 null 的入参 |
@EnumSource | 为参数化测试提供一个枚举入参 |
@CsvFileSource | 读取指定 CSV 文件内容作为参数化测试入参 |
@MethodSource | 读取指定方法的返回值作为参数化测试入参,方法返回需要是一个流 |
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("@ValueSource测试")
public void parameterizedTest1(String string) {
System.out.println(string);
assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") // 指定读取的方法名
@DisplayName("@MethodSource测试")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
3.5 指标监控
未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot 就抽取了 Actuator 场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
在 pom.xml 中引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
最常用的是暴露所有监控信息为 HTTP,在 application.yml 中添加(为了安全起见,禁用所有的 Endpoint 然后手动开启指定的 Endpoint):
management:
endpoints:
enabled-by-default: false
web:
exposure:
include: '*'
endpoint:
health:
show-details: always # 总是显示详细信息。可显示每个模块的状态信息
enabled: true
loggers:
enabled: true
metrics:
enabled: true
访问 http://localhost:8080/actuator/,最常用的 Endpoint 有:
- health:监控状况,返回当前应用的一系列组件健康状况的集合,有任何一个应用是宕机状态,整个就是宕机状态
- metrics:运行时指标,这些信息可以被 pull(主动推送)或者 push(被动获取)方式得到
- loggers:日志记录
3.5.1 定制 health 信息
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Map<String,Object> map = new HashMap<>();
// if 条件中放业务逻辑代码
if(1 == 2){
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}
builder.withDetail("code",100)
.withDetails(map);
}
}
在 http://localhost:8080/actuator/health 下显示:
3.5.2 定制 info 信息
@Component
public class MyInfo implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example", Collections.singletonMap("key", "value"));
}
}
在 http://localhost:8080/actuator/info 下显示:
3.5.3 定制 metrics 信息
3.5.4 定制 EndPoint
package com.sunyu.admin.actuator.endpoint;
@Component
@Endpoint(id = "myService")
public class MyEndPoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
3.5.5 可视化界面
https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
3.6 Profile 功能
项目环境有测试环境、生产环境,SpringBoot 的 profile 功能可以适配多环境的切换,如
# application-test.yaml
server:
port: 8000
# application-prod.yaml
server:
port: 8001
默认配置文件 application.yaml 任何时候都会加载。我们可以指定 profile 配置文件 application-{env}.yaml,env = prod 是生产环境,env = test 是测试环境。如果配置文件中出现同名配置,指定的 profile 配置文件优先。如果我们想启动指定测试环境配置文件,只需在 配置文件 application.yaml 中添加:
spring:
profiles:
active: test
此时我们的项目服务端口变为 http://localhost:8000/。
我们将项目打包为 admin-0.0.1-SNAPSHOT.jar 后,还可以使用命令行修改配置文件的任意值(命令行优先于配置文件):
此时我们的项目环境为生产环境,服务端口变为 http://localhost:8002/。
除此之外,@Profile 标识的类或方法可以实现条件装配功能,例如 @Profile(“prod”) 标识的类或方法只在生产环境中生效, @Profile(“test”) 标识的类或方法只在测试环境中生效。
SpringMVC
MVC 是一种软件架构的思想,将软件按照模型、视图、控制器来划分:
- M(Model)模型层,指工程中的 JavaBean。 一类称为实体类 Bean,专门存储业务数据的,如 Student、User 等;另一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问
- V(View)视图层,指工程中的 html 或 jsp 等页面,作用是与用户进行交互,展示数据
- C(Controller)控制层,指工程中的 servlet ,作用是接收请求和响应浏览器
第 1 章 SpringMVC 的执行流程(⭐)
SpringMVC 常用组件:
- DispatcherServlet:前端控制器。 统一处理请求和响应,由它调用其它组件处理用户的请求
- HandlerMapping:处理器映射器。根据请求的 url、method 等信息查找控制器方法
- Handler:处理器(控制器方法)。对具体的用户请求进行处理
- HandlerAdapter:处理器适配器。执行处理器(控制器方法)
- ViewResolver:视图解析器。进行视图解析得到相应的视图,例如:ThymeleafView、InternalResourceView、 RedirectView
- View:视图
第 2 章 视图和拦截器
2.1 视图
SpringMVC 中的视图是 View 接口,视图的作用渲染数据,将模型 Model 中的数据展示给用户。SpringMVC 的视图默认有转发视图和重定向视图。
2.1.1 ThymeleafView
当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被 SpringMVC 配置文件中所配置的 Thymeleaf 视图解析器解析,得到的是 ThymeleafView 视图。
2.1.2 转发视图(⭐)
当控制器方法中所设置的视图名称以 “forward:” 为前缀时,创建转发视图,此时的视图名称会将前缀去掉,剩余部分作为最终路径通过转发的方式实现跳转。
2.1.3 重定向视图(⭐)
当控制器方法中所设置的视图名称以 “redirect:” 为前缀时,创建重定向视图,此时的视图名称会将前缀去掉,剩余部分作为最终路径通过重定向的方式实现跳转。
2.2 控制器拦截器
SpringMVC 的处理器拦截器类似于 Servlet 开发中的过滤器 Filter,用于对控制器进行预处理和后处理。
2.2.1 拦截器的三个抽象方法(⭐)
- preHandle:控制器方法执行之前执行 preHandle(),返回值为 boolean类型,返回 true 放行调用控制器方法;返回 false 拦截
- postHandle:控制器方法执行之后执行 postHandle(),可以在该方法下对 request 域中的模型和视图做进一步的修改
- afterComplation:视图渲染结束后执行,可以在该方法下实现一些资源清理、记录日志信息等工作
注意: 如果我们配置了多个拦截器。若每个拦截器的 preHandle() 都返回 true,此时多个拦截器的执行顺序和拦截器在 SpringMVC 配置文件的配置顺序有关: preHandle() 会按照配置的顺序执行,而 postHandle() 和 afterComplation() 会按照配置的反序执行。
第 3 章 映射请求(⭐)
@RequestMapping 注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。 SpringMVC 接收到指定的请求,就会寻找在映射关系中对应的控制器方法来处理这个请求。
3.1 @RequestMapping 注解
3.1.1 @RequestMapping 注解位置
- @RequestMapping 标识一个类:设置映射请求的请求路径的初始信息
- @RequestMapping 标识一个方法:设置映射请求请求路径的具体信息
比如以下请求的请求路径为 /test/testRequestMapping:
@Controller
@RequestMapping("/test")
public class RequestMappingController {
@RequestMapping("/testRequestMapping")
public String test(){
return "success";
}
}
3.1.2 @RequestMapping 注解属性(⭐)
- value(必须设置)
value 属性指定请求的 URL 地址,是字符串数组类型。
@RequestMapping (value = {"/test1", "/test2"})
注意: ?
表示任意的单个字符,*
表示任意的 0 个或多个字符, **
表示任意的一层或多层目录,注意在使用时**
前后不能有其他字符,只能使用 /**/xxx
的方式。
- method
method 属性指定请求类型,是数组类型,包含数组元素有:RequestMethod.GET、RequestMethod.POST、RequestMethod.PUT、RequestMethod.DELETE
@RequestMapping (value = "/test", method = {RequestMethod.GET, RequestMethod.POST})
对于处理指定请求方式的控制器方法,SpringMVC 中提供了 @RequestMapping 的派生注解:处理 get 请求的映射 --> @GetMapping;处理 post 请求的映射 --> @PostMapping;处理 put 请求的映射 --> @PutMapping;处理 delete 请求的映射 --> @DeleteMapping。
- params 属性
- params 属性表示必须同时满足表达式才能匹配,是字符串数组类型。有以下四种表达式设置:
-
- “param”:要求请求必须携带 param 请求参数
- “!param”:要求请求必须不能携带 param 请求参数
- “param=value”:要求请求必须携带 param 请求参数且 param = value
- “param!=value”:要求请求必须携带 param 请求参数但是 param != value
@RequestMapping (value = "/test",
method = {RequestMethod.GET, RequestMethod.POST},
params = {"username","password!=123"} ) // 请求路径必须包含 username,且 password 不能为 123
- headers 属性
- headers 属性通过请求头信息匹配请求映射,是字符串数组类型,有以下四种表达式设置:
-
- “header”:要求请求必须携带 header 请求头信息
- “!header”:要求请求必须不能携带 header 请求头信息
- “header=value”:要求请求必须携带 header 请求头信息且 header=value
- “header!=value”:要求请求必须携带 header 请求头信息且 header!=value
3.2 @PathVariable 注解
@PathVariable 注解将占位符传输的数据赋值给控制器方法的形参:
@Controller
public class RequestMappingController {
@RequestMapping(value = "/testRest/{id}/{username}")
public String test(@PathVariable("id") String id,
@PathVariable("username") String username) {
System.out.println(id + ":" + username);
return "target";
}
}
第 4 章 获取请求参数(⭐)
4.1 控制器方法形参获取请求参数
在控制器方法的形参位置,设置和请求参数同名形参:
<!-- test_param.html -->
<a th:href="@{/testParam(username='admin',password=123)}">测试</a>
@Controller
public class ParamController {
@RequestMapping("/testParam")
public String test(String username, String password) {
System.out.println("username:" + username + ",password:" + password);
return "target";
}
}
若请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数。
<!-- test_param.html -->
<form th:action="@{/testParam}" method="post">
用户名:<input type="text" name = "username"><br>
密码:<input type="password" name = "password"><br>
爱好:<input type="checkbox" name = "hobby" value="a">a
<input type="checkbox" name = "hobby" value="b">b
<input type="checkbox" name = "hobby" value="c">c<br>
<input type="submit" value="测试控制器形参获取请求参数">
@Controller
public class ParamController {
@RequestMapping("/testParam")
public String test(String username, String password, String[] hobby) {
System.out.println("username:" + username + ",password:" + password + ",hobby:" + Arrays.toString(hobby));
return "target";
}
}
4.2 @RequestParam 注解
若请求参数名与控制器方法的形参不同名,需要在控制器上加上 @RequestParam 注解。@RequestParam 注解将请求参数和控制器方法的形参创建映射关系。
请求参数名为 user_name:
<!-- test_param.html -->
<a th:href="@{/testParam(user_name='admin',password=123)}">测试</a>
控制器方法中的形参名为 username:
@Controller
public class ParamController {
@RequestMapping("/testParam")
public String test(@RequestParam("user_name") String username, String password) {
System.out.println("username:" + username + ",password:" + password);
return "target";
}
}
两种属性和用法与 @RequestParam 一样的注解 @RequestHeader 和 @CookieValue:
- @RequestHeader 将请求头信息和控制器方法形参创建映射关系
- @CookieValue 将 cookie 数据和控制器方法形参创建映射关系
4.3 通过 POJO 获取请求参数
在控制器方法的形参位置,设置一个实体类(POJO)类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值(数据库的增删改)。
创建实体类 User:
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String sex;
// 无参、全参构造器
// Getter、Setter 方法、toString 方法
}
请求参数:
<!-- test_param.html -->
<form th:action="@{/testPojo}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
性别:<input type="radio" name="sex" value="男">男
<input type="radio" name="sex" value="女">女<br>
年龄:<input type="text" name="age"><br>
<input type="submit" value="使用POJO接受请求参数">
</form>
控制器方法中的形参为实体类 User:
@Controller
public class ParamController {
@RequestMapping("/testPojo")
public String test(User user){
System.out.println(user);
return "target";
}
}
第 5 章 域对象共享数据
域对象主要用在 web 应用中,这个对象本身可以存储一定范围内的所有数据,通过它就能获取和存储数据。请求开始 request 域对象创建,对应响应结束 request 域对象销毁。只要调用它就可以对域内的数据进行增删改查。域对象通用方法有:
- getAttribute(String name) 获取对应的数据
- addAttribute(String name, Object object) 添加对应的数据
- removeAttribute(String name) 移除对应的数据
- setAttribute(String name, Object object) 设置数据
5.1 向 request 域对象共享数据
5.1.1 使用 ModelAndView(推荐)
@Controller
public class ScopeController {
@RequestMapping("/testModelAndView")
public ModelAndView test() {
ModelAndView mav = new ModelAndView();
mav.addObject("testScope", "hello,ModelAndView"); // 向请求域共享数据
mav.setViewName("success"); // 设置视图名称,实现页面跳转
return mav;
}
}
5.1.2 使用 Model 作为控制器形参
@Controller
public class ScopeController {
@RequestMapping("/testModel")
public String test(Model model){
model.addAttribute("testScope", "hello,Model");
return "success";
}
}
5.1.3 使用 Map 作为控制器形参
@Controller
public class ScopeController {
@RequestMapping("/testMap")
public String test(Map<String, Object> map){
map.put("testScope", "hello,Map");
return "success";
}
}
5.1.4 使用 ModelMap 作为控制器形参
@Controller
public class ScopeController {
@RequestMapping("/testModelMap")
public String test(ModelMap modelMap){
modelMap.addAttribute("testScope", "hello,ModelMap");
return "success";
}
}
5.2 向 session 域共享数据
@Controller
public class ScopeController {
@RequestMapping("/testSession")
public String test(HttpSession session){
session.setAttribute("testScope", "hello,session");
return "success";
}
}
5.3 向 application 域共享数据
@Controller
public class ScopeController {
@RequestMapping("/testApplication")
public String test(HttpSession session){
ServletContext application = session.getServletContext();
application.setAttribute("testScope", "hello,application");
return "success";
}
}
第 6 章 报文信息转换器
报文信息转换器 HttpMessageConverter 将请求报文转换为 Java 对象,或将 Java 对象转换为响应报文。
6.1 @RequestBody
@RequestBody 标识控制器方法的形参,使当前请求的请求体就会为该形参赋值。它与第 4 章中的@RequestParam() 类似,只不过 @RequestBody 接收的是请求体里面的数据;而 @RequestParam 接收的是请求参数。
<body>
<form th:action="@{/test}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit">
</form>
</body>
@Controller
public class HttpController {
@RequestMapping("/test")
public String test(RequestEntity<String> requestEntity) {
System.out.println("requestHeader:" + requestEntity.getHeaders());
System.out.println("requestBody:" + requestEntity.getBody());
return "success";
}
}
6.2 @ResponseBody / @RestController (⭐)
@RestController 注解是一个复合注解,标识在控制器类上,就相当于为类添加了 @Controller 注解,并且为类中的每个方法添加了 @ResponseBody 注解。@ResponseBody 标识的控制器方法,该方法的返回值直接作为响应报文的响应体响应到浏览器。@ResponseBody 常用于处理 json 和 ajax。比如我们创建实体类 User,将 User 对象直接作为控制器方法的返回值返回,就会自动转换为 json 格式的字符串:
@Controller
public class HttpController {
@RequestMapping("/test")
@ResponseBody
public User test(){
return new User(1001,"Tom","123",23,"man");
// 浏览器显示:{"id":1001,"username":"Tom","password":"123","age":23,"sex":"man"}
}
}
第 7 章 异常处理
7.1 @ControllerAdvice
类注解 @ControllerAdvice 常结合方法注解 @ExceptionHandler,用于捕获 Controller 中抛出的指定类型异常。
7.2 @ExceptionHandler
@ExceptionHandler 注解中可以添加异常类参数数组,表示需要捕获的异常,比如:
NullPointerException.class | 空指针异常 |
---|---|
ArithmeticException.class | 算数运算异常 |
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String test(Exception ex, Model model){
model.addAttribute("ex", ex);
return "error";
}
}
Mybatis
第 1 章 MyBatis 简介
MyBatis 封装了 JDBC,是一个半自动的 ORM(Object Relation Mapping)框架,支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。ORM 对象-关系映射如下:
第 2 章 ${} 和 #{}(⭐)
MyBatis 获取参数值的两种方式${}
和#{}
。${}
使用字符串拼接的方式拼接 sql,需要手动加单引号;#{}
使用占位符赋值的方式拼接 sql,可以自动添加单引号。
#{}
预编译的 sql 语句执行效率高,并且可以防止注入攻击,效率和安全性都优于${}
。
2.1 单个字面量类型参数
使用${}
和#{}
以任意名称(最好见名识意)获取参数的值。
<!-- User getUserByUsername(String username); -->
<select id="getUserByUsername" resultType="User">
select * from t_user where username = #{username}
</select>
2.2 使用 @Param 标识参数标注多个字面量类型的参数
通过 @Param 注解标识 mapper 接口中的方法参数,会将这些参数自动放在 map 集合中:
<!--User checkLoginByParam(@Param("username") String username, @Param("password") String password); -->
<select id="CheckLoginByParam" resultType="User">
select * from t_user where username = #{username} and password = #{password}
</select>
2.3 只能用 ${} 的情况
2.3.1 动态设置表名
<!-- List<User> getUserByTable(@Param("tableName") String tableName); -->
<select id="getUserByTable" resultType="User">
select * from ${tableName}
</select>
2.3.2 批量删除
<!--int deleteMore(@Param("ids") String ids); -->
<delete id="deleteMore">
delete from t_user where id in (${ids})
</delete>
2.4 insert 获取自增主键
在 mapper.xml 中设置两个属性:useGeneratedKeys 表示当前标签的 sql 是否使用了自增主键;keyProperty 表示自增主键对应的实体类属性:
<!-- void insertUser(User user); -->
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
insert into t_user values (null, #{username}, #{password}, #{age}, #{sex}, #{email})
</insert>
第 3 章 resultType 查询
resultType 是自动映射,用于属性名和表中字段名一致的情况。
3.1 查询单个实体类对象
<!-- User getUserById(@Param("id") int id); -->
<select id="getUserById" resultType="User">
select * from t_user where id = #{id}
</select>
3.2 查询实体类对象的 List 集合
<!-- List<User> getUserList(); -->
<select id="getUserList" resultType="User">
select * from t_user
</select>
3.3 查询单个数据
<!-- int getCount(); -->
<select id="getCount" resultType="Integer">
select count(id) from t_user
</select>
3.4 查询一条 map 集合类型数据
<!-- Map<String, Object> getUserToMap(@Param("id") int id); -->
<select id="getUserToMap" resultType="map">
select * from t_user where id = #{id}
</select>
3.5 查询多条 map 集合类型数据
<!-- Map<String, Object> getAllUserToMap(); -->
<select id="getAllUserToMap" resultType="map">
select * from t_user
</select>
3.6 模糊查询
<!--List<User> getUserByLike(@Param("username") String username);-->
<select id="getUserByLike" resultType="User">
select * from t_user where username like "%"#{mohu}"%"
</select>
第 4 章 resultMap 自定义映射
resultMap 是自定义映射,用于字段名和属性名不一致的情况。在 DataGrip 的 mybatis 数据库下创建表 t_emp 和 t_dept:
4.1 数据库字段和实体类属性名不一致
其中数据库的字段名 emp_name 对应实体类属性 empName,二者名字不一致:
<!-- id 属性表示自定义映射的唯一标识,type 属性表示映射的实体类 -->
<resultMap id="empResultMap" type="Emp">
<!-- property 实体类属性名与 column 表字段名建立映射关系 -->
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<result property="email" column="email"/>
</resultMap>
<!--List<Emp> getAllEmp(); -->
<select id="getAllEmp" resultMap="empResultMap">
select * from t_emp
</select>
4.2 多对一映射处理(分步查询)
需求:查询员工信息以及员工所对应的部门信息。
多个 Emp 对应一个 Dept,所以要在 Emp 中设置 Dept 的属性:
public class Emp {
// ...
private Dept dept;
// ...
}
4.2.1 EmpMapper 接口和 xml 文件
- 先在 EmpMapper 接口中设置查询员工信息的方法 Emp getEmp(@Param(“eid”) Integer eid);
- 编写 EmpMapper.xml:
<resultMap id="empAndDeptResultMap" type="Emp">
<id property="eid" column="eid"/>
<result property="empName" column="emp_name"/>
<result property="age" column="age"/>
<result property="sex" column="sex"/>
<result property="email" column="email"/>
<!-- select 设置分布查询 sql 的唯一标识(全类名.方法名),column 设置分步查询的字段 -->
<association property="dept" select="com.sunyu.mybatis.mapper.DeptMapper.getDept" column="did"/>
</resultMap>
<!-- Emp getEmp(@Param("eid") Integer eid); -->
<select id="getEmp" resultMap="empAndDeptResultMap">
select * from t_emp where eid = #{eid}
</select>
4.2.2 DeptMapper 接口和 xml 文件
- 在 DeptMapper 接口中设置查询部门信息的方法 Dept getDept(@Param(“did”) Integer did);
- 编写 DeptMapper.xml:
<resultMap id="EmpAndDeptResultMap" type="Dept">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
</resultMap>
<!-- Dept getDept(@Param("did") Integer did); -->
<select id="getDept" resultMap="EmpAndDeptResultMap">
select * from t_dept where did = #{did}
</select>
4.3 一对多映射处理(分步查询)
需求:根据部门 id 查询该部门下所有员工信息。
一个 Dept 对应多个 Emp,在 Dept 中设置 Emp 的集合:
public class Dept {
// ...
private List<Emp> emps;
// ...
}
4.3.1 DeptMapper 接口和 xml 文件
- 在 DeptMapper 中设置查询部门信息的方法 Dept getDept(@Param(“did”) Integer did);
- 编写 DeptMapper.xml:
<resultMap id="DeptAndEmpResultMap" type="Dept">
<id property="did" column="did"/>
<result property="deptName" column="dept_name"/>
<collection property="emps"
select="com.sunyu.mybatis.mapper.EmpMapper.getEmps" column="did"/>
</resultMap>
<!-- Dept getDept(@Param("did") Integer did); -->
<select id="getDept" resultMap="DeptAndEmpResultMap">
select * from t_dept where did = #{did}
</select>
4.3.2 EmpMapper 接口和 xml 文件
- 在 EmpMapper 中设置根据部门 id 查询部门中的所有员工的方法 List getEmps(@Param(“did”) Integer did);
- 编写 EmpMapper.xml:
<!-- List<Emp> getEmps(@Param("did") Integer did); -->
<select id="getEmps" resultType="Emp">
select * from t_emp where did = #{did}
</select>
第 5 章 动态 SQL
动态 SQL 技术是一种根据特定条件动态拼装 sql 语句的功能,为了解决 sql 语句拼接字符串时的痛点问题。
5.1 if 和 where 标签
if 标签可通过 test 属性(即传递过来的数据)的字符串表达式进行判断,若表达式的结果为 true,则标签中的内容会执行;反之标签中的内容不会执行。
where 标签和 if 标签一般结合使用:
- 若 where 标签中的 if 条件都不满足,则 where 标签没有任何功能
- 若 where 标签中的 if 条件满足,则 where 标签会自动添加 where 关键字,并且将最前方的 if 标签中多余的 and/or 去掉
<!-- List<Emp> getEmpByCondition(Emp emp); -->
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp
<where>
<if test="empName != null and empName !=''">
emp_name = #{empName}
</if>
<if test="age != null and age !=''">
and age = #{age}
</if>
<if test="sex != null and sex !=''">
and sex = #{sex}
</if>
<if test="email != null and email !=''">
and email = #{email}
</if>
</where>
</select>
5.2 trim 标签
常用属性:
- prefix / suffix:在 trim 标签中的内容的前面 / 后面添加某些内容
- prefixOverrides / suffixOverrides:在 trim 标签中的内容的前面 / 后面去掉某些内容
<!-- List<Emp> getEmpByCondition(Emp emp) -->
<select id="getEmpByCondition" resultType="Emp">
select * from t_emp
<trim prefix="where" suffixOverrides="and|or">
<if test="empName != null and empName !=''">
emp_name = #{empName} and
</if>
<if test="age != null and age !=''">
age = #{age} and
</if>
<if test="sex != null and sex !=''">
sex = #{sex} or
</if>
<if test="email != null and email !=''">
email = #{email}
</if>
</trim>
</select>
5.3 choose、when、otherwise 标签
choose、when、otherwise 相当于 if 、else if 、else,只会执行其中一个 when 标签。
<select id="getEmpByChoose" resultType="Emp">
select * from t_emp
<where>
<choose>
<when test="empName != null and empName != ''">
emp_name = #{empName}
</when>
<when test="age != null and age != ''">
age = #{age}
</when>
<when test="sex != null and sex != ''">
sex = #{sex}
</when>
<when test="email != null and email != ''">
email = #{email}
</when>
<otherwise>
did = 1
</otherwise>
</choose>
</where>
</select>
5.4 foreach 标签
5.4.1 批量删除
<!-- int deleteByArray(@Param("eids") Integer[] eids) -->
<!--
collection 设置要循环的数组或集合
item 表示集合或数组中的每一个数据
separator 设置循环体之间的分隔符
open 设置 foreach 标签中的内容的开始符
close 设置 foreach 标签中的内容的结束符
-->
<!-- delete from t_emp where eid in (6,7,8); -->
<delete id="deleteByArray">
delete from t_emp where eid in
<foreach collection="eids" item="eid" separator="," open="(" close=")">
#{eid}
</foreach>
</delete>
5.4.2 批量增加
<!-- int insertByList(@Param("emps") List<Emp> emps); -->
<insert id="insertByList">
insert into t_emp values
<foreach collection="emps" item="emp" separator=",">
(null, #{emp.empName}, #{emp.age}, #{emp.sex}, #{emp.email}, null)
</foreach>
</insert>
5.5 sql 和 include 标签
sql 标签声明的字段片为公共 sql 片段,在使用的地方通过 include 标签进行引入。
<sql id="empColumns">eid, emp_name, age, sex, email</sql>
<!-- List<Emp> getEmpByCondition(Emp emp); -->
<select id="getEmpByCondition" resultType="Emp">
select <include refid="empColumns"></include> from t_emp
</select>
第 6 章 MyBatis 的缓存(⭐)
6.1 一级缓存
一级缓存默认开启,是 SqlSession 级别的,通过同一个 SqlSession 查询的数据会被缓存,下次查询相同的数据就会从缓存中直接获取,不会从数据库重新访问。
一级缓存失效的三种情况:
- 不同的 SqlSession 对应不同的一级缓存
- 同一个 SqlSession 但是查询条件不同
- 同一个 SqlSession 两次查询期间更新了数据或手动清空了缓存
6.2 二级缓存
二级缓存是 SqlSessionFactory 级别,通过同一个 SqlSessionFactory 创建的 SqlSession 查询的结果会被缓存;再次执行相同的查询语句结果就会从缓存中获取
开启二级缓存的条件有:
- 在 Mapper.xml 映射文件中设置标签
- 二级缓存必须在 SqlSession 关闭或提交之后有效
- 查询数据所转换的实体类类型必须实现序列化的接口(Serializable 序列化接口)
6.3 MyBatis 缓存查询顺序
- 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
- 如果二级缓存没有命中,再查询一级缓存
- 如果一级缓存也没有命中,则查询数据库
- SqlSession 关闭之后,一级缓存中的数据会写入二级缓存
第 7 章 MyBatis 的逆向工程(MBG)
- 正向工程:先创建 Java 实体类,由框架负责根据实体类生成数据库表。
- 逆向工程:先创建数据库表,由框架负责根据数据库表,反向生成 Java 实体类、Mapper 接口、Mapper 映射文件
7.1 添加依赖和插件
创建 MyBatis-MBG 新模块,在 pom.xml 文件中添加依赖(dependency 中的 MySQL 与 plugin 插件中的 MySQL 驱动版本应该相同):
<packaging>jar</packaging>
<dependencies>
<!-- MyBatis 核心依赖包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version>
</dependency>
<!-- junit 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<!-- log4j 日志 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.0</version>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
7.2 创建核心配置文件
在 src/main/resources 下创建 mybatis-config.xml:
<?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>
<properties resource="jdbc.properties"/>
<typeAliases>
<package name="com.sunyu.mybatis.pojo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.sunyu.mybatis.mapper"/>
</mappers>
</configuration>
创建 jdbc.properties:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf8
jdbc.username=root
jdbc.password=123
7.3 创建逆向工程配置文件
在 src/main/resources 下创建逆向工程配置文件,文件名必须是 generatorConfig.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<!-- targetRuntime: 生成逆向工程的版本 -->
<context id="DB2Tables" targetRuntime="MyBatis3">
<!-- 数据库的连接信息 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis?characterEncoding=UTF-8"
userId="root"
password="zxcvbnm5237">
</jdbcConnection>
<!-- javaBean 的生成策略-->
<javaModelGenerator targetPackage="com.sunyu.mybatis.pojo" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<!-- SQL 映射文件的生成策略 -->
<sqlMapGenerator targetPackage="com.sunyu.mybatis.mapper"
targetProject=".\src\main\resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<!-- Mapper 接口的生成策略 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.sunyu.mybatis.mapper" targetProject=".\src\main\java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<!-- tableName 设置创建的数据库表名 -->
<!-- domainObjectName 生成的实体类类名 -->
<table tableName="t_emp" domainObjectName="Emp"/>
<table tableName="t_dept" domainObjectName="Dept"/>
</context>
</generatorConfiguration>
7.4 执行 MBG 插件的 generate 目标
执行结果如下:
7.5 QBC 查询(⭐)
QBC(Query By Criteria)无需写 sql 语句,是一种面向对象的查询方式。它主要由 Criterion 接口和 Expression 类组成。
7.5.1 查询
public class MGBTest {
@Test
public void testMBG(){
SqlSession sqlSession = SqlSessionUtil.getSqlSession();
EmpMapper mapper = sqlSession.getMapper(EmpMapper.class);
// 查询所有数据
List<Emp> empList = mapper.selectByExample(null);
empList.forEach(System.out::println);
/*
* 条件查询:查询名字为张三,且年龄大于 10,或 did=3 的员工集合
*/
EmpExample example = new EmpExample();
// 且逻辑用链式查询
example.createCriteria().andEmpNameEqualTo("张三").andAgeGreaterThan(10);
// 或逻辑用 example.or()
example.or().andDidEqualTo(3);
List<Emp> emps = mapper.selectByExample(example);
emps.forEach(System.out::println);
}
}
7.5.2 添加和修改
- insert / updateByPrimaryKey():通过主键进行数据添加 / 修改,如果某一个值为 null,也会将对应的字段改为 null
- insertSelective() / updateByPrimaryKeySelective():通过主键进行选择性数据添加 / 修改,如果某个值为 null,则不修改这个字段