不为有趣之事,何遣有涯之生
不失其所者久,死而不亡者寿

SpringDataJPA系列(4)

hzqiuxm阅读(5152)

4 Repository 中的方法返回值使用

Repository 返回结构有哪些?

打开 SimpleJpaRepository 直接看它的 Structure 就可以知道,它实现的方法,以及父类接口的方法和返回类型包括:Optional、Iterable、List、Page、Long、Boolean、Entity 对象等,而实际上支持的返回类型还要多一些。

由于 Repository 里面支持 Iterable,所以其实 java 标准的 List、Set 都可以作为返回结果,并且也会支持其子类,Spring Data 里面定义了一个特殊的子类 Steamable,Streamable 可以替代 Iterable 或任何集合类型。

Steamable

这里单独介绍下Steamable,它是一个函数式接口,它继承了Iterable,所以使用它我们可以很方便的返回集合类型的数据。

官方给我们提供了自定义 Streamable 的方法,不过在实际工作中很少出现要自定义保证结果类的情况。其原理很简单,就是实现Streamable接口,自己定义自己的实现类即可

List/Stream/Page/Slice

  • List:根据条件纯列表式返回
  • Stream:通过使用 Java 8 Stream 作为返回类型来逐步处理查询方法的结果,要注意流的关闭
  • Page:带分页信息的返回,分页信息中包含:数据内容、分页数据、当前数据描述等信息。数据量大的时候避免使用,会隐含一次count(*)操作
  • Slice:只查询偏移量,不计算分页数据的返回,不关心总页数。这就是 Page 和 Slice 的主要区别,现代分页方式推荐这种方式的查询

Future /CompletableFuture 异步结果返回

@Async
Future<User> findByFirstname(String firstname); 
@Async
CompletableFuture<User> findOneByFirstname(String firstname); 
@Async
ListenableFuture<User> findOneByLastname(String lastname);

在实际工作中,直接在 Repository 这一层使用异步方法的场景不多,一般都是把异步注解放在 Service 的方法上面,这样的话,可以有一些额外逻辑,如发短信、发邮件、发消息等配合使用。使用异步的时候一定要配置线程池,这点切记,否则“死”得会很难看。万一失败我们会怎么处理?关于事务是怎么处理的呢?这种需要重点考虑的

Reactive 支持

其实Common里面提供的只是接口,而JPA里面没有做相关的Reactive 的实现,但是本身Spring Data Common里面对 Reactive 是支持的。如果我们引入spring-boot-starter-data-mongodb的依赖,会发现它天然地支持着 Reactive 这条线。这些内容会放在后续mongodb专题中讲解

返回结果小结

下表列出了 Spring Data JPA Query Method 机制支持的方法的返回值类型

DTO返回结果支持哪些

比较规范的开发团队中都会有这么一条规范:不能暴露底层数据结构,也就是不允许将实体entity直接返回给出去,所以就诞生出了VO/DTO/DO/PO等概念。

于是在开发中我们必须自己去写各种DO/DTO/VO之间转换,同时还产生了各种工具类:BeanUtil、Mapstruct、Dozer、Orika、ModelMapper、Jmapper等(比较推荐Mapstruct综合性能最佳)
Spring Data 正是考虑到了这一点,引入了Projections映射这一概念, 它指的是和 DB 的查询结果的字段映射关系。允许对专用返回类型进行建模,有选择地返回同一个实体的不同视图对象。
如果我们的User只需要返回name和email,那应该怎么做呢?

方法一:新建实体类和资源库

然后,新增一个 UserOnlyNameEmailEntityRepository,做单独的查询

很显然这种方法比较糟糕,当存在多种返回值的情况,我们难道要定义多个实体类和资源库吗?
当然这种方式也并非一无是处,对那些临时需求,奇葩需求,生命周期和演化方向不同需求,为了保证我们架构和代码的整洁性与业务可扩展性,我们这么做也不一种蛮不错的选择

方法二:定义DTO方式

然后定义单独的查询方法

下面是测试结果代码:

这里需要注意的是,如果我们去看源码的话,看关键的 PreferredConstructorDiscoverer 类时会发现,UserDTO 里面只能有一个全参数构造方法。
所以这种方式的优点就是返回的结果不需要是个实体对象,对 DB 不能进行除了查询之外的任何操作;缺点就是因为DTO要实现转化必须要有set方法,一旦有 set 方法就可以改变里面的值,构造方法不能更改,必须全参数,这样如果是不熟悉 JPA 的新人操作的时候很容易引发 Bug

方法三:POJO接口方式

这种方式与上面两种的区别是只需要定义接口,它的好处是只读,不需要添加构造方法,我们使用起来非常灵活,一般很难产生 Bug


仍然定义一个单独的查询方法:

下面是测试代码:

这个时候会发现我们的 userOnlyName 接口成了一个代理对象,里面通过 Map 的格式包含了我们的要返回字段的值(如:name、email),我们用的时候直接调用接口里面的方法即可,如 userOnlyName.getName() 即可;这种方式的优点是接口为只读,并且语义更清晰。(DTO/VO转换貌似有点不方便)

总结

如果不熟悉JPA各种返回值和特点,我们很难在实际项目中选择正确的使用方法。

越容易上手的技术其实越难以恰到好处的使用。就像Springboot出现后,许多不懂Spring的开发人员一旦遇到问题完全是只能百度和谷歌,如果搜不到,完全就处于懵逼状态,目前有太多的开发人员连二者的区别都弄不清楚。

JPA的使用看起来非常方便,如果没有进一步理解,很容易误用和乱用,从而导致性能和数据安全性方面的问题。使用SpringDataJPA非常简单,但是要用好SpringDataJPA我们还是需要花一些功夫的

SpringDataJPA系统(3)

hzqiuxm阅读(3770)

DQM的命名语法与参数

在工作中,你是否经常为方法名的语义、命名规范而发愁?是否要为不同的查询条件写各种的 SQL 语句?是否为同一个实体的查询,写一个超级通用的查询方法或者 SQL?如果其他开发同事不查看你写的 SQL 语句,而直接看方法名的话,却不知道你想查什么而郁闷?

Spring Data JPA 的 Defining Query Methods(DQM)通过方法名和参数,可以很好地解决上面的问题,也能让我们的方法名的语义更加清晰,开发效率也会提升很多。

可以说Spring Data JPA 的最大特色是利用方法名定义查询方法(Defining Query Methods)来做 CRUD 操作。
DQM 语法共有 2 种,可以实现上面的那些问题:

  • 一种是直接通过方法名就可以实现
  • 另一种是 @Query 手动在方法上定义

定义查询方法的配置和使用方法

若想要实现 CRUD 的操作,常规做法是写一大堆 SQL 语句。但在 JPA 里面,只需要继承 Spring Data Common 里面的任意 Repository 接口或者子接口,然后直接通过方法名就可以实现,神不神奇?

比如我们要增加一个根据Email地址来查询用户,可以在接口中这么写:

然后你不用写具体的实现就可以直接使用该方法来达到根据邮箱地址查询用户的目的了,测试下该方法,完全没问题


方法查询策略设置

在平时的工作中,你可以通过方法名,或者定义方法名上面添加 @Query 注解两种方式来实现 CRUD 的目的,而 Spring 给我们提供了两种切换方式。

在实际生产中还没有遇到要修改默认策略的情况,但我们必须要知道有这样的配置方法,做到心中有数,这样我们才能知道为什么方法名可以,@Query 也可以。

通过 @EnableJpaRepositories 注解来配置方法的查询策略,共有三种方式:

  • Create:直接根据方法名进行创建,规则是根据方法名称的构造进行尝试,一般的方法是从方法名中删除给定的一组已知前缀,并解析该方法的其余部分。如果方法名不符合规则,启动的时候会报异常,这种情况可以理解为,即使配置了 @Query 也是没有用的。

  • USE_DECLARED_QUERY:声明方式创建,启动的时候会尝试找到一个声明的查询,如果没有找到将抛出一个异常,可以理解为必须配置 @Query。

  • CREATE_IF_NOT_FOUND:这个是默认的,除非有特殊需求,可以理解为这是以上 2 种方式的兼容版。先用声明方式(@Query)进行查找,如果没有找到与方法相匹配的查询,那用 Create 的方法名创建规则创建一个查询;这两者都不满足的情况下,启动就会报错。(所以一般用默认就足够了)

DQM语法

该语法是:带查询功能的方法名由查询策略(关键字)+ 查询字段 + 一些限制性条件组成,具有语义清晰、功能完整的特性,我们实际工作中 80% 的 API 查询都可以简单实现。

我们来看一个复杂点的例子,这是一个 and 条件更多、distinct or 排序、忽略大小写的例子。

interface PersonRepository extends Repository<User, Long> {
   // and 的查询关系
   List<User> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
   // 包含 distinct 去重,or 的 sql 语法
   List<User> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
   // 根据 lastname 字段查询忽略大小写
   List<User> findByLastnameIgnoreCase(String lastname);
   // 根据 lastname 和 firstname 查询 equal 并且忽略大小写
   List<User> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); 
  // 对查询结果根据 lastname 排序,正序
   List<User> findByLastnameOrderByFirstnameAsc(String lastname);
  // 对查询结果根据 lastname 排序,倒序
   List<User> findByLastnameOrderByFirstnameDesc(String lastname);
}

下面罗列了DMQ里的一些关键字列表:
可以前往官方看更加详细的介绍:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods

  • 方法名的表达式通常是实体属性连接运算符的组合,如 And、or、Between、LessThan、GreaterThan、Like 等属性连接运算表达式,不同的数据库(NoSQL、MySQL)可能产生的效果不一样,如果遇到问题,我们可以打开 SQL 日志观察。
  • IgnoreCase 可以针对单个属性(如 findByLastnameIgnoreCase(…)),也可以针对查询条件里面所有的实体属性忽略大小写(所有属性必须在 String 情况下,如 findByLastnameAndFirstnameAllIgnoreCase(…))
  • OrderBy 可以在某些属性的排序上提供方向(Asc 或 Desc),称为静态排序,也可以通过一个方便的参数 Sort 实现指定字段的动态排序的查询方法(如 repository.findAll(Sort.by(Sort.Direction.ASC, "myField")))

