开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

SpringBoot 整合缓存Cacheable实战详细使用

发表于 2021-08-18

这是我参与8月更文挑战的第9天,活动详情查看: 8月更文挑战

前言

我知道在接口api项目中,频繁的调用接口获取数据,查询数据库是非常耗费资源的,于是就有了缓存技术,可以把一些不常更新,或者经常使用的数据,缓存起来,然后下次再请求时候,就直接从缓存中获取,不需要再去查询数据,这样可以提供程序性能,增加用户体验,也节省服务资源浪费开销,

在springboot帮你我们做好了整合,有对应的场景启动器start,我们之间引入使用就好了,帮我们整合了各种缓存

1
2
3
4
5
6
xml复制代码    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>

简介

缓存介绍

Spring 从 3.1 开始就引入了对 Cache 的支持。定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术。并支持使用 JCache(JSR-107)注解简化我们的开发。

其使用方法和原理都类似于 Spring 对事务管理的支持。Spring Cache 是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。

Cache 和 CacheManager 接口说明

Cache 接口包含缓存的各种操作集合,你操作缓存就是通过这个接口来操作的。
Cache 接口下 Spring 提供了各种 xxxCache 的实现,比如:RedisCache、EhCache、ConcurrentMapCache

CacheManager 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache。这些 Cache 存在于 CacheManager 的上下文中。

小结

每次调用需要缓存功能的方法时,Spring 会检查指定参数的指定目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点;

  1. 确定方法需要被缓存以及他们的缓存策略
  2. 从缓存中读取之前缓存存储的数据

快速开始

  1. 使用缓存我们需要开启基于注解的缓存,使用 @EnableCaching 标注在 springboot 主启动类上或者配置类上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@SpringBootApplication
@MapperScan(value = {"cn.soboys.kmall.mapper","cn.soboys.kmall.sys.mapper","cn.soboys.kmall.security.mapper"},nameGenerator = UniqueNameGenerator.class)
@ComponentScan(value = {"cn.soboys.kmall"},nameGenerator = UniqueNameGenerator.class)
@EnableCaching //开启缓存注解驱动,否则后面使用的缓存都是无效的
public class WebApplication {
private static ApplicationContext applicationContext;

public static void main(String[] args) {
applicationContext =
SpringApplication.run(WebApplication.class, args);
//displayAllBeans();
}


/**
* 打印所以装载的bean
*/
public static void displayAllBeans() {
String[] allBeanNames = applicationContext.getBeanDefinitionNames();
for (String beanName : allBeanNames) {
System.out.println(beanName);
}
}
}

或者配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* @author kenx
* @version 1.0
* @date 2021/8/17 15:05
* @webSite https://www.soboys.cn/
* 自定义缓存配置
*/
@Configuration
@Slf4j
@EnableCaching //开启缓存注解驱动,否则后面使用的缓存都是无效的
public class CacheConfig {

//自定义配置类配置keyGenerator

@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString() +"]";
}
};
}
}
  1. 标注缓存注解在需要缓存的地方使用@Cacheable注解
1
2
3
4
5
6
7
8
9
10
11
java复制代码@CacheConfig(cacheNames = "menuCache",keyGenerator ="myKeyGenerator" )
public interface IMenuService extends IService<Menu> {
/**
* 获取用户菜单信息
*
* @param username 用户名
* @return
*/
@Cacheable(key = "#username")
List<Menu> getUserMenus(String username);
}

@Cacheable注解有如下一些参数我们可以看到他源码

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
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};

@AliasFor("value")
String[] cacheNames() default {};

String key() default "";

String keyGenerator() default "";

String cacheManager() default "";

String cacheResolver() default "";

String condition() default "";

String unless() default "";

boolean sync() default false;
}

下面介绍一下 @Cacheable 这个注解常用的几个属性:

  1. cacheNames/value :指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定 多个缓存;
  2. key :缓存数据时使用的 key,可以用它来指定。默认是使用方法参数的值。(这个 key 你可以使用 spEL 表达式来编写如 #i d;参数id的值 #a0 #p0 #root.args[0])
  3. keyGenerator :key的生成器;可以自己指定key的生成器的组件id 然后key 和 keyGenerator 二选一使用
  4. cacheManager :可以用来指定缓存管理器。从哪个缓存管理器里面获取缓存。或者cacheResolver指定获取解析器
  5. condition :可以用来指定符合条件的情况下才缓存
1
2
ini复制代码condition = "#id>0"
condition = "#a0>1":第一个参数的值》1的时候才进行缓存
  1. unless :否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。当然你也可以获取到结果进行判断。(通过 #result 获取方法结果)
1
2
ini复制代码unless = "#result == null"
unless = "#a0==2":如果第一个参数的值是2,结果不缓存;
  1. sync :是否使用异步模式。异步模式的情况下unless不支持 默认是方法执行完,以同步的方式将方法返回的结果存在缓存中

cacheNames/value属性

用来指定缓存组件的名字,将方法的返回结果放在哪个缓存中,可以是数组的方式,支持指定多个缓存

1
2
3
4
5
6
7
8
java复制代码 /**
* 获取用户菜单信息
*
* @param username 用户名
* @return
*/
@Cacheable(cacheNames = "menuCache") 或者// @Cacheable(cacheNames = {"menuCache","neCacge"})
List<Menu> getUserMenus(String username);

如果只有一个属性,cacheNames可忽略,直接是value属性默认

key

缓存数据时使用的 key。默认使用的是方法参数的值。可以使用 spEL 表达式去编写。

Cache SpEL available metadata

名称 位置 描述 示例
methodName root对象 当前被调用的方法名 #root.methodname
method root对象 当前被调用的方法 #root.method.name
target root对象 当前被调用的目标对象实例 #root.target
targetClass root对象 当前被调用的目标对象的类 #root.targetClass
args root对象 当前被调用的方法的参数列表 #root.args[0]
caches root对象 当前方法调用使用的缓存列表 #root.caches[0].name
argumentName 执行上下文(avaluation context) 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 #artsian.id
result 执行上下文(evaluation context) 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) #result
1
2
3
java复制代码//key = "#username" 就是参数username
@Cacheable(key = "#username" ,cacheNames = "menuCache")
List<Menu> getUserMenus(String username);

keyGenerator

key 的生成器,可以自己指定 key 的生成器,通过这个生成器来生成 key

定义一个@Bean类,将KeyGenerator添加到Spring容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Configuration
@Slf4j
@EnableCaching //开启缓存注解驱动,否则后面使用的缓存都是无效的
public class CacheConfig {

//自定义配置类配置keyGenerator

@Bean("myKeyGenerator")
public KeyGenerator keyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName()+"["+ Arrays.asList(params).toString() +"]";
}
};
}
}

在使用指定自己的@Cacheable(cacheNames = "menuCache",keyGenerator ="myKeyGenerator" )

注意这样放入缓存中的 key 的生成规则就按照你自定义的 keyGenerator 来生成。不过需要注意的是:@Cacheable 的属性,key 和 keyGenerator 使用的时候,一般二选一。

condition

符合条件的情况下才缓存。方法返回的数据要不要缓存,可以做一个动态判断。

1
2
3
4
5
6
7
8
9
java复制代码 /**
* 获取用户菜单信息
*
* @param username 用户名
* @return
*/
//判断username 用户名是kenx开头才会被缓存
@Cacheable(key = "#username" ,condition = "#username.startsWith('kenx')")
List<Menu> getUserMenus(String username);

unless

否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。

1
2
3
4
5
6
7
8
9
java复制代码 /**
* 获取用户菜单信息
*
* @param username 用户名
* @return
*/
//判断username 用户名是kenx开头不会被缓存
@Cacheable(key = "#username" ,condition = "#username.startsWith('kenx')")
List<Menu> getUserMenus(String username);

spEL 编写 key

当然我们可以全局去配置,cacheNames,keyGenerator属性通过@CacheConfig注解可以用于抽取缓存的公共配置,然后在类加上就可以,eg:如

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//全局配置,下面用到缓存方法,不配置默认使用全局的
@CacheConfig(cacheNames = "menuCache",keyGenerator ="myKeyGenerator" )
public interface IMenuService extends IService<Menu> {
/**
* 获取用户菜单信息
*
* @param username 用户名
* @return
*/
@Cacheabl
List<Menu> getUserMenus(String username);
}

深入使用

@CachePut

