SpringBoot


SpringBoot开发技术


上一篇已经分享了springBoot的功能测试@SpringBootTest,导入起步依赖test,同时分享了关于SpringBoot时代使用外部服务器时的配置类SpringBootServletInitializer;

在功能测试中,模拟请求使用的是TestRestTemplate其getForEnitity就可以得到一个响应,指定响应的数据类型和访问的路径,就可以得到响应体ResponseEntity<响应数据类型>,通过juniper的assertThat就可以进行断言测试,为了能够使用@AfterEache,需要配置一个junit-platform.properties,指定testinstance.lifecycle.default为per_class

工具类CommonUtil的创建使用到正则表达式,不清楚的看之前的blog,对于日期的格式化,we使用的是static块,在其中对成员变量daysLookup和englishFormater进行赋值,就是自定义一个日期格式化DateTimeFormatter,自定义的方式通过DateTimeFormatterBuilder,append自定义格式即可【pattern,text,literal(按字面意义的)】

JPA配合H2进行持久化

JPA是一个全自动的持久层框架,相比Mybatis的半自动有快速搭建项目的优势,同时H2作为嵌入式数据库,可以快速测试Dao层,方便项目早期的编写和测试

使用mybatis或者mybatis-plus: yml中配置数据源【相关的sqlsessionFactoryBean和创建dao对象的MapperScannerConfiger就不需要配置了,自动装配】,在主启动类,加上MapperScan注解,扫描dao,就可以创建dao对象,如果分开xml文件,再yml中配置mapper-locations即可

而对于entity中的类不需要任何处理,因为只要表名和类名相同,字段名和属性名相同就可以进行自动的注入,这里借助的也是反射机制

JPA注解

对应的依赖就是data-jpa的starter

  • @Entity : 标识实体类为JPA实体,程序运行时生成实体类的对应表,比如Article会默认生成的表为article【所以mybatis中对应就不需要指定】,也可以通过name指定表名 @Entity(name = “XXX”)
  • @Table: 设置实体类在数据库中对应的表名
  • @Id : 放在属性上,表明为表中的主键
  • @GeneratedValue: 主键的生成策略【依赖具体的数据库】,默认是Auto自增
  • @Column: 属性对应的字段名自定义设置
  • @Transient: 标识当前属性不被持久化到数据库中; (之前提过),还可以使用transient进行修饰等
  • @Temporal: 使用java.util的时间类型,使用这个注解进行转化
  • @Enumerated: 映射枚举字段以String类型存
  • @MappedSupperclass: 放在类上,表明不再注解被映射为一张表,而属性会映射为子表

相关关系

  • @ManyToOne: 修饰的字段为另外一个entity表,该表与修饰表为多对一的关系,比如多篇Article对应一个User

这里使用JPA的一个不同就是我们需要对实体类加上@Entity注解,表明这个类为实体类,便于插入到H2数据库中【这里没有直接建立数据库】

@Entity
@Getter
@Setter
public class User {
    @Id
    @GeneratedValue
    private Long id;
    //登录名
    private String loginName;
    //名
    private String firstName;
    //姓
    private String lastName;
    //描述
    private String description;

    //相关的get和set方法
}

使用IDEA,旁边还会对该类进行形象化标识

在这里插入图片描述

JPA全自动 repository层继承CrudRepository

JPA全自动框架,和Mybaits的半自动相比要强许多,但是mybaits-plus也是全自动框架,在mybatis基础上功能增强

JPA全自动: repository层的repostory接口都继承BaseRepository接口

Mybatis-plus全自动: mapper层的mapper接口都继承BaseMapper接口

​ service层的service接口都继承IService接口,service实现类都继承ServiceImpl<mapper,entity>实现类;同时实现service接口

创建Repository的时候只需要继承CrudRepository即可,T代表实体类的类型,ID为主键的类型,剩余的工作JPA完成; JPA遵循约定优先配置,只要更具Spring和JPQL进行命名,就可以生成对应的SQL语句