分页与排序

Spring Data JPA 为了方便我们排序和分页,支持了两个特殊类型的参数:Sort 和 Pageable。

Sort 在查询的时候可以实现动态排序

Sort 里面的dierection决定了我们哪些字段的排序方向(ASC 正序、DESC 倒序)

Pageable 在查询的时候可以实现分页效果和动态排序双重效果,我们看下下面这些例子:

Page<User> findByManagerId(String lastname, Pageable pageable);//根据分页参数查询User,返回一个带分页结果的Page(下一课时详解)对象(方法一)
Slice<User>findByManagerId(String lastname, Pageable pageable);//我们根据分页参数返回一个Slice的user结果(方法二)
List<User> findByManagerId(String lastname, Sort sort);//根据排序结果返回一个List(方法三)
List<User> findByManagerId(String lastname, Pageable pageable);//根据分页参数返回一个List对象(方法四)
  • 方法一:允许将 org.springframework.data.domain.Pageable 实例传递给查询方法,将分页参数添加到静态定义的查询中,通过 Page 返回的结果得知可用的元素和页面的总数。这种分页查询方法可能是昂贵的(会默认执行一条 count 的 SQL 语句),所以用的时候要考虑一下使用场景。

  • 方法二:返回结果是 Slice,因为只知道是否有下一个 Slice 可用,而不知道 count,所以当查询较大的结果集时,只知道数据是足够的,也就是说用在业务场景中时不用关心一共有多少页。目前项目开发中越来越推荐使用这个。

  • 方法三:如果只需要排序,需在 org.springframework.data.domain.Sort 参数中添加一个参数,正如上面看到的,只需返回一个 List 也是有可能的。

  • 方法四:排序选项也通过 Pageable 实例处理,在这种情况下,Page 将不会创建构建实际实例所需的附加元数据(即不需要计算和查询分页相关数据),而仅仅用来做限制查询给定范围的实体。

方法二比较实用,下面是它的一个具体实现和测试


限制查询数量

有的时候我们想直接查询前几条数据,也不需要动态排序,那么就可以简单地在方法名字中使用 First 和 Top 关键字,来限制返回条数。
下面是几个方法示例:

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
List<User> findDistinctUserTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
  • 查询方法在使用 First 或 Top 时,数值可以追加到 First 或 Top 后面,指定返回最大结果的大小
  • 如果数字被省略,则假设结果大小为 1
  • 限制表达式也支持 Distinct 关键字
  • 支持将结果包装到 Optional 中
  • 如果将 Pageable 作为参数,以 Top 和 First 后面的数字为准,即分页将在限制结果中应用

关于NULL支持

  • @NonNullApi:在包级别用于声明参数,以及返回值的默认行为是不接受或产生空值的。
  • @NonNull:用于不能为空的参数或返回值(在 @NonNullApi 适用的参数和返回值上不需要)
  • @Nullable:用于可以为空的参数或返回值

在资源库的package-info.java文件中加入@NonNullApi注解,表示该模块下返回值不接受或产生空值

当我们查询参数为空或数据库没有数据返回值为空时,则都会抛出异常,下面是测试示例代码

但好像返回值为空抛异常不是很友好,所以我们可以使用Optional来进行返回值包装,如下所示:

原理解析

每个版本更新后可能支持的语法和效果会有所不同,注意看最新的官方文档介绍。我们这里介绍下其简单实现原理。
通过org.springframework.data.repository.query.parser.PartTree 查看相关源码了解其逻辑和处理方法


根据源码我们也可以分析出来,query method 包含其他的表达式,比如 find、count、delete、exist 等关键字在 by 之前通过正则表达式匹配。

由此可知,我们方法中的关键字不是乱填的,是枚举帮我们定义好的。接下来打开枚举类 Type 源码看下,比什么都清楚。
所以知道了这些通过看源码就可以知道框架支持了哪些逻辑关键字,比如 NotIn、Like、In、Exists 等,有的时候比查文档和任何人写的博客都准确、还快。

总结

DQM的命名规范值得我们去借鉴,在我们开发的给一些方法取名字的时候可以按照这个规范去执行,有利于提升方法的表意性。
我们还可以自己定义一个自己的BaseService,声明了常用的CRUD操作。我们也可以建立自己的 PagingAndSortingService、ComplexityService、SampleService 等来划分不同的 service接口,供不同目的 Service 子类继承。

public interface BaseService<T, ID> {
    Class<T> getDomainClass();
    <S extends T> S save(S entity);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void delete(T entity);
    void deleteById(ID id);
    void deleteAll();
    void deleteAll(Iterable<? extends T> entities);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
    <S extends T> Optional<S> findOne(Example<S> example);
    Optional<T> findById(ID id);
    List<T> findAll();
    List<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);
    <S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
    List<T> findAllById(Iterable<ID> ids);
    long count();
    <S extends T> long count(Example<S> example);
    <S extends T> boolean exists(Example<S> example);
    boolean existsById(ID id);
    void flush();
    <S extends T> S saveAndFlush(S entity);
}

下面是对Baseservice的基本实现

public class BaseServiceImpl<T, ID, R extends JpaRepository<T, ID>> implements BaseService<T, ID> {
    private static final Map<Class, Class> DOMAIN_CLASS_CACHE = new ConcurrentHashMap<>();
    private final R repository;
    public BaseServiceImpl(R repository) {
        this.repository = repository;
    }
    @Override
    public Class<T> getDomainClass() {
        Class thisClass = getClass();
        Class<T> domainClass = DOMAIN_CLASS_CACHE.get(thisClass);
        if (Objects.isNull(domainClass)) {
            domainClass = GenericsUtils.getGenericClass(thisClass, 0);
            DOMAIN_CLASS_CACHE.putIfAbsent(thisClass, domainClass);
        }
        return domainClass;
    }
    protected R getRepository() {
        return repository;
    }
    @Override
    public <S extends T> S save(S entity) {
        return repository.save(entity);
    }
    @Override
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        return repository.saveAll(entities);
    }
    @Override
    public void delete(T entity) {
        repository.delete(entity);
    }
    @Override
    public void deleteById(ID id) {
        repository.deleteById(id);
    }
    @Override
    public void deleteAll() {
        repository.deleteAll();
    }
    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        repository.deleteAll(entities);
    }
    @Override
    public void deleteInBatch(Iterable<T> entities) {
        repository.deleteInBatch(entities);
    }
    @Override
    public void deleteAllInBatch() {
        repository.deleteAllInBatch();
    }
    @Override
    public T getOne(ID id) {
        return repository.getOne(id);
    }
    @Override
    public <S extends T> Optional<S> findOne(Example<S> example) {
        return repository.findOne(example);
    }
    @Override
    public Optional<T> findById(ID id) {
        return repository.findById(id);
    }
    @Override
    public List<T> findAll() {
        return repository.findAll();
    }
    @Override
    public List<T> findAll(Sort sort) {
        return repository.findAll(sort);
    }
    @Override
    public Page<T> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }

    @Override
    public <S extends T> List<S> findAll(Example<S> example) {
        return repository.findAll(example);
    }
    @Override
    public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
        return repository.findAll(example, sort);
    }

    @Override
    public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {
        return repository.findAll(example, pageable);
    }
    @Override
    public List<T> findAllById(Iterable<ID> ids) {
        return repository.findAllById(ids);
    }
    @Override
    public long count() {
        return repository.count();
    }
    @Override
    public <S extends T> long count(Example<S> example) {
        return repository.count(example);
    }
    @Override
    public <S extends T> boolean exists(Example<S> example) {
        return repository.exists(example);
    }
    @Override
    public boolean existsById(ID id) {
        return repository.existsById(id);
    }
    @Override
    public void flush() {
        repository.flush();
    }
    @Override
    public <S extends T> S saveAndFlush(S entity) {
        return repository.saveAndFlush(entity);
    }
}

以上代码就是 BaseService 常用的 CURD 实现代码,我们这里面大部分也是直接调用 Repository 提供的方法。mybatics-plus框架中也实现了基本服务层代码,可以去参考下。

SpringDataJPA系列(2)

hzqiuxm阅读(3532)

Spring Data Common核心Repository

Spring Data Commons依赖关系

我们通过 Gradle 看一下项目依赖,了解一下 Spring Data Common 的依赖关系

通过上图的项目依赖,不难发现,数据库连接用的是 JDBC,连接池用的是 HikariCP,强依赖 Hibernate;Spring Boot Starter Data JPA 依赖 Spring Data JPA;而 Spring Data JPA 依赖 Spring Data Commons。 Spring Data Commons 是终极依赖,所以它是我们重点关注对象

Repository 接口

Repository 是 Spring Data Common 里面的顶级父类接口,操作 DB 的入口类。

Resposiory 是 Spring Data 里面进行数据库操作顶级的抽象接口,里面什么方法都没有,但是如果任何接口继承它,就能得到一个 Repository,还可以实现 JPA 的一些默认实现方法。
Spring 利用 Respository 作为 DAO 操作的 Type,以及利用 Java 动态代理机制就可以实现很多功能,比如为什么接口就能实现 DB 的相关操作?这就是 Spring 框架的高明之处。
Spring 在做动态代理的时候,只要是它的子类或者实现类,再利用 T 类以及 T 类的 主键 ID 类型作为泛型的类型参数,就可以来标记出来、并捕获到要使用的实体类型,就能帮助使用者进行数据库操作。

Repository 类层次关系