@CachePut注解也是一个用来缓存的注解,不过缓存和@Cacheable有明显的区别是即调用方法,又更新缓存数据,也就是执行方法操作之后再来同步更新缓存,所以这个主键常用于更新操作,也可以用于查询,主键属性和@Cacheable有很多类似的参看 @CachePut源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CachePut {
@AliasFor("cacheNames")
String[] value() default {};

@AliasFor("value")
String[] cacheNames() default {};

String key() default "";

String keyGenerator() default "";

String cacheManager() default "";

String cacheResolver() default "";

String condition() default "";

String unless() default "";
}
1
2
3
4
5
6
7
8
9
10
java复制代码/**
* @CachePut:既调用方法,又更新缓存数据;同步更新缓存
* 修改了数据,同时更新缓存
*/
@CachePut(value = {"emp"}, key = "#result.id")
public Employee updateEmp(Employee employee){
employeeMapper.updateEmp(employee);
LOG.info("更新{}号员工数据",employee.getId());
return employee;
}

@CacheEvict

清空缓存
主要属性:

  1. key:指定要清除的数据
  2. allEntries = true:指定清除这个缓存中所有的数据
  3. beforeInvocation = false:默认代表缓存清除操作是在方法执行之后执行
  4. beforeInvocation = true:代表清除缓存操作是在方法运行之前执行
1
2
3
4
5
java复制代码@CacheEvict(value = {"emp"}, beforeInvocation = true,key="#id")
public void deleteEmp(Integer id){
employeeMapper.deleteEmpById(id);
//int i = 10/0;
}

@Caching

@Caching 用于定义复杂的缓存规则,可以集成@Cacheable和 @CachePut

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// @Caching 定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(/*value={"emp"},*/key = "#lastName")
},
put = {
@CachePut(/*value={"emp"},*/key = "#result.id"),
@CachePut(/*value={"emp"},*/key = "#result.email")
}
)
public Employee getEmpByLastName(String lastName){
return employeeMapper.getEmpByLastName(lastName);
}

@CacheConfig

@CacheConfig注解可以用于抽取缓存的公共配置,然后在类加上就可以

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//全局配置,下面用到缓存方法,不配置默认使用全局的
@CacheConfig(cacheNames = "menuCache",keyGenerator ="myKeyGenerator" )
public interface IMenuService extends IService<Menu> {
/**
* 获取用户菜单信息
*
* @param username 用户名
* @return
*/
@Cacheable(key = "#username" )
List<Menu> getUserMenus(String username);
}

参考

  1. SpringBoot之缓存使用教程
  2. 缓存入门

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL explain 中的 rows 究竟是如何计算的

发表于 2021-08-18

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

今天在帮同事处理系统慢 SQL 时遇到几个疑惑的问题,在此简单记录一下,具体描述如下~

【背景铺垫】

相关表:

1
2
3
4
5
sql复制代码CREATE TABLE test_table (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
name varchar(32) NOT NULL,
PRIMARY KEY (id)
) ENGINE = InnoDB CHARSET = utf8mb4;

test_table 表记录数约 12w+

问题描述

相关 SQL:

1
sql复制代码EXPLAIN SELECT COUNT(*)FROM test_tableWHERE id >= 10534	AND id <= 15375;

疑问 1:上述 SQL 理应按 id 主键(聚簇索引)范围查找,为啥 explain 里的 rows 会多余两者之差呢?

在 SQL 结尾处增加 LIMIT 10 后,rows 数值竟然没有任何影响(觉得可能会变为: 10)。

1
sql复制代码EXPLAIN SELECT COUNT(*)FROM test_tableWHERE id >= 10000	LIMIT 10;

疑问 2:LIMIT 值不会影响 rows 的值么?

rows 究竟是怎么计算的呢?

这个 rows 在官网文档中的解释如下:

rows (JSON name: rows)

The rows column indicates the number of rows MySQL believes it must examine to execute the query.

For [InnoDB] tables, this number is an estimate, and may not always be exact.

dev.mysql.com/doc/refman/…

简单理解即:这个 rows 就是 mysql 认为估计需要检测的行数。

为了探究 rows 究竟是如何算出来的,查找 MYSQL 源码来看看:

1
2
3
4
5
6
7
8
sql复制代码文件1:sql/opt_explain_traditional.cc   
关键部分:push(&items, column_buffer.col_rows, nil)

文件2:sql/opt_explain.cc
关键部分:select->quick->records

文件3:sql/opt_range.cc
关键部分:check_quick_select

而 check_quick_select 的功能,在 MySQL 源码中的注释为:

Calculate estimate of number records that will be retrieved by a range scan on given index using given SEL_ARG intervals tree.

简单翻译就是:这个方法仅仅根据给出的关于这个索引的条件和索引本身,来判断需要扫描多少行。

总结

MySQL Explain 里的 rows 这个值

  • 是 MySQL 认为它要检查的行数(仅做参考),而不是结果集里的行数;
  • 同时 SQL 里的 LIMIT 和这个也是没有直接关系的。

另外,很多优化手段,例如关联缓冲区和查询缓存,都无法影响到 rows 的显示。MySQL 可能不必真的读所有它估计到的行,它也不知道任何关于操作系统或硬件缓存的信息。

  • END -

作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长。

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。

Thanks for reading!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里DDD四弹拜读 Domain primitive 应用架

发表于 2021-08-18

这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

阿里殷浩大牛写了DDD系统文章,现在已经更新到每四篇,有很多异于常规的地方,收获良多,总结一下

Domain primitive

对于DDD第一讲,作者介绍的Domain primitive,开始有些反感的,对DDD理论也深知一二,但从没听过有这概念,所以觉得作者是挂羊头卖狗肉的,但读完发现原来是对Value object的升华,牛人就是不一样

Domain Primitive的概念和命名来自于Dan Bergh Johnsson & Daniel Deogun的书 Secure by Design,特意找到了这本书的章节,livebook.manning.com/book/secure…,有兴趣可以看看

对于DP,在《代码大全》中指出过类似概念:ADT

抽象数据类型ADT是指一些数据以及对这些数据所进行的操作的集合

关于使用ADT的建议:

  1. 把常见的底层数据类型创建为ADT并且使用这些ADT,而不再使用底层数据类型
  2. 把像文件这样的常用对象当成ADT
  3. 简单的事物也可以当做ADT:这样可以提高代码的自我说明能力,让代码更容易修改。
  4. 不要让ADT依赖于其存储介质

ADT就是业务上的最小类型,不要去使用编程语言提供的基础类型

在之前的DDD文章中,也指出很多时候的重构不过是大方法拆分成小方法,更SRP一些,其实也什么意义,DDD带来的好处是业务语义显现化,而DP就是一种手段

使用DP后代码遵循了 DRY 原则和单一性原则,作者从四个维度提出DP带来的好处:接口的清晰度(可阅读性)、数据验证和错误处理、业务逻辑代码的清晰度、和可测试性

在实际项目中碰到一个有意义的问题,我们通过OCR接受识别的增值税发票信息

之前的接口是receiveInvoice(String invoiceCode,String invoiceNo,String checkCode,…),接受OCR给的发票结构化信息

发票号码和发票代码是有业务语义,是业务的最小类型,invoiceCode可以从String升级为InvoiceCode

接口变成:receiveInvoice(InvoiceCode invoiceCode,InvoiceNo invoiceNo,CheckCode checkCode),把业务最小单元提取出来了,接口清晰度,业务语义也显现了。可有个有意思的地方,OCR会出错的,一个正常的发票号码是8位,但会被识别成9位,业务上不能是InvoiceNo,可得固化存储这个识别结果,因此这个入口不能过于语义,总不能来一个WrongInvoiceNo

这儿我可能有个误区,把DP作为有语义的数据验证工具类使用了,可DP应该是Value object的升华,得在domain层使用,参数校验还得用Validate

定义

Domain Primitive 是一个在特定领域里,拥有精准定义的、可自我验证的、拥有行为的 Value Object 。

  1. DP是一个传统意义上的Value Object,拥有Immutable的特性
  2. DP是一个完整的概念整体,拥有精准定义
  3. DP使用业务域中的原生语言
  4. DP可以是业务域的最小组成部分、也可以构建复杂组合

原则

  1. 将隐性的概念显性化(Make Implicit Concepts Explicit)
  2. 将隐性的上下文显性化(Make Implicit Context Explicit)
  3. 封装多对象行为(Encapsulate Multi-Object Behavior)

VS Value Object

在 Evans 的 DDD 蓝皮书中,Value Object 更多的是一个非 Entity 的值对象