public interface UserRepository extends CrudRepository<User,Long> {
    //通过登录名查找用户
    User findUserByLoginName(String loginName);
}

只是需要增加额外的方法即可,并且方法名按照Spring的约定,不需要编写sql【xml】,spring会对方法名进行提示

Repository层功能测试 @DataJpaTest

该功能注解结合@Runwith等使用,会完全禁用自动配置,并且只是应用于JPA的相关测试,测试的是使用嵌入式内存数据库, 要测试其他数据库,使用@SpringBootTest加@AutoConfigureTestDatabase

带有该注解的测试都是事务性的,测试结束都会回滚

TestEntityManager

TestEntityManager允许在测试中使用EntityManager,Spring repository是对entiry mamager的抽象,EnityManager就是JPA中用于增删查改的接口,相当于一座连接java对象和数据库的桥梁【之前的处理器测试依赖了TestRestTemplate,是用作响应测试的】可以将其看作对象处理的工具;而repository是数据库访问对象

persist方法就是持久化一个对象; 就是交给entityManager托管,相当于在内存库中【表中的记录】,之后flush一下,托管状态的数据对象就会插入数据库中

在使用repostory进行查询的时候,使用orElse(null) 表示没有找到返回空,否则会出问题

@DataJpaTest
public class RepostoriesTest {

    @Resource
    TestEntityManager entityManager;

    @Resource
    UserRepository userRepository;

    @Resource
    ArticleRepository articleRepository;

    @Test
    public void whenFindByIdOrNull_thenReturnArticle() {
        //创建用户对象
        User cfeng = new User();
        cfeng.setLoginName("Cfeng");
        cfeng.setFirstName("zhang");
        cfeng.setLoginName("ning");
        //托管用户对象,持久化
        entityManager.persist(cfeng);
        //创建一个文章对象
        Article article = new Article();
        article.setTitle("SpringBoot 开发技术");
        article.setHeadline("简介:xxxxx");
        article.setContent("springBoot作为一个快速上手工具,非常优秀");
        article.setAuthor(cfeng);
        //托管,将其持久化,借助的就是EntityManager
        entityManager.persist(article);
        //将托管状态的对象持久化到数据库中
        entityManager.flush();
        //根据ID查询文章
        Article found = articleRepository.findById(article.getId()).orElse(null); //要使用orElse指定查询为空的结果
        //断言保存前的对象与查询结果相等
        assertThat(article).isEqualTo(found);
    }

ApplicationRunner 容器创建监听器

事件驱动,就之前的Servlet中的ServletContextLinstener那种,实现这个接口其中的run方法,那么就会在项目启动的时候,执行run中的操作,所以经常应用比如开启数据库连接,读取配置文件之类的操作; 一般承担初始化的工作

@Configuration
public class BlogConfiguration implements ApplicationRunner {
    //定义两个repository对象
    private final UserRepository userRepository;
    private  final ArticleRepository articleRepository;

    public BlogConfiguration(UserRepository userRepository,ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
        this.userRepository = userRepository;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        //依次向数据库中插入几条数据,save方法的返回值就是插入的这个实例,如果成功
        User saveUser = new User();
        saveUser.setLoginName("xiaohuan");
        saveUser.setFirstName("xiao");
        saveUser.setLastName("huan");
        User huanxiao = userRepository.save(saveUser);
        //在插入两个文章对象
        Article saveArticle1 = new Article();
        saveArticle1.setTitle("前端教程");
        saveArticle1.setHeadline("前端很火");
        saveArticle1.setAuthor(saveUser);
        saveArticle1.setContent("前端主要的几个就是.........");
        Article article1 = articleRepository.save(saveArticle1);
        //再插入一个
        Article saveArticle2 = new Article();
        saveArticle2.setTitle("后端教程");
        saveArticle2.setHeadline("后端很火");
        saveArticle2.setAuthor(saveUser);
        saveArticle2.setContent("后端主要的几个就是.........");
        Article article2 = articleRepository.save(saveArticle1);
    }
}

这里的测试代码非常的easy,测试一下功能,再来写一下一体化测试integration

测试访问/artcle

在测试的时候想要发送rest风格请求,那么就需要进行字符串的拼接,这里要先进行slug格式的转换,所以需要使用String.format方法

String String.format(String fmt, Object… args);

这里就可以使用类似C中的printf的转为符,比如%s代表的就是字符串,%c字符,就会将后面的可变个数的参数按位置给插入到前面的格式串中

