SpringBoot
内容管理
SpringMVC、Data-Access和SpringBoot
SpringBoot只是为了简化Spring和SpringMVC繁琐的配置文件,当然,响应式框架可以不用SpringMVC,Structs或者Webflux都可以,所以还是先简单过一下Spring和SpringMVC相关的概念
Spring (bg)
昨天对于只是分享了一些Spring部分的bg,这里再分享一下MVC和boot的相关,同时以开发为主,分享常用注解,正式开始以开发的角度来梳理SpringBoot,拥抱SpringBoot时代!
spring MVC
springMVC其实就是Spring内置的MVC框架,是一个基于java的实现MVC设计模式的请求驱动的Web框架,MVC就是model,view、controller,核心思想就是将业务逻辑、数据和显示分离来组织代码
在MVC之前,经历了model1时代和model2时代: model1时代就是几乎完全依赖JSP,少量的JavaBean处理数据库连接共同组成项目 -----> model2时代,有了Servlet,所以就是Model(java Bean) + view (Jsp) + controller(Servlet)组成雏形的MVC, 但是model2时代的封装性还是不够,需要重复造轮子【比如之前一个Servlet类只能处理一个请求,实现HttpServlet,重写相关的get和post方法】 ------ > Spring MVC时代: spirng mvc是最优秀的MVC框架,完全契合Spring,SpringMVC下一般将后台分为Servie、Dao、entity层
SpringMVC工作原理
这个和之前的Bean周期一样都是一套流程【Bean中的主要就是解析,实例化,Aware接口判断,初始化(Beanpostprocessor) Beanpostprocessor后置处理的时候SpirngAop会进行织入 ---->增强这个对象的方法,之后注册一下destruction回调 —> 什么时候激活, 使用之后判断是否实现了disposableBean,调用自定义的destroy方法销毁bean】
SpringMVC当然还是会和model2时代的Servlet有关联,最核心的就是中央处理器DispacterServlet; 还有HandlerMapping处理映射器,以及HandlerAdapter处理适配器,ViewResolver视图解析器
- 用户发送请求HttpRequest,直接请求到的是DispatcherServlet
- 解析,根据请求的信息调用HandlerMapping解析到对应的Handler处理器
- 准备执行, 解析到对应的handler之后,由HandlerAdapter管理执行【这里执行的是一个HandlerExecutionChain执行链】
- 执行相关的HandlerExecutionChain中的相关处理器方法,进入Service和数据库处理逻辑返回Model和View给中央调度器
- 中央调度器将viewName传递给ViewResolver,视图解析器返回一个实际的View【逻辑名称】给DispacterServlet
- DispatcherServlet将Model传递给View进行视图的渲染,之后将渲染的视图返回响应用户
Spring框架中的设计模式
首先就两个核心思想来说,AOP运用到了代理设计模式,IOC运用到了工厂模式,单例Bean ----> 单例模式【懒汉式存在安全问题,因为static赋值的是null,可能创建多个对象,饿汉直接赋值,不存在安全问题】
SpringMVC的处理适配器还有AOP的advice适配器都运用到适配器模式
jdbcTemplet,redisTemplate等都使用到了模板方法模式
连接不同的数据库,动态切换数据源 —> 包装器模式
Spring的事件驱动模型 --> 观察者模式
Spring中管理事务的方式
Spring中管理事务主要就是编程式事务和声明式事务
- 编程式事务 : 在代码中通过TransactionTemplate手动编码管理事务,手动rollback等,使用较少
- 声明式事务: 在XML配置文件中配置或者基于注解,通过AOP实现的 【@Transactional】
Spring中的事务传播行为
事务传播行为 ----> 管理业务层方法相互调用的事务问题
当事务方法被另外事务调用的时候,必须要指定传播行为: 被调用的方法到底在当前事务中执行,还是重写开启一个事务
主要的就是4个,常用的式required和requires_new
- TransactionDefinition.PROPAGATION_REQUIRED : 使用最多,使用@Transactional默认就是这个传播行为,如果当前方法存在事务,加入该事务,如果没有,就创建一个新的事务
- …PROPAGATION_REQUIRES_NEW : 不管存在事务与否,都会重新创建一个新的事务【存在就会被挂起】
- …PROPAGATION_NESTED: nested嵌套, 不存在新建,存在就创建一个事务嵌套在当前事务中
- …PROPAGATION_MANDATORY : mandatory: 强制性: 存在事务就加入,不存在抛异常
所以简单记忆就是:required存在加入,不存在创建; requires_new: 不管存在都创建; nested: 都创建,存在时不挂起,嵌套; mandatory: 存在创建嵌套,不存在报错
还有3种隔离级别时非事务的方式运行 事务不会回滚, supports: 有则加入,无则非事务运行; not_supported:以非事务方式运行,不管是否有事务,挂起; never: 非事务运行,存在事务抛异常 【mandatory和never恰好两个极端,一个不能存在抛异常,一个存在抛异常】
Spring事务隔离级别
四个: TransactionDefinition.ISOLATION_ READ_UNCOMMIT ; 对应的一级封锁协议,只是解决了丢失修改,加了X🔒,READ_COMMITED: 对应二级封锁协议:解决了脏读 ,读取过程加S🔒【还有幻影读和不可重复读】;REAPETABLE_READ: 对应三级封锁协议: 解决了不可重复度,但是幻读还是有的【MYSQL的貌似没有】,SERIALIZABLE: 可串行化,没有并发问题,性能太低了【不管操作的是不是同一个数据】
@Transactional(rollbackFor = Exception.class)
@Transactional是事务的全注解,放置在业务方法的上面或者类的上面【该类的所有的public方法都具有该类型的事务属性】,可以通过propagation属性指定事务传播行为,通过isolation指定事务的隔离级别,而rollbackFor属性就是指定当遇到什么类型的异常时回滚,当指定为Exception.class时就会遇到任何异常都回滚
当不加rollbackFor属性时,只有碰到RuntimeException才会回滚; 加了Exception.class,因为Exception是所有异常类的超类,包括运行时或者非运行时异常【检查】,都会回滚
JPA 全自动持久层框架
java persistent API,JPA是一种规范,简化了持久化的开发,SpringDataJPA【Hibernate】是一套全自动框架,自动生成的sql可读性差,Mybatis是半自动的框架,需要手动写sql【所以Mybatis-plus出现与JPA抗衡】
- JPA不管行底层数据库的类型【独立性】,规范标准化,是java官方规定的统一API,但是高级事务和复杂查询自己写sql
- Mybats是半自动的持久层框架,SQL语句是依赖数据库的【因为语法】,所以数据库的移植性差,SQL功底要求高,所以当需要支持多种数据库时,应该使用JPA好一点,完全透明
如果想让entity中的某个字段不被持久化,可以使用注解@Transient transient 瞬态刚好和persisten相反
或者使用transient关键字/final/static修饰变量
SpringBoot自动装配
Spring时代使用第三方依赖,需要写配置文件bean或者通过JavaConfig配置相关的对象,比如使用Mybatis框架,就需要配置sqlSessionFactory对象…
但是SpringBoot项目,不管是Mybatis还是Mybatis-plus,只要加了相关的起步依赖,就会进行自动装配,we只需要在yml中配置数据源的信息; ------ 简化xml开发,核心就是自动装配 【SpirngBoot定义了一套规范,SpringBoot启动时会扫描外部引用jar包的META-INFO/spring.factories
,将文件中配置的类型信息加载到容器(反射),只要外部jar包遵守这套规范,就可以将功能装入springBoot】
- 自动装配 ------- 通过简单的注解和配置就可以在SpringBoot的帮助下实现某块功能
- 如何实现: @SpringBootApplcation注解复合【@ComponentScan可以自定义排除,@Configure】注解中的@EnabledAutoConfiguration就是实现自动装配; 核心功能实现是AutoConfigurationImportSelector实现 -----> 1.查看自动装配开关是否打开, 可以在spring.boot.enableautoconfiguration = true; 2. 获取@Enableautoconfiguration中的exclude和excludename排除相关的 3.获取所有需要自动装配类,读取其META-INF/spring.factories,按需加载组件 -------
满足所有的@ConditionalOnXXX的配置会加载,其余不会加载
【 这么多毕竟】 - 实现starter,starter自动装配需要装配的配置对象,所以自定义的关键就是给出配置对象,@Bean创建对象,@ConditionOnXXX 指明是否装入,之后将这个bean,也就是这配置类给声明到META-INF/spring.factories中,作为enableautoconfiguration…的域
SpringBoot使用
SpringBoot是在Spring的基础上构建起来的项目,最主要的就是自动配置【starter】、独立运行【内置Tomcat】、智能,配置的默认值会根据依赖项的改变自动改变,使用Spirng Boot可以轻松快速构建一个企业级的应用【SpringBoot最主要的就是内嵌服务器,自动配置】
SpringBoot项目创建 CLI
这里最基本的两种方式,基于Spring Initializr初始化和基于maven创建之后手动加入starter 就不再介绍了,因为使用的比较多,并且本人不是很喜欢【slow】
所以这里再介绍一种CLI的方式,类似于之前的Vue-cli命令行的方式
- 首先就是在官网下载,下载地址:JFrog (spring.io)
- 下载之后解压安装包,同时将./spring-2.3.1.RELEASE/bin配置到Path环境变量中【这个配置过多次了,就是为了不需要在bin页面才能执行bin命令】随意位置都可以设别
C:\Users\OMEY-PC>spring --version
Spring CLI v2.3.1.RELEASE
验证是否配置成功,这里按理来说就是bin的路径即可,没有成功可能还没有响应,重试一次即可
- 使用CLI初始化醒目,依赖的是init命令,使用
spring init -list
查看可用的菜蔬 - 使用spirng init -dweb,mysql,XXX 指定初始依赖
- 同时可以指定打包格式package,-p参数即可 spring init -dXXX --build maven -p war
- 同时为了让创建的项目最终就是一个完整的文件,不需要解压,可在后面加上文件目录
创建一个初始依赖为web,基于maven构建的,名为cfeng-blog的springBoot项目
spring init -dweb --build maven -p war cfeng-blog
C:\Users\OMEY-PC>D:
D:\>cd D:\softVM\springBoot
D:\softVM\springBoot>spring init -dweb --build maven -p war cfeng-blog
Using service at https://start.spring.io
Project extracted to 'D:\softVM\springBoot\cfeng-blog'
默认就是在当前目录下创建项目,所以这里博主就直接跳转target文件夹下面执行init命令
这样子创建确实要快一些,之前等IDEAresolving等的着急,创建成功之后在IDEA中导入项目就可以进行操作
这里解释一下几个.文件【以前解释过简单】
- .gitignore: 使用git做版本管理时,需要在这个文件中给出忽略文件的列表
- XXX.iml: 这个文件为IDEA用于存储开发环境相关信息的配置文件
- mvnw.cmd: mvnw与Maven Wrapper与.mvn目录文件结合起来,方便用户不需要安装maven就可以使用maven
HELP.md就一个markdown文件,参考文档,相关的信息写在其中,方便使用嘛
创建的项目还是按照三成架构为好:其中接触这么多,同一层可以有不同的名称: 控制器层:web/controller; 业务层:service 实体层:domain/entity
主启动类BootApplication是需要放在root下面的,也就是和service/controller等包平级的,因为主启动类的@SpringBootApplicaiton注解中的@ComponentScan默认扫描的是启动类所在的包及其子包
当然,不同级也不是没有办法扫描
一: 在主启动类上面再指定@ComponentScan注解指明要扫描的包
二: 使用@Bean注解,著启动类是配置类,可以再其中就new 一个没有扫描到的类的对象,放入boot容器
构建一个单体项目 【 mustache模板引擎,JPA】
对于这种同等级的工具的使用还没有对比过,mustcache作为Vue的重要支撑,是比freemaker更加轻量级的模板引擎【将数据转为view】
mustache模板引擎
这个模板引擎是一个轻量级的非常容易上手的模板引擎,主要就是{{}}就可以了【这里demo不想前后端分离】
{{#}}表示开始,该标记之后的内容都要循环显示 {{/}}标记循环结束 普通的就{{}}
html转义显示 {{&}},渲染的时候Mustcache.render(template,data)
同时又封装复用的概念
{{> header}} 这里的header就是子模板
这就是vue的模板的子模版的概念
这里为了方便进行mustache文件的编写,最好在IDEA中安装handler/mustache插件,插件安装可能出现
idea下载插件一直加载不出来
这个问题依据版本而定,低版本的设置noconnet,高版本的在proxy中配置代理:https://plugins.jetbrains.com 【还有可能是网络连接的问题,毕竟其实就是去官网在线下载】 — 配置之后记得restart, 可能要缓缓,博主就是头天晚上23点配置,尝试半小时无果,第二天就可以了…所以有的时候解决问题可以不要那么焦急,多尝试几次,可能只是反应的问题,没必要再去…
project demo
这里使用CLI的方式创建一个Demo项目
spring init -dweb,mustache,jpa,h2,devtools -p maven cfeng-blog
创建之后就可以看到POM文件的结构如下
其中spring-boot-starter-tomcat是为了方便在外部项目,不使用内嵌的Tomcat【web.xml??—config方式】,同时没有直接导入Junit单元测试依赖,而是直接使用spring-boot-starter-test进行一体化测试【spring的思想DDP测试驱动开发】
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
这里准备创建一个基于mustache的demo,这里不使用mysql,直接使用h2嵌入式数据库【免费】主要是轻量级的,够快,够方便
SpringBootServletInitializer 【SPI技术】
项目中除了常用的主启动类之外,还同级建立了一个XXXServletInitializer继承这个SpringBootServletInitializer,而这个Servlet初始化器又是实现的
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
各位如果觉得不好理解,就简单将其当作web.xml; 因为SpringBoot时代是使用JavaCofig代替xml配置文件,这里的ServletInitializer就是官方提供的,通过SPI机制【server provide interface】实现的
在类路径下面的META-INF/services文件夹中查找文件,自动加载文件中所定义的类,将他们实例化[servlet SCI就是使用的java的SPI技术] 之前的SpringBoot的自动装配也是SPI技术,加载的是starter包下面的spring.factories,进行对象DI
【下一阶段的高并发、高可用,源码分析、计算机基础阶段会详细介绍SPI】
自定义初始化类重写了configure【主要目的是为了打包在外部的服务器运行】
public class BlogServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(BlogApplication.class);
}
}
这里重写的方法借助的是一个关键类SpringBuilder
,这个类的功能就是将项目打包
private final SpringApplication application;
private ConfigurableApplicationContext context;
private SpringApplicationBuilder parent;
private final AtomicBoolean running;
private final Set<Class<?>> sources;
private final Map<String, Object> defaultProperties;
private ConfigurableEnvironment environment;
private Set<String> additionalProfiles;
private boolean registerShutdownHookApplied;
private boolean configuredAsChild;
可以观察其属性,比如第一个就是应用程序,后面就是上下文环境,环境等
使用其source方法就可以加载Application,进行war打包部署工作
@SpringBootTest 功能测试
集成测试的粒度为多个模块,关注的是多个模块协同工作下的工作结果,不同于之前的简单的单元测试关注单独的一个方法或者模块
Spring Test是整合了其他的比如Junit框架的,所以不必要只是引入Junit框架,Spirng Boot Test在Spring Test的基础上进行了增强,增加了切片测试和mock能力 integration一体化
- 单元测试: 面向方法,项目中最常使用,一般使用@Test即可【Junit框架】
- 切片测试: 面向难以测试的边界功能,使用注解@RunWith和@WebMvcTest
- 功能测试: 一般测试的是某个比较完整的业务功能,同时也可以用于切面测试,使用的就是@SpringBootTest
依赖项就是starter-test【Junit,Spring Test,AssertJ,Hamcret,JSONassert,JsonPath等】
一般情况下,使用@SpringBootTest后,Spring会加载所有被管理的Bean,所以基本等同于启动了整个服务, 其参数可以指定web的运行环境
classes指定的是运行的application启动类
- MOCK : 默认值,提供了一个mock环境,整个时候内嵌的服务器没有启动,不会监听端口
- RANDOM_PORT: 启动一个真实的web服务,监听随机的端口
- DEFINED_PORT: 启动真实的服务,监听定义好的端口【yml中配置的port】
- NONE: 启动一个非web的ApplicationContext,不提供mock和web服务
TestRestTemplate 集成测试,模拟HTTP请求 ResponsEntity
TestRestTemplate和RestTemplate类似,专门使用在测试中使用,用于和HTTP API交互,TestRestTemplate是对RestTemplate的封装
public class TestRestTemplate {
private final RestTemplateBuilder builder;
private final HttpClientOption[] httpClientOptions;
private final RestTemplate restTemplate;
...
public void setUriTemplateHandler(UriTemplateHandler handler) {
this.restTemplate.setUriTemplateHandler(handler);
}
关于对TestRestTemplate的基本使用,还需要引入几个类进行辅助
ResponseEntity
: ResponseEntity继承了HttpEntity类,HttpEntity代表一个http请求或者响应实体,其内部有两个成员变量:header及body,代表http请求或响应的header及body,其中的body是泛化的。 所以在测试中我们可以简单将其代表为请求或者响应
assertThat
: Test中的断言机制,可以简单理解为判断假设是否成立
所以这几个类配合,就可以进行简单的集成测试,比如我们测试controller的响应结果是否为200
//响应就是使用的ResponseEntity代替,we可以选择将其解析为String类型, 响应怎么获得,那么就需要我们的响应模拟类TestRestTemplate.getForEntity来发起请求
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... urlVariables) {
return this.restTemplate.getForEntity(url, responseType, urlVariables);
比如这里我们将其解析为String类型
@SpringBootTest(classes = {BlogApplication.class},webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTests {
//Spring测试环境下访问Rest服务
@Autowired
TestRestTemplate restTemplate;
@Test
public void assertBlogPageTitle_Content_StatusCode() {//命名采用驼峰 + 蛇形命名
//访问路径/,String类型解析响应主体entity.body
ResponseEntity<String> entity = restTemplate.getForEntity("/",String.class);
//判断响应的状态码为HttpStatus.OK,200
System.out.println(entity.getBody());
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
//判断entity.body包含"<h1>Hello Cfeng</h1>
assertThat(entity.getBody().contains("<h1>Hello Cfeng</h1>"));
}
}
这里将其打印就是处理器响应的html页面,String类型; 这里就集成测试了模板、控制器协同工作的结果,assertThat的断言都是成功的
但是有时候需要在给定类之前或者之后执行一个方法,此时需要使用注解==@BeforeAll和@AfterAll==, 这个就类似于之前的切面的方法前面和后面切入
但是需要注意的是: 这两个注解默认是只能修饰静态方法的,不能修饰常规方法,想要修饰常规的方法,就需要编写单独的properties配置文件 junit-platform.properties 测试平台
########junit 测试平台junit-platform 配置文件#####################
junit.jupiter.testinstance.lifecycle.default = per_class
配置之后就可以使用After和Before的注解
@BeforeEach
public void setup() {
System.out.println(">> Setup");
}
@Test
public void assertArticlePageTitle_Content_And_StatusCode() {
System.out.println(">> TODO after");
}
@AfterEach
public void teardown() {
System.out.println(">> Tear down 拆卸");
}
}
这里就可以像之前的AOP类似,会在执行任何方法之前或者之后插入our切面, 这里注意版本的配合问题,本人使用@AfterAll和@BeforeAll不生效, 但是看到关于版本的问题,便使用org.junit.jupiter.api.*; 下面的AfterEach和BeforeEach
, 这两个类和上面的两个类都是这个jupiter包下面的
创建工具类CommonUtil
这个demo,我们需要自己封装一些工具方法,还是不要依赖hutool,先封装两个工具类
-
格式化时间
-
**将标题转为slug格式 ** slug就是方便进行URL命名restful
/**
* @author Cfeng
* @date 2022/6/8
*
* 该类为通用工具类,主要封装项目需要使用到的工具
*/
public class CommonUtil {
private static final DateTimeFormatter englishDateFormater;
private static final Map<Long,String> dayLookup;
//静态方法块初始化we日期
static {
dayLookup = buildDayLookup(); //这个映射表,就是对应的天数,变为英语加上后缀th...
englishDateFormater = new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd").appendLiteral(" ")
.appendText(ChronoField.DAY_OF_MONTH,dayLookup).appendLiteral(" ")
.appendPattern("yyyy").toFormatter(Locale.ENGLISH);
}
public static String format(LocalDateTime localDateTime) {
return localDateTime.format(englishDateFormater);
}
/**
* 将title转为slag格式
*也就是将空格转化为-
* eg: this is a title --> this-is-a-title
*
* 要考虑所有的情况,首先最后的换行符\n要替换为空格,还有就是利用正则表达式将一些不是26字母的给替换为空格,之后再拆分,拆分之后
* 再使用Spring的连接方法
*/
public static String toSlug(String title) {
String.join("-",title.toLowerCase().replace("\n"," ")
.replace("[^a-z\\d\\s]"," ").split(" ")).replace("-+","-");
}
/**
* 创建appendText需要用到的参数dayLookup,提供appendText的映射关系
* 创建一个映射表,这个方法是私有的,外部不能直接调用
*/
private static Map<Long,String> buildDayLookup() {
Map<Long,String> ret = new HashMap<>();
for(int i = 1; i <= 31; i++) {
ret.put((long) i, getDayOfMonthSuffix(i));
}
return ret;
}
/**
* 根据当前天数的各位获取对应的后缀
* 也就是转为英语的时候,first,second,third,XXth
*/
private static String getDayOfMonthSuffix(int n) {
switch (n % 10) {
case 1:
return "${n}st";
case 2:
return "${n}nd";
case 3:
return "${n}rd";
default:
return "${n}th";
}
}
}
这里的格式化看着挺复杂,但是是因为要考虑各种情况,比如slug格式化,就要考虑换行,非识别字符等情况,而日期格式化,我们要先建立一个天数映射表daysLookup
🕋明天继续