在Vernon 的 DDD红皮书中,作者更多的关注了Value Object的Immutability、Equals方法、Factory方法等

Domain Primitive 是 Value Object 的进阶版,在原始 VO 的基础上要求每个 DP 拥有概念的整体,而不仅仅是值对象。在 VO 的 Immutable 基础上增加了 Validity 和行为。当然同样的要求无副作用(side-effect free)

VS DTO


应用架构

这一篇算是正常篇,很多文章都是这样的,也是以银行转账为例,但分析得更特彻

应用架构,意指软件系统中固定不变的代码结构、设计模式、规范和组件间的通信方式

一个好的架构应该需要实现以下几个目标:

  1. 独立于框架:架构不应该依赖某个外部的库或框架,不应该被框架的结构所束缚
  2. 独立于UI:前台展示的样式可能会随时发生变化
  3. 独立于底层数据源:无论使用什么数据库,软件架构不应该因不同的底层数据储存方式而产生巨大改变
  4. 独立于外部依赖:无论外部依赖如何变更、升级,业务的核心逻辑不应该随之而大幅变化
  5. 可测试:无论外部依赖什么样的数据库、硬件、UI或服务,业务的逻辑应该都能够快速被验证正确性

这是很多架构的目标,但想想,一个架构是这样,那还剩下什么,框架没了,数据库没了,对于习惯了CRUD的程序员,什么都没了,我们的架构为了什么。真的就是那个模型,一个软件之所以是这个软件的核心模型,也就是domain

DDD是一种设计范式,主张以领域模型为中心驱动整个软件的设计。在DDD中,业务分析和领域建模是软件开发的关键活动。它不关心软件的架构是怎样的。随着技术的发展,我们可能在新版本中更换软件的架构,但是只要业务没有变更,领域模型就是稳定的,无需改动。

事务脚本弊端

一、可维护性能差

可维护性 = 当依赖变化时,有多少代码需要随之改变

二、可拓展性差

可扩展性 = 做新需求或改逻辑时,需要新增/修改多少代码

三、可测试性能差

可测试性 = 运行每个测试用例所花费的时间 * 每个需求所需要增加的测试用例数量

破坏原则

一、单一原则

二、依赖反转原则

三、开放封闭原则

DDD

如果今天能够重新写这段代码,考虑到最终的依赖关系,我们可能先写Domain层的业务逻辑,然后再写Application层的组件编排,最后才写每个外部依赖的具体实现。这种架构思路和代码组织结构就叫做Domain-Driven Design(领域驱动设计,或DDD)。所以DDD不是一个特殊的架构设计,而是所有Transction Script代码经过合理重构后一定会抵达的终点

这段话一针见血,很多时候在讨论DDD时,只是学习战术,什么实体、值对象、聚合,可在做项目时,引入了这些器,其他还不是CRUD,谁真正想过domain,并且以此驱动开发

DDD架构能有效解决传统架构中问题:

  1. 高可维护性:当外部依赖变更时,内部代码只用变更跟外部对接的模块,其他业务逻辑不变
  2. 高可扩展性:做新功能时,绝大部分代码都能利用,仅需要增加核心业务逻辑即可
  3. 高可测试性:每个拆分出来的模块都符合单一性原则,绝大部分不依赖框架,可以快速的单元测试,做到100%覆盖
  4. 代码结构清晰:通过POM module可以解决模块间的依赖关系,所有外接模块都可以单独独立成jar包被复用。当团队形成规范后,可以快速的定位到相关代码

作者的模块划分和依赖关系,细分析之后发现了些妙处

Infrastructure模块包含了Persistence、Messaging、External等模块。比如:Persistence模块包含数据库DAO的实现,包含Data Object、ORM Mapper、Entity到DO的转化类等。Persistence模块要依赖具体的ORM类库,比如MyBatis。如果需要用Spring-Mybatis提供的注解方案,则需要依赖Spring

Persistence从infrastructure剥离出来,解决了之前碰到的循环依赖问题(repository接口在domain层,但现实在infra层,可从maven module依赖讲,domain又是依赖infra模块的)

对于为什么拆分得这么细,是不是也解决这个问题,特意请教了作者,作者回复:

这块儿可拆可不拆,拆的好处是每个模块职责比较简单,但不拆问题也不大的
domain没必要依赖infra啊?domain里自带Repository接口,所以从maven角度来看,infra是依赖domain
infra只依赖application、domain即可。这里面还有一个Start的模块,把infra依赖进来,然后Spring的DI会自动注入的
这里,domain或app依赖的外部的接口而已,这个一般是独立的jar包
这个也可以是ACL里面的Facade,但是具体的调用实现还是infra

之前也思考过,的确得DIP,domain在最下层,infra在上面,但有个问题,把对外部依赖的接口都是放在infra里面的,所以倒置不了,以及之前在《DDD分层》里面提到的

DDD引入repository放在了领域层,一是对应聚合根的概念,二是抽象了数据库访问,,但DDD限界上下文可能不仅限于访问数据库,还可能访问同样属于外部设备的文件、网络与消息队列。为了隔离领域模型与外部设备,同样需要为它们定义抽象的出口端口,这些出口端口该放在哪里呢?如果依然放在领域层,就很难自圆其说。例如,出口端口EventPublisher支持将事件消息发布到消息队列,要将这样的接口放在领域层,就显得不伦不类了。倘若不放在位于内部核心的领域层,就只能放在领域层外部,这又违背了整洁架构思想

对于这段话,发给作者,让他点评了一下,作者回复:

EventPublisher接口就是放在Domain层,只不过namespace不是xxx.domain,而是xxx.messaging之类的
这里面2个概念,一个是maven module怎么搞,一个是什么是Domain层。这两个不是同一件事
Repository、eventpublisher接口再Domain这个module里,但是他们从理论上是infra层的。

我一般的理解:从外部收到的,属于interface层,比如RPC接口、HTTP接口、消息里面的消费者、定时任务等,这些需要转化为Command、Query、Event,然后给到App层。
App主动能去调用到的,比如DB、Message的Publisher、缓存、文件、搜索这些,属于infra层

所以消息相关代码可能会同时存在2层里。这个主要还是看信息的流转方式,都是从interface-》Application-〉infra


Repository模式

这一篇对repository有了更深的了解,之前对repository的认知和实践都太浮浅

  1. repository只能操作聚合根
  2. repository类似dao,当作dao使用
  3. repository是领域层,但从菱形架构中得知,保持领域层的纯洁,放到南向网关

对repository的认知和实践也就这些了,在实践时基本当成dao使用,当然也碰到了想不通的问题

第一点:数据加载与性能平衡问题

repository操作的对象是聚合根,因此加载一个聚合根,就得是一个完整的聚合根,可是有时我们只想加载部分数据,怎么办?
很多人指出依赖懒加载方式来解决,但也有人指出这是通过技术解决设计问题,我也迷茫,到底怎么办呢?写两个方法吧

1
2
3
scss复制代码findOrder(OrderId id);//获取完整的order聚合

findOrderWithoutItems(OrderId id);//只取order不取明细

特别的别扭吧

第二点:更新数据时,只从聚合根操作,那到了repository怎么知道具体操作哪个对象

可能又需要类似第一点,写多个方法了

这些都是很麻烦很现实的问题,为了domain纯洁性,为了DIP而特意加上不合格的repository,是不是更麻烦了呢?

很多时候,其实技术、框架、依赖三方都不怎么变,变得恰恰是domain,产品需求一日三变,难道我们努力的方向有问题?

repository价值

对于这个问题,就是为了DIP吧,作者又重新对比了DAO

DAO的核心价值是封装了拼接SQL、维护数据库连接、事务等琐碎的底层逻辑,让业务开发可以专注于写代码,但是在本质上,DAO的操作还是数据库操作,DAO的某个方法还是在直接操作数据库和数据模型,只是少写了部分代码

在Uncle Bob的《代码整洁之道》一书里,作者用了一个非常形象的描述:

  • 硬件(Hardware):指创造了之后不可(或者很难)变更的东西。数据库对于开发来说,就属于”硬件“,数据库选型后基本上后面不会再变,比如:用了MySQL就很难再改为MongoDB,改造成本过高。
  • 软件(Software):指创造了之后可以随时修改的东西。对于开发来说,业务代码应该追求做”软件“,因为业务流程、规则在不停的变化,我们的代码也应该能随时变化。
  • 固件(Firmware):即那些强烈依赖了硬件的软件。我们常见的是路由器里的固件或安卓的固件等等。固件的特点是对硬件做了抽象,但仅能适配某款硬件,不能通用。所以今天不存在所谓的通用安卓固件,而是每个手机都需要有自己的固件