查看Repository 所在源码的路径,我们可以发现JPA存储库可以分成四个大类:

  • 1处:CoroutineCrudRepository 这条继承关系链是为了支持 Kotlin 语法而实现的
  • 2处:ReactiveCrudRepository 这条线是响应式编程,主要支持当前 NoSQL 方面的操作,因为这方面大部分操作都是分布式的,所以由此我们可以看出 Spring Data 想统一数据操作的“野心”,即想提供关于所有 Data 方面的操作。目前 Reactive 主要有 Cassandra、MongoDB、Redis 的实现。
  • 3处:RxJava2CrudRepository 这条线是为了支持 RxJava 2 做的标准响应式编程的接口,可以看到它有两个版本的实现
  • 4处:CrudRepository 这条继承关系正是我们目前大多数项目开发经常使用到的 JPA 相关的操作接口

再看下Resposiory相关接口的关系图:

  • Repository(org.springframework.data.repository),没有暴露任何方法
  • CrudRepository(org.springframework.data.repository),简单的 Curd 方法
  • PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法
  • QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询
  • JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法
  • JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询
  • QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装
  • SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类
  • QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类

我们使用的时候一般是使得自定义接口继承JpaRepository

分水岭JpaRepository

JpaRepository之前的几个接口都是 Spring Data 为了兼容 NoSQL 而进行的一些抽象封装,而从 JpaRepository 开始是对关系型数据库进行抽象封装。所以你使用mysql数据库的时候都是直接继承JpaRepository。
JpaRepository中主要增加的是一些批量操作的方法,其实现类也是 SimpleJpaRepository。还优化了批量删除的性能,类似于之前 SQL 的 batch 操作,并不是像上面的 deleteAll 来 for 循环删除。其中 flush() 和 saveAndFlush() 提供了手动刷新 session,把对象的值立即更新到数据库里面的机制。
我们都知道 JPA 是 由 Hibernate 实现的,所以有 session 一级缓存的机制,当调用 save() 方法的时候,数据库里面是不会立即变化的。

Resposiory接口与实现类SimpleJpaRepository

我们可以在CrudRepository、PagingAndSortingRepository等接口发现一些方法,而这些方法的实现都是在SimpleJpaRepository类中(如果是其他 NoSQL的 实现如 MongoDB,那实现就在 Spring Data MongoDB 的 jar 里面的 MongoRepositoryImpl)。

我们可以在SimpleJpaRepository源码中看到每个方法的注释,标识出了该方法是实现了哪个接口中的哪个方法

这里特别强调了一下 Delete 和 Save 方法,是因为在实际工作中,看到有的同事画蛇添足:自己在做 Save 的时候先去 Find 一下,其实是没有必要的,Spring JPA 底层都考虑到了。在进行 Update、Delete、Insert 等操作之前会通过 findById 先查询一下实体对象的 ID,然后再去对查询出来的实体对象进行保存操作。而如果在 Delete 的时候,查询到的对象不存在,则直接抛异常。

如果有些业务场景需要进行扩展了,可以继续继承此类。如 QueryDsl 的扩展(虽然不推荐使用了,但我们可以参考它的做法,自定义自己的 SimpleJpaRepository),如果能将此类里面的实现方法看透了,基本上 JPA 中的 API 就能掌握大部分内容。

UserRepository 的实现类是 Spring 启动的时候,利用 Java 动态代理机制帮我们生成的实现类,而真正的实现类就是 SimpleJpaRepository。 SimpleJpaRepository 的实现机制,是通过 EntityManger 进行实体的操作,而 JpaEntityInforMation 里面存在实体的相关信息和 Crud 方法的元数据等。

我们可以在 RepositoryFactorySupport 设置一个断点,启动的时候,在我们的断点处就会发现 UserRepository 的接口会被动态代理成 SimpleJapRepository 的实现


这里需要注意的是每一个 Repository 的子类,都会通过这里的动态代理生成实现类。

总结

在接触了 Repository 的源码之后,我们在工作中遇到过一些类似需要抽象接口和写动态代理的情况,可以从Repository 的源码中获得这些启发:

  • 上面的 7 个大 Repository 接口,我们在使用的时候可以根据实际场景,来继承不同的接口,从而选择暴露不同的 Spring Data Common 给我们提供的已有接口。这其实利用了 Java 语言的 interface 特性,在这里可以好好理解一下 interface 的妙用。
  • 利用源码也可以很好地理解一下 Spring 中动态代理的作用,可以利用这种思想,在改善 MyBatis 的时候使用

SpringDataJPA系列(1)

hzqiuxm阅读(4598)

SpingDataJPA概述


SpringDataJPA似乎越来越流行了,我厂的mysql数据库和MongoDB数据库持久层都依赖了SpringDataJPA。为了更好的使用它,我们内部还对MongoDB的做了进一步的抽象和封装。为了查漏补缺,温故而知新,整理下SpringDataJPA相关知识,Check下实践过程中哪些地方还存在哪些不足,从而进一步的优化之。

几种ORM框架对比

首先看下几种流行ORM的对比

JPA的开源实现

  • 一套 API 标准定义了一套接口,在 javax.persistence 的包下面,用来操作实体对象,执行 CRUD 操作,而实现的框架(Hibernate)替代我们完成所有的事情,让开发者从烦琐的 JDBC 和 SQL 代码中解脱出来,更加聚焦自己的业务代码,并且使架构师架构出来的代码更加可控
  • 定义了一套基于对象的 SQL:Java Persistence Query Language(JPQL),像 Hibernate 一样,我们通过写面向对象(JPQL)而非面向数据库的查询语言(SQL)查询数据,避免了程序与数据库 SQL 语句耦合严重,比较适合跨数据源的场景(一会儿 MySQL,一会儿 Oracle 等)
  • ORM(Object/Relational Metadata)对象注解映射关系,JPA 直接通过注解的方式来表示 Java 的实体对象及元数据对象和数据表之间的映射关系,框架将实体对象与 Session 进行关联,通过操作 Session 中不同实体的状态,从而实现数据库的操作,并实现持久化到数据库表中的操作,与 DB 实现同步。

JPA 的宗旨是为 POJO 提供持久化标准规范,可以集成在 Spring 的全家桶使用,也可以直接写独立 application 使用,任何用到 DB 操作的场景,都可以使用,极大地方便开发和测试,所以 JPA 的理念已经深入人心了。Spring Data JPA、Hibernate 3.2+、TopLink 10.1.3 以及 OpenJPA、QueryDSL 都是实现 JPA 协议的框架

俗话说得好:“未来已经来临,只是尚未流行”,大神资深开发用 Spring Data JPA,编程极客者用 JPA;而普通 Java 开发者,不想去挑战的 Java“搬砖者”用 Mybatis。

SpringData子项目

Spring Data Common 是 Spring Data 所有模块的公共部分,该项目提供了基于 Spring 的共享基础设施,它提供了基于 repository 接口以 DB 操作的一些封装,以及一个坚持在 Java 实体类上标注元数据的模型。

Spring Data 不仅对传统的数据库访问技术如 JDBC、Hibernate、JDO、TopLick、JPA、MyBatis 做了很好的支持和扩展、抽象、提供方便的操作方法,还对 MongoDb、KeyValue、Redis、LDAP、Cassandra 等非关系数据的 NoSQL 做了不同的实现版本,方便我们开发者触类旁通。

下图为目前 Spring Data 的框架分类结构图:

主要接口类关系

注意:图中的UserRepository是我们自己实现的接口,继承了JpaRepository和JpaSpecificationExecutor

7个核心接口

  • Repository(org.springframework.data.repository),没有暴露任何方法;
  • CrudRepository(org.springframework.data.repository),简单的 Curd 方法;
  • PagingAndSortingRepository(org.springframework.data.repository),带分页和排序的方法;
  • QueryByExampleExecutor(org.springframework.data.repository.query),简单 Example 查询;
  • JpaRepository(org.springframework.data.jpa.repository),JPA 的扩展方法;
  • JpaSpecificationExecutor(org.springframework.data.jpa.repository),JpaSpecification 扩展查询;
  • QueryDslPredicateExecutor(org.springframework.data.querydsl),QueryDsl 的封装

2大实现类

  • SimpleJpaRepository(org.springframework.data.jpa.repository.support),JPA 所有接口的默认实现类;
  • QueryDslJpaRepository(org.springframework.data.jpa.repository.support),QueryDsl 的实现类

本章节主要是对JPA有个大致的认知,下一节开始进行核心类与功能的讲解

领域驱动战略篇(5)

hzqiuxm阅读(4996)

DDD实践原则规范

DDD设计原则

聚合根

  • 业务逻辑优先在聚合根边界内完成
  • 对聚合根的设计需要提防上帝对象(God Object),也即用一个大而全的领域对象来实现所有的业务功能

    例如:不同的限界上下文中都有各自的Product,有些Product是聚合根,有些不是

  • 聚合根的实现应该与框架无关,最好是POJO

  • 聚合同时也是存储的单元,这样可以保证持久化的简单,特别适合nosql
  • 聚合根之间的引用通过ID完成
  • 聚合根内部的所有变更都必须通过聚合根完成,聚合根是整个聚合的管理者
  • 如果出现一个事务更新了多个聚合根,重新检查聚合根边界是否出了问题;如果确实是业务需要,可以引入消息机制和事件驱动来保证
  • 聚合根不应该引用基础设施
  • 外界不应该持有聚合根内部数据结构
  • 尽量设计小聚合,以后即使要合并聚合也比拆分更简单
  • 聚合根里面没有实体,并不意味着数据库就只有一张表,可以设计成多张表。DB设计和领域建模没有关系
聚合的三大基本规则
  • 1 只引用聚合根,客户端只能通过聚合根对聚合进行操作
  • 2 聚合间引用必须使用主键
  • 3 在一个事务中只能创建或更新一个聚合(微服务中表现几乎完美结合saga做事务管理,在使用关系型数据库可以适当打破)

实体与值对象

  • 实体对象表示的是具有一定生命周期(聚合根消亡,其他实体也消亡)并且拥有全局唯一标识(ID)的对象
  • 值对象是不变的,也就说一个值对象一旦被创建出来了便不能对其进行变更,如果要变更,必须重新创建一个新的值对象整体替换原有的
  • 区分实体和值对象的一个很重要的原则便是根据相等性来判断,前者通过ID来区别,后者通过属性
  • 将业务概念尽量建模为值对象