 @Test
    public void assertArticlePageTitle_Content_And_StatusCode() {
        System.out.println(">> Assert article page title content and statusCode >>");

        //模拟访问article页面,那么就要传入一个slug格式的title,使用TestRestTemplate进行访问
        String title = "前端教程";
        ResponseEntity<String> entity = restTemplate.getForEntity(String.format("/article/%s",CommonUtil.toSlug(title)),String.class);
        //首先判断响应的状态码是否正常
        assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
        //之后断言响应体中是否包含title,输出响应体
        assertThat(entity.getBody()).contains(title,"前端很火","前端主要的几个就是.........");
        System.out.println(entity.getBody());
    }

提一下,之前的junit-platform.propertis是放在test的resources下面的

HTTP Restful风格

现在的前后端分离的趋势下,将项目改为符合restful风格的接口,访问的路径就应该是article模块,/api/article

@RestController
@RequestMapping("/api/user")
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/")
    public Iterable<User> findAll() {
        return userRepository.findAll();
    }

    @GetMapping("/{loginName}")
    public User findByLoginName(@PathVariable String loginName) {
        User result = userRepository.findUserByLoginName(loginName);
        //查询结果为空时返回404,抛出的是响应状态异常
        if(result == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND,"the user does not exists");
        }
        return result;
    }
}

其中的一部分实现,主要就是返回的都是响应体的数据,这里也需要进行测试,为了避免Dao对controller的影响,需要用Mockito模拟Dao

@WebMvcTest
public class HttpControllersTests {

    @Resource
    private MockMvc mockMvc;

    @MockBean
    private UserRepository userRepository;

    @MockBean
    private ArticleRepository articleRepository;