从上面的描述我们能看出来,数据库在本质上属于“硬件”,DAO 在本质上属于“固件”,而我们自己的代码希望是属于“软件”。但是,固件有个非常不好的特性,那就是会传播,也就是说当一个软件强依赖了固件时,由于固件的限制,会导致软件也变得难以变更,最终让软件变得跟固件一样难以变更

比如我们使用的mybaties,有各种mapper,原来放在dao中,现在放在repository,换汤不换药

我们需要一个模式,能够隔离我们的软件(业务逻辑)和固件/硬件(DAO、DB),让我们的软件变得更加健壮,而这个就是Repository的核心价值

此刻,对菱形架构有点反思,是不是repository就应该是领域层,就是与外界数据来源的隔离,不关心具体是不是数据库,io,都是repository

作者把DTOAssembler放在了application层,是不是有点不太合理,至少不太符合分层架构,应该放在controller中呢?

从使用复杂度角度来看,区分了DO、Entity、DTO带来了代码量的膨胀(从1个变成了3+2+N个)。但是在实际复杂业务场景下,通过功能来区分模型带来的价值是功能性的单一和可测试、可预期,最终反而是逻辑复杂性的降低。

repository规范

传统Data Mapper(DAO)属于“固件”,和底层实现(DB、Cache、文件系统等)强绑定,如果直接使用会导致代码“固化”。所以为了在Repository的设计上体现出“软件”的特性,主要需要注意以下三点:

  1. 接口名称不应该使用底层实现的语法,insert,select,update,delete是sql语法
  2. 出入参不应该使用底层数据格式,操作的是Aggregate Root,避免底层实现逻辑渗透到业务代码中的强保障
  3. 应该避免所谓的“通用”repository模式

Change-Tracking 变更追踪

这是很多文章没提过的,这也解决了上面的第二点问题

对于第一点问题,也特意请教了作者,作者这样回复:

在业务系统里,最核心的目标就是要确保数据的一致性,而性能(包括2次数据库查询、序列化的成本)通常不是大问题。如果为了性能而牺牲一致性,就是捡了芝麻漏了西瓜,未来基本上必然会触发bug。

如果性能实在是瓶颈,说明你的设计出了问题,说明你的查询目标(主订单信息)和写入目标(主子订单集合)是不一致的。这个时候一个通常的建议是用CQRS的方式,Read侧读取的可能是另一个存储(可能是搜索、缓存等),然后写侧是用完整的Aggregate来做变更操作,然后通过消息或binlog同步的方式做读写数据同步。


领域层设计规范

这一讲,对领域层讲解得很充分,大牛就是大牛,DDD只是一个外衣,遮挡不了牛人内涵的韵美,这个系列如果不与DDD联系,就取名牛人叫你怎么写代码,也是相当优秀

一直以来我也认为DDD的基础是面向对象思想,我相对认为DDD与面向对象两者交集很大,重合度很高,结果作者这篇让我认知更深刻了,尤其以游戏为示例,让我更加佩服了,毕竟我在游戏业混了很久,一直自认写的代码还不错,也碰到文章指出的一些问题,可没再深入思考更优解决方案

继承

为什么Composition-over-inheritance?以前只知道继承是强耦合,我们的道是高内聚低耦合,所以不要多过使用继承。之前同事讲过行为要多Composition,但数据层面还得inheritance

作者从OCP角度再次解释这个问题

继承虽然可以通过子类扩展新的行为,但因为子类可能直接依赖父类的实现,导致一个变更可能会影响所有对象

继承虽然能Open for extension,但很难做到Closed for modification。所以今天解决OCP的主要方法是通过Composition-over-inheritance,即通过组合来做到扩展性,而不是通过继承

领域服务

作者讲了三种场景的领域服务

单对象策略型

这个示例,很有新意,实在的,没见过这样写的,有点不理解,特地请教了作者

为什么通过Double Dispatch来反转调用领域服务的方法

这里的问题就是你作为服务提供方,没办法保证Weapon入参一定是合法的。在这里依赖了另一个服务的提前校验,就说明Player没有做校验,那如果因为忘记或者bug没有提前校验呢?
在这里Entity的设计理念是要强保证一致性,这也是为什么要让服务通过参数注入

可能这是事务脚本思维的原因,先判断再执行,而作者的意思是执行本身就应该包含判断,是个整体,不能分两步

跨对象事务型

这个常见,领域服务就是这样来的

Player.attack(monster) 还是 Monster.receiveDamage(Weapon, Player)?放在领域服务就行了

通用组件型

这个平常大多被Utils类给取代了

但如果再增加一个跳跃能力Jumpable呢?一个跑步能力Runnable呢?如果Player可以Move和Jump,Monster可以Move和Run,怎么处理继承关系?要知道Java(以及绝大部分语言)是不支持多父类继承的,所以只能通过重复代码来实现

这个问题,最近在项目正好又碰到了,java继承没法处理,只能接口化处理

但在实体上都得实现相应接口,有些重复,对此也特地请教了作者:

MovementSystem可以共用,但这些实体类都实现Movable,也得重复实现,这个情况是不是没法避免

这个是很正常的,Movable本来就应该是说“我能够Move”,然后要Move就必须要有Position和Velocity。所以在Entity层面重复实现是必须的。只要是接口编程就必然需要这样(除非走mixin,但那个Java不支持)。没办法走Base类就是因为要避免继承,同时base类也没办法实现多父继承。
在有Mixin的语言里理论上是可以避免这种,但是Mixin有自己的问题,同时我们主流编程语言都没有mixin

OOP、ECS、DDD三者的比较:

  • 基于继承关系的OOP代码:OOP的代码最好写,也最容易理解,所有的规则代码都写在对象里,但是当领域规则变得越来越复杂时,其结构会限制它的发展。新的规则有可能会导致代码的整体重构。
  • 基于组件化的ECS代码:ECS代码有最高的灵活性、可复用性、及性能,但极具弱化了实体类的内聚,所有的业务逻辑都写在了服务里,会导致业务的一致性无法保障,对商业系统会有较大的影响。
  • 基于领域对象 + 领域服务的DDD架构:DDD的规则其实最复杂,同时要考虑到实体类的内聚和保证不变性(Invariants),也要考虑跨对象规则代码的归属,甚至要考虑到具体领域服务的调用方式,理解成本比较高。

实体类

在这个过程中,作者也随带说了些实体的规范

因为 Weapon 是实体类,但是Weapon能独立存在,Player不是聚合根,所以Player只能保存WeaponId,而不能直接指向Weapon;
这是对象之间关系的处理,都是这样推荐的,不要对象对联,使用ID关联。但聚合根可以直接对象关联

Entity只能保留自己的状态(或非聚合根的对象)。任何其他的对象,无论是否通过依赖注入的方式弄进来,都会破坏Entity的Invariance,并且还难以单测

对于为什么实体都特地加一个业务实体ID,之前学习有介绍:

身份标识(Identity,或简称为 ID)是实体对象的必要标志,换言之,没有身份标识的领域对象就不是实体。实体的身份标识就好像每个公民的身份证号,用以判断相同类型的不同对象是否代表同一个实体。身份标识除了帮助我们识别实体的同一性之外,主要的目的还是为了管理实体的生命周期。实体的状态是可以变更的,这意味着我们不能根据实体的属性值进行判断,如果没有唯一的身份标识,就无法跟踪实体的状态变更,也就无法正确地保证实体从创建、更改到消亡的生命过程。

一些实体只要求身份标识具有唯一性即可,如评论实体、博客实体或文章实体的身份标识,都可以使用自动增长的 Long 类型或者随机数与 UUID、GUID,这样的身份标识并没有任何业务含义。有些实体的身份标识则规定了一定的组合规则,例如公民实体、员工实体与订单实体的身份标识就不是随意生成的。遵循业务规则生成的身份标识体现了领域概念,例如公民实体的身份标识其实就是“身份证号”这一领域概念。定义规则的好处在于我们可以通过解析身份标识获取有用的领域信息,例如解析身份证号,可以直接获得该公民的部分基础信息,如籍贯、出生日期、性别等,解析订单号即可获知该订单的下单渠道、支付渠道、业务类型与下单日期等。

在设计实体的身份标识时,通常可以将身份标识的类型分为两个层次:通用类型与领域类型。通用类型提供了系统所需的各种生成唯一标识的类型,如基于规则的标识、基于随机数的标识、支持分布式环境唯一性的标识等。这些类型都将放在系统层代码模型的 domain 包中,可以作为整个系统的共享内核

