Spring基础
概念
架构图
(1)核心层
- Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块
(2)AOP层
- AOP:面向切面编程,它依赖核心层容器,目的是==在不改变原有代码的前提下对其进行功能增强==
- Aspects:AOP是思想,Aspects是对AOP思想的具体实现
(3)数据层
- Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术
- Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis
- Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现,也是后期学习的重点内容
(4)Web层
- 这一层的内容将在SpringMVC框架具体学习
(5)Test层
- Spring主要整合了Junit来完成单元测试和集成测试
IoC:反转控制(Inverse Of Control)
业务层实现
|
|
数据层实现
|
|
|
|
解决方案
使用对象时,在程序中==不要主动使用new产生对象==,转换为由==外部==提供对象
IOC
(1)什么是控制反转?
- 使用对象时,由主动new产生对象转换为由==外部==提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。
- 业务层要用数据层的类对象,以前是自己
new
的 - 现在自己不new了,交给
别人[外部]
来创建对象 别人[外部]
就反转控制了数据层对象的创建权- 这种思想就是控制反转
- 业务层要用数据层的类对象,以前是自己
(2)Spring和IOC之间的关系是什么?
- Spring技术对IOC思想进行了实现
- Spring提供了一个容器,称为==IOC容器==,用来充当IOC思想中的"外部"
- IOC思想中的
别人[外部]
指的就是Spring的IOC容器
(3)IOC容器的作用以及内部存放的是什么?
- IOC容器负责对象的创建、初始化等一系列工作,其中包含了数据层和业务层的类对象
- 被创建或被管理的对象在IOC容器中统称为==Bean==
- IOC容器中放的就是一个个的Bean对象
(4)当IOC容器中创建好service和dao对象后,程序能正确执行么?
- 不行,因为service运行需要依赖dao对象
- IOC容器中虽然有service和dao对象
- 但是service对象和dao对象没有任何关系
- 需要把dao对象交给service,也就是说要绑定service和dao对象之间的关系
像这种在容器中建立对象与对象之间的绑定关系就要用到DI
DI:依赖注入(Dependency Injection)
业务层实现
依赖dao对象运行
|
|
数据层实现
|
|
(1)什么是依赖注入呢?
- 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入
- 业务层要用数据层的类对象,以前是自己
new
的 - 现在自己不new了,靠
别人[外部其实指的就是IOC容器]
来给注入进来 - 这种思想就是依赖注入
- 业务层要用数据层的类对象,以前是自己
(2)IOC容器中哪些bean之间要建立依赖关系呢?
- 这个需要程序员根据业务需求提前建立好关系,如业务层需要依赖数据层,service就要和dao建立依赖关系
这两个概念的最终目标就是:==充分解耦==,具体实现靠:
- 使用IOC容器管理bean(IOC)
- 在IOC容器内将有依赖关系的bean进行关系绑定(DI)
- 最终结果为:使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系
入门案例
IOC
思想
(1)Spring是使用容器来管理bean对象的,管理什么?
- 主要管理项目中所使用到的类对象,比如(Service和Dao)
(2)如何将被管理的对象告知IOC容器?
- 使用配置文件
(3)被管理的对象交给IOC容器,要想从容器中获取对象,就先得思考如何获取到IOC容器?
- Spring框架提供相应的接口
(4)IOC容器得到后,如何从容器中获取bean?
- 调用Spring框架提供对应接口中的方法
(5)使用Spring导入哪些坐标?
- 用别人的东西,就需要在pom.xml添加对应的依赖
实现
需求分析:将BookServiceImpl和BookDaoImpl交给Spring管理,并从容器中获取对应的bean对象进行方法调用。
1.创建Maven的java项目
2.pom.xml添加Spring的依赖jar包
3.创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类
4.resources下添加spring配置文件,并完成bean的配置
5.使用Spring提供的接口完成IOC容器的创建
6.从容器中获取对象进行方法调用
步骤1:创建Maven项目
步骤2:添加Spring的依赖jar包
pom.xml
|
|
步骤3:添加案例中需要的类
创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类
|
|
步骤4:添加spring配置文件
resources下添加spring配置文件applicationContext.xml,并完成bean的配置
步骤5:在配置文件中完成bean的配置
|
|
==注意事项:bean定义时id属性在同一个上下文中(配置文件)不能重复==
步骤6:获取IOC容器
使用Spring提供的接口完成IOC容器的创建,创建App类,编写main方法
|
|
步骤7:从容器中获取对象进行方法调用
|
|
步骤8:运行程序
测试结果为:
Spring的IOC入门案例已经完成,但是在 BookServiceImpl
的类中依然存在 BookDaoImpl
对象的new操作,它们之间的耦合度还是比较高,解决就需要用到下面的 DI:依赖注入
。
DI:Dependency Injection:依赖注入
思路
(1)要想实现依赖注入,必须要基于IOC管理Bean
- DI的入门案例要依赖于前面IOC的入门案例
(2)Service中使用new形式创建的Dao对象是否保留?
- 需要删除掉,最终要使用IOC容器中的bean对象
(3)Service中需要的Dao对象如何进入到Service中?
- 在Service中提供方法,让Spring的IOC容器可以通过该方法传入bean对象
(4)Service与Dao间的关系如何描述?
- 使用配置文件
实现
需求:基于IOC入门案例,在BookServiceImpl类中删除new对象的方式,使用Spring的DI完成Dao层的注入
1.删除业务层中使用new的方式创建的dao对象
2.在业务层提供BookDao的setter方法
3.在配置文件中添加依赖注入的配置
4.运行程序调用方法
步骤1: 去除代码中的new
在BookServiceImpl类中,删除业务层中使用new的方式创建的dao对象
|
|
步骤2:为属性提供setter方法
在BookServiceImpl类中,为BookDao提供setter方法
|
|
步骤3:修改配置完成注入
在配置文件中添加依赖注入的配置
|
|
==注意:配置中的两个bookDao的含义是不一样的==
- name=“bookDao"中
bookDao
的作用是让Spring的IOC容器在获取到名称后,将首字母大写,前面加set找对应的setBookDao()
方法进行对象注入 - ref=“bookDao"中
bookDao
的作用是让Spring能在IOC容器中找到id为bookDao
的Bean对象给bookService
进行注入 - 综上所述,对应关系如下:
步骤4:运行程序
运行,测试结果为:
Bean
基础配置
-
用name=”” 起别名,可以多个,用",“分割
-
Bean的作用范围:Spring默认创建单例。创建非单例需要在配置中加入scope=“prototype”。默认singleton
-
适合交给容器进行管理的bean
- 表现层对象
- 业务层对象
- 数据层对象
- 工具对象
-
不适合交给容器进行管理的bean
- 封装实体的域对象
实例化
构造方法
显示提供或不提供无参的构造方法
|
|
静态工厂
配置工厂类和工厂类中的哪个方法
|
|
实例工厂
需要创建工厂的对象
配置:先配置工厂对象的bean 配置factory-method factory-bean
|
|
改进:使用FactoryBean实例化Bean
|
|
|
|
Bean的生命周期
配置:init-method,destory-method
- ctx.registerShutdownHook(); 注册关闭🪝,可以使虚拟机关闭前先关容器
- ctx.close(); 暴力关闭
也可以通过使用接口来实现:
- InitializingBean
- DisposableBean
依赖注入
Setter注入
- 引用类型
- 简单类型
|
|
|
|
构造器注入
- 引用类型
- 简单类型
自动装配
-
Ioc容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配
-
方式:
- 按类型(常用)
1 2
<bean id="bookDao" class="dao.impl.BookDaoImpl"/> <bean id="bookService" class="service.impl.BookServiceImpl" autowire="byType"/>
- 按名称
1 2
<bean id="bookDao" class="dao.impl.BookDaoImpl"/> <bean id="bookService" class="service.impl.BookServiceImpl" autowire="byName"/>
- 按构造方法
- 不启用自动装配
-
自动装配用于引用类型依赖注入,不能对简单类型进行操作
-
使用按类型装配时(
byType
)必须保障容器中相同类型的bean唯一,推荐使用 -
使用按名称装配时(
byName
)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用 -
自动装配优先级低于
setter
注入与构造器
注入,同时出现时自动装配配置失效
集合注入
|
|
管理第三方程序
加载properties
两个bookDao的含义
|
|
- name=“bookDao"中bookDao的作用是让Spring的IOC容器在获取到名称后,将首字母大写,前面加set找对应的setBookDao()方法进行对象注入
- ref=“bookDao"中bookDao的作用是让Spring能在IOC容器中找到id为bookDao的Bean对象给bookService进行注入
|
|
容器
创建容器
|
|
|
|
|
|
获取bean
|
|
注解开发
注解开发定义bean
|
|
|
|
- Spring提供@Component注解的三个衍生注解
- @Controller:用于表现层bean定义
- @Service:用于业务层bean定义
- @Repository:用于数据层bean定义
纯注解开发模型
- 使用java类替代配置文件
|
|
- @Configuration注解用于设定当前类为配置类
- @ComponentScan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式
|
|
加载方式需要变换:
|
|
注解开发bean的作用范围和生命周期
|
|
注解开发依赖注入
- 只有自动装配
- 用的是暴力反射
|
|
ℹ️@Qualifier
用于标记特定的 Bean
当有多个 Bean 类型相同时,@Qualifier
可以帮助 Spring 框架选择所需的 Bean。它通常与 @Autowired
注解一起使用,以精确指定要注入的 Bean。
|
|
|
|
@Qualifier("dog")
明确指定了要注入的 Bean 是 Dog
,而不是 Cat
。
|
|
- 加载properties文件
|
|
- 第三方bean管理
|
|
- 第三方bean依赖注入
TODO🧾 整合Mybatis
TODO🧾 整合JUnit
AOP 面向切面编程
- 作用:在不惊动原始设计的基础上为其进行功能增强
- Spring理念:无入侵式/无侵入式
- 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 在SpringAOP中,理解为方法的执行
- 切入点(PointCut):匹配连接点的式子
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 一个具体方法:
com.itheima.dao
包下的BookDao
接口中的无形参无返回值的save
方法 - 匹配多个方法:所有的
save
方法,所有的get
开头的方法,所有以Dao
结尾的接口中的任意方法,所有带有一个参数的方法
- 一个具体方法:
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 通知(Advice):在切入点处执行的操作,也就是共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
- 通知类:定义通知的类
- 切面(Aspect):描述通知与切入点的对应关系
案例
实现步骤
-
步骤一:
添加spring依赖:1 2 3 4 5 6 7 8 9 10
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version>🚩不支持JDK17,支持的需要6.1.2?6.2.x </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency>
-
因为
spring-context
中已经导入了spring-aop
,所以不需要再单独导入spring-aop
导入AspectJ的jar包,AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以直接采用Spring整合ApsectJ
的方式进行AOP开发。 -
步骤二:
定义接口和实现类 准备环境的时候已经完成了 -
步骤三:
定义通知类和通知 通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。 类名和方法名没有要求,可以任意。
|
|
步骤四:
定义切入点 BookDaoImpl中有两个方法,分别是update()和save(),要增强的是update方法,那么该如何定义呢?
|
|
说明:
- 切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑。
- execution及后面编写的内容,之后会专门去学习。
步骤五:
制作切面 切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?
|
|
绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置
说明:@Before
翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型
这里就会在执行update()之前,来执行method(),输出当前时间的毫秒值
步骤六:
将通知类配给容器并标识其为切面类
|
|
步骤七:
开启注解格式AOP功能 在Spring配置类上,使用@EnableAspectJAutoProxy
注解
|
|
步骤八:
运行程序 这次再来调用update()
|
|
AOP工作流程
- 配的切入点只有使用了才会读取
- 切入点匹配上会创建代理对象
AOP切入点表达式
- 切入点:要进行增强的方法
- 切入点表达式:要进行增强的方法的描述方式
按接口描述或按实现类描述都可以
标准格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
eg:execution(public User service.UserService.findById(int))
- execution:动作关键字,描述切入点的行为动作,例如execution表示执行到指定切入点
- public:访问修饰符,还可以是public,private等,可以省略
- User:返回值,写返回值类型
- service:包名,多级包使用点连接
- UserService:类/接口名称
- findById:方法名
- int:参数,直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
通配符
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
书写技巧
对于切入点表达式的编写其实是很灵活的,那么在编写的时候,有没有什么好的技巧让用用:
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通常
描述接口
,而不描述实现类,如果描述到实现类,就出现紧耦合了 - 访问控制修饰符针对接口开发均采用public描述(
可省略访问控制修饰符描述
) - 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用
*
通配快速描述 包名
书写尽量不使用..
匹配,效率过低,常用*
做单个包描述匹配,或精准匹配接口名/类名
书写名称与模块相关的采用*
匹配,例如UserService书写成*Service
,绑定业务层接口名- 方法名书写以
动词
进行精准匹配
,名词采用*
匹配,例如getById
书写成getBy*
,selectAll
书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
- 通常
不使用异常
作为匹配
规则
AOP通知类型
AOP通知共分为5种类型:
前置通知
后置通知
环绕通知(重点)
返回后通知(了解)
抛出异常后通知(了解)
环绕通知注意事项
- 环绕通知必须依赖形参
ProceedingJoinPoint
才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知 - 通知中如果未使用
ProceedingJoinPoint
对原始方法进行调用将跳过原始方法的执行 - 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,最好设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须要处理Throwable异常
TODO🧾 万次执行效率案例
TODO🧾 AOP通知获取数据
案例:百度网盘密码兼容处理
APP:
|
|
ResourceDao:
|
|
ResourceDaoImpl:
|
|
ResourceService:
|
|
ResourceServiceImpl:
|
|
SpringConfig:
|
|
-
开启SpringAOP的注解
SpringConfig
|
|
-
编写通知类
MyAdvice
|
|
切面的优先级
给切面类增加 @Order
注解,并指定具体的数字,值越小优先级越高
|
|
Spring事务
转账案例
将事务开在业务层
-
开事务:在方法上加注解:🚩
@Transactional
(一般写在接口上) -
配事务管理器
1 2 3 4 5 6
@Bean public PlatformTransactionManager platformTransactionManager(DataSource dataSource){ DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; }
DataSourceTransactionManager
主要用于 JDBC 数据源的事务管理。 -
开启事务注解🚩
@EnableTransactionManagement
1 2 3 4 5 6 7
@Configuration @ComponentScan("yhzz") @PropertySource("jdbc.properties") @EnableTransactionManagement @Import({JdbcConfig.class, MyBatisConfig.class}) public class SpringConfig { }
使用
|
|
|
|
如果有多个事务管理器
可以使用 @Primary
注解来标记主要的事务管理器,或者在 @Transactional
注解中指定特定的事务管理器
|
|
|
|
事务角色
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
- 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
事务属性
事务配置
属性 | 作用 | 示例 |
---|---|---|
readOnly | 设置是否为只读事务 | readOnly = true 只读事务 |
timeout | 设置事务超时时间 | timeout = -1(永不超时) |
rollbackFor | 设置事务回滚异常(class) | rollbackFor{NullPointException.class} |
rollbackForClassName | 设置事务回滚异常(String) | 同上格式为字符串 |
noRollbackFor | 设置事务不回滚异常(class) | noRollbackFor{NullPointExceptior.class} |
noRollbackForClassName | 设置事务不回滚异常(String) | 同上格式为字符串 |
isolation | 设置事务隔离级别 | isolation = Isolation. DEFAULT |
propagation | 设置事务传播行为 | … |
- 属性在**
@Transactional
**注解的参数上进行设置
可能存在的问题:
- 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
事务传播行为的可选值
传播属性 | 事务管理员 | 事务协调员 |
---|---|---|
REQUIRED(默认) | 开启T | 加入T |
无 | 新建T2 | |
REQUIRES_NEW | 开启T | 新建T2 |
无 | 新建T2 | |
SUPPORTS | 开启T | 加入T |
无 | 无 | |
NOT_SUPPORTED | 开启T | 无 |
无 | 无 | |
MANDTORY | 开启T | 加入T |
无 | ERROR | |
NEVER | 开启T | ERROR |
无 | 无 | |
NESTED | 设置savePoint,一旦事务回滚,事务将回滚到savePoint处,交由客户响应提交/回滚 |
- 修改logService改变事务的传播行为
|
|
SpringMVC
概述
- 如果所有的处理都交给Servlet来处理的话,所有的东西都耦合在一起,对后期的维护和扩展极其不利
- 所以将后端服务器Servlet拆分成三层,分别是web、service和dao
web
层主要由servlet
来处理,负责页面请求和数据的收集以及响应结果给前端service
层主要负责业务逻辑的处理dao
层主要负责数据的增删改查操作
- 但
servlet
处理请求和数据时,存在一个问题:一个servlet
只能处理一个请求 - 针对web层进行优化,采用MVC设计模式,将其设计为Controller、View和Model
controller
负责请求和数据的接收,接收后将其转发给service
进行业务处理service
根据需要会调用dao
对数据进行增删改查dao
把数据处理完后,将结果交给service
,service
再交给controller
controller
根据需求组装成Model
和View
,Model
和View
组合起来生成页面,转发给前端浏览器- 这样做的好处就是
controller
可以处理多个请求,并对请求进行分发,执行不同的业务操作
随着互联网的发展,上面的模式因为是同步调用,性能慢慢的跟不是需求,所以异步调用慢慢的走到了前台,是现在比较流行的一种处理方式。
- 因为是异步调用,所以后端不需要返回View视图,将其去除
- 前端如果通过异步调用的方式进行交互,后端就需要将返回的数据转换成JSON格式进行返回
- SpringMVC主要负责的就是
- controller如何接收请求和数据
- 如何将请求和数据转发给业务层
- 如何将响应数据转换成JSON发回到前端
- SpringMVC是一种基于Java实现MVC模型的轻量级Web框架
- 优点
- 使用简单、开发快捷(相比较于Servlet)
- 灵活性强
- 优点
ℹ️为什么一个servlet只能同时处理一个请求,而使用MVC之后的controller可以处理多个请求
这个问题涉及到 Servlet 的工作机制和 Spring MVC 中 Controller 的工作机制的差异。理解这两者的区别可以帮助更好地理解为什么一个 Servlet 通常只能处理一个请求,而 Spring MVC 的 Controller 可以处理多个请求。
Servlet 工作机制
Servlet 是 Java EE 中的服务器端组件,用于处理客户端请求并生成动态响应。Servlet 通过 Servlet 容器(如 Tomcat)进行管理。以下是 Servlet 的工作机制:
- 单线程模型(已经过时,不常用):在早期的 Servlet API 中,有一种单线程模型(通过实现
SingleThreadModel
接口),在这种模型下,每个请求都由一个独立的 Servlet 实例来处理,避免了线程安全问题。但是这种方式资源消耗大,不推荐使用。 - 多线程模型:大多数 Servlet 容器采用的是多线程模型,在这种模型下,同一个 Servlet 实例可以被多个线程同时访问。每个请求都会有一个独立的线程来处理,但这些线程共享同一个 Servlet 实例,因此需要注意线程安全问题。
- 线程安全问题:如果 Servlet 中有共享的实例变量(成员变量),多个线程同时访问这些变量可能会导致数据不一致和其他线程安全问题。因此,开发者需要确保 Servlet 是线程安全的。
Spring MVC 中的 Controller 工作机制
Spring MVC 是一个基于 MVC(Model-View-Controller)设计模式的框架,用于构建 Web 应用程序。Spring MVC 中的 Controller 也由 Servlet 容器管理,但它的工作机制和传统的 Servlet 有一些不同:
- DispatcherServlet:Spring MVC 使用一个特殊的 Servlet,称为
DispatcherServlet
,作为前端控制器(Front Controller)。所有的请求都会先经过DispatcherServlet
,然后由它分发给具体的处理器(Controller)。 - Controller 是普通的 Java 对象:Spring MVC 中的 Controller 只是一些标注了
@Controller
或@RestController
注解的普通 Java 类(POJO)。这些 Controller 由 Spring 的 IoC 容器管理,并且通常是单例的。 - 处理多个请求:由于
DispatcherServlet
使用多线程模型,每个请求都会由一个独立的线程来处理。Spring MVC 通过DispatcherServlet
分发请求到对应的 Controller 方法上,这些方法通常是无状态的(不存储任何成员变量),因此可以同时处理多个请求,而不会出现线程安全问题。
为什么 Controller 可以处理多个请求
- 无状态的处理方法:Spring MVC 的 Controller 方法通常是无状态的,它们不依赖于共享的实例变量,因此不同线程可以安全地同时调用这些方法。
- Spring 的并发支持:Spring 框架本身对并发和线程安全有很好的支持。Controller 方法通常是无状态的,Spring 会通过依赖注入来管理任何需要的状态或服务,从而确保线程安全。
- Servlet 容器的线程模型:Spring MVC 的 Controller 是运行在 Servlet 容器的多线程环境中,这意味着每个请求都会由一个独立的线程处理。由于 Controller 方法本身是线程安全的,它们可以同时处理多个请求。
Servlet配置流程
- 创建web工程(Maven结构)
- 设置tomcat服务器,加载web工程(tomcat插件)
- 导入坐标(Servlet)
- 定义处理请求的功能类(UserServlet)
- 设置请求映射(配置映射关系)
SpringMVC配置流程
- 创建web工程(Maven结构)
- 设置tomcat服务器,加载web工程(tomcat插件)
- 导入坐标(SpringMVC+Servlet)
- 定义处理请求的功能类(UserController)
- 设置请求映射(配置映射关系)
- 将SpringMVC设定加载到Tomcat容器中
- SpringMVC+Servlet坐标
|
|
- 创建SpringMVC控制器类
|
|
- 初始化SpringMVC环境(同Spring环境),设定SpringMVC加载对应的Bean
|
|
- 初始化Servlet容器,加载SpringMVC环境,并设置SpringMVC技术处理的请求
|
|
工作流程解析
- 服务器启动,执行ServletContainerInitConfig类,初始化web容器
- 执行createServletApplicationContext方法,创建了WebApplicationContext对象
- 加载SpringMvcConfig配置类
- 执行
@ComponentScan
加载对应的bean - 加载
UserController
,每个@RequestMapping
的名称对应一个具体的方法 - 执行
getServletMappings
方法,设定SpringMVC拦截请求的路径规则
单次请求过程
- 发送请求
http://localhost:8080/save
- web容器发现该请求满足SpringMVC拦截规则,将请求交给SpringMVC处理
- 解析请求路径/save
- 由/save匹配执行对应的方法save()上面的第5步已经将请求路径和方法建立了对应关系,通过
/save
就能找到对应的save()
方法 - 执行
save()
- 检测到有
@ResponseBody
直接将save()
方法的返回值作为响应体返回给请求方
Bean加载控制
项目结构
com.mtmn
下有 config
、controller
、service
、dao
这四个包
-
config
目录存入的是配置类,写过的配置类有:- ServletContainersInitConfig
- SpringConfig
- SpringMvcConfig
- JdbcConfig
- MybatisConfig
-
controller
目录存放的是SpringMVC
的controller
类 -
service
目录存放的是service
接口和实现类 -
dao
目录存放的是dao/Mapper
接口
controller、service和dao这些类都需要被容器管理成bean对象,那么到底是该让 SpringMVC
加载还是让 Spring
加载呢?
-
SpringMVC控制的bean
- 表现层bean,也就是
controller
包下的类
- 表现层bean,也就是
-
Spring控制的bean
- 业务bean(
Service
) - 功能bean(
DataSource
,SqlSessionFactoryBean
,MapperScannerConfigurer
等)
- 业务bean(
分析清楚谁该管哪些bean以后,接下来要解决的问题是如何让 Spring
和 SpringMVC
分开加载各自的内容。
解决方案:
- 加载Spring控制的bean的时候,
排除掉
SpringMVC控制的bean
具体实现:
- 方式一:Spring加载的bean设定扫描范围
com.mtmn
,排除掉controller
包内的bean
|
|
🚩🚩🚩SpringConfig里的@Configuration也要注释掉
-
出现问题的原因是
- Spring配置类扫描的包是
com.mtmn
- SpringMVC的配置类,
SpringMvcConfig
上有一个@Configuration
注解,也会被Spring扫描到 - SpringMvcConfig上又有一个
@ComponentScan
,把controller类又给扫描进来了 - 所以如果不把
@ComponentScan
注释掉,Spring配置类将Controller排除,但是因为扫描到SpringMVC的配置类,又将其加载回来,演示的效果就出不来 - 解决方案,也简单,把SpringMVC的配置类移出Spring配置类的扫描范围即可。
- Spring配置类扫描的包是
-
方式二:Spring加载的bean设定扫描范围为精确扫描,具体到
service
包,dao
包等
|
|
- 方式三:不区分Spring与SpringMVC的环境,加载到同一个环境中(
了解即可
) - 有了Spring的配置类,要想在tomcat服务器启动将其加载,需要修改ServletContainersInitConfig
|
|
🚩🚩🚩更方便的配置
|
|
请求
设置映射路径
-
导入坐标
1 2 3 4 5 6 7 8 9 10 11 12 13
<!--servlet--> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!--springmvc--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency>
-
问题:
UserController
有一个save方法,访问路径为 http://localhost/save
BookController
也有一个save方法,访问路径为 http://localhost/save
当访问 http://localhost/save
的时候,到底是访问 UserController
还是 BookController
?
法一:修改路径名 @RequestMapping("/user/save”) 缺点:耦合度高
|
|
法二:类和方法都加
|
|
- 当类上和方法上都添加了
@RequestMapping
注解,前端发送请求的时候,要和两个注解的value值相加匹配才能访问到。 @RequestMapping
注解value属性前面加不加/
都可以
请求参数
请求路径设置好后,只要确保页面发送请求地址和后台Controller类中配置的路径一致,就可以接收到前端的请求
-
GET:http://localhost:7777/user/commonParam?name=test&age=18
-
POST:
-
解决Post中文乱码:配置过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
🚩import org.springframework.web.filter.CharacterEncodingFilter; public class ServletContainersInitConfig extends AbstractAnnotationConfigDispatcherServletInitializer { protected Class<?>[] getRootConfigClasses() { return new Class[0]; } protected Class<?>[] getServletConfigClasses() { return new Class[]{SpringMvcConfig.class}; } protected String[] getServletMappings() { return new String[]{"/"}; } //处理乱码问题 🚩@Override protected Filter[] getServletFilters() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("utf-8"); return new Filter[]{filter}; } }
五种类型参数传递
普通
|
|
|
|
- 如果形参与地址参数名不一致,例如地址参数名为
username
,而形参变量名为name
,因为前端给的是username
,后台接收使用的是name
,两个名称对不上,会导致接收数据失败
http://localhost:7777/user/commonParam?username=Helsing&age=1024
解决方案:使用 @RequestParam
注解
|
|
POJO
- POJO参数:请求参数名与形参对象属性名相同,定义POJO类型形参即可接收参数
- http://localhost:7777/user/pojoParam?username=Helsing&age=1024
|
|
- POJO参数接收,前端GET和POST发送请求数据的方式不变。
- 请求参数key的名称要和POJO中属性的名称一致,否则无法封装。
嵌套POJO
http://localhost:7777/user/pojoParam?name=Helsing&age=1024&address.province=Beijing&address.city=Beijing
UserAddress:
|
|
|
|
POJO参数传递user –> User{name=‘Helsing’, age=1024, address=Address{province=‘Beijing’, city=‘Beijing’}}
数组
- 数组参数:请求参数名与形参对象属性名相同且请求参数为多个,定义数组类型即可接收参数
- http://localhost:7777/user/arrayParam?hobbies=sing&hobbies=jump&hobbies=rap&hobbies=basketball
|
|
- 数组参数传递user –> [sing, jump, rap, basketball]
集合
http://localhost:7777/user/listParam?hobbies=sing&hobbies=jump&hobbies=rap&hobbies=basketball
❎错误方式:
|
|
-
运行程序,报错
1
java.lang.IllegalArgumentException: Cannot generate variable name for non-typed Collection parameter type
- 错误原因:SpringMVC将List看做是一个POJO对象来处理,将其创建一个对象并准备把前端的数据封装到对象中,但是List是一个接口无法创建对象,所以报错。
✅正确方式:
- 使用
@RequestParam
注解
|
|
知识点:@RequestParam
名称 | @RequestParam |
---|---|
类型 | 形参注解 |
位置 | SpringMVC控制器方法形参定义前面 |
作用 | 绑定请求参数与处理器方法形参间的关系 |
相关参数 | required:是否为必传参数 defaultValue:参数默认值 |
JSON数据传输参数
常见的三种JSON数据类型:
-
json普通数组([“value1”,“value2”,“value3”,…])
-
json对象({key1:value1,key2:value2,…})
-
json对象数组([{key1:value1,…},{key2:value2,…}])
-
导入坐标
1 2 3 4 5
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency>
-
开启SpringMVC注解支持
使用
@EnableWebMvc
,在SpringMVC的配置类中开启SpringMVC的注解支持,这里面就包含了将JSON转换成对象的功能。
Json普通数组
-
controller中添加
1 2 3 4 5 6
@RequestMapping("/jsonArrayParam") @ResponseBody public String jsonArrayParam(@RequestBody List<String> hobbies) { System.out.println("JSON数组参数传递hobbies --> " + hobbies); return "{'module':'json array param'}"; }
JSON数组参数传递hobbies –> [chang, tiao, rap, lanqiu]
Json对象
|
|
http://localhost:7777/user/jsonPojoParam
JSON对象 –> User{name=‘菲茨罗伊’, age=27, address=Address{province=‘外域’, city=‘萨尔沃’}}
Json对象数组
|
|
http://localhost:7777/user/jsonPojoListParam
JSON对象 –> [User{name=‘菲茨罗伊’, age=27, address=Address{province=‘外域’, city=‘萨尔沃’}}, User{name=‘罗伊’, age=7, address=Address{province=‘域’, city=‘尔沃’}}]
小结
SpringMVC接收JSON数据的实现步骤为:
- 导入jackson包
- 开启SpringMVC注解驱动,在配置类上添加
@EnableWebMvc
注解 - 使用PostMan发送JSON数据
- Controller方法的参数前添加
@RequestBody
注解
知识点1:@EnableWebMvc
名称 | @EnableWebMvc |
---|---|
类型 | 配置类注解 |
位置 | SpringMVC配置类定义上方 |
作用 | 开启SpringMVC多项辅助功能 |
知识点2:@RequestBody
名称 | @RequestBody |
---|---|
类型 | 形参注解 |
位置 | SpringMVC控制器方法形参定义前面 |
作用 | 将请求中请求体所包含的数据传递给请求参数,此注解一个处理器方法只能使用一次 |
@RequestBody
与 @RequestParam
区别
- 区别
@RequestParam
用于接收url地址传参,表单传参【application/x-www-form-urlencoded】@RequestBody
用于接收json数据【application/json】
- 应用
- 后期开发中,发送json格式数据为主,
@RequestBody
应用较广 - 如果发送非json格式数据,选用
@RequestParam
接收请求参数
- 后期开发中,发送json格式数据为主,
日期类型
|
|
SpringMVC默认支持的字符串转日期的格式为 yyyy/MM/dd
解决方案:使用 @DateTimeFormat
注解
|
|
http://localhost:7777/user/dateParam?date1=2077/12/21&date2=1997-02-13
参数传递date1 –> Tue Dec 21 00:00:00 CST 2077 参数传递date2 –> Thu Feb 13 00:00:00 CST 1997 参数传递date3 –> Fri Sep 09 16:34:07 CST 2022
知识点:@DateTimeFormat
名称 | @DateTimeFormat |
---|---|
类型 | 形参注解 |
位置 | SpringMVC控制器方法形参前面 |
作用 | 设定日期时间型数据格式 |
相关属性 | pattern:指定日期时间格式字符串 |
实现原理:
- 前端传递字符串,后端使用日期Date接收
- 前端传递JSON数据,后端使用对象接收
- 前端传递字符串,后端使用Integer接收
- 后台需要的数据类型有很多种
- 在数据的传递过程中存在很多类型的转换
问
:谁来做这个类型转换?
答
:SpringMVC
问
:SpringMVC是如何实现类型转换的?
答
:SpringMVC中提供了很多类型转换接口和实现类
Converter
接口 注意:Converter所属的包为org.springframework.core.convert.converter
|
|
HttpMessageConverter
接口
该接口是实现对象与JSON之间的转换工作
注意:需要在SpringMVC的配置类把 @EnableWebMvc
当做标配配置上去,不要省略
响应
响应页面
- 添加页面
|
|
- 修改UserController
|
|
- 访问http://localhost:7777/toJumpPage
响应数据
文本数据
- 添加
|
|
- 访问http://localhost:7777/toText
json数据
POJO
- 添加
|
|
- 访问http://localhost:7777/toJsonPojo
HttpMessageConverter
接口帮实现了对象与JSON之间的转换工作,只需要在 SpringMvcConfig
配置类上加上 @EnableWebMvc
注解即可
POJO集合
- 添加
|
|
- 访问http://localhost:7777/toJsonList
知识点:@ResponseBody
名称 | @ResponseBody |
---|---|
类型 | 方法\类注解 |
位置 | SpringMVC控制器方法定义上方和控制类上 |
作用 | 设置当前控制器返回值作为响应体, 写在类上,该类的所有方法都有该注解功能 |
相关属性 | pattern:指定日期时间格式字符串 |
说明:
- 该注解可以写在类上或者方法上
- 写在类上就是该类下的所有方法都有
@ReponseBody
功能 - 当方法上有@ReponseBody注解后
- 方法的返回值为字符串,会将其作为文本内容直接响应给前端
- 方法的返回值为对象,会将对象转换成JSON响应给前端
此处又使用到了类型转换,内部还是通过 HttpMessageConverter
接口完成的,所以 Converter
除了前面所说的功能外,它还可以实现:
- 对象转Json数据(POJO -> json)
- 集合转Json数据(Collection -> json)
REST风格
-
按照REST风格访问资源时,使用行为动作区分对资源进行了何种操作
http://localhost/users
查询全部用户信息GET
(查询)http://localhost/users/1
查询指定用户信息GET
(查询)http://localhost/users
添加用户信息POST
(新增/保存)http://localhost/users
修改用户信息PUT
(修改/更新)http://localhost/users/1
删除用户信息DELETE
(删除)
-
REST提供了对应的架构方式,按照这种架构方式设计项目可以降低开发的复杂性,提高系统的可伸缩性
REST中规定
GET
/POST
/PUT
/DELETE
针对的是查询/新增/修改/删除,但如果非要使用GET
请求做删除,这点在程序上运行是可以实现的但是如果大多数人都遵循这种风格,你不遵循,那你写的代码在别人看来就有点莫名其妙了,所以最好还是遵循REST风格
-
描述模块的名称通常使用复数,也就是加s的格式描述,表示此类的资源,而非单个的资源,例如
users
、books
、accounts
…
RESTful
- 根据REST风格对资源进行访问称为
RESTful
- save
|
|
|
|
user save …User{name=‘菲茨罗伊’, age=27}
- delete
|
|
疑问:如果方法形参的名称和路径 {}
中的值不一致,该怎么办?
例如 "/users/{id}"
和 delete(@PathVariable Integer userId)
- 解答:如果这两个值不一致,就无法获取参数,此时可以在注解后面加上属性,让注解的属性值与
{}
中的值一致即可,具体代码如下
|
|
疑问:如果有多个参数需要传递该如何编写?
前端发送请求时使用 localhost:8080/users/9421/Tom
,路径中的 9421
和 Tom
就是想传递的两个参数
- 解答:在路径后面再加一个
/{name}
,同时在方法参数中增加对应属性即可
|
|
user delete …1:w
- update
使用put调用
|
|
user update …User{name=‘菲茨罗伊’, age=27}
- 根据ID查询
|
|
user getById …1
- 查询所有
|
|
user getAll …
整体代码:
|
|
从整体代码来看,有些臃肿,好多代码都是重复的,下一小节就会来解决这个问题
-
小结
-
设定Http请求动作(动词)
1
@RequestMapping(value="",method = RequestMethod.POST|GET|PUT|DELETE)
-
设定请求参数(路径变量)
1 2 3 4
@RequestMapping(value="/users/{id}",method = RequestMethod.DELETE) @ReponseBody public String delete(@PathVariable Integer id){ }
-
知识点:@PathVariable
名称 | @PathVariable |
---|---|
类型 | 形参注解 |
位置 | SpringMVC控制器方法形参定义前面 |
作用 | 绑定路径参数与处理器方法形参间的关系,要求路径参数名与形参名一一对应 |
三个注解 @RequestBody
、@RequestParam
、@PathVariable
之间的区别和应用
- 区别
@RequestParam
用于接收url地址传参或表单传参@RequestBody
用于接收JSON数据@PathVariable
用于接收路径参数,使用{参数名称}描述路径参数
- 应用
- 后期开发中,发送请求参数超过1个时,以JSON格式为主,
@RequestBody
应用较广 - 如果发送非JSON格式数据,选用
@RequestParam
接收请求参数 - 采用
RESTful
进行开发,当参数数量较少时,例如1个,可以采用@PathVariable
接收请求路径变量,通常用于传递id值
- 后期开发中,发送请求参数超过1个时,以JSON格式为主,
RESTful快速开发
做完了上面的 RESTful
的开发,就感觉好麻烦,主要体现在以下三部分
- 每个方法的
@RequestMapping
注解中都定义了访问路径/users
,重复性太高。- 解决方案:将
@RequestMapping
提到类上面,用来定义所有方法共同的访问路径。
- 解决方案:将
- 每个方法的
@RequestMapping
注解中都要使用method属性定义请求方式,重复性太高。- 解决方案:使用
@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
代替
- 解决方案:使用
- 每个方法响应json都需要加上
@ResponseBody
注解,重复性太高。- 解决方案:
- 将
@ResponseBody
提到类上面,让所有的方法都有@ResponseBody
的功能 - 使用
@RestController
注解替换@Controller
与@ResponseBody
注解,简化书写
- 将
- 解决方案:
|
|
user getAll … user getById …1 user delete …1 user update …User{name=‘菲茨罗伊’, age=27} user save …User{name=‘菲茨罗伊’, age=27}
案例
页面访问处理
步骤一:
导入提供好的静态页面步骤二:
访问pages目录下的books.html- 打开浏览器访问
http://localhost:7777/pages/book.html
,报404,为什么呢? - SpringMvcConfig拦截了所有资源路径
- 打开浏览器访问
|
|
SSM整合
流程分析
-
创建工程
-
创建一个Maven的web工程
-
pom.xml添加SSM需要的依赖jar包
-
编写Web项目的入口配置类,实现
1
AbstractAnnotationConfigDispatcherServletInitializer
重写以下方法
getRootConfigClasses()
:返回Spring的配置类 –> 需要SpringConfig
配置类getServletConfigClasses()
:返回SpringMVC的配置类 –> 需要SpringMvcConfig
配置类getServletMappings()
: 设置SpringMVC请求拦截路径规则getServletFilters()
:设置过滤器,解决POST请求中文乱码问题
-
-
SSM整合(重点是各个配置的编写)
-
SpringConfig
-
标识该类为配置类,使用
@Configuration
-
扫描
Service
所在的包,使用@ComponentScan
-
在
Service
层要管理事务,使用@EnableTransactionManagement
-
读取外部的
properties
配置文件,使用@PropertySource
-
整合
Mybatis
需要引入Mybatis相关配置类,使用
@Import
- 第三方数据源配置类
JdbcConfig
- 构建DataSource数据源,DruidDataSouroce,需要注入数据库连接四要素,使用
@Bean
、@Value
- 构建平台事务管理器,DataSourceTransactionManager,使用
@Bean
- Mybatis配置类
MybatisConfig
- 构建
SqlSessionFactoryBean
并设置别名扫描与数据源,使用@Bean
- 构建
MapperScannerConfigurer
并设置DAO层的包扫描
- 第三方数据源配置类
-
-
SpringMvcConfig
- 标识该类为配置类,使用
@Configuratio
- 扫描
Controller
所在的包,使用@ComponentScan
- 开启SpringMVC注解支持,使用
@EnableWebMvc
- 标识该类为配置类,使用
-
-
功能模块(与具体的业务模块有关)
-
创建数据库表
-
根据数据库表创建对应的模型类
-
通过Dao层完成数据库表的增删改查(接口+自动代理)
-
编写
Service
层(Service接口+实现类)@Service
@Transactional
- 整合Junit对业务层进行单元测试
@RunWith
@ContextConfiguration
@Test
-
编写
Controller
层- 接收请求
@RequestMapping
、@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
- 接收数据 简单、POJO、嵌套POJO、集合、数组、JSON数据类型
@RequestParam
@PathVariable
@RequestBody
- 转发业务层
@Autowired
- 响应结果
@ResponseBody
- 接收请求
-
整合配置
分析完毕之后,就一步步来完成的SSM整合
步骤一:
创建Maven的web项目步骤二:
导入坐标
|
|
步骤三:
创建项目包结构config
目录存放的是相关的配置类controller
编写的是Controller类dao
存放的是Dao接口,因为使用的是Mapper接口代理方式,所以没有实现类包service
存的是Service接口,service.impl
存放的是Service实现类resources
:存入的是配置文件,如jdbc.propertiesdomain
:存放类webapp
:目录可以存放静态资源test/java
:存放的是测试类
步骤四:
创建jdbc.properties
|
|
步骤五:
创建JdbcConfig配置类
|
|
步骤六:
创建MyBatisConfig配置类
|
|
ℹ️ Mybatis配置类
public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource)
作用:
- 创建并配置
SqlSessionFactoryBean
实例,用于管理MyBatis的SqlSession
和数据库连接。 - 将数据源(
DataSource
)设置到SqlSessionFactoryBean
中,以便MyBatis可以与数据库进行交互。 - 设置类型别名包(Type Aliases Package),告诉MyBatis在哪里寻找领域对象(Domain Objects)的别名。
配置说明:
DataSource dataSource
:作为方法参数传入,通常在Spring中配置好的数据源,用于连接数据库。setTypeAliasesPackage("domain")
:指定MyBatis扫描哪个包下的Java类,这些类会作为别名注册,可以在SQL映射文件中使用类名作为别名来代替全限定类名。
public MapperScannerConfigurer mapperScannerConfigurer()
作用:
- 创建并配置
MapperScannerConfigurer
实例,用于扫描指定包路径下的MyBatis Mapper接口。 - 这些Mapper接口通常定义了与数据库交互的SQL操作,MyBatis会自动将它们注册为Spring Bean。
配置说明:
setBasePackage("dao")
:指定MyBatis扫描哪个包下的Mapper接口,这些接口中定义了SQL操作,可以通过注解或XML配置文件与SQL语句进行映射。
步骤七:
创建SpringConfig配置类
|
|
步骤八:
创建SpringMvcConfig配置类
|
|
步骤九:
创建ServletContainersInitConfig配置类
|
|
功能模块开发
需求:对表tbl_book进行新增、修改、删除、根据ID查询和查询所有
步骤一:
创建数据库及表
|
|
步骤二:
编写模型类
|
|
步骤三:
编写Dao接口
|
|
步骤四:
编写Service接口及其实现类- BookService
|
|
- BookServiceImpl
|
|
步骤五:
编写Controller类
|
|
统一结果封装
-
目前已经有
三种数据类型
返回给前端了,随着业务的增长,需要返回的数据类型就会越来越多
。那么前端开发人员在解析数据的时候就比较凌乱
了,所以对于前端来说,如果后端能返回一个统一的数据结果
,前端在解析的时候就可以按照一种方式进行解析,开发就会变得更加简单 -
所以现在需要解决的问题就是
如何将返回的结果数据进行统一
,具体如何来做,大体思路如下- 为了封装返回的结果数据:创建结果模型类,封装数据到data属性中
- 可以设置data的数据类型为
Object
,这样data中就可以放任意的结果类型了,包括但不限于上面的boolean
、对象
、集合对象
- 可以设置data的数据类型为
- 为了封装返回的数据是何种操作,以及是否操作成功:封装操作结果到code属性中
- 例如增删改操作返回的都是
true
,那怎么分辨这个true
到底是增
还是删
还是改
呢?就通过这个code
来区分
- 例如增删改操作返回的都是
- 操作失败后,需要封装返回错误信息提示给用户:封装特殊消息到message(msg)属性中
- 例如查询或删除的目标不存在,会返回null,那么此时需要提示
查询/删除的目标不存在,请重试!
- 例如查询或删除的目标不存在,会返回null,那么此时需要提示
- 为了封装返回的结果数据:创建结果模型类,封装数据到data属性中
-
boolean
规则可以自己定 这里前三位是固定的 第四位表示不同的操作 末位表示成功/失败,1成功,0失败
|
|
- 对象
这里的末尾是0,表示失败操作 第四位是2,区别于上面的1,表示是不同的操作类型 msg给用户提示信息,不是必有项
|
|
- 对象集合
这里末尾操作是1,表示成功操作 data中显示的是对象集合 没有msg
|
|
- 结果类
|
|
- 错误码类
|
|
- 修改BookController
|
|
统一异常处理
异常的种类,以及出现异常的原因:
- 框架内部抛出的异常:因
使用不合规
导致 - 数据层抛出的异常:因使用
外部服务器故障
导致(例如:服务器访问超时) - 业务层抛出的异常:因
业务逻辑书写错误
导致(例如:遍历业务书写操作,导致索引越界异常等) - 表现层抛出的异常:因
数据收集
、校验
等规则导致(例如:不匹配的数据类型间转换导致异常) - 工具类抛出的异常:因工具类
书写不严谨
,健壮性不足
导致(例如:必需要释放的连接,长时间未释放等)
在开发的 任何一个位置
都可能会出现异常,而且这些异常是 不能避免的
,所以就需要对这些异常来 进行处理
。
思考
:
各个层级均出现异常,那么异常处理代码要写在哪一层?
所有的异常均抛出到表现层进行处理
异常的种类很多,表现层如何将所有的异常都处理到呢?
异常分类
表现层处理异常,每个方法中单独书写,代码书写量巨大,且意义不强,如何解决呢?
AOP
异常处理器
- contorller.ProjectExceptionAdvice
|
|
知识点:@RestControllerAdvice
说明:此注解自带 @ResponseBody
注解与 @Component
注解,具备对应的功能
名称 | @RestControllerAdvice |
---|---|
类型 | 类注解 |
位置 | Rest风格开发的控制器增强类定义上方 |
作用 | 为Rest风格开发的控制器类做增强 |
知识点:@ExceptionHandler
说明:此类方法可以根据处理的异常不同,制作多个方法分别处理对应的异常
名称 | @ExceptionHandler |
---|---|
类型 | 方法注解 |
位置 | 专用于异常处理的控制器方法上方 |
作用 | 设置指定异常的处理方案,功能等同于控制器方法, 出现异常后终止原始控制器执行,并转入当前方法执行 |
项目异常处理方案
异常分类
因为异常的种类有很多,如果每一个异常都对应一个 @ExceptionHandler
,那得写多少个方法来处理各自的异常,所以在处理异常之前,需要对异常进行一个分类:
-
业务异常
(BusinessException)- 规范的用户行为产生的异常
- 用户在页面输入内容的时候未按照指定格式进行数据填写,如在年龄框输入的是字符串
- 不规范的用户行为操作产生的异常
- 如用户手改URL,故意传递错误数据
localhost:8080/books/略略略
- 如用户手改URL,故意传递错误数据
- 规范的用户行为产生的异常
-
系统异常
(SystemException)- 项目运行过程中可预计,但无法避免的异常
- 如服务器宕机
- 项目运行过程中可预计,但无法避免的异常
-
其他异常
(Exception)- 编程人员未预期到的异常
- 如:系统找不到指定文件
- 编程人员未预期到的异常
将异常分类以后,针对不同类型的异常,要提供具体的解决方案
异常解决方案
-
业务异常
(BusinessException)- 发送对应消息传递给用户,提醒规范操作
- 大家常见的就是提示用户名已存在或密码格式不正确等
- 发送对应消息传递给用户,提醒规范操作
-
系统异常
(SystemException)- 发送固定消息传递给用户,安抚用户
- 系统繁忙,请稍后再试
- 系统正在维护升级,请稍后再试
- 系统出问题,请联系系统管理员等
- 发送特定消息给运维人员,提醒维护
- 可以发送短信、邮箱或者是公司内部通信软件
- 记录日志
- 发消息给运维和记录日志对用户来说是不可见的,属于后台程序
- 发送固定消息传递给用户,安抚用户
-
其他异常
(Exception)- 发送固定消息传递给用户,安抚用户
- 发送特定消息给编程人员,提醒维护(纳入预期范围内)
- 一般是程序没有考虑全,比如未做非空校验等
- 记录日志
具体实现
-
思路:
- 先通过自定义异常,完成
BusinessException
和SystemException
的定义 - 将其他异常包装成自定义异常类型
- 在异常处理器类中对不同的异常进行处理
- 先通过自定义异常,完成
-
步骤一:
自定义异常类 -
exception.SystemException
|
|
- exception.BussinessException
|
|
- 让自定义异常类继承
RuntimeException
的好处是,后期在抛出这两个异常的时候,就不用在try..catch..
或throws
了 - 自定义异常类中添加
code
属性的原因是为了更好的区分异常是来自哪个业务的 步骤二:
将其他异常包成自定义异常- Code报错码
|
|
- 模拟异常
|
|
步骤三:
处理器类中处理自定义异常
|
|
前后端协议联调
SpringMVC对静态资源放行
- 新建SpringMVCSupport类,继承
WebMvcConfigurationSupport
,并重写addResourceHandlers()
方法
|
|
- 同时也需要让SpringMvcConfig扫描到的配置类
|
|
- 前端页面
|
|
拦截器
- 浏览器发送一个请求,会先到Tomcat服务器的web服务器
- Tomcat服务器接收到请求后,会先去判断请求的是
静态资源
还是动态资源
- 如果是静态资源,会直接到Tomcat的项目部署目录下直接访问
- 如果是动态资源,就需要交给项目的后台代码进行处理
- 在找到具体的方法之前,可以去配置过滤器(可以配置多个),按照顺序进行执行(在这里就可以进行权限校验)
- 然后进入到中央处理器(SpringMVC中的内容),SpringMVC会根据配置的规则进行拦截
- 如果满足规则,则进行处理,找到其对应的
Controller
类中的方法进行之星,完成后返回结果 - 如果不满足规则,则不进行处理
- 这个时候,如果需要在每个Controller方法执行的前后添加业务,具体该如何来实现?
- 这个就是拦截器要做的事
- 拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMVC中动态拦截控制器方法的执行
作用
:- 在指定的方法调用前后执行预先设定的代码
- 阻止原始方法的执行
总结:
拦截器就是用来作增强
- 但是这个拦截器貌似跟之前学的过滤器很像啊,不管是从作用上来看还是从执行顺序上来看
- 那么拦截器和过滤器之间的区别是什么呢?
归属不同:
Filter属于Servlet技术,而Interceptor属于SpringMVC技术拦截内容不同:
Filter对所有访问进行增强,Interceptor仅对SpringMVC的访问进行增强
- 那么拦截器和过滤器之间的区别是什么呢?
拦截器案例
- 创建拦截器类:
controller.interceptor
下创建ProjectInterceptor
类,实现HandlerInterceptor
接口,并重写其中的三个方法
|
|
- 配置拦截器:
|
|
- SpringMvc添加SpringMvcSupport包扫描
|
|
拦截器中的 preHandler
方法,如果返回true,则代表放行,会执行原始 Controller
类中要请求的方法,如果返回 false
,则代表拦截,后面的就不会再执行了。
- 简化SpringMvcSupport的编写
可以让
SpringMvcConfig
类实现WebMvcConfigurer
接口,然后直接在SpringMvcConfig
中写SpringMvcSupport
的东西,这样就不用再写SpringMvcSupport
类了,全都在SpringMvcConfig
中写
|
|
- 拦截器的执行流程
-
当有拦截器后,请求会先进入
preHandle
方法,- 如果方法返回
true
,则放行继续执行后面的handle(Controller的方法)和后面的方法 - 如果返回
false
,则直接跳过后面方法的执行。
- 如果方法返回
拦截器参数
前置处理方法
原始方法之前运行preHandle
|
|
request:
请求对象response:
响应对象handler:
被调用的处理器对象,本质上是一个方法对象,对反射中的Method对象进行了再包装
使用request对象可以获取请求数据中的内容,如获取请求头的 Content-Type
|
|
控制台输出如下,成功输出了Content-Type application/json
preHandle…application/json book save …Book{书名=‘书名测试数据’, 价格=0.0} postHandle afterCompletion
使用handler参数,可以获取方法的相关信息
|
|
控制台输出如下,成功输出了方法名 save
preHandle…save book save …Book{书名=‘书名测试数据’, 价格=0.0} postHandle afterCompletion
后置处理方法
原始方法运行后运行,如果原始方法被拦截,则不执行
|
|
前三个参数和上面的是一致的。
modelAndView:
如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
因为现在都是返回json数据,所以该参数的使用率不高。
完成处理方法
拦截器最后执行的方法,无论原始方法是否执行
|
|
前三个参数与上面的是一致的。
ex:
如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
因为现在已经有全局异常处理器类,所以该参数的使用率也不高。
这三个方法中,最常用的是 preHandle
,在这个方法中可以通过返回值来决定是否要进行放行,可以把业务逻辑放在该方法中,如果满足业务则返回 true
放行,不满足则返回 false
拦截。
配置多个拦截器
步骤一:
创建拦截器类 直接复制一份改个名,改个输出语句
|
|
步骤二:
配置拦截器类
|
|
- 重新启动服务器,使用PostMan发送请求,控制台输出如下
preHandle… preHandle…222 book getById …9527 postHandle…222 postHandle afterCompletion…222 afterCompletion
- 当配置多个拦截器时,形成拦截器链
- 拦截器链的运行顺序参照拦截器添加顺序为准
- 当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
- 当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
preHandle:
与配置顺序相同,必定运行postHandle:
与配置顺序相反,可能不运行afterCompletion:
与配置顺序相反,可能不运行。
Maven进阶
分模块开发实现
前面已经完成了SSM整合,接下来就基于SSM整合的项目来实现对项目的拆分。
环境准备
复制一份之前的ssm项目,重命名为 maven_01_ssm
抽取domain层
-
步骤一:创建新模块
- 创建一个名为
maven_02_pojo
的maven项目
- 创建一个名为
-
步骤二:项目中创建domain包
- 在
maven_02_pojo
中创建com.mtmn.domain
包,并将maven_01_ssm
的Book类拷贝到该包中
- 在
-
步骤三:删除原项目中的domain包
- 删除后,
maven_01_ssm
项目中用到Book
的类中都会爆红 - 要想解决上述问题,需要在
maven_01_ssm
中添加maven_02_pojo
的依赖。
- 删除后,
-
步骤四:在
maven_01_ssm项目的pom.xml添加
maven_02_pojo的依赖
1 2 3 4 5
<dependency> <groupId>com.mtmn</groupId> <artifactId>maven_02_pojo</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 因为添加了依赖,所以在
maven_01_ssm
中就已经能找到Book类,所以刚才的爆红提示就会消失。
- 因为添加了依赖,所以在
-
步骤五:编译maven_01_ssm项目控制台会报错
Failed to execute goal on project maven_01_ssm: Could not resolve dependencies for project com.mtmn:maven_01_ssm:jar:1.0-SNAPSHOT: Could not find artifact com.mtmn:maven_02_pojo:jar:1.0-SNAPSHOT -> [Help 1]
意思就是找不到maven_02_pojo这个jar包
- 为啥找不到呢?
- 原因是Maven会从本地仓库找对应的jar包,但是本地仓库又不存在该jar包所以会报错。
- 在IDEA中是有
maven_02_pojo
这个项目,所以只需要将maven_02_pojo
项目安装到本地仓库即可。
- 为啥找不到呢?
-
步骤六:将项目安装本地仓库
- 将需要被依赖的项目
maven_02_pojo
,使用maven的install
命令,把其安装到Maven的本地仓库中 - 之后再次执行
maven_01_ssm
的compile
的命令后,就已经能够成功编译。
- 将需要被依赖的项目
抽取dao层
-
步骤一:
创建新模块- 创建一个名为
maven_03_dao
的maven项目
- 创建一个名为
-
步骤二:
项目中创建dao
包-
在
maven_03_dao
项目中创建com.mtmn.dao
包,并将maven_01_ssm
中BookDao类拷贝到该包中 -
在maven_03_dao中会有如下几个问题需要解决下
-
项目maven_03_dao的BookDao接口中Book类找不到报错
-
解决方案在
maven_03_dao
项目的pom.xml中添加maven_02_pojo
项目1 2 3 4 5
<dependency> <groupId>com.mtmn</groupId> <artifactId>maven_02_pojo</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
项目maven_03_dao的BookDao接口中,Mybatis的增删改查注解报错
-
解决方案在
maven_03_dao
项目的pom.xml中添加mybatis
的相关依赖1 2 3 4 5 6 7 8 9 10 11
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency>
-
-
-
最后记得使用maven的
install
命令,把其安装到Maven的本地仓库中 -
步骤三:
删除原项目中的dao
包- 删除Dao包以后,因为
maven_01_ssm
中的BookServiceImpl类中有使用到Dao的内容,所以需要在maven_01_ssm
的pom.xml添加maven_03_dao
的依赖
1 2 3 4 5
<dependency> <groupId>com.mtmn</groupId> <artifactId>maven_03_dao</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- 删除Dao包以后,因为
-
步骤四:
运行测试- 启动Tomcat服务器,访问
http://localhost:7777/pages/books.html
- 将抽取后的项目进行运行,测试之前的增删改查功能依然能够使用。
- 启动Tomcat服务器,访问
小结
对于项目的拆分,大致会有如下几个步骤
- 创建Maven模块
- 书写模块代码
- 分模块开发需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分。拆分方式可以按照功能拆也可以按照模块拆。
- 通过maven指令安装模块到本地仓库(install 指令)
- 由于maven指令只能安装到自己电脑的仓库里,那么团队内部开发需要发布模块功能,需要到团队内部可共享的仓库中(私服),私服后面会讲解。
依赖管理
现在已经能把项目拆分成一个个独立的模块,当在其他项目中想要使用独立出来的这些模块,只需要在其pom.xml使用 <dependency>
标签来进行jar包的引入即可。
<dependency>
其实就是依赖,关于依赖管理里面都涉及哪些内容,就一个个来学习下:
- 依赖传递
- 可选依赖
- 排除依赖
先来说说什么是依赖:
- 依赖指当前项目运行所需的jar一个项目可以设置多个依赖。
- 格式为:
|
|
依赖传递与冲突问题
说明:A代表自己的项目;B,C,D,E,F,G代表的是项目所依赖的jar包;D1和D2、E1和E2代表是相同jar包的不同版本
- A依赖了B和C,B和C有分别依赖了其他jar包,所以在A项目中就可以使用上面所有jar包,这就是所说的依赖传递
- 依赖传递有直接依赖和间接依赖
- 相对于A来说,A直接依赖B和C,间接依赖了D1,E1,G,F,D2和E2
- 相对于B来说,B直接依赖了D1和E1,间接依赖了G
- 直接依赖和间接依赖是一个相对的概念
- 因为有依赖传递的存在,就会导致jar包在依赖的过程中出现冲突问题,具体什么是冲突?Maven是如何解决冲突的?
这里所说的 依赖冲突
是指项目依赖的某一个jar包,有多个不同的版本,因而造成类包版本冲突。
情况一:
在maven_01_ssm的pom.xml中添加两个不同版本的Junit依赖:
|
|
调换位置,刷新maven面板,会发现,maven的dependencies面板上总是显示使用的是后加载的jar包 于是得出一个结论:
- 特殊优先:当同级配置了相同资源的不同版本,后配置的覆盖先配置的。
情况二:
路径优先:当依赖中出现相同的资源时,层级越深,优先级越低,层级越浅,优先级越高- A通过B间接依赖到E1
- A通过C间接依赖到E2
- A就会间接依赖到E1和E2,Maven会按照层级来选择,E1是2度,E2是3度,所以最终会选择E1
情况三:
声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的- A通过B间接依赖到D1
- A通过C间接依赖到D2
- D1和D2都是两度,这个时候就不能按照层级来选择,需要按照声明来,谁先声明用谁,也就是说B在C之前声明,这个时候使用的是D1,反之则为D2
但是对于上面的结果,也不用刻意去记,一切以maven的dependencies面板上显示的为准
可选依赖和排除依赖
依赖传递介绍完以后,思考一个问题,假如
maven_01_ssm
依赖了maven_03_dao
maven_03_dao
依赖了maven_02_pojo
- 因为现在有依赖传递,所以
maven_01_ssm
能够使用到maven_02_pojo
的内容 - 如果说现在不想让
maven_01_ssm
依赖到maven_02_pojo
,有哪些解决方案?
说明:在真实使用的过程中,maven_01_ssm
中是需要用到 maven_02_pojo
的,这里只是用这个例子描述的需求。因为有时候,maven_03_dao
出于某些因素的考虑,就是不想让别人使用自己所依赖的 maven_02_pojo
。
-
方案一:
可选依赖- 可选依赖指对外隐藏当前所依赖的资源—
不透明
- 在
maven_03_dao
的pom.xml,在引入maven_02_pojo
的时候,添加optional
1 2 3 4 5 6 7
<dependency> <groupId>com.mtmn</groupId> <artifactId>maven_02_pojo</artifactId> <version>1.0-SNAPSHOT</version> <!--可选依赖是隐藏当前工程所依赖的资源,隐藏后对应资源将不具有依赖传递--> <optional>true</optional> </dependency>
- 可选依赖指对外隐藏当前所依赖的资源—
-
方案二:
排除依赖- 排除依赖指主动断开依赖的资源,被排除的资源无需指定版本—
不需要
- 前面已经通过可选依赖实现了阻断
maven_02_pojo
的依赖传递,对于排除依赖,则指的是已经有依赖的事实,也就是说maven_01_ssm
项目中已经通过依赖传递用到了maven_02_pojo
,此时需要做的是将其进行排除,所以接下来需要修改maven_01_ssm
的pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13
<dependency> <groupId>com.mtmn</groupId> <artifactId>maven_03_dao</artifactId> <version>1.0-SNAPSHOT</version> <!--排除依赖是隐藏当前资源对应的依赖关系--> <exclusions> <!--这里可以排除多个依赖,只要你有需求--> <exclusion> <groupId>com.mtmn</groupId> <artifactId>maven_02_pojo</artifactId> </exclusion> </exclusions> </dependency>
- 排除依赖指主动断开依赖的资源,被排除的资源无需指定版本—
介绍完这两种方式后,简单来梳理下,就是
A依赖B,B依赖C
,C
通过依赖传递会被A
使用到,现在要想办法让A
不去依赖C
- 可选依赖是在B上设置
<optional>
,A
不知道有C
的存在, - 排除依赖是在A上设置
<exclusions>
,A
知道有C
的存在,主动将其排除掉。
聚合与继承
的项目已经从以前的单模块,变成了现在的多模块开发。项目一旦变成了多模块开发以后,就会引发一些问题,在这一节中主要会学习两个内容 聚合
和 继承
,用这两个知识来解决下分模块后的一些问题。
聚合
- 分模块开发后,需要将这四个项目都安装到本地仓库,目前只能通过项目Maven面板的install来安装,并且需要安装四个,如果的项目足够多,那一个个install也挺麻烦的
- 如果四个项目都已经安装成功,当ssm_pojo发生变化后,就得将ssm_pojo重新安装到maven仓库,但是为了确保对ssm_pojo的修改不会影响到其他模块(比如将pojo类中的一个属性删除,如果其他模块调用了这个属性,那必然报错),需要对所有模块重新编译,看看有没有问题。然后还需要将所有模块再install一遍
项目少的话还好,但是如果项目多的话,一个个操作项目就容易出现漏掉或重复操作的问题,所以就想能不能抽取一个项目,把所有的项目管理起来,以后再想操作这些项目,只需要操作抽取的这个项目,这样就省事儿多了
这就要用到接下来讲的 聚合
了
- 所谓聚合:将多个模块组织成一个整体,同时进行项目构建的过程称为聚合
- 聚合工程:通常是一个不具有业务功能的
空
工程 - 作用:使用聚合工程可以将多个工程编组,通过对聚合工程的构建,实现对所包含的所有模块进行同步构建
- 当工程中某个模块发生更新后,必须保障工程中与更新模块关联的模块同步更新,此时就可以使用聚合工程来解决批量模块同步构建的问题
具体实现步骤如下:
步骤一:
创建一个空的maven项目步骤二:
将项目打包方式改为pom
|
|
说明:项目的打包方式,接触到的有三种,分别是
- jar:默认情况,说明该项目为java项目
- war:说明该项目为web项目
- pom:说明该项目为聚合或继承(后面会讲)项目
步骤三:
pom.xml添加所要管理的项目
|
|
步骤四:
使用聚合统一管理项目 在maven面板上点击compile,会发现所有受管理的项目都会被执行编译,这就是聚合工程的作用
[INFO] maven_02_pojo Maven Webapp … SUCCESS [ 0.677 s] [INFO] maven_03_dao … SUCCESS [ 0.027 s] [INFO] maven_01_ssm Maven Webapp … SUCCESS [ 0.100 s] [INFO] maven_00_parent … SUCCESS [ 0.001 s]
说明:聚合工程管理的项目在进行运行的时候,会按照项目与项目之间的依赖关系来自动决定执行的顺序和配置的顺序无关。
虽然配置的顺序是 123
,但是执行的时候按照依赖关系编译是 231
那么聚合的知识就讲解完了,最后总结一句话就是,聚合工程主要是用来管理项目。
继承
已经完成了使用聚合工程去管理项目,聚合工程进行某一个构建操作,其他被其管理的项目也会执行相同的构建操作。那么接下来,再来分析下,多模块开发存在的另外一个问题,重复配置
的问题,先来看张图:
spring-webmvc
、spring-jdbc
在三个项目模块中都有出现,这样就出现了重复的内容spring-test
只在ssm_crm和ssm_goods中出现,而在ssm_order中没有,这里是部分重复的内容- 使用的spring版本目前是
5.2.10.RELEASE
,假如后期要想升级spring版本,所有跟Spring相关jar包都得被修改,涉及到的项目越多,维护成本越高
面对上面这些问题,就得用到接下来要学习的 继承
- 所谓继承:描述的是两个工程间的关系,与java中的继承类似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承
- 作用:
- 简化配置
- 减少版本冲突
接下来,到程序中去看看继承该如何实现
步骤一:
创建一个空的Maven项目并将其打包方式设置为pom
|
|
步骤二:
在子工程中设置其父工程
|
|
-
步骤三:
优化子项目共有依赖导入问题- 将子项目共同使用的jar包都抽取出来,维护在父项目的pom.xml中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mtmn</groupId> <artifactId>maven_00_parent</artifactId> <version>1.0-SNAPSHOT</version> <!--设置打包方式--> <packaging>pom</packaging> <!--设置管理的项目名称--> <modules> <module>../maven_01_ssm</module> <module>../maven_02_pojo</module> <module>../maven_03_dao</module> </modules> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.0</version> </dependency> </dependencies> </project>
- 删除子项目中已经被抽取到父项目的pom.xml中的jar包
- 删除完后,你会发现父项目中有依赖对应的jar包,子项目虽然已经将重复的依赖删除掉了,但是刷新的时候,子项目中所需要的jar包依然存在。
- 当项目的
<parent>
标签被移除掉,会发现多出来的jar包依赖也会随之消失。
- 在父项目中修改jar包的版本,刷新后,子项目中的jar包版本也随之变化
-
那么现在就可以解决了刚才提到的第一个问题,将子项目中的公共jar包抽取到父工程中进行统一添加依赖,这样做的可以简化配置,并且当父工程中所依赖的jar包版本发生变化,所有子项目中对应的jar包版本也会跟着更新。
-
步骤四:
优化子项目依赖版本问题 如果把所有用到的jar包都管理在父项目的pom.xml,看上去更简单些,但是这样就会导致有很多项目引入了过多自己不需要的jar包。如上面看到的这张图:如果把所有的依赖都放在了父工程中进行统一维护,就会导致ssm_order项目中多引入了
spring-test
的jar包,如果这样的jar包过多的话,对于ssm_order来说也是一种"负担”。
那针对于这种部分项目有的jar包,该如何管理优化呢?
- 在父工程中的pom.xml中定义依赖管理
|
|
- 将maven_01_ssm的pom.xml中的junit依赖删除掉,刷新Maven
刷新后,在maven_01_ssm项目中找不到junit依赖,所以得出一个结论
<dependencyManagement>
标签不真正引入jar包,而是配置可供子项目选择的jar包依赖 子项目要想使用它所提供的这些jar包,需要自己添加依赖,并且不需要指定<version>
- 在maven_01_ssm的pom.xml添加junit的依赖
|
|
注意:这里就不需要添加版本了,这样做的好处就是当父工程 dependencyManagement
标签中的版本发生变化后,子项目中的依赖版本也会跟着发生变化
- 在maven_03_dao的pom.xml添加junit的依赖
|
|
这个时候,maven_01_ssm和maven_03_dao这两个项目中的junit版本就会跟随着父项目中的标签 dependencyManagement
中junit的版本发生变化而变化。不需要junit的项目就不需要添加对应的依赖即可(maven_02_pojo中就没添加)
至此继承就已经学习完了,总结来说,继承可以帮助做两件事
- 将所有项目公共的jar包依赖提取到父工程的pom.xml中,子项目就可以不用重复编写,简化开发
- 将所有项目的jar包配置到父工程的
dependencyManagement
标签下,实现版本管理,方便维护dependencyManagement
标签不真正引入jar包,只是管理jar包的版本- 子项目在引入的时候,只需要指定groupId和artifactId,不需要加version
- 当
dependencyManagement
标签中jar包版本发生变化,所有子项目中用到该jar包的地方对应的版本会自动随之更新
最后总结一句话就是,父工程主要是用来快速配置依赖jar包和管理项目中所使用的资源
。
-
小结
-
继承的实现步骤:
- 创建Maven模块,设置打包类型为pom
1
<packaging>pom</packaging>
- 在父工程的pom文件中配置依赖关系(子工程将沿用父工程中的依赖关系),一般只抽取子项目中公有的jar包
1 2 3 4 5 6 7 8
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.10.RELEASE</version> </dependency> ... </dependencies>
- 在父工程中配置子工程中可选的依赖关系
1 2 3 4 5 6 7 8 9 10
<dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> </dependencies> ... </dependencyManagement>
- 在子工程中配置当前工程所继承的父工程
1 2 3 4 5 6 7 8
<!--定义该工程的父工程--> <parent> <groupId>com.mtmn</groupId> <artifactId>maven_01_parent</artifactId> <version>1.0-RELEASE</version> <!--填写父工程的pom文件,可以不写--> <relativePath>../maven_01_parent/pom.xml</relativePath> </parent>
- 在子工程中配置使用父工程中可选依赖的坐标
1 2 3 4 5 6
<dependencies> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> </dependencies>
-
注意事项:
- 子工程中使用父工程中的可选依赖时,仅需要提供群组id和项目id,无需提供版本,版本由父工程统一提供,避免版本冲突
- 子工程中还可以定义父工程中没有定义的依赖关系,只不过不能被父工程进行版本统一管理。
聚合与继承的区别
聚合与继承分别的作用:
- 聚合用于快速构建项目,对项目进行管理
- 继承用于快速配置和管理子项目中所使用jar包的版本
聚合和继承的相同点:
- 聚合与继承的pom.xml文件打包方式均为pom,可以将两种关系制作到同一个pom文件中
- 聚合与继承均属于设计型模块,并无实际的模块内容
聚合和继承的不同点:
- 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些
- 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己
属性
问题分析
前面已经在父工程中的dependencyManagement标签中对项目中所使用的jar包版本进行了统一的管理,但是如果在标签中有如下的内容:
|
|
如果现在想更新Spring的版本,就会发现依然需要更新多个jar包的版本,这样的话还是有可能出现漏改导致程序出问题,而且改起来也是比较麻烦。 问题清楚后,需要解决的话,就可以参考咱们java基础所学习的变量,声明一个变量,在其他地方使用该变量,当变量的值发生变化后,所有使用变量的地方也会跟着变化 例如
|
|
然后将依赖的版本号替换成 spring_version
|
|
解决步骤
步骤一:
父工程中定义属性
|
|
步骤二:
修改依赖的version
|
|
此时,只需要更新父工程中properties标签中所维护的jar包版本,所有子项目中的版本也就跟着更新。当然除了将spring相关版本进行维护,可以将其他的jar包版本也进行抽取,这样就可以对项目中所有jar包的版本进行统一维护
说明:使用 properties
标签来定义属性,在 properties
标签内自定义标签名当做属性名,自定义标签内的值即为属性值
例如:<spring.version>5.2.10.RELEASE</spring.version>
,属性名为 spring.version
,属性值为 5.2.10.RELEASE
在其他地方引用变量时用 ${变量名}
配置文件加载属性
Maven中的属性已经介绍过了,现在也已经能够通过Maven来集中管理Maven中依赖jar包的版本。但是又有新的需求,就是想让Maven对于属性的管理范围能更大些,比如之前项目中的 jdbc.properties
,这个配置文件中的属性,能不能也来让Maven进行管理呢?
答案是肯定的,具体的实现步骤如下
步骤一:
父工程定义属性
|
|
步骤二:
jdbc.properties文件中引用属性
|
|
步骤三:
设置maven过滤文件范围 直接在properties中引用属性,看起来怪怪的,properties怎么能直接用到maven中配置的属性呢? 所以还需要来配置一下,让maven_01_ssm/src/main/resources
目录下的jdbc.properties
文件可以解析${}
|
|
-
步骤四:
测试是否生效 测试的时候,只需要将maven_01_ssm项目进行打包,然后在本地仓库观察打包结果中最终生成的内容是否为Maven中配置的内容。 -
存在的问题
如果不只是maven_01_ssm
项目需要有属性被父工程管理,如果还有多个项目需要配置,该如何实现呢?方式一
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<build> <resources> <!--设置资源目录,并设置能够解析${}--> <resource> <directory>../maven_01_ssm/src/main/resources</directory> <filtering>true</filtering> </resource> <resource> <directory>../maven_02_pojo/src/main/resources</directory> <filtering>true</filtering> </resource> ... </resources> </build>
可以一个一个配,但是项目足够多的话,这样还是比较繁琐的
方式二:
1 2 3 4 5 6 7 8 9 10 11 12
<build> <resources> <!-- ${project.basedir}: 当前项目所在目录,子项目继承了父项目, 相当于所有的子项目都添加了资源目录的过滤 --> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources> </build>
说明:如果打包过程中出现错误 Error assembling WAR: webxml attribute is required
原因就是Maven发现你的项目为web项目,就会去找web项目的入口web.xml(配置文件配置的方式),发现没有找到,就会报错。
解决方案1:
在maven_02_ssm项目的src\main\webapp\WEB-INF\
添加一个web.xml文件
|
|
解决方案2:
配置maven打包war时,忽略web.xml检查
|
|
上面所使用的都是Maven的自定义属性,除了 ${project.basedir}
,它属于Maven的内置系统属性。
在Maven中的属性分为:
- 自定义属性(常用)
- 内置属性
- Setting属性
- Java系统属性
- 环境变量属性
属性分类 | 引用格式 | 示例 |
---|---|---|
自定义属性 | ${自定义属性名} | ${spring.vension} |
内置属性 | ${内置属性名} | ${basedir} 、${version} |
setting属性 | ${setting.属性名} | ${settings.localRepository} |
ava系统属性 | ${系统属性分类.系统属性名} | ${user.home} |
环境变量属性 | ${env.环境变量属性名} | ${env.JAVA_HOME} |
版本管理
关于这个版本管理解决的问题是,在Maven创建项目和引用别人项目的时候,都看到过如下内容:
|
|
这里面有两个单词,SNAPSHOT和RELEASE,它们所代表的含义是什么呢?
- SNAPSHOT(快照版本)
- 项目开发过程中临时输出的版本,称为快照版本
- 快照版本会随着开发的进展不断更新
- RELEASE(发布版本)
- 项目开发到一定阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的
- 即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本
除了上面的工程版本,还经常能看到一些发布版本:
- alpha版:内测版,bug多不稳定内部版本不断添加新功能
- beta版:公测版,不稳定(比alpha稳定些),bug相对较多不断添加新功能
- 纯数字版
对于这些版本,简单认识下即可
多环境配置与应用
多环境开发
- 平常都是在自己的开发环境进行开发
- 当开发完成后,需要把开发的功能部署到测试环境供测试人员进行测试使用
- 等测试人员测试通过后,会将项目部署到生成环境上线使用。
- 这个时候就有一个问题是,不同环境的配置是不相同的,如不可能让三个环境都用一个数据库,所以就会有三个数据库的url配置,
- 在项目中如何配置?
- 要想实现不同环境之间的配置切换又该如何来实现呢?
maven提供配置多种环境的设定,帮助开发者在使用过程中快速切换环境。具体实现步骤如下
步骤一:
父工程配置多个环境,并指定默认激活环境
|
|
步骤二:
执行install查看env_dep环境是否生效 在本地仓库找到打包的war包,看看jdbc.properties配置文件中的url是否为jdbc:mysql://127.1.1.1:3306/ssm_db
步骤三:
切换默认环境为生产环境
|
|
步骤四:
执行install并查看env_pro环境是否生效 查看到的结果为:jdbc:mysql://127.2.2.2:3306/ssm_db
虽然已经能够实现不同环境的切换,但是每次切换都是需要手动修改,如何来实现在不改变代码的前提下完成环境的切换呢?步骤无:
命令行实现环境切换 在命令后加上环境id:mvn install -P env_test
步骤六:
执行安装并查看env_test环境是否生效 查看到的结果为:jdbc:mysql://127.3.3.3:3306/ssm_db
所以总结来说,对于多环境切换只需要两步即可:
- 父工程中定义多环境
|
|
- 使用多环境(构建过程)
|
|
跳过测试
前面在执行 install
指令的时候,Maven都会按照顺序从上往下依次执行,每次都会执行 test
,
对于 test
来说有它存在的意义,
- 可以确保每次打包或者安装的时候,程序的正确性,假如测试已经通过在没有修改程序的前提下再次执行打包或安装命令,由于顺序执行,测试会被再次执行,就有点耗费时间了。
- 功能开发过程中有部分模块还没有开发完毕,测试无法通过,但是想要把其中某一部分进行快速打包,此时由于测试环境失败就会导致打包失败。
遇到上面这些情况的时候,就想跳过测试执行下面的构建命令,具体实现方式有很多:
方式一:
IDEA工具实现跳过测试 IDEA的maven面板上有一个按钮,点击之后可以跳过测试,不过此种方式会跳过所有的测试,如果想更精细的控制哪些跳过,哪些不跳过,那么就需要使用配置插件的方式来完成了方式二:
配置插件实现跳过测试 在父工程中的pom.xml中添加测试插件配置
|
|
skipTests:
如果为true,则跳过所有测试,如果为false,则不跳过测试
excludes:
哪些测试类不参与测试,即排除,针对skipTests为false来设置的
includes:
哪些测试类要参与测试,即包含,针对skipTests为true来设置的
方式三:
命令行跳过测试 使用Maven的命令行,mvn 指令 -D skipTests
注意事项:
- 执行的项目构建指令必须包含测试生命周期,否则无效果。例如执行compile生命周期,不经过test生命周期。
- 该命令可以不借助IDEA,直接使用cmd命令行进行跳过测试,需要注意的是cmd要在pom.xml所在目录下进行执行。
私服
这一小节,主要学习的内容是:
- 私服简介
- 私服仓库分类
- 资源上传与下载
首先来说一说什么是私服
私服简介
- 张三负责ssm_crm的开发,自己写了一个ssm_pojo模块,要想使用直接将ssm_pojo安装到本地仓库即可
- 李四负责ssm_order的开发,需要用到张三所写的ssm_pojo模块,这个时候如何将张三写的ssm_pojo模块交给李四呢?
- 如果直接拷贝,那么团队之间的jar包管理会非常混乱而且容器出错,这个时候就想能不能将写好的项目上传到中央仓库,谁想用就直接联网下载即可
- Maven的中央仓库不允许私人上传自己的jar包,那么就得换种思路,自己搭建一个类似于中央仓库的东西,把自己的内容上传上去,其他人就可以从上面下载jar包使用
- 这个类似于中央仓库的东西就是接下来要学习的私服
所以到这就有两个概念,一个是私服,一个是中央仓库
私服:
公司内部搭建的用于存储Maven资源的服务器远程仓库:
Maven开发团队维护的用于存储Maven资源的服务器
所以说:
- 私服是一台独立的服务器,用于解决团队内部的资源共享与资源同步问题
搭建Maven私服的方式有很多,介绍其中一种使用量比较大的实现方式:
- Nexus
- Sonatype公司的一款maven私服产品
- 下载地址:https://help.sonatype.com/repomanager3/download
关于nexus的下载安装和初次登录,这里就不介绍了
私服仓库分类
- 在没有私服的情况下,自己创建的服务都是安装在Maven的本地仓库中
- 私服中也有仓库,要把自己的资源上传到私服,最终也是放在私服的仓库中
- 其他人要想使用你所上传的资源,就需要从私服的仓库中获取
- 当要使用的资源不是自己写的,是远程中央仓库有的第三方jar包,这个时候就需要从远程中央仓库下载,每个开发者都去远程中央仓库下速度比较慢(中央仓库服务器在国外)
- 私服就再准备一个仓库,用来专门存储从远程中央仓库下载的第三方jar包,第一次访问没有就会去远程中央仓库下载,下次再访问就直接走私服下载
- 前面在介绍版本管理的时候提到过有
SNAPSHOT
和RELEASE
,如果把这两类的都放到同一个仓库,比较混乱,所以私服就把这两个种jar包放入不同的仓库 - 上面已经介绍了有三种仓库,一种是存放
SNAPSHOT
的,一种是存放RELEASE
还有一种是存放从远程仓库下载的第三方jar包,那么在获取资源的时候要从哪个仓库种获取呢? - 为了方便获取,将所有的仓库编成一个组,只需要访问仓库组去获取资源。
所有私服仓库总共分为三大类:
- 宿主仓库hosted
- 保存无法从中央仓库获取的资源
- 自主研发
- 第三方非开源项目,比如Oracle,因为是付费产品,所以中央仓库没有
- 代理仓库proxy
- 代理远程仓库,通过nexus访问其他公共仓库,例如中央仓库
- 仓库组group
- 将若干个仓库组成一个群组,简化配置
- 仓库组不能保存资源,属于设计型仓库
仓库类别 | 英文名称 | 功能 | 关联操作 |
---|---|---|---|
宿主仓库 | hosted | 保存自主研发+第三方资源 | 上传 |
代理仓库 | proxy | 代理连接中央仓库 | 下载 |
仓库组 | group | 为仓库编组简化下载操作 | 下载 |
本地仓库访问私服配置
- 通过IDEA将开发的模块上传到私服,中间是要经过本地Maven的
- 本地Maven需要知道私服的访问地址以及私服访问的用户名和密码
- 私服中的仓库很多,Maven最终要把资源上传到哪个仓库?
- Maven下载的时候,又需要携带用户名和密码到私服上找对应的仓库组进行下载,然后再给IDEA
- 上面所说的这些内容,需要在本地Maven的配置文件
settings.xml
中进行配置。
步骤一:
私服上配置仓库 新建两个仓库,type选hosted,version policy 一个选release,一个选snapshot步骤二:
配置本地Maven对私服的访问权限
|
|
步骤三:
配置私服的访问路径
|
|
最后记得将新创建的两个仓库加入到maven-public的成员中,至此本地仓库就能与私服进行交互了
私服资源的下载和上传
本地仓库与私服已经建立了连接,接下来就需要往私服上上传资源和下载资源,具体的实现步骤如下
步骤一:
配置工程上传私服的具体位置
|
|
步骤二:
发布资源到私服 maven面板中运行deploy
,或者执行maven命令mvn deploy
注意:
- 要发布的项目都需要配置
distributionManagement
标签,要么在自己的pom.xml中配置,要么在其父项目中配置,然后子项目中继承父项目即可。 - 如果报401错误,尝试将maven的setting.xml文件复制到
C:\Users\username\.m2
目录下,然后在重新进行deploy
现在发布是在blog-snapshot仓库中,如果想发布到blog-release仓库中就需要将项目pom.xml中的version修改成RELEASE即可。
|
|
如果私服中没有对应的jar,会去中央仓库下载,速度很慢。可以配置让私服去阿里云中下载依赖。
修改maven-central的Remote storage为 http://maven.aliyun.com/nexus/content/groups/public
至此私服的搭建就已经完成,相对来说有点麻烦,但是步骤都比较固定
SpringBoot
SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来 简化
Spring应用的 初始搭建
以及 开发过程
。
使用了Spring框架后已经简化了的开发,而SpringBoot又是对Spring开发进行简化的,可想而知SpringBoot使用的简单及广泛性。
既然SpringBoot是用来简化Spring开发的,那就先回顾一下,以SpringMVC开发为例
- 创建新模块,选择Spring初始化,并配置模块相关基础信息
要换成java8需要换源 https://start.aliyun.com/
IDEA2023版本创建Spring项目只能勾选17和21却无法使用Java8的完美解决方案
- 选择当前模块需要使用的技术集
- 开发控制器类
|
|
- 运行自动生成的Application类
对比一下 Spring
程序和 SpringBoot
程序。
类/配置文件 | Spring | SpringBoot |
---|---|---|
pom文件中的坐标 | 手工添加 | 勾选添加 |
web3.e配置类 | 手工制作 | 无 |
Spring/SpringMVC配置类 | 手工制作 | 无 |
控制器 | 手工制作 | 手工制作 |
基于Idea的 Spring Initializr
快速构建 SpringBoot
工程时需要联网。
不用idea也可以:
首先进入SpringBoot官网 https://spring.io/projects/spring-boot ,拉到页面最下方,会有一个 Quickstart your project
然后点击 Spring Initializr
超链接
SpringBoot工程快速启动
- 问题引入
以后和前端开发人员协同开发,而前端开发人员需要测试前端程序就需要后端开启服务器,这就受制于后端开发人员。为了摆脱这个受制,前端开发人员尝试着在自己电脑上安装
Tomcat
和Idea
,在自己电脑上启动后端程序,这显然不现实。 后端可以将SpringBoot
工程打成jar
包,该jar
包运行不依赖于Tomcat
和Idea
这些工具也可以正常运行,只是这个jar
包在运行过程中连接和自己程序相同的Mysql
数据库即可,这样就可以解决这个问题。 - 那现在问题就是如何打包
由于在构建
SpringBoot
工程时已经在pom.xml
中配置了如下插件
|
|
所以只需要使用 Maven
的 package
指令打包就会在 target
目录下生成对应的 Jar
包。
注意:该插件必须配置,不然打好的 jar
包也是有问题的。
- 启动
进入
jar
包所在位置,在命令提示符
中输入如下命令
|
|
执行上述命令就可以看到 SpringBoot
运行的日志信息
|
|
SpringBoot概述
- 自动配置。这个是用来解决
Spring
程序配置繁琐的问题 - 起步依赖。这个是用来解决
Spring
程序依赖设置繁琐的问题
切换web服务器
现在启动工程使用的是 tomcat
服务器,那能不能不使用 tomcat
而使用 jetty
服务器。而要切换 web
服务器就需要将默认的 tomcat
服务器给排除掉,怎么排除呢?需要用到前面学的知识 排除依赖
,使用 exclusion
标签
|
|
然后还要引入 jetty
服务器。
|
|
接下来再次运行引导类,在日志信息中就可以看到使用的是jetty服务器
|
|
Jetty比Tomcat更轻量级,可扩展性更强(相较于Tomcat),谷歌应用引擎(GAE)已经全面切换为]etty
小结:通过切换服务器,不难发现在使用 SpringBoot
换技术时只需要导入该技术的 起步依赖
即可。
配置文件
格式一:properties
|
|
- 改端口号
格式二:yml
|
|
格式三:yaml
|
|
- 主要写yml格式
- 优先级:properties > yml > yaml
- 改日志级别
yaml格式
YAML(YAML Ain’t Markup Language),一种数据序列化格式。这种格式的配置文件在近些年已经占有主导地位,那么这种配置文件和前期使用的配置文件是有一些优势的,先看之前使用的配置文件。
- 最开始使用的是
xml
,格式如下:
|
|
- 而
properties
类型的配置文件如下
|
|
yaml
类型的配置文件内容如下
|
|
- 通过对比,得出yaml的优点有:
- 容易阅读
yaml
类型的配置文件比xml
类型的配置文件更容易阅读,结构更加清晰
- 容易与脚本语言交互(暂时还体会不到,后面会了解)
- 以数据为核心,重数据轻格式
yaml
更注重数据,而xml
更注重格式
- 容易阅读
- YAML 文件扩展名:
.yml
(主流).yaml
上面两种后缀名都可以,以后使用更多的还是 yml
的。
-
yml语法规则
- 大小写敏感
- 属性层级关系使用多行描述,每行结尾使用冒号结束
- 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
- 空格的个数并不重要,只要保证同层级的左侧对齐即可。
- 属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
- ## 表示注释
核心规则:数据前面要加空格与冒号隔开
- 数组数据在数据书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔,例如
1 2 3 4 5 6 7 8
enterprise: name: Helsing age: 16 tel: 400-957-241 subject: - Java - Python - C#
yaml配置文件数据读取
环境准备
- 修改
resource
目录下的application.yml
配置文件
|
|
- 在
com.mtmn.domain
包下新建一个Enterprise类,用来封装数据
|
|
读取配置文件
-
方式一:
使用 @Value注解
- 使用
@Value("表达式")
注解可以从配合文件中读取数据,注解中用于读取属性名引用方式是:${一级属性名.二级属性名……}
- 可以在
BookController
中使用@Value
注解读取配合文件数据,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@RestController @RequestMapping("/books") public class BookController { @Value("${lesson}") private String lesson; @Value("${server.port}") private Integer port; @Value("${enterprise.subject[0]}") private String subject_0; @GetMapping("/{id}") public String getById(@PathVariable Integer id) { System.out.println(lesson); System.out.println(port); System.out.println(subject_0); return "hello , spring boot!"; } }
- 使用PostMan发送请求,控制台输出如下,成功获取到了数据
1 2 3
SpringBoot 80 Java
- 使用
-
方式二:
使用Environment对象
- 上面方式读取到的数据特别零散,
SpringBoot
还可以使用@Autowired
注解注入Environment
对象的方式读取数据。这种方式SpringBoot
会将配置文件中所有的数据封装到Environment
对象中,如果需要使用哪个数据只需要通过调用Environment
对象的getProperty(String name)
方法获取。具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@RestController @RequestMapping("/books") public class BookController { @Autowired private Environment environment; @GetMapping("/{id}") public String getById(@PathVariable Integer id) { System.out.println(environment.getProperty("lesson")); System.out.println(environment.getProperty("enterprise.name")); System.out.println(environment.getProperty("enterprise.subject[1]")); return "hello , spring boot!"; } }
- 使用PostMan发送请求,控制台输出如下,成功获取到了数据
1 2 3
SpringBoot Helsing Python
注意:这种方式在开发中很少用,因为框架内含大量数据
- 上面方式读取到的数据特别零散,
-
⭐️⭐️⭐️
方式三: 常用
使用自定义对象`SpringBoot
-
还提供了将配置文件中的数据封装到自定义的实体类对象中的方式。具体操作如下:
-
将实体类
bean
的创建交给Spring
管理。- 在类上添加
@Component
注解
- 在类上添加
-
使用
@ConfigurationProperties
注解表示加载配置文件
- 在该注解中也可以使用
prefix
属性指定只加载指定前缀的数据
- 在该注解中也可以使用
-
在
BookController
中进行注入
-
-
具体代码如下
Enterprise
实体类内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
@Component @ConfigurationProperties(prefix = "enterprise") public class Enterprise { private String name; private int age; private String tel; private String[] subject; 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 getTel() { return tel; } public void setTel(String tel) { this.tel = tel; } public String[] getSubject() { return subject; } public void setSubject(String[] subject) { this.subject = subject; } @Override public String toString() { return "Enterprise{" + "name='" + name + '\'' + ", age=" + age + ", tel='" + tel + '\'' + ", subject=" + Arrays.toString(subject) + '}'; } }
- BooKController内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
@RestController @RequestMapping("/books") public class BookController { @Autowired private Enterprise enterprise; @GetMapping("/{id}") public String getById(@PathVariable Integer id) { System.out.println(enterprise); System.out.println(enterprise.getAge()); System.out.println(enterprise.getName()); System.out.println(enterprise.getTel()); return "hello , spring boot!"; } }
- 使用PostMan发送请求,控制台输出如下,成功获取到了数据
1 2 3 4
Enterprise{name='Helsing', age=16, tel='400-957-241', subject=[Java, Python, C#]} 16 Helsing 400-957-241
可能遇到的问题:
- 在Enterprise实体类上遇到
Spring Boot Configuration Annotation Processor not configured
警告提示
解决方案
- 在
pom.xml
中添加如下依赖即可
1 2 3 4 5
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
-
多环境配置
以后在工作中,对于开发环境、测试环境、生产环境的配置肯定都不相同,比如开发阶段会在自己的电脑上安装 mysql
,连接自己电脑上的 mysql
即可,但是项目开发完毕后要上线就需要改配置,将环境的配置改为线上环境的。
来回的修改配置会很麻烦,而 SpringBoot
给开发者提供了多环境的快捷配置,需要切换环境时只需要改一个配置即可。不同类型的配置文件多环境开发的配置都不相同,接下来对不同类型的配置文件进行说明
yaml文件
在 application.yml
中使用 ---
来分割不同的配置,内容如下
|
|
上面配置中 spring.profiles
是用来给不同的配置起名字的。而如何告知 SpringBoot
使用哪段配置呢?可以使用如下配置来启用都一段配置
|
|
综上所述,application.yml
配置文件内容如下
|
|
注意:在上面配置中给不同配置起名字的 spring.profiles
配置项已经过时。最新用来起名字的配置项是
|
|
那现在就可以尝试启用不同的环境,来观察启用端口号,证明是否真的启用了不同的环境
properties文件
properties 类型的配置文件配置多环境需要
定义不同的配置文件
application-dev.properties
是开发环境的配置文件。在该文件中配置端口号为80
|
|
application-test.properties
是测试环境的配置文件。在该文件中配置端口号为81
|
|
application-pro.properties
是生产环境的配置文件。在该文件中配置端口号为82
|
|
SpringBoot
只会默认加载名为application.properties
的配置文件,所以需要在application.properties
配置文件中设置启用哪个配置文件,配置如下:
|
|
命令行启动参数设置
使用 SpringBoot
开发的程序以后都是打成 jar
包,通过 java -jar xxx.jar
的方式启动服务的。那么就存在一个问题,如何切换环境呢?因为配置文件打到的jar包中了。
知道 jar
包其实就是一个压缩包,可以解压缩,然后修改配置,最后再打成jar包就可以了。这种方式显然有点麻烦,而 SpringBoot
提供了在运行 jar
时设置开启指定的环境的方式,如下
|
|
那么这种方式能不能临时修改端口号呢?也是可以的,可以通过如下方式
|
|
当然也可以同时设置多个配置,比如即指定启用哪个环境配置,又临时指定端口,如下
|
|
那现在命令行配置的端口号是9421,配置文件中的端口号为82,那么结果将会是多少呢?
测试后就会发现命令行设置的端口号优先级高(也就是使用的是命令行设置的端口号),配置的优先级其实 SpringBoot
官网已经进行了说明,详情参见 https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
如果使用了多种方式配合同一个配置项,优先级高的生效。
配置文件分类
有这样的场景,开发完毕后需要测试人员进行测试,由于测试环境和开发环境的很多配置都不相同,所以测试人员在运行的工程时需要临时修改很多配置,如下
|
|
针对这种情况,SpringBoot
定义了配置文件不同的放置的位置;而放在不同位置的优先级时不同的。
-
SpringBoot
中4级配置文件放置位置:- 1级:classpath:application.yml
- 2级:classpath:config/application.yml
- 3级:file :application.yml
- 4级:file :config/application.yml
说明:级别越高的优先级越高
下面验证这个优先级
1级和2级
1级就是resource目录下的 application.yml
,2级是在resource目录下新建一个config文件,在其中新建 application.yml
1级
|
|
2级
|
|
启动引导类,控制台输出的为81端口
Tomcat initialized with port(s): 81 (http)
3级和4级
先将工程打成一个jar包,进入到jar包的目录下,创建 application.yml
配置文件,而在该配合文件中将端口号设置为 82
在 jar
包所在位置创建 config
文件夹,在该文件夹下创建 application.yml
配置文件,而在该配合文件中将端口号设置为 83
3级
|
|
4级
|
|
在命令行使用以下命令运行程序
|
|
运行后日志信息如下,端口为83
Tomcat initialized with port(s): 83 (http)
通过这个结果可以得出一个结论 config
下的配置文件优先于类路径下的配置文件。
整合Junit
首先回顾一下 Spring
整合 junit
|
|
使用 @RunWith
注解指定运行器,使用 @ContextConfiguration
注解来指定配置类或者配置文件。
而 SpringBoot
整合 junit
特别简单,分为以下三步完成
- 在测试类上添加
SpringBootTest
注解 - 使用
@Autowired
注入要测试的资源 - 定义测试方法进行测试
环境准备
- 创建一个新的SpringBoot工程
- 在com.mtmn.service包下创建BookService接口
|
|
- 在com.mtmn.service.impl包下创建BookService接口的实现类,并重写其方法
|
|
编写测试类
在 test/java
下创建 com.mtmn
包,在该包下创建测试类,将 BookService
注入到该测试类中
|
|
运行测试方法,控制台成功输出
book service is running …
注意:这里的引导类所在包必须是测试类所在包及其子包。
例如:
- 引导类所在包是
com.mtmn
- 测试类所在包是
com.mtmn
如果不满足这个要求的话,就需要在使用 @SpringBootTest
注解时,使用 classes
属性指定引导类的字节码对象。如 @SpringBootTest(classes = XxxApplication.class)
整合Mybatis
回顾Spring整合MyBatis
之前Spring整合MyBatis时,需要定义很多配置类
- SpringConfig配置类
|
|
- 导入JdbcConfig配置类
|
|
- 导入MyBatisConfig配置类
- 定义
SqlSessionFactoryBean
- 定义映射配置
- 定义
|
|
SpringBoot整合MyBatis
- 创建一个新的模块
注意选择技术集的时候,要勾选
MyBatis Framework
和MySQL Driver
- 建库建表
|
|
- 定义实体类
|
|
- 定义dao接口 在com.mtmn.dao包下定义BookDao接口
|
|
- 定义测试类
|
|
- 编写配置
|
|
- 测试
运行测试方法,会报错
No qualifying bean of type 'com.mtmn.dao.BookDao'
,没有类型为“com.mtmn.dao.BookDao”的限定bean 为什么会出现这种情况呢?之前在配置MyBatis时,配置了如下内容
|
|
Mybatis
会扫描接口并创建接口的代码对象交给 Spring
管理,但是现在并没有告诉 Mybatis
哪个是 dao
接口。
而要解决这个问题需要在 BookDao
接口上使用 @Mapper
,BookDao
接口修改为
|
|
注意:
SpringBoot
版本低于2.4.3(不含),Mysql驱动版本大于8.0时,需要在url连接串中配置时区 jdbc:mysql:///learn?serverTimezone=UTC
,或在MySQL数据库端配置时区解决此问题
- 使用Druid数据源
现在并没有指定数据源,
SpringBoot
有默认的数据源,也可以指定使用Druid
数据源,按照以下步骤实现
|
|
- 在
application.yml
修改配置文件配置 可以通过spring.datasource.type
来配置使用什么数据源。配置文件内容可以改进为
|
|
案例
- pom.xml
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.mtmn</groupId> <artifactId>LearnSB</artifactId> <version>0.0.1-SNAPSHOT</version> <name>LearnSB</name> <description>LearnSB</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.7.6</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <mainClass>com.mtmn.LearnSbApplication</mainClass> <skip>true</skip> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
- domain.Book
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
package com.mtmn.domain; import java.util.Objects; public class Book { private Integer id; private String type; private String name; private String description; public Book() { } public Book(Integer id, String type, String name, String description) { this.id = id; this.type = type; this.name = name; this.description = description; } @Override public boolean equals(Object o) { if (this == o) { return true; }; if (o == null || getClass() != o.getClass()) { return false; }; Book book = (Book) o; return Objects.equals(id, book.id) && Objects.equals(type, book.type) && Objects.equals(name, book.name) && Objects.equals(description, book.description); } @Override public int hashCode() { return Objects.hash(id, type, name, description); } @Override public String toString() { return "Book{" + "id=" + id + ", type='" + type + '\'' + ", name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
- domain.Code
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
package com.mtmn.domain; public class Code { public static final Integer SAVE_OK = 20011; public static final Integer UPDATE_OK = 20021; public static final Integer DELETE_OK = 20031; public static final Integer GET_OK = 20041; public static final Integer SAVE_ERR = 20010; public static final Integer UPDATE_ERR = 20020; public static final Integer DELETE_ERR = 20030; public static final Integer GET_ERR = 20040; public static final Integer SYSTEM_ERR = 50001; public static final Integer SYSTEM_TIMEOUT_ERR = 50002; public static final Integer SYSTEM_UNKNOW_ERR = 59999; public static final Integer BUSINESS_ERR = 60001; }
- domain.Result
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
package com.mtmn.domain; public class Result { //描述统一格式中的编码,用于区分操作,可以简化配置0或1表示成功失败 private Integer code; //描述统一格式中的数据 private Object data; //描述统一格式中的消息,可选属性 private String msg; public Result() { } //构造器可以根据自己的需要来编写 public Result(Integer code, Object data) { this.code = code; this.data = data; } public Result(Integer code, Object data, String msg) { this.code = code; this.data = data; this.msg = msg; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return "Result{" + "code=" + code + ", data=" + data + ", msg='" + msg + '\'' + '}'; } }
- service.BookService
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
package com.mtmn.service; import com.mtmn.domain.Book; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Transactional public interface BookService { /** * 保存 * @param book * @return */ boolean save(Book book); /** * 修改 * @param book * @return */ boolean update(Book book); /** * 按id删除 * @param id * @return */ boolean delete(Integer id); /** * 按id查询 * @param id * @return */ Book getById(Integer id); /** * 查询所有 * @return */ List<Book> getAll(); }
- service.impl.BookServiceImpl
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
package com.mtmn.service.impl; import com.mtmn.dao.BookDao; import com.mtmn.domain.Book; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.PathVariable; import com.mtmn.service.BookService; import java.util.List; @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override public boolean save(Book book) { return bookDao.save(book) > 0; } @Override public boolean update(Book book) { return bookDao.update(book) > 0; } @Override public boolean delete(@PathVariable Integer id) { return bookDao.delete(id) > 0; } @Override public Book getById(Integer id) { return bookDao.getById(id); } @Override public List<Book> getAll() { return bookDao.getAll(); } }
- dao.BookDao
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
package com.mtmn.dao; import com.mtmn.domain.Book; import org.apache.ibatis.annotations.*; import java.util.List; @Mapper public interface BookDao { @Insert(value = "insert into tbl_book values (null, #{type}, #{name}, #{description})") int save(Book book); @Update(value = "update tbl_book set type=#{type}, name=#{name}, description=#{description} where id=#{id}") int update(Book book); @Delete(value = "delete from tbl_book where id=#{id}") int delete(Integer id); @Select(value = "select * from tbl_book") List<Book> getAll(); @Select(value = "select * from tbl_book where id=#{id}") Book getById(Integer id); }
- controller.BookContorller
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
package com.mtmn.contorller; import com.mtmn.domain.Book; import com.mtmn.domain.Code; import com.mtmn.domain.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import com.mtmn.service.BookService; import java.util.List; @RestController @RequestMapping("/books") public class BookController { @Autowired private BookService bookService; @PostMapping public Result save(@RequestBody Book book) { boolean flag = bookService.save(book); return new Result(flag ? Code.SAVE_OK : Code.SAVE_ERR, flag); } @PutMapping public Result update(@RequestBody Book book) { boolean flag = bookService.update(book); return new Result(flag ? Code.UPDATE_OK : Code.UPDATE_ERR, flag); } @DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) { boolean flag = bookService.delete(id); return new Result(flag ? Code.DELETE_OK : Code.DELETE_ERR, flag); } @GetMapping("/{id}") public Result getById(@PathVariable Integer id) { Book book = bookService.getById(id); Integer code = book == null ? Code.GET_ERR : Code.GET_OK; String msg = book == null ? "数据查询失败,请重试!" : ""; return new Result(code, book, msg); } @GetMapping public Result getAll() { List<Book> books = bookService.getAll(); Integer code = books == null ? Code.GET_ERR : Code.GET_OK; String msg = books == null ? "数据查询失败,请重试!" : ""; return new Result(code, books, msg); } }
- contorller.ProjectExceptionAdvice
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
package com.mtmn.contorller; import com.mtmn.domain.Code; import com.mtmn.domain.Result; import com.mtmn.exception.BusinessException; import com.mtmn.exception.SystemException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class ProjectExceptionAdvice { @ExceptionHandler(Exception.class) public Result doException(Exception ex) { return new Result(Code.SYSTEM_UNKNOW_ERR, null, "系统繁忙,请稍后再试!"); } @ExceptionHandler(SystemException.class) public Result doSystemException(SystemException ex) { return new Result(ex.getCode(), null, ex.getMessage()); } @ExceptionHandler(BusinessException.class) public Result doBusinessException(BusinessException ex) { return new Result(ex.getCode(), null, ex.getMessage()); } }
- exception.BusinessException
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
package com.mtmn.exception; public class BusinessException extends RuntimeException{ private Integer code; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public BusinessException() { } public BusinessException(Integer code) { this.code = code; } public BusinessException(Integer code, String message) { super(message); this.code = code; } public BusinessException(Integer code, String message, Throwable cause) { super(message, cause); this.code = code; } }
- exception.SystemException
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
package com.mtmn.exception; public class SystemException extends RuntimeException { private Integer code; public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public SystemException() { } public SystemException(Integer code) { this.code = code; } public SystemException(Integer code, String message) { super(message); this.code = code; } public SystemException(Integer code, String message, Throwable cause) { super(message, cause); this.code = code; } }
- static/pages/books.html
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
<!DOCTYPE html> <html> <head> <!-- 页面meta --> <meta charset="utf-8"> <title>SpringMVC案例</title> <!-- 引入样式 --> <link rel="stylesheet" href="../plugins/elementui/index.css"> <link rel="stylesheet" href="../plugins/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="../css/style.css"> </head> <body class="hold-transition"> <div id="app"> <div class="content-header"> <h1>图书管理</h1> </div> <div class="app-container"> <div class="box"> <div class="filter-container"> <el-input placeholder="图书名称" style="width: 200px;" class="filter-item"></el-input> <el-button class="dalfBut">查询</el-button> <el-button type="primary" class="butT" @click="openSave()">新建</el-button> </div> <el-table size="small" current-row-key="id" :data="dataList" stripe highlight-current-row> <el-table-column type="index" align="center" label="序号"></el-table-column> <el-table-column prop="type" label="图书类别" align="center"></el-table-column> <el-table-column prop="name" label="图书名称" align="center"></el-table-column> <el-table-column prop="description" label="描述" align="center"></el-table-column> <el-table-column label="操作" align="center"> <template slot-scope="scope"> <el-button type="primary" size="mini" @click="openEdit(scope.row)">编辑</el-button> <el-button size="mini" type="danger" @click="deleteBook(scope.row)">删除</el-button> </template> </el-table-column> </el-table> <div class="pagination-container"> <el-pagination class="pagiantion" @current-change="handleCurrentChange" :current-page="pagination.currentPage" :page-size="pagination.pageSize" layout="total, prev, pager, next, jumper" :total="pagination.total"> </el-pagination> </div> <!-- 新增标签弹层 --> <div class="add-form"> <el-dialog title="新增图书" :visible.sync="dialogFormVisible"> <el-form ref="dataAddForm" :model="formData" :rules="rules" label-position="right" label-width="100px"> <el-row> <el-col :span="12"> <el-form-item label="图书类别" prop="type"> <el-input v-model="formData.type"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="图书名称" prop="name"> <el-input v-model="formData.name"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="描述"> <el-input v-model="formData.description" type="textarea"></el-input> </el-form-item> </el-col> </el-row> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogFormVisible = false">取消</el-button> <el-button type="primary" @click="saveBook()">确定</el-button> </div> </el-dialog> </div> </div> <!-- 编辑标签弹层 --> <div class="add-form"> <el-dialog title="编辑检查项" :visible.sync="dialogFormVisible4Edit"> <el-form ref="dataEditForm" :model="formData" :rules="rules" label-position="right" label-width="100px"> <el-row> <el-col :span="12"> <el-form-item label="图书类别" prop="type"> <el-input v-model="formData.type"/> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="图书名称" prop="name"> <el-input v-model="formData.name"/> </el-form-item> </el-col> </el-row> <el-row> <el-col :span="24"> <el-form-item label="描述"> <el-input v-model="formData.description" type="textarea"></el-input> </el-form-item> </el-col> </el-row> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="dialogFormVisible4Edit = false">取消</el-button> <el-button type="primary" @click="handleEdit()">确定</el-button> </div> </el-dialog> </div> </div> </div> </body> <!-- 引入组件库 --> <script src="../js/vue.js"></script> <script src="../plugins/elementui/index.js"></script> <script type="text/javascript" src="../js/jquery.min.js"></script> <script src="../js/axios-0.18.0.js"></script> <script> var vue = new Vue({ el: '#app', data: { dataList: [],//当前页要展示的分页列表数据 formData: {},//表单数据 dialogFormVisible: false,//增加表单是否可见 dialogFormVisible4Edit: false,//编辑表单是否可见 pagination: {},//分页模型数据,暂时弃用 }, //钩子函数,VUE对象初始化完成后自动执行 created() { this.getAll(); }, methods: { // 重置表单 resetForm() { this.formData = {}; }, // 弹出添加窗口 openSave() { this.dialogFormVisible = true; //每次弹出表单的时候,都重置一下数据 this.resetForm(); }, //添加 saveBook() { axios.post("/books",this.formData).then((res)=>{ //20011是成功的状态码,成功之后就关闭对话框,并显示添加成功 if (res.data.code === 20011){ this.dialogFormVisible = false; this.$message.success("添加成功") //20010是失败的状态码,失败后给用户提示信息 }else if(res.data.code === 20010){ this.$message.error("添加失败"); //如果前两个都不满足,那就是SYSTEM_UNKNOW_ERR,未知异常了,显示未知异常的错误提示信息安抚用户情绪 }else { this.$message.error(res.data.msg); } }).finally(()=>{ this.getAll(); }) }, //主页列表查询 getAll() { axios.get("/books").then((res)=>{ this.dataList = res.data.data; }) }, openEdit(row) { axios.get("/books/" + row.id).then((res) => { if (res.data.code === 20041) { this.formData = res.data.data; this.dialogFormVisible4Edit = true; } else { this.$message.error(res.data.msg); } }); }, deleteBook(row) { this.$confirm("此操作永久删除当前数据,是否继续?","提示",{ type:'info' }).then(()=> { axios.delete("/books/" + row.id).then((res) => { if (res.data.code === 20031) { this.$message.success("删除成功") } else if (res.data.code === 20030) { this.$message.error("删除失败") } }).finally(() => { this.getAll(); }); }).catch(()=>{ this.$message.info("取消删除操作") }) }, handleEdit() { axios.put("/books", this.formData).then((res) => { if (res.data.code === 20021) { this.dialogFormVisible4Edit = false; this.$message.success("修改成功") } else if (res.data.code === 20020) { this.$message.error("修改失败") } else { this.$message.error(res.data.msg); } }).finally(() => { this.getAll(); }); } } }) </script> </html>
Mybatis-Plus
MyBatisPlus入门案例与简介
入门案例
- MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发,提高效率
- 开发方式
- 基于MyBatis使用MyBatisPlus
- 基于Spring使用MyBatisPlus
- 基于SpringBoot使用MyBatisPlus(重点)
由于刚刚才学完SpringBoot,所以现在直接使用SpringBoot来构建项目,官网的快速开始也是直接用的SpringBoot
步骤一:
创建数据库和表
|
|
步骤二:
创建SpringBoot工程 只需要勾选MySQL,不用勾选MyBatis了步骤三:
补全依赖 导入德鲁伊和MyBatisPlus的坐标
|
|
步骤四:
编写数据库连接四要素 还是将application的后缀名改为yml,以后配置都是用yml来配置 注意要设置一下时区,不然可能会报错(指高版本的mysql)
|
|
步骤五:
根据数据表来创建对应的模型类 注意id是Long类型,至于为什么是Long,接着往下看
|
|
步骤六:
创建dao接口
|
|
只需要在类上方加一个 @Mapper
注解,同时继承 BaseMapper<>
,泛型写创建的模型类的类型
然后这样就能完成单表的CRUD了
步骤七:
测试 以后连简单的CRUD都不用写了 SpringBoot的测试类也是简单的一批,只需要一个@SpringBootTest
注解就能完成(创建SpringBoot工程的时候已经帮自动弄好了) 测试类里需要什么东西就用@Autowired
自动装配,测试方法上用@Test
注解
|
|
selectList() 方法的参数为 MP 内置的条件封装器 Wrapper,所以不填写就是无任何条件
现在运行测试方法,看看控制台
User{id=1, name=‘Tom’, password=‘tom’, age=3, tel=‘18866668888’} User{id=2, name=‘Jerry’, password=‘jerry’, age=4, tel=‘16688886666’} User{id=3, name=‘Jock’, password=‘123456’, age=41, tel=‘18812345678’} User{id=4, name=‘略略略’, password=‘nigger’, age=15, tel=‘4006184000’}
MyBatisPlus简介
MyBatisPlus的官网为:https://mp.baomidou.com/ ,没错就是个拼音,苞米豆,因为域名被抢注了,但是粉丝也捐赠了一个 https://mybatis.plus 域名
MP旨在成为MyBatis的最好搭档,而不是替换掉MyBatis,从名称上来看也是这个意思,一个MyBatis的plus版本,在原有的MyBatis上做增强,其底层仍然是MyBatis的东西,所以当然也可以在MP中写MyBatis的内容
对于MP的深入学习,可以多看看官方文档,锻炼自己自学的能力,毕竟不是所有知识都有像这样的网课,更多的还是自己看文档,挖源码。
MP的特性:
无侵入:
只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑损耗小:
启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作强大的 CRUD 操作:
内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求支持 Lambda 形式调用
:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错支持主键自动生成:
支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题支持 ActiveRecord 模式:
支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作支持自定义全局通用操作:
支持全局通用方法注入( Write once, use anywhere )内置代码生成器:
采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用内置分页插件:
基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询分页插件支持多种数据库:
支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库内置性能分析插件:
可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询内置全局拦截插件:
提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
小结
SpringBoot集成MyBatisPlus非常的简单,只需要导入 MyBatisPlus
的坐标,然后令dao类继承 BaseMapper
,写上泛型,类上方加 @Mapper
注解
可能存在的疑问:
- 我甚至都没写在哪个表里查,为什么能自动识别是在我刚刚创建的表里查?
- 注意创建的表,和对应的模型类,是同一个名,默认情况是在同名的表中查找
- 那我要是表明和模型类的名不一样,那咋整?
- 在模型类的上方加上
@TableName
注解- 例如数据表叫
tb_user
但数据类叫User
,那么就在User类上加@TableName("tb_user")
注解
- 例如数据表叫
- 在模型类的上方加上
标准数据层开发
标准的CRUD使用
先来看看MP给提供了哪些方法
功能 | 自定义接口 | MP接口 |
---|---|---|
新增 | boolean save(T t) | int insert(T t) |
删除 | boolean delete(int id) | int deleteById(Serializable id) |
修改 | boolean update(T t) | int updateById(T t) |
根据id查询 | T getById(int id) | T selectById(Serializable id) |
查询全部 | List<T> getAll() |
List<T> selectList() |
分页查询 | PageInfo<T> getAll(int page,int size) |
IPage<T> selectPage(IPage<T> page) |
按条件查询 | List<T> getAll(Condition condition) |
IPage<T> selectPage(Wrapper<T> queryWrapper) |
新增
|
|
参数类型是泛型,也就是当初继承BaseMapper的时候,填的泛型,返回值是int类型,0代表添加失败,1代表添加成功
|
|
随便写一个User的数据,运行程序,然后去数据库看看新增是否成功
1572364408896622593 Seto MUSICIAN 23 4005129421
这个主键自增id看着有点奇怪,但现在你知道为什么要将id设为long类型了吧
删除
|
|
- 参数类型为什么是一个序列化类
Serializable
- 通过查看String的源码,你会发现String实现了Serializable接口,而且Number类也实现了Serializable接口
- Number类又是Float,Double,Long等类的父类
- 那现在能作为主键的数据类型,都已经是Serializable类型的子类了
- MP使用Serializable类型当做参数类型,就好比用Object类型来接收所有类型一样
- 返回值类型是int
- 数据删除成功返回1
- 未删除数据返回0。
- 那下面就来删除刚刚添加的数据,注意末尾加个L
|
|
删除完毕之后,刷新数据库,看看是否删除成功
修改
|
|
- T:泛型,需要修改的数据内容,注意因为是根据ID进行修改,所以传入的对象中需要有ID属性值
- int:返回值
- 修改成功后返回1
- 未修改数据返回0
|
|
修改功能只修改指定的字段,未指定的字段保持原样
根据ID查询
|
|
- Serializable:参数类型,主键ID的值
- T:根据ID查询只会返回一条数据
|
|
控制台输出如下
User(id=1, name=Alen, password=tom, age=3, tel=18866668888)
查询全部
|
|
- Wrapper:用来构建条件查询的条件,目前没有可直接传为Null
|
|
控制台输出如下
User(id=1, name=Alen, password=tom, age=3, tel=18866668888) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=3, name=Jock, password=123456, age=41, tel=18812345678) User(id=4, name=传智播客, password=itcast, age=15, tel=4006184000)
- 方法都测试完了,那你们有没有想过,这些方法都是谁提供的呢?
- 想都不用想,肯定是当初继承的
BaseMapper
,里面的方法还有很多,后面再慢慢学习
- 想都不用想,肯定是当初继承的
Lombok
- 代码写到这,发现之前的dao接口,都不用自己写了,只需要继承BaseMapper,用他提供的方法就好了
- 但是现在我还想偷点懒,毕竟懒是第一生产力,之前手写模型类的时候,创建好对应的属性,然后用IDEA的Alt+Insert快捷键,快速生成get和set方法,toSring,各种构造器(有需要的话)等
- U1S1项目做这么久,写模型类都给我写烦了,有没有更简单的方式呢?
- 答案当然是有的,可以使用Lombok,一个Java类库,提供了一组注解,来简化的POJO模型类开发
具体步骤如下
步骤一:
添加Lombok依赖
|
|
版本不用写,SpringBoot中已经管理了lombok的版本,
-
步骤二:
在模型类上添加注解Lombok常见的注解有:
@Setter:
为模型类的属性提供setter方法@Getter:
为模型类的属性提供getter方法@ToString:
为模型类的属性提供toString方法@EqualsAndHashCode:
为模型类的属性提供equals和hashcode方法@Data:
是个组合注解,包含上面的注解的功能@NoArgsConstructor:
提供一个无参构造函数@AllArgsConstructor:
提供一个包含所有参数的构造函数
1 2 3 4 5 6 7 8 9 10
@Data @AllArgsConstructor @NoArgsConstructor public class User { private Long id; private String name; private String password; private Integer age; private String tel; }
说明:Lombok只是简化模型类的编写,之前的方法也能用 例如你有特殊的构造器需求,只想要name和password这两个参数,那么可以手写一个
|
|
分页功能
基础的增删改查功能就完成了,现在进行分页功能的学习
|
|
- IPage用来构建分页查询条件
- Wrapper:用来构建条件查询的条件,暂时没有条件可以传一个null
- 返回值IPage是什么意思,后面会说明
具体的使用步骤如下
步骤一:
调用方法传入参数获取返回值
|
|
步骤二:
设置分页拦截器
它能够自动拦截查询请求,并在SQL语句中加入分页逻辑,从而返回特定页的数据
|
|
步骤三:
运行测试程序 运行程序,结果如下,符合的预期
当前页码1 本页条数3 总页数2 总条数5 [User(id=1, name=Alen, password=tom, age=3, tel=18866668888), User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666), User(id=3, name=Jock, password=123456, age=41, tel=18812345678)]
DQL编程控制
增删改查四个操作中,查询是非常重要的也是非常复杂的操作,这部分主要学习的内容有:
- 条件查询方式
- 查询投影
- 查询条件设定
- 字段映射与表名映射
条件查询
条件查询的类
- MP将复杂的SQL查询语句都做了封装,使用编程的方式来完成查询条件的组合
- 之前在写CRUD时,都看到了一个Wrapper类,当初都是赋一个null值,但其实这个类就是用来查询的
构建条件查询
QueryWrapper
小于用lt,大于用gt 回想之前在html页面中,如果需要用到小于号或者大于号,需要用对应的html实体来替换 小于号的实体是<
,大于号的实体是>
|
|
运行测试方法,结果如下
[User(id=1, name=Alen, password=tom, age=3, tel=18866668888), User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666), User(id=4, name=kyle, password=cyan, age=15, tel=4006184000)]
这种方法有个弊端,那就是字段名是字符串类型,没有提示信息和自动补全,如果写错了,那就查不出来
QueryWrapper
的基础上,使用lambda
|
|
LambdaQueryWrapper
方式二解决了方式一的弊端,但是要多些一个lambda(),那方式三就来解决方式二的弊端,使用LambdaQueryWrapper,就可以不写lambda()
|
|
多条件查询
上面三种都是单条件的查询,那现在想进行多条件的查询,该如何编写代码呢?
需求:查询表中年龄在10~30岁的用户信息
|
|
构建多条件的时候,还可以使用链式编程
|
|
-
可能存在的疑问
- MP怎么就知道你这俩条件是AND的关系呢,那我要是想用OR的关系,该咋整
-
解答
- 默认就是AND的关系,如果需要OR关系,用or()链接就可以了
1
lqw.gt(User::getAge, 10).or().lt(User::getAge, 30);
需求:查询年龄小于10,或者年龄大于30的用户信息
|
|
null值判定
- 在做条件查询的时候,一般都会有很多条件供用户查询
- 这些条件用户可以选择用,也可以选择不用
- 之前是通过动态SQL来实现的
|
|
- 那现在试试在MP里怎么写
需求:查询数据库表中,根据输入年龄范围来查询符合条件的记录 用户在输入值的时候, 如果只输入第一个框,说明要查询大于该年龄的用户 如果只输入第二个框,说明要查询小于该年龄的用户 如果两个框都输入了,说明要查询年龄在两个范围之间的用户
-
问题一:后台如果想接收前端的两个数据,该如何接收?
- 可以使用两个简单数据类型,也可以使用一个模型类,但是User类中目前只有一个age属性
1 2 3 4 5 6 7 8 9
@TableName("tb_user") @Data public class User { private Long id; private String name; private String password; private Integer age; private String tel; }
-
使用一个age属性,如何去接收页面上的两个值呢?这个时候有两个解决方案
- 方案一:添加属性age2,这种做法可以但是会影响到原模型类的属性内容
- 方案二:新建一个模型类,让其继承User类,并在其中添加age2属性,UserQuery在拥有User属性后同时添加了age2属性。
1 2 3 4 5
@Data @TableName("tb_user") public class UserQuery extends User{ private Integer age2; }
-
环境准备好后,实现下刚才的需求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
@Test void testQueryWrapper() { LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>(); UserQuery uq = new UserQuery(); uq.setAge(10); uq.setAge2(30); if (null != uq.getAge()) { lqw.gt(User::getAge, uq.getAge()); } if (null != uq.getAge2()) { lqw.lt(User::getAge, uq.getAge2()); } for (User user : userDao.selectList(lqw)) { System.out.println(user); } }
- 上面的写法可以完成条件为非空的判断,但是问题很明显,如果条件多的话,每个条件都需要判断,代码量就比较大,来看MP给提供的简化方式
- lt还有一个重载的方法,当condition为true时,添加条件,为false时,不添加条件
1 2 3
public Children lt(boolean condition, R column, Object val) { return this.addCondition(condition, column, SqlKeyword.LT, val); }
- 故可以把if的判断操作,放到lt和gt方法中当做参数来写
1 2 3 4 5 6 7 8 9 10 11 12
@Test void testQueryWrapper() { LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>(); UserQuery uq = new UserQuery(); uq.setAge(10); uq.setAge2(30); lqw.gt(null != uq.getAge(), User::getAge, uq.getAge()) .lt(null != uq.getAge2(), User::getAge, uq.getAge2()); for (User user : userDao.selectList(lqw)) { System.out.println(user); } }
查询投影
查询指定字段
目前在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,所说的查询投影即不查询所有字段,只查询出指定内容的数据。
具体如何来实现?
|
|
select(…)方法用来设置查询的字段列,可以设置多个
|
|
控制台输出如下
User(id=null, name=Alen, password=null, age=null, tel=null) User(id=null, name=Jerry, password=null, age=null, tel=null) User(id=null, name=Jock, password=null, age=null, tel=null) User(id=null, name=kyle, password=null, age=null, tel=null) User(id=null, name=Seto, password=null, age=null, tel=null)
如果使用的不是lambda,就需要手动指定字段
|
|
聚合查询
需求:聚合函数查询,完成count、max、min、avg、sum的使用
- count:总记录数
- max:最大值
- min:最小值
- avg:平均值
- sum:求和
- count
- max
- min
- avg
- sum
|
|
控制台输出
{count=5}
分组查询
|
|
控制台输出如下
{maxAge=3} {maxAge=4} {maxAge=41} {maxAge=15} {maxAge=23}
注意:
- 聚合与分组查询,无法使用lambda表达式来完成
- MP只是对MyBatis的增强,如果MP实现不了,可以直接在DAO接口中使用MyBatis的方式实现
查询条件
前面只使用了lt()和gt(),除了这两个方法外,MP还封装了很多条件对应的方法
- 范围匹配(> 、 = 、between)
- 模糊匹配(like)
- 空判定(null)
- 包含性匹配(in)
- 分组(group)
- 排序(order)
- ……
等值查询
需求:根据用户名和密码查询用户信息
|
|
控制台输出如下
User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421)
- eq(): 相当于
=
,对应的sql语句为
|
|
- selectList:查询结果为多个或者单个
- selectOne:查询结果为单个
范围查询
需求:对年龄进行范围查询,使用lt()、le()、gt()、ge()、between()进行范围查询
|
|
控制台输出如下
User(id=4, name=kyle, password=cyan, age=15, tel=4006184000) User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421)
- gt():大于(>)
- ge():大于等于(>=)
- lt():小于(<)
- lte():小于等于(<=)
- between():between ? and ?
模糊查询
需求:查询表中name属性的值以 J
开头的用户信息,使用like进行模糊查询
|
|
控制台输出如下
User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=3, name=Jock, password=123456, age=41, tel=18812345678)
- like():前后加百分号,如 %J%,相当于包含J的name
- likeLeft():前面加百分号,如 %J,相当于J结尾的name
- likeRight():后面加百分号,如 J%,相当于J开头的name
需求:查询表中name属性的值包含 e
的用户信息,使用like进行模糊查询
|
|
控制台输出如下
User(id=1, name=Alen, password=tom, age=3, tel=18866668888) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=4, name=kyle, password=cyan, age=15, tel=4006184000) User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421)
排序查询
需求:查询所有数据,然后按照age降序
|
|
控制台输出如下
User(id=3, name=Jock, password=123456, age=41, tel=18812345678) User(id=1572385590169579521, name=Seto, password=MUSICIAN, age=23, tel=4005129421) User(id=4, name=kyle, password=cyan, age=15, tel=4006184000) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=1, name=Alen, password=tom, age=3, tel=18866668888)
遇到想用的功能,先自己用一个试试,方法名和形参名都很见名知意,遇到不确定的用法,再去官方文档查阅资料
映射匹配兼容性
在上面的案例中,做查询的时候,数据表中的字段名与模型类中的属性名一致,查询的时候没有问题,那么问题就来了
-
问题一:
表字段与模型类编码属性不一致
- 当表的列名和模型类的属性名发生不一致,就会导致数据封装不到模型对象,这个时候就需要其中一方做出修改,那如果前提是两边都不能改又该如何解决?
- MP给提供了一个注解
@TableField
,使用该注解可以实现模型类属性名和表的列名之间的映射关系 - 例如表中密码字段为
pwd
,而模型类属性名为password
,那就可以用@TableField
注解来实现他们之间的映射关系
1 2 3 4 5 6 7 8 9 10
@TableName("tb_user") @Data public class User { private Long id; private String name; @TableField("pwd") private String password; private Integer age; private String tel; }
MyBatis 并没有提供类似 MyBatis-Plus 中的
@TableField
注解。对于字段映射,MyBatis 通常通过 XML 映射文件或注解来定义。例如,使用 XML 映射文件:
1 2 3 4
<resultMap id="UserResultMap" type="User"> <result property="id" column="user_id"/> <result property="password" column="pwd"/> </resultMap>
或者使用注解:
1 2 3 4 5 6
@Select("SELECT user_id, pwd FROM user WHERE user_id = #{id}") @Results({ @Result(property = "id", column = "user_id"), @Result(property = "password", column = "pwd") }) User selectUserById(int id);
-
问题二:
编码中添加了数据库中未定义的属性
- 当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:
Unknown column '多出来的字段名称' in 'field list'
- 具体的解决方案用到的还是
@TableField
注解,它有一个属性叫exist
,设置该字段是否在数据库表中存在,如果设置为false则不存在,生成sql语句查询的时候,就不会再查询该字段了。
1 2 3 4 5 6 7 8 9 10 11 12
@TableName("tb_user") @Data public class User { private Long id; private String name; @TableField("pwd") private String password; private Integer age; private String tel; @TableField(exist = false) private Integer online; }
- 当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:
-
问题三:
采用默认查询开放了更多的字段查看权限
- 查询表中所有的列的数据,就可能把一些敏感数据查询到返回给前端,这个时候就需要限制哪些字段默认不要进行查询。解决方案是
@TableField
注解的一个属性叫select
,该属性设置默认是否需要查询该字段的值,true(默认值)表示默认查询该字段,false表示默认不查询该字段。 - 例如像密码这种的敏感字段,不应该查询出来作为JSON返回给前端,不安全
1 2 3 4 5 6 7 8 9 10 11 12
@TableName("tb_user") @Data public class User { private Long id; private String name; @TableField(value = "pwd",select = false) private String password; private Integer age; private String tel; @TableField(exist = false) private Integer online; }
- 查询表中所有的列的数据,就可能把一些敏感数据查询到返回给前端,这个时候就需要限制哪些字段默认不要进行查询。解决方案是
知识点:@TableField
名称 | @TableField |
---|---|
类型 | 属性注解 |
位置 | 模型类属性定义上方 |
作用 | 设置当前属性对应的数据库表中的字段关系 |
相关属性 | value(默认):设置数据库表字段名称 exist:设置属性在数据库表字段中是否存在,默认为true,此属性不能与value合并使用 select:设置属性是否参与查询,此属性与select()映射配置不冲突 |
-
问题四:
表名与编码开发设计不同步
- 这个问题其实在一开始就解决过了,现在再来回顾一遍
- 该问题主要是表的名称和模型类的名称不一致,导致查询失败,这个时候通常会报如下错误信息
Table 'databaseName.tableNaem' doesn't exist
- 解决方案是使用MP提供的另外一个注解
@TableName
来设置表与模型类之间的对应关系。
知识点:@TableName
名称 | @TableName |
---|---|
类型 | 类注解 |
位置 | 模型类定义上方 |
作用 | 设置当前类对应于数据库表关系 |
相关属性 | value(默认):设置数据库表名称 |
DML编程控制
查询相关的操作已经介绍完了,紧接着需要对另外三个,增删改进行内容的讲解。挨个来说明下,首先是新增(insert)中的内容。
id生成策略控制
前面在新增数据的时候,主键ID是一个很长的Long类型,现在想要主键按照数据表字段进行自增长,在解决这个问题之前,先来分析一下ID的生成策略
- 不同的表,应用不同的id生成策略
- 日志:自增(1 2 3 4)
- 购物订单:特殊规则(线下购物发票,下次可以留意一下)
- 外卖订单:关联地区日期等信息(这个我熟,举个例子10 04 20220921 13 14,例如10表示北京市,04表示朝阳区,20220921表示日期等)
- 关系表:可以省略ID
- ……
- 不同的业务采用的ID生成方式应该是不一样的,那么在MP中都提供了哪些主键生成策略,以及该如何进行选择?
- 在这里又需要用到MP的一个注解叫
@TableId
- 在这里又需要用到MP的一个注解叫
知识点:@TableId
名称 | @TableId |
---|---|
类型 | 属性注解 |
位置 | 模型类中用于表示主键的属性定义上方 |
作用 | 设置当前类中主键属性的生成策略 |
相关属性 | value(默认):设置数据库表主键名称 type:设置主键属性的生成策略,值查照IdType的枚举值 |
AUTO策略
步骤一:
设置生成策略为AUTO
|
|
步骤二:
设置自动增量为5,将4之后的数据都删掉,防止影响的结果步骤三:
运行新增方法
|
|
会发现,新增成功,并且主键id也是从5开始
进入源码来看看还有什么生成策略
|
|
- NONE: 不设置id生成策略
- INPUT:用户手工输入id
- ASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)
- ASSIGN_UUID:以UUID生成算法作为id生成策略
- 其他的几个策略均已过时,都将被ASSIGN_ID和ASSIGN_UUID代替掉。
拓展: 分布式ID是什么?
- 当数据量足够大的时候,一台数据库服务器存储不下,这个时候就需要多台数据库服务器进行存储
- 比如订单表就有可能被存储在不同的服务器上
- 如果用数据库表的自增主键,因为在两台服务器上所以会出现冲突
- 这个时候就需要一个全局唯一ID,这个ID就是分布式ID。
INPUT策略
步骤一:
将ID生成策略改为INPUT
|
|
步骤二:
运行新增方法 注意这里需要手动设置ID了
|
|
查看数据库,ID确实是设置的值
ASSIGN_ID策略
步骤一:
设置生成策略为ASSIGN_ID
|
|
步骤二:
运行新增方法 这里就不要手动设置ID了
|
|
查看结果,生成的ID就是一个Long类型的数据,生成ID时,使用的是雪花算法
雪花算法(SnowFlake),是Twitter官方给出的算法实现 是用Scala写的。其生成的结果是一个64bit大小整数
- 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
- 41bit-时间戳,用来记录时间戳,毫秒级
- 10bit-工作机器id,用来记录工作机器id,其中高位5bit是数据中心ID其取值范围0-31,低位5bit是工作节点ID其取值范围0-31,两个组合起来最多可以容纳1024个节点
- 序列号占用12bit,每个节点每毫秒0开始不断累加,最多可以累加到4095,一共可以产生4096个ID
ASSIGN_UUID策略
步骤一:
设置生成策略为ASSIGN_UUID
|
|
步骤二:
修改表的主键类型 主键类型设置为varchar,长度要大于32,因为UUID生成的主键为32位,如果长度小的话就会导致插入失败。步骤三:
运行新增方法
|
|
ID生成策略对比
介绍完了这些主键ID的生成策略,那么以后开发用哪个呢?
- NONE:不设置ID生成策略,MP不自动生成,约定于INPUT,所以这两种方式都需要用户手动设置(SET方法),但是手动设置的第一个问题就是容易出错,加了相同的ID造成主键冲突,为了保证主键不冲突就得做很多判定,实现起来较为复杂
- AUTO:数据库ID自增,这种策略适合在数据库服务器只有一台的情况下使用,不可作为分布式ID使用
- ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证ID唯一,但是生成的主键是32位的字符串,长度过长占用空间,而且不能排序,查询性能也慢
- ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序,性能也高,但是生成的策略与服务器时间有关,如果修改了系统时间,也有可能出现重复的主键
- 综上所述,每一种主键的策略都有自己的优缺点,根据自己的项目业务需求的实际情况来使用,才是最明智的选择
简化配置
- 模型类主键策略设置
如果要在项目中的每一个模型类上都需要使用相同的生成策略,比如你有Book表,User表,Student表,Score表等好多个表,如果你每一个表的主键生成策略都是ASSIGN_ID,那就可以用yml配置文件来简化开发,不用在每一个表的id上都加上
@TableId(type = IdType.ASSIGN_ID)
|
|
-
数据库表与模型类的映射关系
MP会默认将模型类的类名名首字母小写作为表名使用,假如数据库表的名称都以
tb_
开头,那么就需要将所有的模型类上添加@TableName("tb_TABLENAME")
,这样做很繁琐,有没有更简单的方式呢?- 可以在配置文件中设置表的前缀
1 2 3 4 5
mybatis-plus: global-config: db-config: id-type: assign_id table-prefix: tb_
设置表的前缀内容,这样MP就会拿
tb_
加上模型类的首字母小写,就刚好组装成数据库的表名(前提是你的表名得规范命名,别瞎起花里胡哨的名)。将User类的@TableName
注解去掉,再次运行新增方法1 2 3 4 5 6 7 8 9
@Data public class User { @TableId(type = IdType.ASSIGN_ID) private Long id; private String name; private String password; private Integer age; private String tel; }
多记录操作
这部分其实没有新内容,MP已经提供好了针对多记录的删除和查询,自己多看看API就好了
需求:根据传入的id集合将数据库表中的数据删除掉。
|
|
执行成功后,数据库表中的数据就会按照指定的id进行删除。上面三个数据是我之前新增插入的,可以随便换成数据库中有的id
需求:根据传入的ID集合查询用户信息
|
|
控制台输出如下
User(id=1, name=Alen, password=tom, age=3, tel=18866668888) User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666) User(id=3, name=Jock, password=123456, age=41, tel=18812345678)
逻辑删除
- 这是一个员工和其所办理的合同表,一个员工可以办理多张合同表
- 员工ID为1的张业绩,办理了三个合同,但是她现在想离职跳槽了,需要将员工表中的数据进行删除,执行DELETE操作
- 如果表在设计的时候有主外键关系,那么同时也要将合同表中的张业绩的数据删掉
- 后来公司要统计今年的总业绩,发现这数据咋对不上呢,业绩这么少,原因是张业绩办理的合同信息被删掉了
- 如果只删除员工,却不删除员工对应的合同表数据,那么合同的员工编号对应的员工信息不存在,那么就会产生垃圾数据,出现无主合同,根本不知道有张业绩这个人的存在
- 经过的分析之后,不应该将表中的数据删除掉,得留着,但是又得把离职的人和在职的人区分开,这样就解决了上述问题
- 区分的方式,就是在员工表中添加一列数据
deleted
,如果为0说明在职员工,如果离职则将其改完1,(0和1所代表的含义是可以自定义的)
所以对于删除操作业务问题来说有:
- 物理删除:业务数据从数据库中丢弃,执行的是delete操作
- 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作
MP中逻辑删除具体该如何实现?
步骤一:
修改数据库表,添加deleted
列 字段名任意,类型int,长度1,默认值0(个人习惯,你随便)步骤二:
实体类添加属性 还得修改对应的pojo类,增加delete属性(属性名也任意,对不上用@TableField
来添加映射关系 标识新增的字段为逻辑删除字段,使用@TableLogic
|
|
步骤三:
运行删除方法 没有就自己写一个呗
|
|
从测试结果来看,逻辑删除最后走的是update操作,执行的是 UPDATE tb_user SET deleted=1 WHERE id=? AND deleted=0
,会将指定的字段修改成删除状态对应的值。
-
思考:逻辑删除,对查询有没有影响呢?
- 执行查询操作
1 2 3 4 5 6
@Test void testSelectAll() { for (User user : userDao.selectList(null)) { System.out.println(user); } }
从日志中可以看到执行的SQL语句如下,WHERE条件中,规定只查询deleted字段为0的数据
1
SELECT id,name,password,age,tel,deleted FROM tb_user WHERE deleted=0
输出结果当然也没有ID为1的数据了
User(id=2, name=Jerry, password=jerry, age=4, tel=16688886666, deleted=0) User(id=3, name=Jock, password=123456, age=41, tel=18812345678, deleted=0) User(id=4, name=kyle, password=cyan, age=15, tel=4006184000, deleted=0) User(id=6, name=Helsing, password=HELL_SING, age=531, tel=4006669999, deleted=0)
- 如果还是想把已经删除的数据都查询出来该如何实现呢?
1 2 3 4 5 6
@Mapper public interface UserDao extends BaseMapper<User> { //查询所有数据包含已经被删除的数据 @Select("select * from tb_user") public List<User> selectAll(); }
-
如果每个表都要有逻辑删除,那么就需要在每个模型类的属性上添加
@TableLogic
注解,如何优化?- 在配置文件中添加全局配置,如下:
1 2 3 4 5 6 7 8 9
mybatis-plus: global-config: db-config: ## 逻辑删除字段名 logic-delete-field: deleted ## 逻辑删除字面值:未删除为0 logic-not-delete-value: 0 ## 逻辑删除字面值:删除为1 logic-delete-value: 1
使用yml配置文件配置了之后,就不需要在模型类上用
@TableLogic
注解了
介绍完逻辑删除,逻辑删除的本质为修改操作。如果加了逻辑删除字段,查询数据时也会自动带上逻辑删除字段。 执行的SQL语句为:
|
|
知识点:@TableLogic
名称 | @TableLogic |
---|---|
类型 | 属性注解 |
位置 | 模型类中用于表示删除字段的属性定义上方 |
作用 | 标识该字段为进行逻辑删除的字段 |
相关属性 | value:逻辑未删除值 delval:逻辑删除值 |
乐观锁
概念
在学乐观锁之前,还是先由一个案例来引入
- 业务并发现象带来的问题:秒杀
- 加入有100个商品在售,为了保证每个商品只能被一个人购买,如何保证不会超买或者重复卖
- 对于这一类的问题,其实有很多的解决方案可以使用
- 第一个最先想到的就是锁,锁在一台服务器中是可以解决的,但是如果在多台服务器下就没办法控制,比如12306有两台服务器在进行卖票,在两台服务器上都添加锁的话,那也有可能会在同一时刻有两个线程在卖票,还是会出现并发问题
- 接下来介绍的这种方式就是针对于小型企业的解决方案,因为数据库本身的性能就是个瓶颈,如果对其并发超过2000以上的就需要考虑其他解决方案了
简单来说,乐观锁主要解决的问题是,当要更新一条记录的时候,希望这条记录没有被别人更新
实现思路
- 数据库表中添加
version
字段,比如默认值给个1 - 第一个线程要修改数据之前,取出记录时,获取当前的version=1
- 第二个线程要修改数据之前,取出记录时,获取当前的version=1
- 第一个线程执行更新时
- set version = newVersion where version = oldVersion
- newVersion = version + 1
- oldVersion = version
- set version = newVersion where version = oldVersion
- 第二个线程执行更新时
- set version = newVersion where version = oldVersion
- newVersion = version + 1
- oldVersion = version
- set version = newVersion where version = oldVersion
- 假如这两个线程都来更新数据,第一个和第二个线程都可能先执行
- 假如第一个线程先执行更新,会将version改为2
- 那么第二个线程再更新的时候,set version = 2 where version = 1,此时数据库表的version已经是2了,所以第二个线程修改失败
- 假如第二个线程先执行更新,会将version改为2
- 那么第一个线程再更新的时候,set version = 2 where version = 1,此时数据库表的version已经是2了,所以第一个线程修改失败
- 假如第一个线程先执行更新,会将version改为2
实现步骤
步骤一:
数据库表添加列 加一列version,长度给个11,默认值设为1步骤二:
在模型类中添加对应的属性
|
|
步骤三:
添加乐观锁拦截器
|
|
步骤四:
执行更新操作
|
|
查看日志的SQL语句
==> Preparing: UPDATE tb_user SET name=?, password=?, age=?, tel=?, version=? WHERE id=? AND version=? ==> Parameters: Person(String), tom(String), 3(Integer), 18866668888(String), 2(Integer), 1(Long), 1(Integer)
传递的是1(oldVersion),MP会将1进行加1,变成2,然后更新回到数据库中(newVersion)
大概分析完乐观锁的实现步骤以后,模拟一种加锁的情况,看看能不能实现多个人修改同一个数据的时候,只能有一个人修改成功。
|
|
至此,乐观锁的实现就已经完成了
快速开发
代码生成器原理分析
官方文档地址:https://baomidou.com/pages/981406/
通过观察之前写的代码,会发现其中有很多重复的内容,于是MP抽取了这些重复的地方,做成了一个模板供使用 要想完成代码自动生成,需要有以下内容:
- 模板: MyBatisPlus提供,可以自己提供,但是麻烦,不建议
- 数据库相关配置:读取数据库获取表和字段信息
- 开发者自定义配置:手工配置,比如ID生成策略
代码生成器实现
步骤一:
创建一个Maven项目步骤二:
导入对应的jar包
|
|
步骤三:
编写引导类
|
|
步骤四:
创建代码生成类
|
|
对于代码生成器中的代码内容,可以直接从官方文档中获取代码进行修改,https://baomidou.com/pages/981406/
步骤五:
运行程序
运行成功后,会在当前项目中生成很多代码,代码包含 controller
,service
,mapper
和 entity
等
至此代码生成器就已经完成工作,能快速根据数据库表来创建对应的类,简化的代码开发。
初期还是不建议直接使用代码生成器,还是多自己手写几遍比较好
MP中Service的CRUD
回顾之前业务层代码的编写,编写接口和对应的实现类:
|
|
接口和实现类有了以后,需要在接口和实现类中声明方法
|
|
MP看到上面的代码以后就说这些方法也是比较固定和通用的,那我来帮你抽取下,所以MP提供了一个Service接口和实现类,分别是:IService
和 ServiceImpl
,后者是对前者的一个具体实现。
以后自己写的Service就可以进行如下修改:
|
|
修改以后的好处是,MP已经帮把业务层的一些基础的增删改查都已经实现了,可以直接进行使用。
编写测试类进行测试:
|
|