资源库

  • 资源库(Repository)就是用来持久化聚合根的,只有聚合根才“配得上”拥有Repository,所以一个聚合对应一个资源库
  • 有的DDD实践者甚至认为一个纯粹的Repository只应该包含save和byId两个方法(我们不强求,还要结合具体业务,特别是读操作)
  • 保证资源库接口不要混入基础设施的实现
  • 在领域层,只有领域服务才依赖于资源库
  • 要持久化除根实体在外的其他实体或值对象,必须交由聚合对应的资源库来实现(会不会导致资源库非常庞大?是不是要引入CQRS)
    > 还是可以单独更新/替换聚合中的其他实体或值对象的,有一个方法saveAggr(),还可以有saveEntity()方法
  • 实体的持久化要依赖聚合根
  • 值对象的持久化可独立,通过层级对象来实现(自定义一个拥有id的父类)

工厂

  • 简单的聚合根创建过程,直接在聚合根中实现Factory方法
  • 复杂度的聚合根创建过程,使用独立的Factory类
  • 自定义的创建方法含义是业务上的创建,而构造方法是技术语言上的创建

领域服务

  • 聚合根中不合适放置的业务逻辑才考虑放到DomainService中
  • 应用服务是必须要有的,领域服务不是,它是对非行为方法的一种妥协(通常是一些业务代理,生成ID类)

命令对象

  • 外部向领域模型发起的一次命令操作,Controller都要进行封装
  • 写操作一般以command作为后缀,传递给应用服务
  • 读操作一般以Representation作为后缀,传递给客户端
    public OrderId createOrder(CreateOrderCommand command) ; 
    public void changeProductCount(String id, ChangeProductCountCommand command) ;
    public void pay(String id, PayOrderCommand command) ;
    public OrderRepresentation toRepresentation(Order order);

业务中读写操作

  • 写操作中,我们需要严格地按照“应用服务 -> 聚合根 -> 资源库”的结构进行编码
  • 读操作中,领域模型中的对象不能直接返回给客户端

  • 以上三种方式,优先推荐基于数据模型的方式,一些类似于遵循JPA规范的框架,采用基于领域模型的方式也可以;CQRS比较庞大,需要结合业务场景慎重考虑

与工具技术结合使用原则

Lombok

会使用到Lombok的对象一般有领域实体对象、Command、Representation、DomainEvent。
Lombok不能随意使用,不然谁都可以任意调用,破坏了“最小权限”和“完整性”原则

请启用lombok的配置文件
  • 在项目的根目录中创建lombok.config文件,内容如下:
lombok.anyConstructor.addConstructorProperties=true # 方便Jackson找到对应的构造函数
lombok.addLombokGeneratedAnnotation = true # 将Lombok生成的代码标记为`Generated`,便于JaCoCo等排除这些代码
实体对象只用@Build和@Getter
  • @Builder会在没有自定义构造函数的情况下创建全参构造函数,结合先前的lombok.anyConstructor.addConstructorProperties=true,可以满足Jackson的反序列化
  • 外加自定义的create()方法来保证构建的完备性,其内部可以使用builder进行创建
  • 为了方便测试代码准备测试数据,测试代码可以调用builder直接构建对象而不用create()方法
@Builder
@Getter
public class Product extends BaseAggregate {
    private String id;
    private String name;
    private String description;
    private BigDecimal price;
    private Instant createdAt;
    private int inventory;
    private String categoryId;

    public static Product create(String name, String description, BigDecimal price, String categoryId) {
        Product product = Product.builder()
                .id(newUuid())
                .name(name)
                .description(description)
                .price(price)
                .createdAt(Instant.now())
                .inventory(0)
                .categoryId(categoryId)
                .build();
        product.raiseEvent(new ProductCreatedEvent(product.getId(), name, description, price, product.getCreatedAt()));
        return product;
    }
}
值对象只能使用@value
  • @value相当于是@Data的不可变形式
  • 不要去使用@Build,因为值对象的创建无法保证完备性
  • 对于Command对象,应该使用JSR-380中的注解来完成验证
@Value
public class CreateProductCommand {

    @NotBlank(message = "产品名字不能为空")
    private String name;

    @NotBlank(message = "产品描述不能为空")
    private String description;

    @NotNull(message = "产品价格不能为空")
    private BigDecimal price;

    @NotBlank(message = "产品所属目录不能为空")
    private String categoryId;
}
领域事件需要使用@ConstructorProperties和@Getter
  • DomainEvent一般是有继承关系的,Lombok创建父类并不优雅,使用@ConstructorProperties好看一点
  • @ConstructorProperties()是Spring提供的显示构造函数注解,
@Getter
public class ProductNameUpdatedEvent extends ProductEvent {
    private String oldName;
    private String newName;

    @ConstructorProperties({"productId", "oldName", "newName"})
    public ProductNameUpdatedEvent(String productId, String oldName, String newName) {
        super(productId);
        this.oldName = oldName;
        this.newName = newName;
    }
}

中间父类ProductEvent为:

@Getter
public abstract class ProductEvent extends DomainEvent {
    private String productId;

    protected ProductEvent(String productId) {
        this.productId = productId;
    }
}

最终的父类DomainEvent为:

@Getter
public abstract class DomainEvent {
    private String _id = UuidGenerator.newUuid();
    private Instant _createdAt = now();

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + "[" + _id + "]";
    }
}

这样既保证了调用方不会乱调构造函数,也保证了Jackson能够正确的反序列化

@Data用于基础设施层
  • @Data:主要用于基础设施层,在使用一些框架时如有需要可以使用,比如在java项目中使用mapstruct在各个对象之间进行复制时

类名规范

约束与规范

  • 如何检查代码是否范围了约束?
    1 使用自定义元注解的方式,比如@Aggregate表示一个聚合根,然后用代码检查聚合根是不是有互相引用的情况
    2 聚合根和实体用不同的类后缀,通过解析判断该类后缀文件有没有互相引用

推荐:ArchUnit和jQAssistant

测试驱动编程(3)

hzqiuxm阅读(2916)

测试驱动编程—模拟消除依赖

模拟框架Mockito

大道至简

什么要模拟

单元测试的要点就在于验证单个单元是否正常,而不考虑依赖,TDD中的单元测试尤其如此。
对于内部依赖,我们应该已对其进行测试过;对于外部依赖(JDK包),我们应该信任它们
消除依赖的两种手段:合理的设计和模拟实现,合理的设计与具体的业务有关,本次只介绍模拟实现如何去实践
下图是一个实际的关系,我们的目标就是通过模拟简化关系,方便测试

名词解释

测试替身的其它名字:哑元对象(dummy object)、测试存根(test stub)、测试间谍(test spy)、模拟对象(mock object)、伪造对象(fake object)

Mockito常用注解

使用的模拟框架是Mockito,它的常用注解如下:

  • @Mock:用于模拟的创建,使得测试类更具可读性(不调用真实方式,默认返回都是null),需要配对@ExtendWith(MockitoExtension.class)才能使用
  • @Spy:用于创建间谍实例,代替spy(Object)方法(调用真实方式)
  • @InjectMocks:用于自动实例化测试对象,并将所有的@Mock或@Spy注解字段依赖项注入其中(类似Spring框架中自动注入)
  • @Captor:用于创建参数捕获器

要处理所有上述注释,请MockitoAnnotations.initMocks(testClass); 必须至少使用一次。 要处理注释,我们可以使用内置的运行器MockitoJUnitRunner或规则MockitoRule 。 我们还可以在@Before注释的Junit方法中显式调用initMocks()方法。

Mockito常用静态方法

除了使用注解,我们还需要用到是它的三个主要静态方法:

  • mock():创建模拟对象,还可以使用when()和given()指定模拟行为
  • spy():实现部分模拟,调用实际的对象
  • verify():检查调用方法时提供的参数是否是指定参数,是一种断言

Mockito测试流程三部曲

  • 模拟:mock一个模拟对象

    模拟一个List对象,它会给所有方法添加基本实现,返回值和由方法的返回类型决定,如 int 会返回 0,布尔值返回 false。对于其他 type 会返回 null

  • 打桩:Stub打桩设置预期

    指定条件和预期返回

  • 验证:验证预期和实际值是否一致

基础用法

  • 无返回值,使用notify

  • 监视对象

  • 抛出异常

  • 模拟传入参数


    Mockito 提供 argument matchers 机制,例如 anyString() 匹配任何 String 参数,anyInt() 匹配任何 int 参数,anySet() 匹配任何 Set,any() 则意味着参数为任意值。自定义类型也可以,如 any(User.class)

可变返回结果

之前我们thenReturn 是返回结果都是写死的,如果要让被测试的方法不写死,返回实际结果并让我们可以获取到应该怎么做呢?
利用 InvocationOnMock 提供的方法可以获取 mock 方法的调用信息。下面是它提供的方法:

  • getArguments() 调用后会以 Object 数组的方式返回 mock 方法调用的参数
  • getMethod() 返回 java.lang.reflect.Method 对象
  • getMock() 返回 mock 对象
  • callRealMethod() 真实方法调用,如果 mock 的是接口它将会抛出异常

验证verfily

由程序员自己来决定验证结果,可以关注调用参数、返回结果、调用次数(times(0))
verify 也可以像 when 那样使用模拟参数,若方法中的某一个参数使用了matcher,则所有的参数都必须使用 matcher


在最后的验证时如果只输入字符串”hello”是会报错的,必须使用 Matchers 类内建的 eq 方法

对象监视spy

spy 的意思是你可以修改某个真实对象的某些方法的行为特征,而不改变他的基本行为特征

spy 保留了 list 的大部分功能,只是将它的 size() 方法改写了。不过 spy 在使用的时候有很多地方需要注意,一不小心就会导致问题,所以不到万不得已还是不要用 spy

示例实战

升级版井字游戏

“井字游戏”第二版的需求很简单:添加永久性存储,让玩家能够保存游戏的当前状态,以便以后接着玩。

需求一