总结

大牛写的文章就是深入浅出,丰富饱满,还是多拜读,多温故,不是因为DDD他们变得这么优秀,而是他们本身就很优秀,也许他们心中早有DDD了,只是没有DDD这个名字而已

希望有一天我也能写出这么好的文章

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用jsoup获取歌曲 1开局扯犊子 2页面分析 3代

发表于 2021-08-18

1.开局扯犊子

上次编写了爬取笔趣阁小说的示例,这次咱就上网抑云借几首歌听听

2.页面分析

首先咱先进入我们的目标网站 music.163.com/#/artist?id…

image.png

找到我们的目标列表 song-list-pre-cache 以及最重要的歌曲id。

因为网抑云有做登录 所以还得拿到我们自己的cookie。

image.png

这里由于原链有加密 ,咱不会破解,后面在某度中找到了个连接music.163.com/song/media/… 通过这个链接我们能直接获取到歌曲的MP3地址,接下来就是代码的实现了。

3.代码实现

首先还是引入依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.2</version>
</dependency>

主要代码

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
ini复制代码public class MusicSpider {


private String downloadUrl = "http://music.163.com/song/media/outer/url?id=";

private String path = "D:/music/";

/**
* 获取歌曲列表
* @param url
*/
public void getSongList(String url) {
Document document = getHtml(url);
if (document != null) {
Elements elements = document.select("#song-list-pre-data");
System.out.println("json数据如下" + elements.text());
String resJson = elements.text(); //解析歌曲列表
JSONArray jsonArray = JSONObject.parseArray(resJson);
//遍历获取歌曲信息
for (int i = 0; i < jsonArray.size(); i++) {
Music music = new Music();
String singer = jsonArray.getJSONObject(i).getJSONArray("artists").getJSONObject(0).get("name").toString();
music.setSinger(singer);
music.setSongUrl(downloadUrl + jsonArray.getJSONObject(i).get("id").toString());
music.setSong(jsonArray.getJSONObject(i).get("name").toString());
try {
if(i<jsonArray.size()){
Thread.sleep(10000);
System.out.println("休息10秒继续爬 ");
downloadFile(music.getSongUrl(), path + music.getSong() + "-" + music.getSinger() + ".mp3");
}
} catch (Exception e) {
e.printStackTrace();
}
}

}
}


/**
* 下载文件
* @param fileUrl 文件地址
* @param fileLocal 文件保存地址
* @throws Exception
*/
public void downloadFile(String fileUrl, String fileLocal) throws Exception {
URL url = new URL(fileUrl);
HttpURLConnection urlCon = (HttpURLConnection) url.openConnection();
urlCon.setConnectTimeout(6000);
urlCon.setReadTimeout(6000);
int code = urlCon.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
throw new Exception("文件读取失败");
}

//读文件流;
DataInputStream in = new DataInputStream(urlCon.getInputStream());
DataOutputStream out = new DataOutputStream(new FileOutputStream(fileLocal));
byte[] buffer = new byte[2048];
int count = 0;
while ((count = in.read(buffer)) > 0) {
out.write(buffer, 0, count);
}
out.close();
in.close();

}

/**
* 获得页面
* @param url
* @return
*/
private Document getHtml(String url) {
List<Header> headerList = new ArrayList<>();
headerList.add(new BasicHeader("origin", "music.163.com"));
headerList.add(new BasicHeader("referer", "https://music.163.com/"));
headerList.add(new BasicHeader("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"));
headerList.add(new BasicHeader("cookie", "这里是储存你登录的cookie"));
String result = HttpClientUtil.doGet(url, headerList, "utf-8");
if (result != null && !result.contains("n-for404")) {
return Jsoup.parse(result);
}
return null;
}

}

储存信息的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码@Data
public class Music {

private String singer;

private String song;

private String songUrl;

public Music(){

}

public Music(String singer,String song,String songUrl){
this.singer = singer;
this.song = song;
this.songUrl =songUrl;
}
}

最后启动

1
2
3
4
5
typescript复制代码 public static void main(String[] args){
MusicSpider spider = new MusicSpider();
//这里需要自己手动更改歌手的id ,不知道为什么拿不到排行榜的数据,暂时先这样,以后如果解决了再修改
spider.getSongList("https://music.163.com/artist?id=3684");
}

4.运行效果

image.png

image.png

5.总结

基础功能是实现了,不过由于有些奇奇怪怪的问题,拿不到排行榜的内容,只能暂时通过半自动的方式来拿歌曲,各位大佬如果有解决方法可以联系我,还有部分歌曲因为版权等原因是拿不到具体的数据的。害 坑还是挺多的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Cloud Eureka Client 端服务注册

发表于 2021-08-17

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

文章目的

  • 梳理 Eureka 服务注册的流程
  • 深入 服务注册的源码

对应配置类

Eureka Client 的配置是通过 EurekaClientAutoConfiguration 和 EurekaDiscoveryClientConfiguration 进行处理的

1
2
3
4
5
6
7
java复制代码C- EurekaDiscoveryClientConfiguration
public EurekaDiscoveryClient discoveryClient(EurekaClient client,
EurekaClientConfig clientConfig) {
return new EurekaDiscoveryClient(client, clientConfig);
}

C- EurekaClientAutoConfiguration

二 . 服务的注册

来看一下 Eureka 是如何注册服务的 , Eureka 通过 @EnableDiscoveryClient 开启服务的注册

Eureka 的服务注册的主要线路为 Lifecycle 发起 EurekaServiceRegistry 的生命周期控制

服务注册核心的类为 : EurekaServiceRegistry , 这个类提供了以下几个方法

1
2
3
4
5
jAVA复制代码C- EurekaServiceRegistry
M- register : 注册应用
M- deregister
M- setStatus : 更新状态
M- getStatus

2.1 Eureka 注册的起点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// C- EurekaServiceRegistry
public void start() {
// only set the port if the nonSecurePort or securePort is 0 and this.port != 0
if (this.port.get() != 0) {
if (this.registration.getNonSecurePort() == 0) {
this.registration.setNonSecurePort(this.port.get());
}

if (this.registration.getSecurePort() == 0 && this.registration.isSecure()) {
this.registration.setSecurePort(this.port.get());
}
}

// 只有在nonSecurePort大于0且由于下面的containerPortInitializer未运行时才初始化
if (!this.running.get() && this.registration.getNonSecurePort() > 0) {
// 2.1.1 -> 发起注册逻辑
this.serviceRegistry.register(this.registration);
// 发出 InstanceRegisteredEvent 事件
this.context.publishEvent(new InstanceRegisteredEvent<>(this,
this.registration.getInstanceConfig()));
this.running.set(true);
}
}

补充 : start 的入口

1
2
3
4
5
6
java复制代码// Start 方法基于 Lifecycle 接口 , 这个接口用于控制Bean 的生命周期 ,当 Application 发生 开启和停止时 , 都会调用对应的钩子事件
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}

2.1.1 注册服务

Step 1 : register 注册的起点

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// C- EurekaServiceRegistry
public void register(EurekaRegistration reg) {
maybeInitializeClient(reg);

// 通过修改状态发起注册逻辑
// PS : deregister 就是设置为 InstanceInfo.InstanceStatus.DOWN
reg.getApplicationInfoManager()
.setInstanceStatus(reg.getInstanceConfig().getInitialStatus());

// 健康检查 ,此处暂时不深入
reg.getHealthCheckHandler().ifAvailable(healthCheckHandler -> reg
.getEurekaClient().registerHealthCheck(healthCheckHandler));
}

Step 2 : 修改状态 , 调用监听器

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
java复制代码C- EurekaServiceRegistry
public synchronized void setInstanceStatus(InstanceStatus status) {
// InstanceStatus.UP
InstanceStatus next = instanceStatusMapper.map(status);
if (next == null) {
return;
}

InstanceStatus prev = instanceInfo.setStatus(next);
if (prev != null) {
for (StatusChangeListener listener : listeners.values()) {
// 可以看到 , 通过调用监听器发送状态改变的事件 -> Step 3
listener.notify(new StatusChangeEvent(prev, next));
}
}
}

// 补充 : StatusChangeListener 对象
public static interface StatusChangeListener {
String getId();
void notify(StatusChangeEvent statusChangeEvent);
}