    @Test
    public void listAricles() throws Exception {
        User saveUser = new User();
        saveUser.setLoginName("xiaohuan");
        saveUser.setFirstName("xiao");
        saveUser.setLastName("huan");

        Article saveArticle1 = new Article();
        saveArticle1.setTitle("前端教程");
        saveArticle1.setHeadline("前端很火");
        saveArticle1.setAuthor(saveUser);
        saveArticle1.setContent("前端主要的几个就是.........");

        Article saveArticle2 = new Article();
        saveArticle2.setTitle("后端教程");
        saveArticle2.setHeadline("后端很火");
        saveArticle2.setAuthor(saveUser);
        saveArticle2.setContent("后端主要的几个就是.........");
        //模拟articleRepository.findAllOrderByAddedAtDesc(),返回文章列表
        //使用的mockito的when和then
        when(articleRepository.findAllByOrderByAddedAtDesc()).thenReturn(Arrays.asList(saveArticle1,saveArticle2));
        //diaoyong jiekou /api/article
        mockMvc.perform(MockMvcRequestBuilders.get("/api/article"))
                //期望的调用结果statuscode为2xx
                .andExpect(status().is2xxSuccessful())
                //期望的调用结果类型为JSON
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                //期望的返回值的内容,使用$.[0],$.[1]分别代指不同的数据对象
                .andExpect(jsonPath("$.[0].author.loginName").value(saveUser.getLoginName()))
                .andExpect(jsonPath("$.[0].slug").value(saveArticle1.getSlug()));
    }

这里的测试的关键和之前的controller不同,之前只是使用了assertThat,这里使用到了mockito,要将MockMvc对象注入到容器中,之后就可以使用@MockBean来模拟repository层的对象

模拟repository的返回值,这个时候需要使用到mockito的when,thenReturn方法,当执行repository的某个方法的时候,就返回XXX数据,这里使用的注解为@WebMvcTest,所以发起请求不再是使用之前的一体化测试中的TestRestTemplate,而是使用mockMvc的perform方法,这个方法伴随着测试,就是配合accept方法确定响应的数据类型,同时使用andExcept来确定这里的响应的内容

响应的内容的断言,mockmvc中,类似于之前的assertThat【如果需要进行切入,那么就需要在test的Resource中配置,main中的切面不是,不需要这个配置】
$.[0]   代表的就是返回的Json对象集合,.[0]就是取出集合中的第一个元素,代表的就是响应的对象
eg:
	.andExcept("$.[0].author.loginName").value(XXX)
【这里和之前的assertThat(xxx).isEqualTo(XXX)类似】

自动配置与外部配置

其实前面大部分的内容都是SpringBoot的测试…@SpringBootTest,@WebMvcTest,@DataJpaTest

SpringBoot的自动配置之前分享过,就是@AutoConfigruation会去扫描外部的starter遵守SPI规范创建的【服务发现机制】META-INF/factories,扫描将需要自动装配的对象放入到springBoot容器中即可

而外部配置则是使用properties、yml文件,结合注解@Value,@ConfigurationProperties,可以将配置注入程序 【比如在yml中配置的自定义数据就需要使用@Value才能在程序中使用】

配置启用的优先级最高的就是Devtools中的全局配置文件,.spring-boot-devtools.properties【要启用devtools,引入了devtools依赖】

【port的配置不一定是在yml中进行,在run/configuration中也可以图形化配置(博主不喜欢界面,比如boot-CLI好用多了)】

application.yml

配置文件的优先级: properties > yml > yaml

项目中使用h2嵌入式DB

为了快捷搭建demo,快速调试,可以使用h2嵌入式数据库,这里我们首先就直接使用IDEA的database模块,创建一个h2数据库

创建时,选择embed选项,代表嵌入,同时设置url路径为: jdbc:h2:~cfeng,这里就是在yml中配置的连接url,创建好之后,可以测试连接一下,测试连接不成功,那可能没有开启相关的environment,所以需要到Edit configuration中进行设置一下为MODULES …; 之后就可以了

但是需要注意: 如果在yml中设置为: jdbc:h2:~/cfengBase;MV_STORE=false

那么会报错,因为后面的选项是关闭了MV的STORE

  • 之后就是在yml中进行配置
spring:
  datasource:
    url: jdbc:h2:~/cfengBase
    driver-class-name: org.h2.Driver
    username: cfeng
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update
    database: h2
    show-sql: true
    open-in-view: true   #这个是为了进行视图显示,SpringBoot提示的Warn
  h2:
    console:
      enabled: true
      path: /h2-console

首先就是配置dataSource,这里的url就是刚刚创建的嵌入式h2,同时driver就是h2.Driver;之后就是设置jpa的参数,包括ddl-auto代表启动时的权限,还有展示show-sql和指定database为h2, 之后就是设置h2的参数,比如enabled就是启用,还有path设置路径,这里可以使用IDEA,所以可以不配置

  • 创建repository对象,继承JpaRepository【CrudRepositoty】也行,可以不加@Repository,因为继承了之后默认会常见相关的Bean对象
public interface BlogUserRepository extends JpaRepository<BlogUser,Long> {
    //通过登录名查找用户
    BlogUser findUserByLoginName(String loginName);
}
  • 创建相关的类操作repository对象

自动注入三种方式 final + @RequiredArgsConstrutor