作为玩家,我希望把当前下的棋能够保存起来,以便于我能看到下的历史记录
需求分析:需要保存的信息有轮次、X和Y坐标以及玩家
我这边打算以mongoDB数据库来进行持久化保存,选用什么数据库和测试驱动没什么关系
准备好依赖和环境配置:

-- 依赖
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
-- 配置
spring:
    data:
      mongodb:
        host: 192.168.3.112
        port: 27117
        database: tic-tac

我们的数据库叫tic-tac,并打算将数据保存在一个叫game的mongoDB集合中;接下来设计一个持久化类,用来保存游戏的相关数据
(示例项目源码链接将在文末给出,大家不用太关注技术细节,关注在功能和测试模拟上)

一般刚接触单元测试的开发人员的第一反应就是先初始化一个数据在数据库中,然后测试下能不能查到,类似下面代码:

上面是这些spring的相关配置和资源库自动注入,大家不用关心,和具体业务无关

1处表示构建一个游戏记录,然后将它保存到数据库中
2处表示根据唯一编号从数据库中取出游戏保存记录,检验下唯一编号是否对应的上

初看起来好像这个测试并没有什么问题,但是其实这里存在至少四个大问题:1 构建一个游戏记录本身就需要操作数据库,而且运行每个测试方法都要构建,很多数据库会导致主键冲突;2 构建记录本身需要依赖具体的数据库,需要配置一大堆额外的东西(数据库驱动、获取实例、进行连接、释放资源等等);3 万一有人修改了数据库的数据,测试用例将失败,你每次测试的时候得告诉别人别动我的数据!;4 因为设计到了数据库操作,要时刻保证你的数据库运行正常,因为测试用例往往很多,所以你还要忍耐相当长时间的数据库操作(本来单元测试都是在内存中快速运行的)

等等我们的目的就是要测试下井字游戏的逻辑,现在怎么好像变得在测试数据库了?数据库作为外部依赖我们原则上应该信任它才对呀,所以我们需要使用mockit来模拟数据库操作

所以我们如果只是测试下的一步棋被保存了,其实就是调用了资源库的保存方法即可,我们默认是信任资源库能保存成功的

模拟的对象的方式有二种一种是使用注解,一种是使用静态方法:


上面是静态方法方式


上面是注解的方式,需要在测试类上添加@ExtendWith(MockitoExtension.class)

井字游戏涉及到的几个类职责如下:

  • TicTacToeBean:存储游戏状态信息的实体类
  • TicTacService:提供存储服务,与资源库进行协作
  • TicTacRepository:对实体类数据进行持久化

下面是一个保存一步棋的测试示例,采用了注解的方式

注意:可以看到我们的测试类并没有使用@SpringBootTest注解,单元测试已经脱离了Spring上下文环境
1处模拟了资源库和服务,因为需要服务中需要调用资源库对数据库进行操作,所以需要将资源库注入到服务中,采用了@InjectMocks注解
2处是构建一个某步棋的状态信息
3处是调用服务保存下棋的信息
4处是验证,我们关心的是验证资源库的save方法是不是被调用了一次

因为还没有saveMove方法,所以编译直接报错了,这个是我们的的阶段,接着实现一个空方法,让编译通过

执行下测试用例后,发现报错了,因为和预期的不一样

预期是要产生一次数据库的save操作,结果实际上是0,然后实现刚才添加的空方法,让其变绿

跑下测试用了,发现变绿了,并查看下测试报告

我们目前只是测试了服务中的saveMove()方法,其实资源库的方法我们也应该测试下,由于项目中我们使用的基于SpringJpaData项目下的Spring Data MongoDB框架来进行操作数据库,底层实现和默认的方法都是遵循JPA规范的,不需要我们定义各种增删改查方法。各种方法的返回值也确定,比如之前的Save方法,其接口如下:

所以我们测试成功和失败的时候分别返回的是当前保存对象以及空对象,示例代码如下:

跑下测试用例,验证我们的结果

我们发现其中需要的游戏状态信息是重复代码,所以可以重构下,重构完毕后记得验证下测试用例

需求二

刚才只是实现了其中一步游戏信息状态的保存,在游戏过程中,我们还需要保存每一步的信息,并且每次重新开始的时候要清空数据库
在实施需求二之前,我们先把第一版的测试用例加进来,运行保证其正确性
经过分析大概有这么几个阶段:

  • 游戏开始时,初始化一个全新的游戏状态信息对象,同时删除原来的游戏记录

以上两个方法比较简单和之前的类似,就直接给出代码了,不一小步一小步的讲解了,完成后一定要保证测试用例全部通过

  • 每当玩家下一步就保存起来

运行测试用例发现报错了

因为游戏的逻辑中目前我们还没有调用保存,经过分析,我们应该在setBoard()中保存,如果在play中编写,第一版的大量测试用例需要重构。
目前该方法已经具备了保存的所有参数信息(不熟悉该方法的需要回顾下井字游戏的第一版本,即该系列教程的第一篇)
当前的方法如下所示,接收的是多个参数,我们需要重构为一个TicTacBean

重构后如下:

play方法中调用的地方也应该做相应的修改:

运行所有的测试用例,第一版游戏的用例和第二版游戏用例都全部通过,大功告成

需求三

当玩家是继续游戏时,读取当前游戏的状态然后继续
作为玩家,我想保存当前游戏,以便于我下次可以继续上次的游戏
需要保存的游戏信息:棋盘以及最后的玩家,我这边就先不给出步骤了,请大家自行实现下

PS:代码仓库 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的jinggame2模块

总结

单元测试的本质就是要限定好单元的边界,如果单元测试的过程在为如何解析请求报文或数据库连接之类事情烦恼,那么你的单元测试很有可能超过边界了

为了在边界内快速响应结果做出业务实现,在涉及到数据库等第三方依赖的时候,会使用模拟的方式来解决。Mockito是一个优秀的模拟框架,在性能和灵活性上做到了很好的平衡,值得大家去熟悉其常用的API和注解