// 补充 : StatusChangeEvent 对象
public class StatusChangeEvent extends DiscoveryEvent {
// 这个对象中记录了2个状态 , 之前和现在的, 但是这个好像只是为了打印日志 , 也没啥区别 TODO
private final InstanceInfo.InstanceStatus current;
private final InstanceInfo.InstanceStatus previous;
}

Step 3 : 发起发更新逻辑 , 对远程服务器进行更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码C- InstanceInfoReplicator
public boolean onDemandUpdate() {
if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
// 省略不必要的关注点 , 可以看到这里通过 ScheduledExecutorService 发起了定时任务
scheduler.submit(new Runnable() {
@Override
public void run() {
// private final AtomicReference<Future> scheduledPeriodicRef;
Future latestPeriodic = scheduledPeriodicRef.get();
if (latestPeriodic != null && !latestPeriodic.isDone()) {
latestPeriodic.cancel(false);
}
// 更新本地实例信息并复制到远程服务器 -> Step 4
InstanceInfoReplicator.this.run();
}
});

}
}

补充 : InstanceInfoReplicator 的作用

  • 配置了单个更新线程,以保证对远程服务器的连续更新
  • 更新任务可以通过onDemandUpdate() 按需调度
  • 任务处理速率受突发大小限制
  • 一个新的更新任务总是自动调度在一个较早的更新任务之后。
    • 但如果启动了按需更新任务,则定时的自动更新任务将被丢弃(新的按需更新任务将在新的按需更新任务之后调度新的任务)

Step 4 : 调用 InstanceInfoReplicator # run

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public void run() {
try {
// 刷新当前本地InstanceInfo , InstanceInfo上的isdirty标志被设置为true
discoveryClient.refreshInstanceInfo();

Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
// 最核心的一句 : 发起服务注册
discoveryClient.register();
// 如果unsetDirtyTimestamp 匹配 lastDirtyTimestamp,则取消dirty标志
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
} finally {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}

2.2 注册服务

此处就开始通过 DiscoveryClient 进行注册逻辑 :

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码C- DiscoveryClient
boolean register() throws Throwable {
EurekaHttpResponse<Void> httpResponse;
try {
// 发起注册逻辑
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
throw e;
}
// 这个返回在业务上没有太大的作用
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

2.3 register 循环调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JAVA复制代码// 这里和下方的模式非常相似 ,都是传个匿名函数进去 , 再由里面发起回调
C- EurekaHttpClientDecorator
public EurekaHttpResponse<Void> register(final InstanceInfo info) {
return execute(new RequestExecutor<Void>() {
@Override
public EurekaHttpResponse<Void> execute(EurekaHttpClient delegate) {
// 通过该回调函数进行链式调用
return delegate.register(info);
}

@Override
public RequestType getRequestType() {
return RequestType.Register;
}
});
}

细节点 : 轮询调用

Eureka-Client-Registry.png

// 此处进行了一个链式调用 ,分别调用了

  • C- SessionedEurekaHttpClient : 强制在一个定期的间隔(一个会话)重新连接,防止客户端永远坚持一个特定的Eureka服务器实例
  • C- RetryableEurekaHttpClient : 在集群中的后续服务器上重试失败的请求
  • C- RedirectingEurekaHttpClient : 该Client 会进行 Server 重定向 , 并且针对最终解析的端点执行请求
  • C- MetricsCollectingEurekaHttpClient : 收集和统计JerseyApplicationClient发送请求和响应的行为信息
1
2
3
4
5
6
7
java复制代码// 通常内部是通过 clientFactory 构建下一个 HttpClient  , 这个对象在 EurekaHttpClients 中通过构造器传入
TransportUtils.getOrSetAnotherClient(eurekaHttpClientRef, clientFactory.newClient());


// 这几个里面有几个关注点 :
// 关注点一 : 原子对象类
private final AtomicReference<EurekaHttpClient> eurekaHttpClientRef = new AtomicReference<>();

补充 : 调用的目的 , 为什么这里要循环几个 HttpClient ?

查看这几个 HttpClient ,感觉他们应该更像是 Filter ,只不过他们的职能是发起请求 ,而结构类似于 Filter 过滤链

关于这一块可以单章看一下 , 这里只关注最后的一个 , 也就是 MetricsCollectingEurekaHttpClient

2.4 核心 HttpClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码C- MetricsCollectingEurekaHttpClient
protected <R> EurekaHttpResponse<R> execute(RequestExecutor<R> requestExecutor) {
EurekaHttpClientRequestMetrics requestMetrics = metricsByRequestType.get(requestExecutor.getRequestType());
// 度量执行某些代码所花费的时间
Stopwatch stopwatch = requestMetrics.latencyTimer.start();
try {
// HttpClient 中最常见的模式就说这种匿名类的回调
EurekaHttpResponse<R> httpResponse = requestExecutor.execute(delegate);
requestMetrics.countersByStatus.get(mappedStatus(httpResponse)).increment();
return httpResponse;
} catch (Exception e) {
requestMetrics.connectionErrors.increment();
exceptionsMetric.count(e);
throw e;
} finally {
// 计时结束
stopwatch.stop();
}
}

补充 : JerseyApplicationClient 的创建

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
java复制代码// Step 1 : JerseyEurekaHttpClientFactory 的创建
public TransportClientFactory newTransportClientFactory(EurekaClientConfig clientConfig,
Collection<ClientFilter> additionalFilters, InstanceInfo myInstanceInfo, Optional<SSLContext> sslContext,
Optional<HostnameVerifier> hostnameVerifier) {
final TransportClientFactory jerseyFactory = JerseyEurekaHttpClientFactory.create(
clientConfig,
additionalFilters,
myInstanceInfo,
new EurekaClientIdentity(myInstanceInfo.getIPAddr()),
sslContext,
hostnameVerifier
);

// 通过工厂创建
final TransportClientFactory metricsFactory = MetricsCollectingEurekaHttpClient.createFactory(jerseyFactory);

return new TransportClientFactory() {
@Override
public EurekaHttpClient newClient(EurekaEndpoint serviceUrl) {
// 匿名对象回调函数
return metricsFactory.newClient(serviceUrl);
}
};
}

// Step 2 : 构造器注入
private MetricsCollectingEurekaHttpClient(EurekaHttpClient delegate,
Map<RequestType, EurekaHttpClientRequestMetrics> metricsByRequestType,
ExceptionsMetric exceptionsMetric,
boolean shutdownMetrics) {
....

2.5 发起 Server 请求流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码// C- AbstractJerseyEurekaHttpClient
public EurekaHttpResponse<Void> register(InstanceInfo info) {
// http://localhost:8088/eureka/
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
// 准备 Build 用于构造 Request
// serviceUrl -> http://localhost:8088/eureka/
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);

// 发起请求 , 此处使用的是 jersey
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (response != null) {
response.close();
}
}
}

这里就会调用 Server 端 ApplicationResource , 具体流程后面说 Server 端时看看

三 . 补充

3.1 HttpClient 的结构设计

HttpClient 的这种调用模式是一种典型的 .. 设计模式 , 其中大量使用了 AtomicReference 原之类的特性 ,

Eureka-System-TransportClientFactory.png

3.2 为什么 Eureka 里面大量使用了原子类 ?

首先 : 原子类主要通过 CAS (compare and swap) + volatile 和 native 方法来保证原子操作

前文中看到使用了 ScheduledExecutorService 发起了注册 , 实际debug 中发现这个里面实际上存在很多多线程调用 .

总结

之前看比较新的框架 , 往往可以看到很多 Spring 的影子 , 很多都会选择用 Spring 作为管理 .

但是看了Eureka 的框架才意识到 , 其实很多框架是没有和 Spring 集成的 , Spring 很好用 ,但是Eureka 如果用了 , 返回会显得很臃肿

eureke 中的技术栈有一定年限了, 但是不意味着不好用了 , 这一套模式前几年我还用过 , 现在看起来 , 颇有感触

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Spring Boot 快速入门】九、Spring Boo

发表于 2021-08-17

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

收录专栏

Spring Boot 快速入门

Java全栈架构师

前言

  Mybatis在持久层框架中还是比较火的,经常在项目中需要创建很多的Bean,并在Xml中书写大量的sql语句进行CRUD。很多简单而频繁的SQL可以直接使用MyBatis-Plus去解决,下面就开始了解MyBatis-Plus。

初始MyBatis-Plus

  MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

MyBatis-Plus特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  • 损耗小:启动即会自动注入基本 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 操作智能分析阻断,也可自定义拦截规则,预防误操作。

  来源于MyBatis-Plus官网,欢迎大家补充更多的特点。

快速开始

依赖

  好了, MyBatis-Plus大体介绍已经完成了,大家也明白了MyBatis-Plus是为了简化新生代的农民工的码量而有的产物。本文将结合目前常用的Spring Boot进行项目开发,快速搭建一套Spring Boot集成MyBatis-Plus的简单Demo。
  因为是基于Maven搭建的Spring Boot集成MyBatis-Plus的项目。所以首先是添加依赖,主要包含两部分依赖,依赖信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xml复制代码 <!--    mybatis-plus    -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.0</version>
</dependency>
<!-- mybatis-plus -->

设置application.yml

  在 application.yml配置文件中添加MySql 数据库的相关配置:其中主要包含数据库的链接、用户名、用户密码、数据源类型,数据源驱动类名等基础信息。
  当然也可以加入MyBatis-Plus的相关配置信息,

  • db-type: 数据库的类型信息mysql
  • id-type: 主键id的创建方式AUTO
  • logic-delete-field: dataStatus #全局逻辑删除字段值 3.3.0开始支持,详情看下面。
  • logic-delete-value: 0 # 逻辑已删除值(默认为 99)
  • logic-not-delete-value: 1 # 逻辑未删除值(默认为 1)
  • mapper-locations: 自动配置并扫描本地mapper.xml所在的文件路径,例如:sclasspath*:mapper/**/*.xml
  • typeAliasesPackage:自动配置并扫描本地SQL实体对象所在的包信息,例如: java.zhan.entity
  • typeEnumsPackage: 自动配置并扫描项目中使用的枚举信息所在的包,例如:java.zhan.enums;java.zhan.test.enums
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yaml复制代码spring:
datasource:
url: jdbc:p6spy:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: admin
password: 123456
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.p6spy.engine.spy.P6SpyDriver

mybatis-plus:
global-config:
db-config:
db-type: mysql
id-type: AUTO
logic-delete-field: dataState
logic-delete-value: 0
logic-not-delete-value: 1
banner: false
mapper-locations: classpath*:mapper/**/*.xml
typeAliasesPackage: java.zhan.entity
typeEnumsPackage: java.zhan.enums;java.zhan.test.enums

分页

Spring boot方式项目的分页方式需要设置MybatisPlusConfig,其中注解MapperScan扫描mapper文件所在的包

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
less复制代码@EnableTransactionManagement
@Configuration
@MapperScan("java.zhan.**.mapper")
public class MybatisPlusConfig {

// 旧版
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}

// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

}