  1. 首先就是常见的属性注入【其实就是对应xml配置文件中的】使用@Autowired加上@qulifier或者@Resource就可以
 @Resource
 private BlogUserRepository blogUserRepository;

缺点就是这里的bean是可以修改的,如果一个关键的大型项目,不小心设置为null,那么就无了
  1. Setter注入

set注入在xml中就是构建之后直接就会查找对应的set方法,如果使用注解的方式,就会显得比上面的方式繁琐一些

private BlogUserRepository blogUserRepository
    
public void setBlogUserRepository(BlogUserRepository blogUserRepository) {
    this.blogUserRepository = blogUserRepository;
}
  1. 构造器注入,final + 构造器, springboot推荐的方式
 private final BlogUserRepository blogUserRepository;

 public BlogUserController(BlogUserRepository blogUserRepository) {
      this.blogUserRepository = blogUserRepository;
  }

当然这里可以借助Lombok插件简化,毕竟就是一个构造器

@RequiredArgsConstructor  //配合final属性进行自动注入
public class BlogUserController {

 private final BlogUserRepository blogUserRepository;

因为这里SpringBoot在发现其有属性之后,就会使用反射机制并且在容器中寻找对应的对象,通过构造方法注入;之前的@Autowired等类型注解就是直接找到对象赋值给属性,推荐使用构造器的方式,final修饰,不可修改

@ConfigurationProperties 配置文件映射

这个注解的作用就是实现配置文件的属性值映射为一个具体的对象,对于yml来讲,就是一级下面的所有的子属性都是该类的属性

可以加上@Component来将其变为对象放入spring容器,或者也可以通过在主启动类上面加上一个@EnableConfigurationProperties(BlogProperties.class), 创建其对象放入容器,思路和之前MapperScan等类似 //扫描需要进行配置文件属性注入的类

//@ConfigurationProperties要能够被主启动类发现,需要在主启动类上加上EnableXXX启用这个配置文件接收类
@ConfigurationProperties("blog")

响应中文乱码问题

server:
  port: 8086
  servlet:
    context-path:
    encoding:
      charset: UTF-8
      force: true
      enabled: true

就和之前的原生的Servlet中直接设置contentType,还有后面直接配置一个Spring时代配置一个字符过滤器,到了更加easy的SpringBoot时代,直接配置servet.servlet.endcoding即可; 当然还是离不开Servlet

org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL via JDBC Statement

  • 出现这个问题,说明是表创建有问题,如果你使用的Mysql,可以检查一下方言,也就是SpringBoot配置中的datasource-plateform是否是正确的,不同的版本对应不同的方言

  • 当然,h2不是这个问题,那么问题还可能就是表名或者类名使用了H2的关键字; 我的问题就在于之前的用户表名为User,而H2中就默认有User这个关键字,所以报错了,这个时候修改为BlogUser就可以成功创建了

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘articleRepository’: Invocation of init method failed;

  • 出现这个问题,如果你的repository没有想用作repository,那么加一个NoRepoistoryBean注解即可,就不会创建bean
  • 这里不关@Repository什么事【可加可不加】,因为继承了JpaRepository或者CrudRepository就会自动创建Bean; 问题主要在于全自动的JPA框架,要求自定义dao方法遵循JPA规范; 也就是说自定义的方法的名称是不能随意写的,因为JPA不需要写SQL,解析sql就是依靠JPA规范下的方法名称,比如findAllByOrderByAddedAtDesc; 这个名称就是不能改变的; -----> 这里的含义就是查询所有的对象按照时间降序排列 【我之前就是因为这里手快了方法名写错了一个字母报错的】

SpringBoot会自动扫描这些repository接口床架具体的实现对象放入容器中,就和之前扫描dao接口相同,但是这里不需要相关的Scan,而Mybatis中需要MapperScan

🎄程序出现异常的时候不要急忙去网上找答案,先分析一下异常的含义,出现同一个错误的原因不同,还是通过异常的提示信息直接定位来的轻松🎄

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