【springBoot】SpringMVC(bg),SpringBootServletInitializer,springBoot一体化测试

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工作原理

SpringMVC工作流程 的图像结果

这个和之前的Bean周期一样都是一套流程【Bean中的主要就是解析,实例化,Aware接口判断,初始化(Beanpostprocessor) Beanpostprocessor后置处理的时候SpirngAop会进行织入 ---->增强这个对象的方法,之后注册一下destruction回调 —> 什么时候激活, 使用之后判断是否实现了disposableBean,调用自定义的destroy方法销毁bean】

SpringMVC当然还是会和model2时代的Servlet有关联,最核心的就是中央处理器DispacterServlet; 还有HandlerMapping处理映射器,以及HandlerAdapter处理适配器,ViewResolver视图解析器

img

  • 用户发送请求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

🕋明天继续

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值