DemoMybatisPlusApplication

启动类中需要特别注意的是@MapperScan(value = “com.example.demo.mapper”),需要制定启动扫描包的路径。

1
2
3
4
5
6
7
8
9
less复制代码@SpringBootApplication
@MapperScan(value = "com.example.demo.mapper")
public class DemoMybatisPlusApplication {

public static void main(String[] args) {
SpringApplication.run(DemoMybatisPlusApplication.class, args);
}

}

测试

本文基于以上搭建的简单的Spring Boot集成MyBatis-Plus进行单元测试,主要测试有新增用户,查询用户,查询所有用户等。更多测试需要根据业务需要进行测试。

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
typescript复制代码    /**
* @MethodName: insertUser
* @Description: 新增
* @param
* @Return: void
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2020/6/20
**/
@Test
void insertUser(){
User user = new User();
user.setUsername("gust");
user.setName("测试");
user.setCreatetime(new Date());
user.setSalt(1+"");
user.setPassword("1233123");
user.setState(0);
userService.save(user);
}


/**
* @MethodName: getUserByUserName
* @Description: 获取单个对象
* @param
* @Return: void
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2020/6/20
**/
@Test
void getUserByUserName() {
QueryWrapper<User> quserQueryWrapper = new QueryWrapper<>();
quserQueryWrapper.lambda().eq(User::getUserName,"admin");
User user = userService.getOne(quserQueryWrapper);
System.out.println(user);
}

/**
* @MethodName: getAllUser
* @Description: 获取所有的用户
* @param
* @Return: void
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2020/6/20
**/
@Test
void getAllUser(){
List<User> userList = userService.list();
userList.forEach(user -> System.out.println(user));
}

结语

  这样MyBatis-Plus与Spring Boot集成成功啦。MyBatis-Plus具有强大的功能,代码生成器、CRUD 接口、条件构造器、分页插件、 Sequence主键、自定义ID生成器、逻辑删除、通用枚举、字段类型处理器、自动填充功能、SQL注入器、执行SQL分析打印、数据安全保护、多数据源等更多的功能。大家可以参考MyBatis-Plus官网查询了解更多。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

推荐阅读:

【Spring Boot 快速入门】一、我的第一个Spring Boot项目启动啦!

【Spring Boot 快速入门】二、周末建立了Spring Boot专栏,欢迎学习交流

【Spring Boot 快速入门】三、Spring Boot集成MyBatis,可以连接数据库啦!

【Spring Boot 快速入门】四、Spring Boot集成JUnit

【Spring Boot 快速入门】五、Spring Boot集成Swagger UI

【Spring Boot 快速入门】六、Spring Boot集成Lombok

【Spring Boot 快速入门】七、Spring Boot集成Redis

【Spring Boot 快速入门】八、Spring Boot集成RabbitMQ

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

IDEA在编译项目常见问题

发表于 2021-08-17

(一)IntelliJ Idea编译报错:请使用 -source 7 或更高版本以启用 diamond 运算符。

最近在使用IntelliJ Idea遇到了挫折,分享出来给大家,问题由来是我导入了外部的java文件,结果就报错了

网上搜了各种解决方法都不行,崩溃的节奏啊,终于皇天不负有心人,让我同事解决了,希望对大家也有所帮助吧,原来IntelliJ Idea 默认的jdk是1.5。所以试了各种修改都没起作用。我们必须这样修改

选择7.0以上的版本即可,唉,浪费了半天时间。

(二)IDEA Error:java: Compilation failed: internal java compiler error

错误原因

导致这个错误的原因主要是因为jdk版本问题,此处有两个原因,一个是编译版本不匹配,一个是当前项目jdk版本不支持。

查看项目的jdk

File ->Project Structure->Project Settings ->Project或使用快捷键Ctrl+Alt+shift+S打开项目的jdk配置:

查看工程的jdk

点击上图中Modules查看对应jdk版本:

查看java编译器版本

导入java项目时此处处问题的概率比较多。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我在公司用Jmeter刚压一天就崩了,请求、响应数据都为空?

发表于 2021-08-17

文章前景,公司服务器需要压两天,在使用GUI模式下,本机Jmeter压了一天就蹦了,而且期间有错误请求,但是返回查看日志时,日志信息的请求、响应时间都显示为空。测试面试宝典

注意:

第6点Jmeter5.0更改放在Reporting 下

Linux服务器用命令执行了jmeter脚本,在本地查看结果时发现结果树种的“请求、响应数据”都显示为空,有错误日志中也看不出所以然,请看演示!

  1. 先执行脚本:执行成功(…end of run),但是发现有两个错误


2. 从服务器到处rmw_*.jtl结果放在本地jmeter中查看,发现数据都显示空


3. 再查看jmeter.log日志,发现失败的请求并未有错误日志(当然验证的参数错误,并非配置等错误)


4. 接下来怎么办呢?在jmeter.properties文件中修改配置

jmeter.save.saveservice.response_data=true

jmeter.save.saveservice.samplerData=true


5. 在user.properties文件中追加配置

jmeter.save.saveservice.output_format=xml

jmeter.save.saveservice.response_data=true

jmeter.save.saveservice.samplerData=true

jmeter.save.saveservice.requestHeaders=true

jmeter.save.saveservice.url=true

jmeter.save.saveservice.responseHeaders=true


6. 配置好后重新执行一边脚本,执行成功


7. 导入本地jmeter结果树查看发现请求响应数据展示了


8. 再解释一下linux执行后字段详解

➤:9个请求,2.5秒,tps:3.6/s,平均响应时间454ms,最小响应时间46ms,最大相应请求2047ms,2错误(错误率22.22%)测试面试宝典

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java基础之BIO/NIO/AIO模型

发表于 2021-08-17

很多文章在谈论到BIO、NIO、AIO的时候仅仅是抛出一堆定义,以及一些生动的例子。看似很好理解。但是并没有将最基础的本质原理显现出来,如果没有没有从IO的原理出发的话是很难理解这三者之间的区别的。所以本篇文章从Java是如何进行IO操作为开头进行分析。