一个模拟测试过程有三步:1 模拟(对象);2 打桩(预期);3 验证(结果),然后结合`红-绿-重构``大法,就是良好的TDD实践之路。

我们都希望自己写的代码持续的焕发出活力,想持续焕发活力,你必须为以后重构提供坚实的基础。敏捷大行其道的今天,TDD也是敏捷思想核心技术之环的一个重要环节,没有技术实践的敏捷注定是失败的,没有TDD为代表的测试驱动技术,技术之环同样也不够完善。

测试驱动编程(2)

hzqiuxm阅读(3446)

测试驱动编程-进阶单元测试

单元测试

要打造出出类拔萃的作品,你必须专注于最小的细节

单元测试正确打开方式

  • 单元测试应该是测试流程中占比最重的部分
  • 单元测试通常来说是指方法,但是有些情况下,整个类乃至整个应用程序都可视为单元
  • 单元测试是为了缩小其它(功能,集成)测试的范围,如果一个组织主要依赖于手动测试,这本身就是有问题的
  • 单元测试编写更容易,也能让代码的问题尽早暴露
  • 单元测试为代码重构提供了有力的支持

各类测试比较


下图测试金字塔展示的是UI测试、集成测试、单元测试之间的比重,单元测试要比其它测试多得多

拿最常见的用户注册测试场景举例,最常看到的测试用例就是要考虑用户名为空、密码为空、用户名格式、密码格式、用户名是否存在、密码安全等级等等。

仅为测试这一项功能,就可能需要数十个测试,再加上边界值等情况可能导致上百个测试用例,如果是人工的UI测试,那么代价将是非常大的。

如果使用单元测试来做这些会简单和方便的多,人工只需要填写一个集成测试,检查是否调用了正确的后端方法即可,而无需关注细节。当注册逻辑发生改变了也没关系,因为新单元测试会自动进行测试。

TDD中的单元测试

TDD中的单元测试和传统的单元测试有什么不同呢?答案是:时机
看过我之前写的一篇文章,测试驱动编程快速入门的应该都知道了一个名字:红绿重构

传统先写业务再写单元测试,主要是为了验证业务代码写的对不对,而TDD中的单元测试主要目标是作为驱动开发和设计的动力,定义了我们应该做什么以及做到什么程度,验证只是副产品。
完成的工作量和具体的测试(单元、功能、集成)有关,TDD中的单元测试制订接下来完成尽可能小的任务,要求我们遵循一些设计原则(KISS、SOLID),确保代码的可测试性和解耦性。
TDD迫使我们详细地考虑需求和设计,编写整洁而可行的代码,以及创建可执行的需求并频繁重构。

示例实战

接收同事的需求

你的同事正在开发一个远程控制轮船的程序,他已经完成了一部分工作,但是他休假去了, 现在任务交给了你。你的同事是一个TDD的践行者,你接下来要在他完成的工作基础上继续开发

下图是你同事设计的几个主要的类:

  • Ship:表示轮船,目前没有任何方法,继续要迭代的地方
  • Point:表示二维坐标
  • Direction:代表方向的枚举类以及左右转弯的方法
  • Planet:地球,轮船航行的地方,包含最大的航行坐标点以及分布了哪些障碍物设置
  • Location:轮船当前方位,包含所在的坐标以及当前的行驶方向,可以前进后退、进行航行

下面是你同事已经完成的测试验证方法,所以请你放心的改造下去

方向测试类

轮船行驶范围(地球)测试类

二维坐标测试类

方位(包含方向与二维坐标)测试类

控制方向和坐标参考系如下:

特别要注意的是地球是圆的,起点和终点是重叠的。例如:往x轴最远处前进将回到x=1的坐标点

开始迭代需求

上面看到同事之前写的代码所有测试经过都是绿灯,我们可以放心的进行后面的需求迭代了

故事迭代1

我们需要给定轮船的起始位置以及方向

因为每次控制轮船的提前肯定是要有一艘轮船,所以在每个测试方法前都应该需要一艘船,我们把它放在@BeforeEach注解的方法里

Ship默认只有无参的构造函数所以报错了,这也是正常的红-绿-重构阶段,接下来我们进入绿阶段

写一个测试方法测试下

故事迭代2

轮船可以前进或后退

在编写轮船可以前进后退的时候,我们是不是要编写各个方向的前进和后退测试方法呢?类似下面的

该测试方法测试的是轮船往北移动,Y轴的坐标将加1
实际上我们不应该这么去编写测试,原因有三:1,我们的测试方法的结果依赖了其它方法、类的内部工作原理(否则我们怎么知道y轴需要检测的是8);2,会导致编写多个测试方法,我们肯定要把东南西北各个方向相关的方法都写一遍;3,依赖的其它方法虽然经过测试,但是如果发生变化,我们需要找到调用这个方法的所有地方进行修改

这里我们可以巧妙利用已经存在的方位方法进行测试用例编写:

编写moveForward()方法,使其变绿

轮船前进的完成了,后退的功能非常相似,就不详细说明了

故事迭代3

轮船可以左转或右转

和故事迭代2有点类似,在之前辅助类Location中包含了左右转的方法,我们可以结合实现

故事迭代4

轮船接收一个指令然后完成相应的动作(比如:lrfb表示左转右转前进再后退)

每个命令都用一个字符表示:f表示前进、b表示后退、l表示左转、r表示右转
先编写一个测试用例,接收一个字符串,解析并调用相关的方法

完成 的步骤后进入 绿

测试下结果没问题

故事迭代5

轮船行驶的地球是圆的,所以可以从一边的尽头跨越到另外一边

目前为止,轮船的构造函数只有一个参数location,代表轮船的方位,现在需要引入一个planet参数代表行驶的范围(水域大小)

新增一个两个参数的Ship构造函数,使其变绿

我们发现其实之前有过ship单个参数的测试方法,而且还有一个初始化方位的方法,其实我们可以将其重构成每次测试前都初始化一艘带方位和行驶范围的轮船

刚才的测试方法就可以简化为

Ship的构造参数也只需要保留一个就可以了

接下来编写一个往东边再前进回到x轴坐标1的测试方法,因为我们初始化的时候船就是朝东的,所以不同重新设置方向了

跑下测试用例肯定报错的,我们期望的是1,但是实际是51,超出边界了

度假中的同事之前已经在location辅助类中完成了跨越边界的逻辑,所以我们只要重构下ship中的前后形式方法,带上边界范围即可

运行下ship的所有测试方法,全部绿灯,故事迭代完成

故事迭代6

每次移动需要进行障碍检测,遇到障碍的话停在原处报告遇到的障碍

地球上的水域不是畅通无阻的,有暗礁和陆地,所以我们得在行驶区域中加入障碍物,轮船行驶的时候先检查是否有有障碍物

我们首先在初始化的时候在行驶区域内加入障碍物

重构下初始化的方法

无需添加实现,测试就可以通过
接下来我们测试有障碍物的时候,轮船无法行驶,我们的设计是让接收指令的方法返回一个字符串,代表每条指令的执行情况

根据测试代码,障碍物和船形式的路线如下:(先往蓝色箭头指示方向,后往绿色箭头指示方向)

执行下肯定是报错的,因为我们之前逻辑不支持障碍物,接下来要重构下我们的业务代码:
第一个地方:调用位置帮助类的时候,前进后退的方法要加上障碍物信息

第二个地方:指令接收方法要判断每次执行的结果,拼接成字符串返回

运行下我们的测试类

至此我们的遥控轮船开发完成,同事度假回来了,后面的需求交给他吧,毕竟我们也成为了TDD的践行者,让他放心的迭代吧

PS:验收代码仓库 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的ship模块

测试驱动编程(1)

hzqiuxm阅读(2783)

java测试驱动编程简明教程

本篇教程基于junit5,不熟悉的人可以先去官网了解下:https://junit.org/junit5/ 当然比较推荐我个人整理的junit5的简明教程,可以帮你省下不少时间

测试驱动编程实战

我们的原始需求

开发一个“井”字游戏,双方在3X3的网格中画X和O,最先在水平、垂直、对角线上将自己3个标记连起来的玩家获胜

  • 初始用户故事:作为玩家,可以将棋子放在3X3棋盘上任何没有棋子的地方

故事验收:

  • 1 如果棋子放在超出X轴边界地方,引发RuntimeException;
  • 2 如果棋子放在超出Y轴边界地方,引发RuntimeException;
  • 3 如果棋子放在已经有棋子的地方,引发RuntimeException

开始红-绿-重构

  • 首先我们在业务模块创建一个井字游戏的实现类 JingGameService
  • 然后利用Idea工具创建对应的测试类(手动也可以,这里只是图个方便)

选择创建一个在每个测试方法之前都执行的方法

  • 添加一个初始化井字游戏实例的方法

  • 编写X轴越界的测试方法

注意,我们创建的井字游戏类还没有任何的方法,所以肯定会报错的,这个过程就是红-绿-重构过程中的阶段

  • 为了进入绿阶段我们实现play方法

  • 执行下该测试方法

我们看到测试通过了,这个过程就是红-绿-重构过程中的绿阶段

  • 我们还可以查看下Junit5自动生成的测试报告

  • 此时我们的代码比较简单也没有重构的必要,继续完成其他的验收测试

  • Y轴的异常逻辑和X的如同一辙,我们马上就可以实现


执行下整个类的测试,发现都是绿的,此时我们又经过了一次红-绿-重构的过程(重构没做?因为目前还不用嘛)

  • 好了,只剩下最后一个验收测试了:确定棋子在棋盘内,并且该位置是没有棋子的
  • 先完成位置冲突的测试用例

执行下,不出意外的报错了:

  • 接下来进入绿阶段,进行业务代码的编写

执行下整个测试类,全部通过:

  • 此时的代码其实已经可以重构了,因为我们的判断语句比较多,不能很好的表达业务含义,并且这些判断可能其它地方也会用到,应该抽取为不同的方法,用明确的含义表示出来该步骤的意图

下面是一个代码重构的参考:

重构完记得重新执行下测试类哦

PS:使用Idea重构很方便,只要二步
第一步:选中要重构的代码

第二步:起个方法名和选择需要的参数

  • 好了到目前为止,需求1的用户故事结束了

进入需求2

我们得加入处理玩家落子的规则,验收测试如下:

  • 白子先下
  • 如果上一次是白子,那么这次是黑子
  • 如果上一次是黑子,那么这次是白子

我们开始红-绿-重构的过程:
先编写测试方法,当白子先下,下一个玩家应该是白,我们用W表示白子,B表示黑子

的过程完成了,接下来是绿的过程,编写业务方法

我们只要编写一行代码就能实现了,然后继续完成白方黑方交替下的过程
编写白方完成后轮到黑方测试方法:

修改刚才的nextPalyer方法,将它变绿:


因为我们修改了nextPalyer方法,之前的用例也要重新跑一次,保证一直是绿的
那么我们需要编写黑方下完后应该是白方的逻辑吗?如果你写完测试方法,发现业务代码啥也不用动就能正确通过了,那么说明这个测试是多余的,我们应该删除掉
OK,我们又完成了第二个需求

进入需求3

终于进入该游戏的关键规则环节了:如何获胜?最先在水平、垂直、对角线上将自己3个标记连起来的玩家获胜
我们需要检查三个方向:水平,垂直,对角线,当没有分出胜负时,我们也要返回没有赢家,先来完成这个

看来我们又要重构/修改play方法了

之前我们是使用0或1来表示是否有棋子的,现在不仅要知道某个位置上是否有棋子,还要知道棋子是谁下的,所以保存的时候直接保存是谁下的



继续编写垂直方向的

写了水平的,垂直的也就简单了

最后是对角线三连,注意对角线有二种可能

因为都只有唯一的一条线,判断起来也更加简单了

最后所有的用例跑一次看看

完美,所有的用例都通过了,需求3也完成了

进入需求4

最后我们要处理下平局,毕竟这个游戏的大多数结局可能是平局
平局只有一种可能,就是棋盘满了,所以最后的验收测试是,当棋盘满了,游戏显示为平局

进入阶段

进入绿阶段


最后看下测试结果:

好了,一个超级简单的井字游戏就告一个段落了,代码层面还有一些优化的地方,比如获胜的判断这块。当然这不是本教程的主题,我主要演示的是如何通过测试驱动来进行业务需求/用户故事的开发

我们可以清晰的体会到,我们在编写的时候没有一开始就写业务代码,而是分析需求的验收测试,然后将验收测试编程Junit5的测试代码,当然这个过程中,代码肯定是会报错的,报错的原因有二类:

  • 编译错误:缺少必要的方法和参数
  • 运行错误:逻辑上的不满足

我们要做的事情就是把不通过的测试用例都变绿,如此反复进行。当我们发现代码臃肿难以阅读时,就应该及时的进行重构,我们可以放心的进行重构,因为有之前的测试为我们检验重构后的结果,让我们不用过于担心代码重构的不可控。

测试覆盖率

最后我们看下Junit5提供的测试覆盖率功能:

在Idea的右上角可以看到下图所示,类、方法、代码行的测试覆盖率情况,让我们做到心里有数,有多少代码或方法是测试过的

一些忠告

  • 测试方法的名称:使用given(前置条件,一般如果有公共的前置,方法中可省略)/when(描述操作)/then(描述期望的结果)
  • 红-绿-重构的过程:保证短节奏式的交替进行

最后的话

本文讲述的只是一个非常简单的测试驱动需求例子,现实情况中需求比较复杂。我们需要转变的是我们开发系统的思想,一开始可能你还不是很熟练,但是随着练习次数,你将会喜欢上这种方法。就像当初学习DDD的时候,上手比较困难,各种概念难以理解,但是一旦你上手后,你再也不想回到以前那种贫血失血模型的开发中去了。

如果你立志成为一个优秀的工程师,那么测试驱动编程是你一定要掌握好的技能。

PS:本教程的源码分享在了 https://gitee.com/hzqiuxm/tdd-demo-lessons.git 的jingame模块

OKR思考与实战

hzqiuxm阅读(4829)

OKR思考与实战笔记

本文主要来自对黄勇老师课程相关学习的总结和思考,以及自己在OKR实践过程中的一些经验与复盘,很早时候就拜读过黄勇老师架构探险系列书籍以及技术博文,一直十分推崇。

快速入门

为什么要学习OKR?

OKR主要有以下四个优势:

  • 1)专注:OKR使方向更加聚焦,专注解决核心问题,提倡要事第一原则;
  • 2)透明:OKR让沟通更加透明,增强彼此间的信任,建立共识,达成共赢;
  • 3)激励:OKR设定的目标挑战越大,成就感越大,对人的激励效果就越好;
  • 4)简单:OKR理解容易且用法简答,用数据和事实说话,结果容易度量;
    OKR不只是优秀的目标管理工具,也是高效的沟通框架,更是有效的激励手段,还是自我成长思维

看看OKR的流行趋势:

OKR除了推动战略落地、优化绩效管理,还有其它几个场景:管理变革项目、激发组织创新、强化组织文化、提升管理水平

什么是OKR?

OKR是一套科学的目标管理工作法,它由O和KR两部分组成,O表示Object(目标),KR表示Key Result

好OKR标准

遵循SMART基本原则:

O和KR分别遵循的标准:

KPI和OKR相爱相杀

下图是二者的区别:

OKR 以过程论成败,KPI 以结果论英雄
OKR 重视过程、关注成长、勇于挑战,而 KPI 关注结果、被动执行、利益挂钩,所以从员工可接受程度来看,似乎 OKR 更有“人情味”,更能体现人的价值

OKR不能做的事

目标是可以包含绩效的,但是千万不能把绩效和考核挂钩,更不要和奖金或薪资挂钩,否则OKR就会被你彻底玩坏了

比如,研发团队在交付项目时总是延期,你希望将“无延期上线”这项绩效指标放入 OKR,这样做是可以的,但你一定不要告诉团队“如果项目发生延期需扣工资,如果提前或准时上线可发奖金”,否则 OKR 就会变成一种利益驱动的工具,以后一旦缺乏利益,团队也就缺乏动力了,最终结果是 OKR 被“玩坏”了

适用场景

  • 推算型、机械化的工作,更加适合用KPI,这类工作一般有现成的指令、规定和方法可以遵循
  • 探索型、创造性的工作,更加适合OKR

相互结合

KPI 中包括的绩效指标一定是只看结果,而不看过程的,只要结果达到了就行,而 OKR 需要更多地关心过程,从结果中判断目标的具体达成情况。因此,我们需要打造一个“以 OKR 为主并以 KPI 为辅”的研发团队。

企业中的快速落地

使用四层目标金字塔:

我们在制定目标的时候,尽可能与上级目标保持对齐,而不是完全对齐
千万不要让HR去主导,否则很容易变成KPI2.0
企业和部门或小队实施起来难度是不一样的,请遵循下面三个步骤:

  1. 一定不要强制性地去推广 OKR,更不要在大家概念不一致的情况下就大规模推广 OKR。
  2. 当大家对 OKR 理解不一致时,一定要请外部 OKR 专家到企业内部做 OKR 培训,才能快速建立共识。
  3. 要充分发挥你的领导力去影响身边的人,让高管们对 OKR 表示认同,让团队成员们尝到 OKR 的甜头,这才是我们作为领导者需要去做的事情。

研发团队如何实施OKR?

首先要判断研发团队的内驱力如何?主要是个人成长和团队贡献两个方面

内驱力不够的话,建立先培养或激发出一定内驱力,或者先在内驱力强的几个人身上先试实行
先决条件1:部门应该对产品的设计、开发和交付的整个过程负责
先决条件2:公司的成败很大程度上取决于产品本身的特性和表现,对销售市场环节的依赖度有限

产品型互联网公司发展路径模型:

产品型公司的发展规律告诉我们,企业往往不是依赖销售目标去牵引,而是通过产品技术竞争力去推动的

  • 初创阶段:技术产品负责人要对企业希望达成的商业目标、目标客户选择,以及要解决的客户问题上,心知肚明;

    该阶段有意义的OKR建议:识别标杆客户、提升销售转化率、完善关键产品特性、消除量产缺陷、偿还技术债务

  • PMF后阶段:首要目标就是思考同倍的资源增加能否带来同倍的业绩增长

    该阶段OKR建议:打造中台系统、提供数据分析能力、赋能业务团队、提升运营效率

操作指南

打造OKR土壤

团队文化是否达标
  • 面对沟通时,是否公开透明?
  • 面对交付时,是否信守承诺?
  • 面对压力时,是否用于挑战?

在目标“制定”过程中,通过跟自己的上级、平级、下级沟通,从而实现目标对齐。在目标“执行”过程中,我们需要不断跟踪目标的进度,一旦有调整就需要及时沟通。在目标“评估”过程中,我们需要对目标进行评估和复盘,需要跟团队一起开会讨论,这又会产生更进一步的沟通。

既然制定了目标,那就一定要为之努力并争取最大化地实现目标,而不是一遇到困难就轻易放弃。这绝不是 OKR 所推崇的价值观。如果你发现你的团队在承诺上做得还不够,那么你现在首先要做的就是,建立起遵守诺言的团队文化。

团队内驱力是否达标?

内驱力不强的团队其实更加需要OKR来打造团队文化,OKR是领导和员工建立信任的纽带
好好利用团队1V1沟通方式,慢慢走近员工的内心
打造内驱力团队文化的三点实战心得:

  • 先管理好自己:自己先按照你要打造的团队文化标准去做
  • 给团队更多的空间:通过聊天和分享为团队搭建施展能力的舞台
  • 文化由团队说了算:你是教练,文化不需要贴在墙上,写在纸片上,它应该在每个团队成员的身上
正确与认清OKR使用
  • 不需要一上来就全员推行,要找到符合OKR的土壤才行
  • 作为领导者,只需要影响自己身边值得被影响的人

  • 制订OKR不是拿团队的KR作为自己的O,而是用自己的O去对其团队的O
  • 乐观的说一个公司的成功取决于大家的努力,悲观的说其实是少数人的努力

制定基本原则

有效使用O的三点
  • 方向明确:目标不能太抽象,也不能含混不清,要让每个人都能看得懂
  • 目标对齐:下级要对齐上级的目标,即向上对齐,平级还要相互对齐,即水平对齐
  • 时间限制:目标需要有时间边界,不能遥遥无期,而且时间边界要恰到好处
有效使用KR的三点
  • 有挑战性:关键结果要有挑战性,拒绝平庸,拒绝不切实际,要做到“跳一跳,就能够得着”。
  • 容易度量:无法度量的关键结果是没有任何意义的,宁可抛弃。
  • 不是任务:关键结果需要对目标产生直接影响,它不是日常任务,也不是行动计划
OKR三板斧
  • 制定 OKR:自顶向下地制定目标,自底向上对齐目标
  • 执行 OKR:唯有做到持续追踪、持续更新,才能做到拥抱变化
  • 评估 OKR:周期性地评估和复盘,持续改进、持续迭代

轻松定制OKR

在OKR制订的过程中一定要沟通和评审

如何制定O
  • O其实是做什么,尽量让O看起来有使命感
  • O的制订要注意满足三条核心原则:1 有实际价值,所以你要深度思考;2 能激励人心,所以你要善于表达;3 能短期实现,所以要脚踏实地
  • O尽量以动词开头,带上制订O的动机
如何制定KR
  • KR其实是怎么做层面的
  • KR是定性的,它可能是一个数字,一个范围,一个里程碑事件
  • KR要对当事人或部门有一定的挑战性,不能太低也不能太高
  • 用信心指数来表达KR的挑战性,初始值为5/10,表示完成和不能完成的概率是50%

推动OKR的技巧

OKR推行过程中常见问题
  • 很长时间大家都没有去更新
  • 没有完成OKR的理由:工作太忙,根本没时间
  • 完成的不好理由:制订时候对其的O,最后因为同级的O变了,导致自己的O失去意义或价值
  • 不能及时同步大家OKR的执行情况
OKR推行技巧
  • 每天更新,每周回顾,每月优化,每季复盘
  • 将OKR和日常工作关联起来,通过任务去驱动
  • OKR要有固定的周期

OKR的评估

  • 最好的OKR评估方式就是自评,而OKR评估也是自我管理的有效手段
  • 评价结果就分为四档:0、0.3、0.7、1
  • 一般 OKR 平均得分在 0.6~0.7 范围内,就会被认为完成得很好。如果得到 1,可能就需要反思是否目标设定得太低
    信心指数与评分的关系图:

OKR的复盘

  • 1 审视目标:当初定目标原因,完成的结果如何?为什么是这个结果?
  • 2 回顾过程:你是如何执行的?
  • 3 分析得失:哪些做得好?哪些做得不好?分别是什么原因?
  • 4 总结规律 :如果再次做相同的事情,你会怎么做?收获了哪些规律、方法论?

OKR实施需要一位教练角色

在OKR布道推广的时候,如果你还不是OKR专家时,你需要借力请一位OKR专家来帮助落地,善于借力也是领导力的表现之一
当你成为OKR教练时,你要注意在扮演教练角色时:1 学会引导;2 保持学习;3 不做权威

如何将OKR和"个人利益"挂钩

原则:OKR 不要与个人利益直接挂钩,但可以与个人利益间接关联

【OKR定位】
OKR解耦公司:1 将绩效作为目标,放到OKR中进行管理;2 通过OKR来实现个人成长与团队贡献;3 衡量员工的成长和贡献速度,来提升员工岗位级别;4 当岗位级别提升时,决定薪资的具体涨幅

上面的链条关系: 绩效--目标--成长/贡献--岗位级别--薪资
本职工作完成可以拿到工资,OKR完成的好进行升级加薪

【本职工作和OKR】
那么问题来了:本职过于饱和的情况下,哪有时间去完成OKR呢?升级加薪迎娶白富美岂不是东方夜谭?
OKR的分类:

  • 承诺型:是我们必须努力完成的,例子:项目及时上线
  • 愿景型:是我们需要挑战才能实现的,例子:提升开发效率,优化系统架构

建议:将本职工作放入承诺型OKR,然后将个人成长与团队愿景放入愿景型OKR,二者互相集合
在一个OKR周期中,必须要有一个愿景型OKR
优先完成承诺型OKR(重要且紧急的事),再去完成愿景型OKR(重要非紧急的事)
技术人员不要陷入生产代码陷阱中,否则生产的只是代码而不是价值

管理心经

改变研发是"成本中心"思想定位

有句话对技术的定位很准确:在一个公司中,创业初期技术的重要性往往被高估,后面稳定长期发展时技术的重要性又往往被低估
很多不懂技术的人都会有:技术部门属于成本中心,销售部门才是利润中心,我们想要改变这个认知,可以做以下几个方面的努力:

  • 让其他部门的核心人员了解一个工程开发的基本流程
  • 让其他人了解到软件开发是一个很复杂的体系,是一个工程性要求极强的工作
  • 支撑技术部门各岗位之间的协作,不仅要求大家有较强的逻辑思维能力,还要有一系列的技术规范和研发流程
  • 利用向大家同步OKR来体现技术团队的价值,同时工程师也要懂得自己代码是如何变现的过程

向上管理的套路

向上管理的本质就是上级对你的期望,所以一定要弄清楚这一点,请看下面这个常见的例子

  • 场景一

  • 场景二:

  • 场景三:

如果选择回答一,那么可能会给团队埋下工作效率低,士气下降的祸根
如果选择回答二,将影响老板对你的信任,他可能会说:如何提高团队效率,是你们团队负责人的事
如果选择回答三,那么老板一般都会说,其它部门我会去问下,你这边先开始

那么如何利用OKR的思维来进行向上管理呢?

面对老板996强制加班要求,一般人内心都是拒绝和不认同的,我们希望带领的是一支能“打仗”的队伍,该拼的时候一定要能拼,而不是为了加班而去加班,做样子给别人看,而且阿里等企业的成功,那绝不是单纯靠长期加班而取得的,而且人家有完善的加班体系和福利制度作保障,如果只要求马儿一样跑,但却没有相同的草,那就是耍流氓

所以我们要先弄清楚老板的O:我明白您的意思,那么您期望达到的目标是什么呢?
老板答案无外乎:项目早点上线、提升大家工作效率、有耕更多的产出,肯定不会有老板说:我就是想大家工作时间长一点

那么此时我们归纳出老板的O了,例如:成为一家工作效率高的公司

同时我们表态:是的,我们应该不断提高我们的工作效率,我有个想法,希望你能给我些建议;既然要关注工作效率,那么我们就应该用某个指标科学合理的去衡量工作效率,一般称之为 人效,我们每个部门可以先根据自己人员情况,计算下当前的人效,然后定一个目标,逐步来提高对大家的要求。既然是一家互联网公司,利用数据驱动的方式也更能体现我们文化和价值

OKR和项目开发结合

  • OKR比较适合迭代式的项目开发
  • 每次迭代的O尽量少,迭代周期一般为二周
  • 技术、产品、业务达成OKR共识
  • 迭代的时候开发将产品需求或用户故事拆分为多个任务

活学活用

招聘与考核

  • 与HR一起制定招聘的OKR,设置明确的时间期限和关键路径

  • 和候选人一起制订试用期OKR,让新人明确自己的目标是什么

人才激励

作者朋友圈有效激励小调查:

  • 第一:金钱激励最有效,但时效短
  • 第二:个人成长
  • 第三:认同感
  • 第四:其他福利

我们来了解下激励四象限:

  • 物质激励:金钱、福利
  • 精神激励:成长、认同、期权、荣誉、参与、特权、贡献、工作
    仅用物质激励显然不够持久,只用精神激励显然也不够刺激,物质激励与精神激励相结合,才能产生更强烈、更持久的激励效果

激励的三要素:

  • 1 及时性:激励越及时,激励效果越好
  • 2 多样性:激励方式越多,使用上灵活性也更强
  • 3 差异性:激励要有差异,也要有个性

CRF敏捷绩效管理

  • C:Conversation(对话):员工找管理者1V1的聊天(员工找管理者吃饭,管理者买单,反之亦然)

    对话环节注意点:1 在整个对话过程中,要围绕员工的“个人 OKR”进行引导,并有针对性地进行辅导;2 自始至终,你都在扮演一位“教练”的角色,你的目的是让“队员”产出高绩效;3 需要打造出“双向辅导”的交流氛围,你帮他成为更好的员工,他帮你成为更好的管理者。

  • R:Feedback(反馈):双向反馈,可以根据实际情况,选择实名或匿名

    反馈环节注意点: 1 当每季度 OKR 自评完毕后,即可进入到 CFR 反馈环节;2 反馈要做到尽可能具体,要能聚焦在某件事情上;3 不要依据反馈的结果,调整奖金多少与加薪幅度

  • F:Recognition(认可):一种激励方式,多鼓励员工之间进行相互认可

    认可环节注意点:1 认可要及时;2 认可要全面,阳光普照;3 认可要真实,认可具体的事情

两种OKR类型,评分要求:

OKR易踩的坑

  • 将OKR用于绩效考核
  • 用KPI思维做OKR
  • O数量太多,不够聚焦
  • KR不够量化,无法评估
  • 将KR当成任务或待办清单
  • KR挑战不够或不切实际
  • 忽略OKR的上下平级对齐
  • 忽略OKR的更新于复盘
  • 忽略OKR的评分环节
  • 用OKR替代了KPI

OKR十诫

  • 1 OKR 不与绩效考核相关,不与奖金或薪资直接挂钩。
  • 2 OKR 必须向上对齐,一起努力实现组织目标。
  • 3 OKR 是动态的,需要根据实际情况来灵活调整。
  • 4 OKR 是公开透明的,每个人都能看到所有人的 OKR。
  • 5 OKR 需要具备一定挑战,是通过努力才能达成的。
  • 6 OKR 要有评分,评分不是越高越好,越高说明挑战越不够。
  • 7 OKR 目的在于让事情变得聚焦,在正确的轨道上做出努力。
  • 8 OKR 重在沟通,通过沟通达到目标的理解一致性。
  • 9 OKR 是工作法,也是管理方法,它能激励团队取得成功。
  • 10 OKR 需做出承诺并付出行动,它是思想,也是文化

Docker简明教程(13)

hzqiuxm阅读(2814)

Docker网络管理

网络基础基础

  • 科普下常见的两种网络分层结构类型

一个是7层一个是4层,4层是对7的一个归纳和抽象,标准的模型是7层的

  • 常用查看网卡命令: ls /sys/class/net / 或者 ip a

  • 关于docker的几个网卡的简单介绍
    执行命令ip a

默认创建的容器都是在docker0这个network中,可以创建自己的network,指定容器运行在特定的network中

namespaceh和veth-pair技术

veth pair :Virtual Ethernet Pair,是一个成对的端口(veth-pair的link),可以实现两个namespace之间通信

观察容器中veth-pair对

  • 创建两个tomcat容器
docker run -d --name tomcat01 -p 8081:8080 tomcat
docker run -d --name tomcat02 -p 8082:8080 tomcat

查看宿主机网络情况:ip a

从上图中我们发现生成了两个veth-pair的link,但是它们其实不是一对,应为二者明显数字不是连续的

我们再看下两个tomcat容器的网路情况:docker exec -it tomcat01 ip a

其中eth0@if6和eth0@if7是一对veth-pair,eth0@if8和eth0@if9是一对veth-pair;它们都通过docker0来进行通信
我们使用类似命令 docker exec -it tomcat1 ping 172.17.0.3可以在tomcat01和tomcat02之间互相ping通

其原理示意图如下:

自定义网络配置

  • docker默认的网络配置方式 docker network ls

桥接 bridge(默认):虚拟机和宿主机通过桥接方式直连物理网络
主机host:通过共享宿主机ip通信去通信,和宿主机的网络配置显示是一模一样的
none:和外界完全不同的服务,特殊环境下使用

  • 创建自己的network: docker network create my-network

查看docker链接的各容器网络情况:docker network inspect bridge

可以把其它容器加入到多个network中实现之间的通信
自定义的 network中容器间可以通过名字直接访问(自动添加了DNS记录),默认的docker0是只能通过ip地址访问
如果是默认的docker0的network想通过容器名字直接访问,就要通过--link显性的指定要链接的容器
但是--link在生产环境中很少去使用,更加提倡自定义自己的bridge式network去管理容器

所以单机docker的通信最佳方案就是自己创建network,通过容器名称访问

跨network通信

当我们把类似的应用加入到不同的network中时,它们之属于不同的网段,肯定是不能互相通信的
比如新增一个tomcat3,指定在我们刚才创建的network下:docker run -d --name tomcat03 --network my-network -p 8083:8080 tomcat

观察发现tomcat3的网段是在172.18.0.2,而之前的是172.17.0.*,二者是不同的, 通过 docker exec -it tomcat03 ping 172.17.0.3测试是ping不通的

我们观察my-network的情况,也发现了该network中也只有一个容器

要想让tomcat3和tomcat1能互通该怎么办呢?一个想法就是把tomcat1加入到my-network中来
执行命令 docker network connect my-network tomcat01

omcat01已经被加入到了my-network当中,分到的IP是172.18.0.3,这个是时候我们不仅可以通过IP进行ping通,甚至可以根据容器名称来进行,大大提升了开发的灵活性

跨主机通信

  • 简单的方案就是通过端口映射来解决,通过外网,依赖DNS
  • 另外一个是通过vxlan技术实现,组建成内网,内部地址不能冲突,底层技术是overlay
    大致示意图如下:


这块内容涉及到比较专业的网络知识,详情待到后续补充新的文章再细说了

参考文献

docker官方文档: https://docs.docker.com/

欢迎加入极客江湖

进入江湖关于作者