1. Java中的IO原理

根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

image.png

输入设备(比如键盘)和输出设备(比如显示屏)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。

从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。

为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。

像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。
并且,用户空间的程序不能直接访问内核空间。当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。

因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间

我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和相应) 。

当应用程序发起 I/O 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

首先Java中的IO都是依赖操作系统内核进行的,我们程序中的IO读写其实调用的是操作系统内核中的read&write两大系统调用。

那内核是如何进行IO交互的呢?

  1. 网卡收到经过网线传来的网络数据时,会并将网络数据写到内存中。
  2. 当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便得知有新数据到来,再通过网卡中断程序去处理数据。
  3. 将内存中的网络数据写入到对应socket的接收缓冲区中。
  4. 当接收缓冲区的数据写好之后,应用程序开始进行数据处理。

对应抽象到java的socket代码简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码public class SocketServer {
public static void main(String[] args) throws Exception {
// 监听指定的端口
int port = 8080;
ServerSocket server = new ServerSocket(port);
// server将一直等待连接的到来
Socket socket = server.accept();
// 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
int len;
while ((len = inputStream.read(bytes)) != -1) {
//获取数据进行处理
String message = new String(bytes, 0, len,"UTF-8");
}
// socket、server,流关闭操作,省略不表
}
}

可以看到这个过程和底层内核的网络IO很类似,主要体现在accept()等待从网络中的请求到来然后bytes[]数组作为缓冲区等待数据填满后进行处理。而BIO、NIO、AIO之间的区别就在于这些操作是同步还是异步,阻塞还是非阻塞。

所以我们引出同步异步,阻塞与非阻塞的概念。

2. 同步与异步

同步和异步指的是一个执行流程中每个方法是否必须依赖前一个方法完成后才可以继续执行。

假设我们的执行流程中:依次是方法一和方法二。

  • 同步:指的是调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。即方法二一定要等到方法一执行完成后才可以执行。
  • 异步:指的是调用立刻返回,调用者不必等待方法内的代码执行结束,就可以继续后续的行为。(具体方法内的代码交由另外的线程执行完成后,可能会进行回调)。即执行方法一的时候,直接交给其他线程执行,不由主线程执行,也就不会阻塞主线程,所以方法二不必等到方法一完成即可开始执行。

同步与异步关注的是方法的执行方是主线程还是其他线程,主线程的话需要等待方法执行完成,其他线程的话无需等待立刻返回方法调用,主线程可以直接执行接下来的代码。

同步与异步是从多个线程之间的协调来实现效率差异。

为什么需要异步呢?

笔者认为异步的本质就是为了解决主线程的阻塞,所以网上很多讨论把同步异步、阻塞非阻塞进行了四种组合,其中一种就有异步阻塞这一情形,如果异步也是阻塞的?那为什么要特地进行异步操作呢?

3. 阻塞与非阻塞

阻塞与非阻塞指的是单个线程内遇到同步等待时,是否在原地不做任何操作。

  • 阻塞 指的是遇到同步等待后,一直在原地等待同步方法处理完成。
  • 非阻塞 指的是遇到同步等待,不在原地等待,先去做其他的操作,隔断时间再来观察同步方法是否完成。

阻塞与非阻塞关注的是线程是否在原地等待。

4. Java中的I/O模型

4.1 BIO (Blocking I/O)

BIO 属于同步阻塞 IO 模型

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。
image.png

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

4.2 NIO (Non-blocking/New I/O)

Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。

Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。

跟着我的思路往下看看,相信你会得到答案!

我们先来看看 同步非阻塞 IO 模型。

image.png

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

这个时候,I/O 多路复用模型 就上场了。

image.png

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。

目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,是目前几乎在所有的操作系统上都有支持

  • select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
  • epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

image.png

4.3 AIO (Asynchronous I/O)

AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
image.png

目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。

最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。
image.png

相关文章

  1. juejin.cn/post/693984…
  2. www.cnblogs.com/2019wxw/p/1…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于AOP及自定注解,实现接口操作日志记录Starter 业

发表于 2021-08-17

业务场景

最近开发项目中有一个用户查看操作日志需求,因为我们后端是多个SpringBoot微服务提供接口,于是就准备开发一个日志记录的Starter,具体方案就是基于AOP+自定义注解,记录用户的操作日志。

1
markdown复制代码* 我会通过代码注释的方式介绍每个业务,结合注释阅读更易理解

搭建Starter项目

  • rest-log-spring-boot-start
  • 自定义注解 RestLog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface RestLog {

/**
* 模块名称
*/
String module();

/**
* 操作
*/
String action();

/**
* 描述
*/
String describe() default "";
}
1
复制代码我们业务场景需要记录用户操作的模块,做了什么操作,描述信息为拓展字段,可以根据实际需求增加更多信息。
  • 定义日志对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Data // Lombok注解 
public class LogBean {
private Long id;
// 操作userId
private String userId;
// 模块
private String module;
// 操作
private String action;
// 描述信息
private String describe;
// 接口地址
private String api;
// GET POST DELETE PUT...
private String method;
// 接口参数信息
private String paramJson;
// 操作时间
private Date restTime;
}
  • 定义切面
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
scss复制代码@Aspect
@Component
public class RestLogAspect {

private static final Logger LOG = LoggerFactory.getLogger(RestOperationLogAspect.class);

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

/**
* 生成日志后发送的目标topic
*/
@Value("${rest-log.topic:}")
private String topic;

/**
* 切面
*/
@Pointcut("@annotation(com.demo.RestLog)")
private void restLogCut() {
// 操作日志切面
}

/**
* 执行后日志存储操作
*
* @param joinPoint
*/
@After("restLogCut()")
public void after(JoinPoint joinPoint) {
try {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获取注解对象
OperationLog annotation = method.getAnnotation(RestLog.class);
// 获取请求对象 HttpServletRequest
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 请求经过网关 鉴权通过 网关会像请求头中添加userId信息
String userId = request.getHeader("Auth-Userid");
if(userId == null) {
// 内部请求不记录日志 微服务之间接口不仅仅给前端调用 服务间也会有业务调用 服务间调用不会经过网关 也就不会有用户信息
return;
}
// 获取参数名称
String[] names = signature.getParameterNames();
// 获取参数值
Object[] args = joinPoint.getArgs();
// 将接口请求参数封装成json 便于存储及解析
JSONObject paramJson = new JSONObject();
for(int i = 0; i <names.lenth; i++) {
paramJson.put(names[i], args[i]);
}
// 创建日志对象 用于传输日志信息
LogBean logBean = new LogBean();
logBean.setUserId(userId);
logBean.setModule(annotation.module());
logBean.setAction(annotation.action());
logBean.setDescribe(annotation.describe());
logBean.setApi(request.getRequestURI());
logBean.setMethod(request.getMethod());
logBean.setParamJson();
logBean.setRestTime(new Date());
// 判断是否配置发送的topic 配置了则进行发送
if(StringUtils.isNotBlank(topic)) {
// 将日志内容发送到kafka
kafkaTemplate.send(topic, JSONObject.toJSONString(logBean));
}
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
}

总结:1、切面业务中,进行了请求头的判断(判断是微服务间请求还是客户端请求,避免微服务间的rest请求被记入日志中);2、将请求参数,模块信息等等封装为一个实体,发送到kafka,降低日志业务对接口性能影响;3、采用kafka发送能够更加灵活的应用于微服务系统,日志管理服务监听topic,微服务集成Stater后配置这个topic,最终微服务的操作日志系统就可以做到快速集成,统一管理。

应用starter

springboot服务引入starter依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.demo</groupId>
<artifactId>rest-log-spring-boot-start</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

添加配置到application.yaml

1
2
less复制代码rest-log:
topic: rest_log

在接口上添加注解

1
2
3
4
5
6
7
8
9
java复制代码@RestController
@RequestMapping("/demo")
public class DemoController {
@PostMapping
@RestLog(module = "测试模块", action = "新增", describe = "这是一个描述")
public void add(@RequestBody Demo demo) {
// 省略实现
}
}

添加kafka监听(只需要在提供日志存储及查询业务的服务添加监听)

1
2
3
4
5
6
7
java复制代码@Service
public class LogListener {
@KafkaListener(topics = "iot-original-history-message", containerFactory = "batchConsumerFactory")
public void listener(List<ConsumerRecord<String, String>> records) {
// 遍历记录 实现存储业务即可 省略
}
}

Kafka批量消费配置可以参考之前文章有介绍

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…559560561…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%