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

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


  • 首页

  • 归档

  • 搜索

盘点 SpringIOC Bean 创建之属性注入

发表于 2021-05-19

总文档 :文章目录

Github : github.com/black-ant

一 . 前言

上一篇说了盘点 SpringIOC : Bean 创建主流程 , 这一篇把第二个主要环节 属性注入

还是来看一个很常见的图 , 来源于 @ topjava.cn/article/139…

beanSelf.jpg

1.1 这篇文档的属性注入到底包含了哪些内容 ?

PS :无意中发现这篇文章不能只局限于 populateBean , 所以后续会在这个小章节陆续完善如下信息 , 感兴趣的可以持续关注 !

Bean 的属性处理主要范围为为Bean 进行注入 ,按照我们一般了解的 , Bean 注入主要包括三种注入方式 : 属性 / 构造器 / setter

  • 方式一 : 通过构造器 @Autowired 主流程
  • 方式二 : 通过属性上 @Autowired 主流程
  • 方式三 : 通过 setter 方式 @Autowired 主流程

方式一 : 通过构造器 @Autowired 主流程

1
2
3
4
5
java复制代码@Autowired
public AutowiredService(CommonService commonService, BeanAService beanAService) {
this.commonService = commonService;
this.beanAService = beanAService;
}

image-20210519112021433.png

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//上图是一张 doCreateBean 方法中 , 执行 createBeanInstance(beanName, mbd, args) 的属性图 
//可以看到 , 在 BeanWrapper 中 , 2个 Autowired 的对象已经处理了

// 这里被触发的主要原因是因为 createBeanInstance 中如下代码 :
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}

// autowireConstructor 核心代码
C- ConstructorResolver
M- autowireConstructor : 处理构造器注入 -> 附录二

方式二 : 通过属性上 @Autowired 主流程

image.png

上图可以看到 , createBeanInstance 后 2个属性均未开始加载 , 但是我们又知道 , 属性的注入是在 initializeBean前完成 , 以保证初始化方法可用.

1
2
3
4
5
6
7
8
9
java复制代码// 在处理 populateBean 时 , 会对 InstantiationAwareBeanPostProcessor 进行处理 , 其中主要有这些

org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor
org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker
org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
// .....

最终的核心就是 AutowiredAnnotationBeanPostProcessor , 这个我们在 populateBean 后面细讲

方式三 : setter

1
java复制代码//方式三和方式二类型 , 处理的 PostProcessor 主要为 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor

二 . 属性注入流程

区别于上面的 autowired , populateBean 还主要处理了什么?

为了更好的理解流程 , 这里要区分一下 , 除了 autowired 注入属性外 , populateBean 中还有一个重要的概念叫 PropertyValues

1
2
3
java复制代码TODO : 这一块有大问题 , 其来源始终没有弄懂,  查阅资料大多数也是一笔带过 
// 这里先埋坑 , 晚上再详细的看看 , 猜测是由于老式 xml 方式构建对象时 , 有个 property 属性
// 如果有清楚的 , 麻烦提点一下 ,非常感谢

我们来好好的看一下 属性注入的流程

2.1 属性注入的入口

1
2
3
java复制代码C- AbstractAutowireCapableBeanFactory 
M173_05- doCreateBean(beanName, mbdToUse, args) : 创建 Bean 对象
- populateBean : 属性注入操作 -> M173_30

2.2 属性注入主流程

属性的注入主流程是在 doCreateBean 中 , 回忆循环依赖的相关概念 ,在调用populateBean 之前 , 已经产生了一个对象 , 该对象未完成相关的属性注入

先来看一下传入的属性

populateBean(beanName, mbd, instanceWrapper);

image.png

image.png

PS:属性注入是在 InitializingBean 之前 , 所以相关的初始化方法可以使用 @Autowired 对象 , 这个后续详细说说

属性注入 populateBean 逻辑

逻辑整体上分为7步 :

  • Step 1 : BeanWrapper 空实例的处理 : 实例化之前要对空实例进行判断和处理
  • Step 2 : BeanPostProcessors 处理
  • Step 3 : 为 bean 注入属性
  • Step 4 : 判断是否注册了 InstantiationAwareBeanPostProcessors 以及是否需要依赖检查
  • Step 5 : BeanPostProcessor 处理 , 此处主要是 InstantiationAwareBeanPostProcessor 的处理
  • Step 6 : 依赖检查
  • Step 7 : applyPropertyValues(beanName, mbd, bw, pvs)
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
java复制代码
C173- AbstractAutowireCapableBeanFactory
M173_30- populateBean(String beanName, RootBeanDefinition mbd,BeanWrapper bw)
P- String beanName
P- RootBeanDefinition mbd
P- @Nullable BeanWrapper bw
1- BeanWrapper 空实例的处理 : 实例化之前要对空实例进行判断和处理
- 如果 BeanWrapper 为空 , 且mbd有值 ,无法对空实例初始化 , 抛出 BeanCreationException
- 如果 BeanWrapper 为空 , 且mbd无属性 , 直接返回
2- BeanPostProcessors 处理
?- 这里是PostProcesssors 最后一次加载机会 , 该方法为bean中定义bean 初始化时默认做的操作
IF- bean 不是合成的 , bean 持有 InstantiationAwareBeanPostProcessor ->
FOR- 迭代所有的 BeanPostProcessors : getBeanPostProcessors
- InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation
?- 当返回值为 false 的情况下会停止整个bean 的属性注入 , 直接return , 跳过属性注入操作
3- 为 bean 注入属性
- new MutablePropertyValues(pvs) -- 获取bean 的属性值
- PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null) -- pvs 来源
- 判断是否有自动注入的注解 -> 自动注入属性
- 将 PropertyValues 封装成 MutablePropertyValues 对象 , 该对象允许对属性进行简单操作 -> PS:M173_30_3
- 通过 mbd.getResolvedAutowireMode() 区别是名称注入还是类型注入 -> 同步不同的类型注入 -> PS:M173_30_4
- AUTOWIRE_BY_NAME -- autowireByName(beanName, mbd, bw, newPvs);
- AUTOWIRE_BY_TYPE -- autowireByType(beanName, mbd, bw, newPvs);
4- 判断是否注册了 InstantiationAwareBeanPostProcessors 以及是否需要依赖检查 (needsDepCheck)
- 是否注册 : hasInstantiationAwareBeanPostProcessors()
- 是否依赖检查 : mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE
5- 当步骤4 告知需要 BeanPostProcessor 处理 , 即 hasInstAwareBpps + BeanPostProcessor 不为空
- pvs 为 null 会 通过 mbd.getPropertyValues(); 获取一个
- 遍历 BeanPostProcessor 数组 ,instanceof InstantiationAwareBeanPostProcessor, 对Bean 进行前置处理
- PropertyValues = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName); --- 获取 PropertyValues
- 如果 PropertyValues 为null , 则通过 ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); 完成构建
- filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
6- 依赖检查
- filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
- checkDependencies(beanName, mbd, filteredPds, pvs); -- 依赖检查,对应 depends-on 属性
7- applyPropertyValues(beanName, mbd, bw, pvs); --- 将属性应用到 bean 中


C175- AbstractBeanDefinition : MutablePropertyValues 类的作用
M175_01- getResolvedAutowireMode : 获得 Autowired 的类型

PS:M173_30_3 MutablePropertyValues 的作用

允许简单的属性操作,并提供构造函数来支持从Map深度复制和构造

1
2
3
java复制代码C190- MutablePropertyValues
F190_01- List<PropertyValue> propertyValueList
F190_02- Set<String> processedProperties

PS:M173_30_4 通过不同类型注入

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码C173- AbstractAutowireCapableBeanFactory
M173_10- autowireByName : 根据属性名称,完成自动依赖注入
- 获取Bean 对象中非简单属性 (非基本类型的对象)
FOR- 循环该该属性数组 , 递归通过name获取相关的bean
- 将相关的bean 加入 MutablePropertyValues
- 属性依赖注入 -- registerDependentBean(propertyName, beanName);
- Map<String, Set<String>> dependentBeanMap : beanName - > 依赖 beanName 的集合
- Map<String, Set<String>> dependenciesForBeanMap : 依赖 beanName - > beanName 的集合
- 1 获取 beanName
- 2 添加 <canonicalName, <dependentBeanName>> 到 dependentBeanMap 中
- 3 添加 <dependentBeanName, <canonicalName>> 到 dependenciesForBeanMap 中
M173_11- autowireByType : 根据属性类型,完成自动依赖注入
- 整体方法类似于byName , 逻辑上是找到需要依赖注入的属性,然后通过迭代的方式寻找所匹配的 bean

M173_10 autowireByName 源码

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
java复制代码protected void autowireByName(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
for (String propertyName : propertyNames) {
// 判断 Bean 是否加载了
if (containsBean(propertyName)) {
Object bean = getBean(propertyName);
pvs.add(propertyName, bean);
// 为给定bean注册一个依赖bean,以便在销毁给定bean之前销毁它
registerDependentBean(propertyName, beanName);
//... log
}
else {
//... log
}
}
}

// Pro 1 : containsBean 详情
- containsSingleton : 单例模式的缓存 , 详情可以看循环依赖篇
- containsBeanDefinition : beanDefinitionMap.containsKey(beanName) , registerBeanDefinition 时添加
- parentBeanFactory.containsBean : 从 Parent 中获取

// Pro 2 : registerDependentBean 逻辑 ,其中干了这样几件事
public void registerDependentBean(String beanName, String dependentBeanName) {
// 确定原始名称,将别名解析为规范名称 , 这里实际上就是对 aliasMap 循环
String canonicalName = canonicalName(beanName);

// 对 dependentBeanMap 上锁 , 保证多线程唯一
// dependentBeanMap : 谁依赖我 ,bean名称到依赖bean名称的集合
synchronized (this.dependentBeanMap) {
Set<String> dependentBeans =this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
if (!dependentBeans.add(dependentBeanName)) {
return;
}
}

// dependenciesForBeanMap : 我依赖谁 , bean名称到bean依赖项的bean名称的集合
synchronized (this.dependenciesForBeanMap) {
Set<String> dependenciesForBean =
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
dependenciesForBean.add(canonicalName);
}
}

// PS : 关于 dependentBeanMap 和 dependenciesForBeanMap 可以看看这篇文档 :
https://blog.csdn.net/xieyinghao_bupt/article/details/109552054

image.png

M173_11 autowireByType 源码

ps : 这里一开始案例太少 ,没看懂 , 回头翻了一下 死磕系列的笔记 , 终于搞清楚了

这一段参照死磕 IOC , 仅补充部分逻辑

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
java复制代码protected void autowireByType(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
// 定义类型转换方法的接口
// 使用自定义的 TypeConverter,用于取代默认的 PropertyEditor 机制
TypeConverter converter = getCustomTypeConverter();
if (converter == null) {
// 通常上面的那个接口都为null , 这里就会直接使用 BeanWrapper
converter = bw;
}

Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
// 获取非简单属性
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
// 遍历 propertyName 数组
for (String propertyName : propertyNames) {
try {
// 获取 PropertyDescriptor 实例
PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
if (Object.class != pd.getPropertyType()) {
// 探测指定属性的 set 方法
MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
boolean eager = !(bw.getWrappedInstance() instanceof PriorityOrdered);
DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
// 解析指定 beanName 的属性所匹配的值,并把解析到的属性名称存储在 autowiredBeanNames 中
// 当属性存在过个封装 bean 时将会找到所有匹配的 bean 并将其注入
Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
if (autowiredArgument != null) {
pvs.add(propertyName, autowiredArgument);
}
// 遍历 autowiredBeanName 数组
for (String autowiredBeanName : autowiredBeanNames) {
// 属性依赖注入
registerDependentBean(autowiredBeanName, beanName);
}
autowiredBeanNames.clear();
}
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
}
}
}


PS: autowireByType 可以通过 @Bean(initMethod = "initMethod", autowire = Autowire.BY_TYPE) 进行触发

// Pro 1 : PropertyDescriptor 对象
作用 : PropertyDescriptor描述了Java bean通过一对访问器方法导出的一个属性
方法 : getPropertyDescriptor : 获取包装对象的特定属性的属性描述符


// Pro 2 : BeanUtils.getWriteMethodParameter(pd)
- Method writeMethod = pd.getWriteMethod() : 获取应用于写入属性值的方法
- 这里应该就是 构造器和 Setter 等逻辑 -> PS:M173_11_01
- new MethodParameter(writeMethod, 0)


// Pro 3 : DependencyDescriptor
描述符,用于即将注入的特定依赖项。包装构造函数参数、方法参数或字段,允许对它们的元数据进行统一访问。

PS:M173_11_01 可以看到这里是 setter 方法
image.png

关于默认注入类型的问题

之前看资料的时候 , 一直都是说 AutoWired 默认使用 byType , 但是实际debug 中 ,发现并没有 , 在属性注入的时候 , 返回的类型为 0 , 即AUTOWIRE_NO , 其实这个是要分情况处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
public int getResolvedAutowireMode() {
if (this.autowireMode == AUTOWIRE_AUTODETECT) {
// Work out whether to apply setter autowiring or constructor autowiring.
// If it has a no-arg constructor it's deemed to be setter autowiring,
// otherwise we'll try constructor autowiring.
Constructor<?>[] constructors = getBeanClass().getConstructors();
for (Constructor<?> constructor : constructors) {
if (constructor.getParameterCount() == 0) {
return AUTOWIRE_BY_TYPE;
}
}
return AUTOWIRE_CONSTRUCTOR;
}
else {
return this.autowireMode;
}
}

resolveDependency 详情

resolveDependency 方法主要被 autowireByType 调用 , 此外在 AutowiredAnnotationBeanPostProcessor 中也有所涉及

作用 : 针对此工厂中定义的bean解析指定的依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}

PS:M173_30_5 InstantiationAwareBeanPostProcessor 结构

结构 : BeanPostProcessor的子接口

功能 : 添加了一个实例化之前的回调函数和一个实例化之后但在显式属性设置或自动装配发生之前的回调函数

常用 : 创建带有特殊target Source的代理(池化目标、延迟初始化目标等) , 实现额外的注入策略

特点 : 这个接口中 , 除了BeanPostProcessor默认的 postProcessBeforeInstantiation , postProcessAfterInstantiation 的方法外 , 还有一个额外的方法 postProcessProperties , postProcessPropertyValues

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
java复制代码// public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor
// 此处通过 hasInstantiationAwareBeanPostProcessors 判断是否继承相关的对象


// ===========================================
// Pro 1 : 配置的起点
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
// PS : hasInstantiationAwareBeanPostProcessors 归属于父类 AbstractBeanFactory
private volatile boolean hasInstantiationAwareBeanPostProcessors;
//.........
}


// ===========================================
// Pro 2 : 属性的设置
// 可以看到 , 这里通过 addBeanPostProcessor 中配置开关
public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
this.beanPostProcessors.remove(beanPostProcessor);
if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) {
this.hasInstantiationAwareBeanPostProcessors = true;
}
if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) {
this.hasDestructionAwareBeanPostProcessors = true;
}
this.beanPostProcessors.add(beanPostProcessor);
}

// ===========================================
// Pro 3 : 看一下 InstantiationAwareBeanPostProcessor 的调用时
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

// Step 3 : BeanPostProcessors 处理 , 主要是 postProcessAfterInstantiation 前置处理
// PS : 这里只有继承了 InstantiationAwareBeanPostProcessor 的类才能进行 postProcessAfterInstantiation
// ..................
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
}

// ...................
// Step 5 : BeanPostProcessor 处理 , 此处主要是 InstantiationAwareBeanPostProcessor 的处理
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 实现InstantiationAwareBeanPostProcessor 特有的 postProcessProperties
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
// 实现InstantiationAwareBeanPostProcessor 特有的 postProcessPropertyValues
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}


// ===========================================
// Pro 4 : postProcessProperties 和 postProcessPropertyValues 的作用

postProcessProperties :
- 在工厂将给定的属性值应用到给定bean之前对它们进行后处理,不需要任何属性描述符.

postProcessPropertyValues :
- 在工厂将给定的属性值应用到给定bean之前对它们进行后处理。
- 允许替换要应用的属性值,通常是通过基于原始的PropertyValues创建一个新的MutablePropertyValues实例,添加或删除特定的值

InstantiationAwareBeanPostProcessor.png

2.3 PropertyValue 和 applyPropertyValues 的作用

整个流程中 , 反复出现了一个属性 PropertyValue , 我们来看一下其中的前世今生

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
java复制代码// M173_11 源代码
// TODO
C173- AbstractAutowireCapableBeanFactory
M173_15- applyPropertyValues : 属性的注入操作
?- 之前的操作只是完成了所有注入属性的获取,将获取的属性封装在 PropertyValues 的实例对象 pvs 中
?- 此处将属性应用到已经实例化的 bean 中
1- BeanWrapperImpl.setSecurityContext
?- 设置 BeanWrapperImpl 的 SecurityContext 属性
2- 准备属性
- MutablePropertyValues mpvs
- List<PropertyValue> original
3- 获取属性集合
- original = mpvs.getPropertyValueList() : mpvs 就是 pvs , 只不过转换过
/ original = Arrays.asList(pvs.getPropertyValues())
// 此处有个中间节点 : 如果已经设置完成 , 则直接完成配置 , 并且 return
4- 准备属性
- TypeConverter converter = getCustomTypeConverter()
- new BeanDefinitionValueResolver(this, beanName, mbd, converter) : 用于解析未被解析的对象
- List<PropertyValue> deepCopy = new ArrayList<>(original.size())
5- For 循环 List<PropertyValue> original , 对 PropertyValue 进行转换
?- 属性是不能直接设置进去的 , 需要通过处理再设置
- 判断属性值是否转换 , 非 MutablePropertyValues 类型会直接使用原始类型
- 否则先获取 propertyName , originalValue
// 注意 , 此处进行了 Autowired 处理
- 如果是 Autowired 类型
- Method writeMethod = bw.getPropertyDescriptor(propertyName).getWriteMethod()
?- 如果是 Autowired 属性 , 获取包装对象的特定属性的属性描述符后再获取应用于写入属性值的方法
- new DependencyDescriptor(new MethodParameter(writeMethod, 0), true)
?- 为方法或构造函数参数创建新的描述符
// 注意 , 这里开始进行属性转换 , 分为2步
5.1- convertedValue = valueResolver.resolveValueIfNecessary
?- 给定PropertyValue,返回一个值,在必要时解析对工厂中其他bean的任何引用
5.2- convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter)
?- 如果可写 , 且不是给定的属性路径是否指示索引或嵌套属性
- setConvertedValue + 添加到 4步 List<PropertyValue> deepCopy 集合中
6- 注入核心 : 为属性设置属性值 --  bw.setPropertyValues(mpvs);

M173_15 applyPropertyValues 源代码

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
89
90
91
92
93
94
95
96
97
98
java复制代码protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
if (pvs.isEmpty()) {
return;
}
// Step 1 : 设置 BeanWrapperImpl 的 SecurityContext 属性
if (System.getSecurityManager() != null && bw instanceof BeanWrapperImpl) {
((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext());
}

// Step 2 : 属性准备
MutablePropertyValues mpvs = null;
List<PropertyValue> original;

// Step 3 : 获取属性集合
if (pvs instanceof MutablePropertyValues) {
mpvs = (MutablePropertyValues) pvs;
// 已转换直接返回
if (mpvs.isConverted()) {
try {
bw.setPropertyValues(mpvs);
return;
}
catch (BeansException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Error setting property values", ex);
}
}
original = mpvs.getPropertyValueList();
}
else {
original = Arrays.asList(pvs.getPropertyValues());
}

// Step 4 : 属性准备
TypeConverter converter = getCustomTypeConverter();
if (converter == null) {
converter = bw;
}
BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter);

// Create a deep copy, resolving any references for values.
List<PropertyValue> deepCopy = new ArrayList<>(original.size());
boolean resolveNecessary = false;
// Step 5 :属性循环转换
for (PropertyValue pv : original) {
if (pv.isConverted()) {
deepCopy.add(pv);
}
else {
String propertyName = pv.getName();
Object originalValue = pv.getValue();
if (originalValue == AutowiredPropertyMarker.INSTANCE) {
Method writeMethod = bw.getPropertyDescriptor(propertyName).getWriteMethod();
if (writeMethod == null) {
throw new IllegalArgumentException("Autowire marker for property without write method: " + pv);
}
originalValue = new DependencyDescriptor(new MethodParameter(writeMethod, 0), true);
}
Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
Object convertedValue = resolvedValue;
boolean convertible = bw.isWritableProperty(propertyName) &&
!PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);
if (convertible) {
convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter);
}
// Possibly store converted value in merged bean definition,
// in order to avoid re-conversion for every created bean instance.
if (resolvedValue == originalValue) {
if (convertible) {
pv.setConvertedValue(convertedValue);
}
deepCopy.add(pv);
}
else if (convertible && originalValue instanceof TypedStringValue &&
!((TypedStringValue) originalValue).isDynamic() &&
!(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) {
pv.setConvertedValue(convertedValue);
deepCopy.add(pv);
}
else {
resolveNecessary = true;
deepCopy.add(new PropertyValue(pv, convertedValue));
}
}
}
if (mpvs != null && !resolveNecessary) {
mpvs.setConverted();
}

// Step 6 : 注入核心
try {
bw.setPropertyValues(new MutablePropertyValues(deepCopy));
}
catch (BeansException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Error setting property values", ex);
}
}

总结

看起来挺长的 , 但是回头看了死磕系列感觉还有很多地方自己还没有看懂 , 后面有时间把这个坑填上

太晚了 , 比不过卷王们 ,洗洗睡了

附录

附录一 : M173_30 populateBean 核心代码

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
java复制代码protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
// BeanWrapper 空实例的处理 : 实例化之前要对空实例进行判断和处理
if (bw == null) {
// 如果 BeanWrapper 为空 , 且mbd有值 ,无法对空实例初始化 , 抛出 BeanCreationException
if (mbd.hasPropertyValues()) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
}
else {
// 如果 BeanWrapper 为空 , 且mbd无属性 , 直接返回
return;
}
}

// isSynthetic : 返回此bean定义是否是“合成的”,即不是由应用程序本身定义的
// hasInstantiationAwareBeanPostProcessors : bean 持有 InstantiationAwareBeanPostProcessor
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
// 循环所有的
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 此处只会处理 InstantiationAwareBeanPostProcessor
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
}

// 获取bean 的属性值
PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

int resolvedAutowireMode = mbd.getResolvedAutowireMode();
// 判断是否有自动注入的注解 -> 自动注入属性
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
// 将 PropertyValues 封装成 MutablePropertyValues 对象 , 该对象允许对属性进行简单操作 -> PS:M173_30_3
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// 通过 mbd.getResolvedAutowireMode() 区别是名称注入还是类型注入 -> 同步不同的类型注入 -> PS:M173_30_4
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}

if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

// 判断是否注册了 InstantiationAwareBeanPostProcessors 以及是否需要依赖检查 (needsDepCheck)
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
boolean needsDepCheck = (mbd.getDependencyCheck() != AbstractBeanDefinition.DEPENDENCY_CHECK_NONE);

PropertyDescriptor[] filteredPds = null;
if (hasInstAwareBpps) {
if (pvs == null) {
pvs = mbd.getPropertyValues();
}
//
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}
if (needsDepCheck) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
// 依赖检查,对应 depends-on 属性
checkDependencies(beanName, mbd, filteredPds, pvs);
}

if (pvs != null) {
// 将属性应用到 bean 中
applyPropertyValues(beanName, mbd, bw, pvs);
}
}

附录二 : autowireConstructor 主方法 TODO

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
java复制代码public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
@Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {

BeanWrapperImpl bw = new BeanWrapperImpl();
this.beanFactory.initBeanWrapper(bw);

Constructor<?> constructorToUse = null;
ArgumentsHolder argsHolderToUse = null;
Object[] argsToUse = null;

if (explicitArgs != null) {
argsToUse = explicitArgs;
}
else {
Object[] argsToResolve = null;
synchronized (mbd.constructorArgumentLock) {
constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod;
if (constructorToUse != null && mbd.constructorArgumentsResolved) {
// Found a cached constructor...
argsToUse = mbd.resolvedConstructorArguments;
if (argsToUse == null) {
argsToResolve = mbd.preparedConstructorArguments;
}
}
}
if (argsToResolve != null) {
argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve, true);
}
}

if (constructorToUse == null || argsToUse == null) {
// Take specified constructors, if any.
Constructor<?>[] candidates = chosenCtors;
if (candidates == null) {
Class<?> beanClass = mbd.getBeanClass();
try {
candidates = (mbd.isNonPublicAccessAllowed() ?
beanClass.getDeclaredConstructors() : beanClass.getConstructors());
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
}

if (candidates.length == 1 && explicitArgs == null && !mbd.hasConstructorArgumentValues()) {
Constructor<?> uniqueCandidate = candidates[0];
if (uniqueCandidate.getParameterCount() == 0) {
synchronized (mbd.constructorArgumentLock) {
mbd.resolvedConstructorOrFactoryMethod = uniqueCandidate;
mbd.constructorArgumentsResolved = true;
mbd.resolvedConstructorArguments = EMPTY_ARGS;
}
bw.setBeanInstance(instantiate(beanName, mbd, uniqueCandidate, EMPTY_ARGS));
return bw;
}
}

// Need to resolve the constructor.
boolean autowiring = (chosenCtors != null ||
mbd.getResolvedAutowireMode() == AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR);
ConstructorArgumentValues resolvedValues = null;

int minNrOfArgs;
if (explicitArgs != null) {
minNrOfArgs = explicitArgs.length;
}
else {
ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
resolvedValues = new ConstructorArgumentValues();
minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
}

AutowireUtils.sortConstructors(candidates);
int minTypeDiffWeight = Integer.MAX_VALUE;
Set<Constructor<?>> ambiguousConstructors = null;
LinkedList<UnsatisfiedDependencyException> causes = null;

for (Constructor<?> candidate : candidates) {

int parameterCount = candidate.getParameterCount();

if (constructorToUse != null && argsToUse != null && argsToUse.length > parameterCount) {
// Already found greedy constructor that can be satisfied ->
// do not look any further, there are only less greedy constructors left.
break;
}
if (parameterCount < minNrOfArgs) {
continue;
}

ArgumentsHolder argsHolder;
Class<?>[] paramTypes = candidate.getParameterTypes();
if (resolvedValues != null) {
try {
String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, parameterCount);
if (paramNames == null) {
ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
if (pnd != null) {
paramNames = pnd.getParameterNames(candidate);
}
}
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
}
catch (UnsatisfiedDependencyException ex) {
// Swallow and try next constructor.
if (causes == null) {
causes = new LinkedList<>();
}
causes.add(ex);
continue;
}
}
else {
// Explicit arguments given -> arguments length must match exactly.
if (parameterCount != explicitArgs.length) {
continue;
}
argsHolder = new ArgumentsHolder(explicitArgs);
}

int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
// Choose this constructor if it represents the closest match.
if (typeDiffWeight < minTypeDiffWeight) {
constructorToUse = candidate;
argsHolderToUse = argsHolder;
argsToUse = argsHolder.arguments;
minTypeDiffWeight = typeDiffWeight;
ambiguousConstructors = null;
}
else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) {
if (ambiguousConstructors == null) {
ambiguousConstructors = new LinkedHashSet<>();
ambiguousConstructors.add(constructorToUse);
}
ambiguousConstructors.add(candidate);
}
}

if (constructorToUse == null) {
if (causes != null) {
UnsatisfiedDependencyException ex = causes.removeLast();
for (Exception cause : causes) {
this.beanFactory.onSuppressedException(cause);
}
throw ex;
}
throw new BeanCreationException(...);
}
else if (ambiguousConstructors != null && !mbd.isLenientConstructorResolution()) {
throw new BeanCreationException(...);
}

if (explicitArgs == null && argsHolderToUse != null) {
argsHolderToUse.storeCache(mbd, constructorToUse);
}
}

Assert.state(argsToUse != null, "Unresolved constructor arguments");
bw.setBeanInstance(instantiate(beanName, mbd, constructorToUse, argsToUse));
return bw;
}

附录三 : @Value 的注入时机

1
2
java复制代码
TODO

本文转载自: 掘金

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

Gradle 构建工具

发表于 2021-05-18

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

Gradle 作为官方主推的构建系统,目前已经深度应用于 Android 的多个技术体系中,例如组件化开发、产物构建、单元测试等。可见,要成为 Android 高级工程师 Gradle 是必须掌握的知识点。

本文是 Gradle 构建工具系列的第 3 篇文章,完整文章目录请移步到文章末尾~


前言

  • 如今,项目开发已经不再是单兵作战的时代,而往往是多团队、多组件协同开发。此时,我们会发布组件 & 管理组件的技巧;
  • 在这篇文章里,我将带你理解组件的基本概念,以及组件发布 & 快照预览 & 依赖切换的实战应用经验。如果能帮上忙,请务必点赞加关注,这对我真的非常重要;
  • 本文相关代码可以从 DemoHall·MavenPulish 下载查看。

目录


  1. 概念剖析

1.1 什么是 POM?

POM(Project Object Model)指项目对象模型,用于描述项目构件的基本信息。一个有效的 POM 节点中主要包含一下信息:

配置 描述 举例(’com.github.bumptech.glide:glide:4.11.0’)
groupId 组织 / 公司的名称 com.github.bumptech.glide
artifactId([ˈɑːtɪfækt]) 组件的名称 glide
version 组件的版本 4.11.0
packaging 打包的格式 aar

1.2 什么是仓库(repository)?

在项目中,我们会需要依赖各种各样的二方库或三方库,这些依赖一定会存放在某个位置(Place),这个 “位置” 就叫做仓库。使用仓库可以帮助我们管理项目构件,例如 jar、aar 等等。

主流的构建工具都有三个层次的仓库概念:

  • 1、本地仓库: 无论使用 Linux 还是 Window,计算机中会有一个目录用来存放从中央仓库或远程仓库下载的依赖文件;
  • 2、中央仓库: 开源社区提供的仓库,是绝大多数开源库的存放位置。比如 Maven 社区的中央仓库 Maven Central;
  • 3、私有仓库: 公司或组织的自定义仓库,可以理解为二方库的存放位置。

构建时搜索依赖的顺序如下:

  • 1、在本地仓库搜索,如果搜索不到,执行步骤 2;
  • 2、在中央仓库和私有仓库中搜索,搜索顺序按照repositories中声明的顺序依次查找。如果找到,则下载依赖文件到本地仓库,否则执行步骤 3;
  • 3、如果最终找不到依赖项,则抛出错误 “无法找到依赖项”。

如何在项目中声明仓库:

Winodws 下 Gradle 默认的本地仓库目录:C:/Users/Administrator/.gradle/caches/modules-2/files-2.1而 Mac OS 下是/User/用户名/.gradle/caches/modules-2/files-2.1。Gradle 不会默认执行远程仓库和中央仓库,需要在项目级或模块级 build.gradle 文件中声明。例如:
0.

项目级别 build.gradle

1
2
3
4
5
6
7
8
9
10
css复制代码buildscript {
repositories {
[Gradle 插件的仓库]
}
}
allprojects {
repositories {
[项目中所有模块依赖的仓库]
}
}

模块级别 build.gradle

1
2
3
css复制代码repositories{
[当前模块依赖的仓库]
}

Gradle 支持多种类型的仓库,例如 Maven、ivy、flatDir。其中 flatDir 一般用于指定本地 aar 文件的地址。更多分析在 第 4.2 节。

1
2
3
4
5
bash复制代码repositories{
maven { url '...' }
ivy { url '...' }
flatDir { dirs '...' }
}

Gradle 内置了一些常用中央仓库的路径,可以直接通过函数获取。例如:

1
2
3
scss复制代码google()        // https://dl.google.com/dl/android/maven2/
mavenCentral() // https://repo.maven.apache.org/maven2/
jCenter() // https://jcenter.bintray.com/

有时候,直接访问中央仓库的速度太慢,此时可以尝试替换为国内大厂的中央仓库镜像。例如:

1
2
3
rust复制代码maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }

1.3 什么是 Gradle 插件?

「Gradle 插件」和「Gradle」这两个概念是比较容易混淆的。Gradle 是构建工具,相当于一个构建环境;而 Gradle 插件本质上就是具体的构建任务,我们将一个构建任务模块化抽离出来,提供给其他项目复用,就是一个 Gradle 插件。例如:

  • com.android.application:Android 应用插件
  • com.android.library:Android 模块插件
  • kotlin-android:Android Kotlin 插件
  • maven & maven-pulish:Maven 插件

1.4 快照(SNAPSHOT)有什么用?

快照是一种特殊的版本,与常规版本最大的不同是:快照版本每次构建时都会在远程仓库中检查最新的快照。

  • 快照版本:1.0.0-SNAPSHOT
  • 常规版本:1.0.0

为什么会有这种设计呢(牺牲编译时间)?因为在大型软件项目中,往往是多个团队(或多个同学)协同开发不同模块,例如 A 模块依赖 B 模块,两个模块并行开发。如果模块 B 不使用快照版本(例如版本为 1.0.0),那么当 B 模块在开发阶段需要更新,A 模块就无法接收到更新。因为 A 模块本地仓库中已经下载了 B 模块的 1.0.0 版本,所以构建时不会重复去下载远程仓库中更新的版本。

直接的解决办法可以清除 A 模块的本地仓库缓存,或者每次 B 模块更新都升级版本,很显然两个办法都不灵活,频繁升级版本也是对版本号的滥用,不利于版本管理。而如果模块 B 使用快照版本(1.0.0-SNAPSHOT),A 模块每次构建都会去检查远程仓库是否有 B 模块的新快照,就可以保证一直依赖 B 模块的最新版本。

总的来说,SNAPSHOT 适合快节奏协同开发阶段,代表着不稳定 & 开发中的版本。常规版本适合于正式发布版本,如果正式版本使用 SNAPSHOT,会导致重复构建正式版本不稳定。


  1. Maven 构建生命周期

我们需要使用 Maven 插件来发布类库,简单理解下 Maven 构建的生命周期,主要分为以下个步骤:

任务 阶段 描述
compile 编译 编译源代码
test 测试 执行单元测试
package 打包 创建发布组件,如 jar、aar
install 安装 安装组件包到本地仓库
deploy / upload 部署 上传组件包到远程仓库


—— 图片引用自网络


  1. 如何发布组件(artifacts)?

在 Gradle 中发布组件,可以使用以下两个 Maven 插件:

  • Maven Plugin(旧版)
  • Maven Publish Plugin

3.1 发布到本地仓库

我们需要使用 Maven 插件的uploadArchives任务,并且需要指定组件的信息。例如:

模块级 build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码plugins {
id 'com.android.library'
id 'kotlin-android'
id 'maven'
}
...
uploadArchives {
repositories {
mavenDeployer {
// 发布地址:直接发布到项目本地路径
repository(url: uri('../repository'))
// 组件信息:com.pengxr.demo:maven:v1.0.0
pom.groupId = "com.pengxr.demo"
pom.artifactId = "maven"
pom.version = "v1.0.0"
}
}
}

执行 Gradle Sync 之后,就可以在 Gradle 窗口该模块的 Tasks 列表中找到名为uploadArchives的任务。执行任务,完成后项目中会新增一个repository目录,里面就是新发布的组件。

注意事项:

  • 1、升级到 Android Stidio 4.2 之后,如果在 Gradle 栏目中找不到 Task 列表,在设置里取消勾选此项即可:
  • 2、无法发布应用模块
1
2
3
4
5
bash复制代码plugins {
id 'com.android.application' // 无法发布应用模块
id 'kotlin-android'
id 'maven'
}

3.2 使用 nexus 搭建私有仓库

发布组件到本地仓库只能单机使用,在实际工作中,我们往往需要将组件发布给其他团队成员使用。此时,可以将组件发布到 局域网私有仓库。最常见的私有仓库管理工具是 Nexus [ˈneksəs]。按照以下步骤搭建环境:

  • 1、下载 Nexus 安装包: 这里以 Mac 环境为例:下载地址;
  • 2、启动 Nexus 服务进程: 进入安装路径/nexus-3.30.1-01/bin,在终端运行命令:
1
2
3
4
5
6
7
bash复制代码./nexus start
./nexus status

输出:nexus is running. 表示启动成功

需要停止服务时,可以执行命令:
./nexus stop
  • 3、浏览器打开 http://127.0.0.1:8081/,进入 Nexus 管理页面

  • 4、点击右上角 Sign in 登录: 默认账号名是 admin,首次登录会弹窗提示密码的存储位置(根据指示到相应路径下的文件中找到密码复制粘贴过来),登录成功后界面如下:

这个列表包含了所有的 Nexus 仓库,点击 “Copy” 按钮,可以复制仓库的 URL 地址。其中两个仓库比较常用:

maven-release:策略为 Release 的宿主类型仓库,用于部署内部组件的发布版本;

maven-snapshots:策略为 Shapshot 的宿主类型仓库,用于部署内部组件的快照版本。

类型(Type):group(仓库组)、hosted(宿主)、proxy(代理)和 virtual(虚拟);
格式(Format):maven1、maven2、nuget

  • 5、发布到指定仓库: 在模块级 build.gradle 中增加配置:

模块级 build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
php复制代码apply plugin: 'maven' // Maven 插件
...
uploadArchives {
repositories {
mavenDeployer {
// url:仓库路径
// userName:账号名
// password:密码
repository(url: "http://127.0.0.1:8081/repository/maven-releases/"){
authentication(userName: "admin", password: "pengxurui123")
}

pom.groupId = "com.pengxr.demo"
pom.artifactId = "maven"
pom.version = "v1.0.0"
}
}
}

执行任务,发布成功后可以在 nexus 管理平台上看到新发布的类库:

  • 6、依赖类库: 在项目级 build.gradle 声明远程仓库,在模块级 build.gradle 中依赖类库。

项目级 build.gradle

1
2
3
4
5
6
7
scss复制代码allprojects {
repositories {
google()
mavenCentral()
maven { url "http://127.0.0.1:8081/repository/maven-releases/" }
}
}

模块级 build.gradle

1
2
3
4
arduino复制代码dependencies {
...
implementation 'com.github.pengxurui:MavenPuhlish:v1.0.4'
}

提示: 当然了,实际项目中 nexus 不可能配置在本机上,而是会配置在局域网服务器中。

3.3 发布到 Github 仓库

如果你需要将开源,那么就需要发布到公共仓库,这一节介绍发布到 Github 的步骤:

  • 1、依赖 Github Maven 插件: 在项目级 build.gradle 中添加插件依赖:

项目级 build.gradle

1
2
3
4
arduino复制代码dependencies {
...
classpath "com.github.dcendents:android-maven-gradle-plugin:1.5" // // GitHub Maven 插件
}
  • 2、应用 Github Maven 插件: 在发布模块的 build.gradle 中应用插件:

模块级 build.gradle

1
arduino复制代码apply plugin: 'com.github.dcendents.android-maven' // GitHub Maven 插件
  • 3、声明 group: 同时在发布模块的 build.gradle 中声明组件的 groupId:

模块级 build.gradle

1
2
csharp复制代码apply plugin: 'com.github.dcendents.android-maven' // GitHub Maven 插件
group = 'com.github.pengxurui' // github 的用户名
  • 4、将项目 push 到 Github
  • 5、在 Github 上创建 Release Tag(在本地创建 Tag 再推到 Gtihub 也一样):

  • 6、将项目上传到 JitPack: 打开 jitpack.io/, 将项目链接复制到输入栏,点击 Look up,等待编译完成。到这里就完成发布了。

  • 7、依赖类库: 在项目级 build.gradle 声明远程仓库,在模块级 build.gradle 中依赖类库。

项目级 build.gradle

1
2
3
4
5
6
7
scss复制代码allprojects {
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

模块级 build.gradle

1
2
3
4
arduino复制代码dependencies {
...
implementation 'com.github.pengxurui:MavenPuhlish:v1.0.4'
}

踩坑记录:

  • 模块 build.gradle 中应用了 Github Maven 插件后,要去掉 uploadArchives 任务,否则构建会报错。
1
2
vbnet复制代码Exception is:
java.lang.IllegalAccessError: tried to access method org.gradle.api.internal.artifacts.DefaultModuleVersionIdentifier.<init>(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V from class org.gradle.api.plugins.AndroidMavenPlugin$8

3.4 指定发布二进制文件

使用新版 Maven 插件,可以直接以指定二进制文件的方式发布组件。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码apply plugin: 'maven-publish'

publishing {
publications {
[任务名](MavenPublication) {
groupId MAVEN_GROUP_ID
artifactId MAVEN_ARTIFACTID
version MAVEN_VERSION
artifact([文件路径])
}
}
repositories {
maven {
// 发布仓库路径
url MAVEN_RELEASE_URL
}
}
}

  1. 实战应用

4.1 封装通用发布脚本

随着项目组件化程度加深,越来越多组件需要发布到 Maven 仓库,此时就很有必要将 Maven 发布能力封装为一个通用脚本,步骤如下:

  • 步骤 1:封装发布脚本:

maven.gradle

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
javascript复制代码apply plugin: 'maven'

uploadArchives {
repositories {
mavenDeployer {
// 是否快照版本
def isSnapShot = Boolean.valueOf(MAVEN_IS_SNAPSHOT)
def versionName = MAVEN_VERSION
if (isSnapShot) {
versionName += "-SNAPSHOT"
}
// 组件信息
pom.groupId = MAVEN_GROUP_ID
pom.artifactId = MAVEN_ARTIFACTID
pom.version = versionName

// 快照仓库路径
snapshotRepository(url: uri(MAVEN_SNAPSHOT_URL)) {
authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME)
}
// 发布仓库路径
repository(url: uri(MAVEN_RELEASE_URL)) {
authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME)
}

println("###################################"
+ "\nuploadArchives = " + pom.groupId + ":" + pom.artifactId + ":" + pom.version + "." + pom.packaging
+ "\nrepository =" + (isSnapshot ? MAVEN_SNAPSHOT_URL : MAVEN_RELEASE_URL)
+ "\n###################################"
)
}
}
}

这段脚本会读取 MAVEN_IS_SNAPSHOT 配置参数,如果为 true,会在版本号后追加 -SNAPSHOT 后缀,表示快照版本。随后声明了两个仓库:repository(…) 声明的是 Release 仓库地址,而 snapshotRepository(…) 声明的是快照仓库地址。Maven 会自动将版本号带 -SNAPSHOT 后缀的组件发布到 snapshotRepository(…) 仓库中,这样就 自动将正式版本和快照版本分发的不同仓库中。

当然了,不用 snapshotRepository(…) 也有办法实现:

1
2
3
4
css复制代码def url = isSnapShot ? MAVEN_SNAPSHOT_URL : MAVEN_RELEASE_URL
repository(url: url) {
authentication(userName: MAVEN_USERNAME, password: MAVEN_USERNAME)
}
  • 步骤 2:声明项目级配置参数:

项目级 gradle.properties

1
2
3
4
5
6
7
ini复制代码MAVEN_SNAPSHOT_URL = /Users/pengxurui/workspace/public/DemoHall/snapshotRepository
MAVEN_RELEASE_URL = /Users/pengxurui/workspace/public/DemoHall/releaseRepository
MAVEN_USERNAME =
MAVEN_PASSWORD =
MAVEN_IS_SNAPSHOT = true
MAVEN_GROUP_ID = com.pengxr.demo
...
参数 描述
MAVEN_SNAPSHOT_URL 快照仓库地址
MAVEN_RELEASE_URL 发布仓库地址
MAVEN_USERNAME 仓库账号
MAVEN_PASSWORD 仓库密码
MAVEN_IS_SNAPSHOT 是否快照版本
MAVEN_GROUP_ID 组织 / 公司的名称
MAVEN_ARTIFACTID 组件的名称(在发布模块配置)
MAVEN_VERSION 组件的版本(在发布模块配置)
  • 步骤 3:在发布模块应用脚本

模块级 build.gradle

1
2
csharp复制代码apply from: '../maven.gradle'
...
  • 步骤 4:在发布模块配置参数 (模块级配置参数会覆盖项目级配置参数)

模块级 gradle.properties

1
2
3
4
ini复制代码MAVEN_ARTIFACTID = maven
MAVEN_VERSION = v1.0.0
MAVEN_IS_SNAPSHOT = true
...

完成以上步骤并 Sync 后,就可以在 Gradle 窗口中该模块下找到 uploadArchives 任务,执行发布:

1
2
3
4
5
6
7
8
9
10
11
12
13
ruby复制代码输出:

Executing tasks: [uploadArchives] in project /Users/pengxurui/workspace/public/DemoHall/MavenPublish/lib

> Configure project :lib
###################################
uploadArchives = com.pengxr.demo:maven:v1.0.0-SNAPSHOT.jar
repository =/Users/pengxurui/workspace/public/DemoHall/snapshotRepository
###################################

> Task :lib:preBuild UP-TO-DATE

...
  • 步骤 5:依赖组件: 在项目级 build.gradle 中声明依赖仓库,在模块级 build.gradle 中声明依赖:

项目级 build.gradle

1
2
3
4
5
6
7
markdown复制代码allprojects {
repositories {
maven { url MAVEN_RELEASE_URL }
maven { url MAVEN_SNAPSHOT_URL }
...
}
}

模块级 build.gradle

1
2
3
arduino复制代码dependencies {
implementation "com.pengxr.demo:maven:v1.0.0+"
}

其中,版本号 v1.0.0+ 中的 “+” 号表示依赖最大的版本号,优先正式版本。比如远程仓库中存在 v1.0.0,v1.0.0.1,v1.0.0.1-SNAPSHOT 三个类库,那么 v1.0.0+ 依赖的是其 v1.0.0.1。

+ 号和 -SNAPSHOT 的区别?

  • 号影响类库版本的选择,而 -SNAPSHOT 影响是否向远程仓库更新最新版本。

完整代码和演示工程你可以直接下载查看: MavenPublish 下载路径。Demo 里配置的仓库都为本地仓库,在实际项目中,你需要替换为你公司内的私有仓库。

4.2 引用本地 aar 包

有时候,我们直接依赖第三方或第二方提供的 aar 文件。例如:

1
2
3
4
5
6
lua复制代码- aarlib
\ libs
- lib-debug-aar
- build.gradle // api(name: 'lib-debug', ext: 'aar')

输出:Unable to resolve dependency for ':aarlib@debugUnitTest/compileClasspath': Could not resolve :lib-debug.

但是,这样并不能成功依赖。你需要 build.gradle 文件中声明 aar 的 Flat Directory 仓库地址。你可以放在 android{} 节点内,或者直接放在根节点,效果是一样的。例如:

aarlib 模块 build.gradle

1
2
3
4
5
6
7
8
9
10
php复制代码dependencies {
...
api(name: 'lib-debug', ext: 'aar')
}

repositories {
flatDir {
dirs "libs"
}
}

现在你就可以成功依赖了。但如果存在另一个依赖 aarlib 的模块,而这个模块又需要依赖 lib-debug.aar,还是会出依赖不到的问题:

1
2
3
4
5
6
7
markdown复制代码- app
- build.gradle // implementation project(':aarlib')
|
- aarlib
\ libs
- lib-debug-aar
- build.gradle // api(name: 'lib-debug', ext: 'aar')

此时,你同样需要在 app 模块里声明 aar 的 Flat Directory 仓库地址。

app 模块 build.gradle

1
2
3
4
5
6
7
8
9
10
java复制代码dependencies {
...
implementation project(':aarlib')
}

repositories {
flatDir {
dirs project(':aarlib').file('libs')
}
}

4.3 引用本地 aar 包(优化)

如果项目组件结构比较简单,第 4.2 节的方法就足够应对本地引用 aar 的问题。否则还是会遇到一些麻烦的,你需要在每个模块的 build.gradle 中都声明 repositories.flatDir{},有办法优化吗?

  • 方法 1:直接依赖改为间接依赖: 新建模块封装 aar,对外部提供外观 API
  • 方法 2:统一将 aar 文件放置在一个文件夹,并在项目级 build.gradle 中声明仓库地址:

项目级 build.gradle

1
2
3
4
5
6
7
scss复制代码allprojects {
repositories {
google()
mavenCentral()
flatDir { dirs project(':aarlib').file('libs') } // 文件夹要放在某个 module 内
}
}

模块级 build.gradle

1
2
less复制代码api(name: 'lib-debug', ext: 'aar') // 允许间接依赖 aar
implementation(name: 'lib-debug', ext: 'aar') // 不允许间接依赖 aar
  • 方法 3:二次打包 aar: 以上方法在单工程项目下表现良好,但在如果你们的项目包括多个工程,那还是有点麻烦的,有办法优化吗?你可以对 aar 文件二次打包,并发布到 Maven 仓库,这样你就不需要声明 Flat 本地仓库。
1
2
3
4
5
markdown复制代码- aarpacker
\ libs
- lib.aar
- lib2.aar
- build.gradle

aarpacker build.gradle

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
javascript复制代码apply plugin: 'maven-publish'

def libPath = project.getProjectDir().getAbsolutePath()

publishing {
publications {
lib1(MavenPublication) {
groupId MAVEN_GROUP_ID
artifactId "lib"
version "v1.0.0"
artifact(libPath + "/libs/lib.aar")
}

lib2(MavenPublication) {
groupId MAVEN_GROUP_ID
artifactId "lib2"
version "v1.0.0"
artifact(libPath + "/libs/lib2.aar")
}
}
repositories {
maven {
// 发布仓库路径
url MAVEN_RELEASE_URL

// 本地仓库地址不适用账号密码
// > Failed to publish publication 'maven' to repository 'maven'
// > Authentication scheme 'all'(Authentication) is not supported by protocol 'file'
// credentials(PasswordCredentials) {
// username = MAVEN_USERNAME
// password = MAVEN_PASSWORD
// }
}
}
}

4.4 合并引用 Library module

当 Library module 编译完成后,最终会生成 aar 文件,但其中并不包含 compile/implementation 引用的其他 Library module 的代码或资源。然而在组件化开发中,有的时候我们希望发布的 aar 文件中需要包含 Library module 的内容,需要怎么做呢?有两种方法:

  • 方法 1:Python 脚本合并: 编写 Python 脚本,实现将全部相关的资源解压到一个 Library module 中,再生成 aar 文件
  • 方法 2:Gradle 编译任务: 调整 Gradle 编译任务,实现将全部相关的资源同时打包到 aar 中。

4.5 宿主工程调试类库

增加开关字段:

1
2
3
4
5
6
7
css复制代码dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
if (useLocalLib.toBoolean())
implementation project(":lib")
else
implementation 'com.pengxr.demo:maven:v1.0.0+'
}

参考资料

  • Maven、Maven Plugin、Maven Publish Plugin、JitPack.io —— 官方文档
  • Maven 教程 —— 菜鸟教程
  • Android Studio 将项目发布到 Maven 仓库(3 种方式最新最全) —— open-Xu 著
  • 使用 Gradle 发布工件到 Maven 仓库 —— 精装机械师 著
  • Android 组件化实战之利用 Maven 优雅地调试 SDK —— 奇舞团技术团队 著
  • 51 信用卡 Android 架构演进实践 —— 51 技术团队 著
  • Android 组件化架构(第 6 章)—— 苍王著

推荐阅读

Gradle 构建工具完整目录如下(2023/07/12 更新):

  • #1 为什么说 Gradle 是 Android 进阶绕不去的坎
  • #2 手把手带你自定义 Gradle 插件
  • #3 Maven 发布插件使用攻略(以 Nexus / Jitpack 为例)
  • #4 来开源吧!发布开源组件到 MavenCentral 仓库超详细攻略
  • #5 又冲突了!如何理解依赖冲突与版本决议?

整理中…

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

JAVA设计模式之策略模式

发表于 2021-05-18

前言

本系列文章参考《设计模式之禅》、菜鸟教程网以及网上的一些文章进行归纳总结,并结合自身开发应用。设计模式的命名以《设计模式之禅》为准。

设计模式仅是一些开发者在日常开发中的编码技巧的汇总并非固定不变,可根据项目业务实际情况进行扩展和应用,切不可被这个束缚。更不要为了使用而使用,设计模式是一把双刃剑,过度的设计会导致代码的可读性下降,代码的体积增加。

系列文章不会详细介绍设计模式的《七大原则》,也不会对设计模式进行分类。这样只会增加学习和记忆的成本,也会导致使用时的思想固化,总在想这么设计是否符合xx原则,是否是xx设计模式,xx模式是什么类型等等,不是本系列文章的所希望看到的,目标只有一个,结合日常开发分享自身的应用,以提供一种代码优化的思路。

学习然后忘记,也许是一种最好的方式。

就像俗话说的那样:天下本没有路,走的人多了,就变成了路。在我看来,设计模式也一样,它并非是一种定律,而是前辈们总结下来的经验,我们学习并结合实际加以利用,而不是生搬硬套。

定义

官腔:定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

人话:有一个公共的逻辑接口或抽象类,且有一个控制类对实现了公共逻辑接口的类进行控制,在引用时知道该调用哪一个接口的策略类。

应用场景

样例1

为了方便没有了解过设计模式的小白快速了解,这里放一个网上到处都是的计算器模型。

在一个简单的计算器中我们需要关注计算器的运算符即可。我们假定这个计算器非常非常简单,仅支持加减乘除。

先定义一个运算符接口。

1
2
3
4
5
6
7
csharp复制代码public interface IOperator {
/**
* 计算逻辑
* @return 结果
*/
int doOperator(int num1, int num2);
}

代码比较简单,只做整形的计算。

加法实现:

1
2
3
4
5
6
7
java复制代码public class AddOperator implements IOperator{

@Override
public int doOperator(int num1, int num2) {
return num1 + num2;
}
}

乘法实现:

1
2
3
4
5
6
java复制代码public class MultiplyOperator implements IOperator{
@Override
public int doOperator(int num1, int num2) {
return num1 * num2;
}
}

减法和除法就不写了,参考上面即可。

上面这些IOperator的实现即为不同的运算策略。光有策略还不行,我们需要一个控制类,对其进行统一管理。所有的引用通过这个管理类来实现。管理类的实现方式有很多种,千万别局限于网上的一两个demo。

方式一:

通过构造函数将策略(IOperator)注入到管理类属性中,然后直接调用exec执行即可。

demo中相当于构建了一个加法的控制类,然后再执行传参和运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码//控制类
public class StrategyManager_1 {
private final IOperator operator;

public StrategyManager_1(IOperator operator) {
this.operator = operator;
}

public int exec(int num1, int num2){
return operator.doOperator(num1, num2);
}

//引用方式
public static void main(String[] args) {
AddOperator operator = new AddOperator();
StrategyManager_1 manager_1 = new StrategyManager_1(operator);
int exec = manager_1.exec(1, 2);
System.out.println(exec);
}
}

方式二:

还可以使用switch语句,通过输入的运算符进行匹配计算。

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

public static int exec(int num, int num1, String operator) {
switch (operator) {
case "+" :
return new AddOperator().doOperator(num, num1);
case "*" :
return new MultiplyOperator().doOperator(num, num1);
default:
return 0;
}
}

public static void main(String[] args) {
StrategyManager_2.exec(1, 2, "+");
}
}

这种方式不需要使用者去构建策略对象,有点类似于简单的工厂模式,只需要传入一个标识即可,且调用方式更加简单,个人更推荐这种方式。实际开发中也不建议让使用者自己去封装策略对象,即增加了调用者的学习成本,也可能会引起各种意想不到的错误。要考虑后人是在你的基础上开发的,他们不知道也不需要知道你的策略对象,因为策略对象是你的业务的封装,而使用者并不知道。

真实情况下的计算器,难道要用户去创建一个处理加法的策略对象AddOperator吗?显然不可能,用户只关系输入的数字、运算符和结果。

实现的方式可能还有很多,待各位自行发现。

样例2

跟大家分享一下,在一个springboot项目中,如何编写一个策略模式的接口。

最近正好写过一个对外开放的接口,用于接受其他项目同步来的用户信息。但是不同的项目同步来的用户信息的结构和内容是不同的,有些需要进行定制业务开发。

以此背景为例,假设有两个项目(A 和 B)需要同步数据给我。

条件:

1.A项目传入的仅有用户姓名和手机号,B项目传入的仅有身份证和姓名。

2.A项目需要接受完成后同步发送短信,B项目需要同步的时候根据身份证计算年龄和生日。

3.任何一种情况同步前和完成后都需要记录日志。

1.创建入参对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scala复制代码//剥离出来公共的参数
@Data
public class ReceiveBean {
private String name;
/**
* 来源
*/
private int source;
}

//B项目同步来的参数
@EqualsAndHashCode(callSuper = true)
@Data
public class BReceiveBean extends ReceiveBean{
private String idNo;
}

//A项目同步来的参数
@EqualsAndHashCode(callSuper = true)
@Data
public class AReceiveBean extends ReceiveBean{
private String phone;
}

实体bean中使用了lombok相关的注解代替了显示的get/set方法。

在ReceiveBean中有一个关键的source字段用来区别渠道的来源,可用其他的方式代替。

2.定义顶层的策略接口

1
2
3
4
5
6
7
8
9
10
csharp复制代码public interface IReceiveService {

/**
* 接受的参数为json格式的字符串
* 由调用者进行转换
* @param bean 入参
*/
void receive(ReceiveBean bean);

}

使用字符串或Map对象能提供更好的兼容性,也可以在内部进行转换,这里为了方便外部的引用将转换的过程转交给调用者。

3.抽象层面接口实现

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
typescript复制代码@SuppressWarnings("unused")
public abstract class AbstractReceiveService implements IReceiveService{

//可通过覆盖这个方法定制接受之前的处理,替换bean的属性
protected void receivePre(ReceiveBean bean) {
//记录日志
}

//通过覆盖这个方法定制接受之后的处理
//这个可能会不满足,若需要的话可以添加一个map类型的参数,用来保存处理过程中产生数据
protected void receiveAfter(ReceiveBean bean) {
//记录日志
}

//需要被重写,各类型的定制服务
protected abstract void doAction(ReceiveBean bean);

@Override
public void receive(ReceiveBean bean) {
//业务开始处理之前的操作
receivePre(bean);
//业务开始处理
doAction(bean);
//业务处理之后的操作
receiveAfter(bean);
}
}

抽象类内部实现了receive方法,这里要看具体的业务,虽然有定制的服务,但总体逻辑相同的可以这么实现,若差别较大,可在子类中覆盖receive方法,自行实现,否则就默认走相同的流程。

整体还算比较简单,receive方法内进行了参数的转换,并且调用了receivePre和receiveAfter进行业务前后的处理,两个方法内部都做了日志保存,这属于默认操作,也可以通过覆盖这两个方法进行业务和参数的定制,可以影响后续的处理。doAction方法可以酌情处理,我将其作为一个抽象方法,要求子类必须去实现各自的业务处理操作,也可以和receivePre和receiveAfter连个方法一样提供默认实现。

拿其中一个A项目的实现类看一下:

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
java复制代码@Service
public class AReceiveService extends AbstractReceiveService{

@Override
protected void doAction(ReceiveBean bean) {
//保存记录
// 使用的时候需要对bean进行强转
AReceiveBean ab = (AReceiveBean) bean;
}


@Override
protected void receivePre(ReceiveBean bean) {
super.receivePre(bean);
//记录日志的同时进行其他操作
// 使用的时候需要对bean进行强转
AReceiveBean ab = (AReceiveBean) bean;
}

@Override
protected void receiveAfter(ReceiveBean bean) {
super.receiveAfter(bean);
//发送短信
// 使用的时候需要对bean进行强转
AReceiveBean ab = (AReceiveBean) bean;
}
}

内部可选择性的覆盖方法,若非必要的话。

4.控制类

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
typescript复制代码@SuppressWarnings("unused")
public enum ReceiveSourceEnum {
A(1, "aReceiveService") {
@Override
public ReceiveBean coverJson(String json) {
return JSON.parseObject(json, AReceiveBean.class);
}
},
B(2, "bReceiveService") {
@Override
public ReceiveBean coverJson(String json) {
return JSON.parseObject(json, BReceiveBean.class);
}
};

private final int source;

private final String serviceName;

/**
* 在此定制转换成不同类型的bean入参
*
* @param json json格式入参
* @return 结果
*/
public abstract ReceiveBean coverJson(String json);


//获取枚举资源
public static ReceiveSourceEnum getWithSource(int source) {
for (ReceiveSourceEnum sourceEnum : ReceiveSourceEnum.values()) {
if (sourceEnum.getSource() == source) {
return sourceEnum;
}
}
throw new DemoException("source类型不存在!");
}
//获取服务
public static IReceiveService getReceiveService(int source) {
ReceiveSourceEnum sourceEnum = getWithSource(source);
Assert.notNull(sourceEnum, "source is null");
IReceiveService receiveService = (IReceiveService)SpringContextUtil.getBean(sourceEnum.getServiceName());
Assert.notNull(sourceEnum, "service is null");
return receiveService;
}

ReceiveSourceEnum(int source, String serviceName) {
this.source = source;
this.serviceName = serviceName;
}

public int getSource() {
return source;
}

public String getServiceName() {
return serviceName;
}
}

将ReceiveSourceEnum作为控制类,内部提供两个静态的方法getReceiveService和getWithSource,获取服务和枚举类型。

枚举类中定义了一个用于将json格式字符串转换成对象的抽象方法coverJson。因为接受的参数为json格式字符串,这样可以提高接口的扩展性,内部转换成对应的参数实体类方便后续操作。

getReceiveService内部通过服务名称从spring容器中换取bean。这种方式可以静态的调用服务且方法简单。

换取的方法网上有很多,这里只提供一种:

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
typescript复制代码@Component
public class SpringContextUtil implements ApplicationContextAware {

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}

//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}

//通过name获取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}

//通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}

//通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}

}

5.引用方法

使用springboot的单元测试。

注意:

不能使用main函数直接引用,否则会发现SpringContextUtil中的ApplicationContext永远都是null,原因是classloader导致的。

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
typescript复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class TestBean {

@Test
public void testContextUtil() {
ReceiveBean bean = new ReceiveBean();
bean.setName("123");
bean.setSource(1);
String paramStr = JSON.toJSONString(bean);
ReceiveBean receiveBean = coverJsonToBean(paramStr);
ReceiveSourceEnum.getReceiveService(receiveBean.getSource()).receive(receiveBean);
}

private static ReceiveBean coverJsonToBean(String json){
JSONObject jo = JSONObject.parseObject(json);
if (!jo.containsKey("source")) {
throw new DemoException("传入参数缺失:source");
}
//存在强转错误的风险
int source = (int)jo.get("source");
ReceiveSourceEnum sourceEnum = ReceiveSourceEnum.getWithSource(source);
return sourceEnum.coverJson(json);
}
}

不过这样也可以看出来对于使用者来说,调用非常简单。若后续对接了C项目或其他项目,仅需扩展AbstractReceiveService类和ReceiveSourceEnum的类型即可。

这里仅是我的一种思路,一种参考,肯定还有其他更好的实现方式。

UML图

图片来自于菜鸟教程。(偷懒中= =)

小结

最早了解策略模式是为了优化代码中大量的if-else语句,虽然一些情况下可以通过switch语句和enum类去实现,但扩展性并不好,且代码会分散在当前业务类中,当对接方比较少时还可以勉强看。比较多的时候,就会发现,一个入口方法内,整合了大量的定制业务,阅读和维护都时分困难,并且业务类变得时分庞大。加之缺少注释说明,后人维护起来简直是噩梦级的。

策略模式下,将不同的渠道提取公共的方法置于父类中,将定制的业务在各自的子类中实现,使得整体流程时分清晰,且业务类不会无限膨胀。修改和扩展只要不涉及公共部分,可放心变更,不用担心牵一发而动全身,每种渠道的定制业务相对独立。

若对接方完全无法适用公共的部分,也不用担心,大可直接覆盖策略方法,重写业务逻辑,依然可以保证同一个入口。

本文转载自: 掘金

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

IDEA启动项目报错:command line is too

发表于 2021-05-18

最近重装用了社区版的IDEA,可真是不太平,各种问题都跑出来了,虽然今天的问题跟是不是社区版没啥关系。

启动项目时,如下图报错,就是启动不起来(硬是不起来)

image-20210218214456949

解决方法:

方法1

找到启动不起来的项目的配置启动项,编辑,在Configuration标签下找到Shorten command line:

选中第三或第四个:JAR manifest或classpath file。

即可解决,如果没找到说明你就真的是没找到而已,再仔细找找。或者,IDEA版本过低(我这里是IDEA19.2的社区版)

image-20210218213911731

通常我是用方法1,这里还有个方法2,稍微麻烦点,我倒是没用过

方法2

修改项目下 .idea\workspace.xml

找到标签

1
xml复制代码  <component name="PropertiesComponent">

在标签内部加一行

1
xml复制代码    <property name="dynamic.classpath" value="true"/>

其他属性不动,即可

image-20210218214810431

最后,这个问题我没有细究它的发生原因、过程之类的,看报错信息大概是启动指令过长之类的,也是百度后解决的,貌似只有在社区版才会出现此问题。

本文转载自: 掘金

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

SpringCloudRPC远程调用核心原理:代理模式与RP

发表于 2021-05-18

关注公众号“:Java架构师联盟,每日更新技术好文

代理模式与RPC客户端实现类

本节首先介绍客户端RPC远程调用实现类的职责,然后从基础原理讲起,依次介绍代理模式的原理、使用静态代理模式实现RPC客户端类、使用动态代理模式实现RPC客户端类,一步一步地接近Feign RPC的核心原理知识。

客户端RPC远程调用实现类的职责

客户端RPC实现类位于远程调用Java接口和Provider微服务实例之间,承担了以下职责:

(1)拼装REST请求:根据Java接口的参数,拼装目标REST接口的URL。

(2)发送请求和获取结果:通过Java HTTP组件(如HttpClient)调用Provider微服务实例的REST接口,并且获取REST响应。

(3)结果解码:解析REST接口的响应结果,封装成目标POJO对象(Java接口的返回类型)并且返回。

RPC远程调用客户端实现类的职责如图3-1所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-1 RPC远程调用客户端实现类的职责

使用Feign进行RPC远程调用时,对于每一个Java远程调用接口,Feign都会生成一个RPC远程调用客户端实现类,只是对于开发者来说这个实现类是透明的,感觉不到这个实现类的存在。

Feign为DemoClient接口生成的RPC客户端实现类大致如图3-2所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-2 Feign为DemoClient接口生成的RPC客户端实现类参考图

由于看不到Feign的RPC客户端实现类的任何源码,初学者会感觉到很神奇,感觉这就是一个黑盒子。下面从原始的、简单的RPC远程调用客户端实现类开始为大家逐步地揭开Feign的RPC客户端实现类的神秘面纱。

在一点点揭开RPC远程调用客户端实现类的面纱之前,先模拟一个Feign远程调用Java接口,对应demo-provider服务的两个REST接口。

模拟的远程调用Java接口为MockDemoClient,它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码package com.crazymaker.demo.proxy.FeignMock;
...
@RestController(value = TestConstants.DEMO_CLIENT_PATH)
public interface MockDemoClient
{ /**
*远程调用接口的方法,完成REST接口api/demo/hello/v1的远程调用
*REST接口功能:返回hello world
*@return JSON响应实例
*/
@GetMapping(name = "api/demo/hello/v1")
RestOut<JSONObject> hello();
/**
*远程调用接口的方法,完成REST接口api/demo/echo/{0}/v1的远程调用
*REST接口功能:回显输入的信息
*@return echo回显消息JSON响应实例
*/
@GetMapping(name = "api/demo/echo/{0}/v1")
RestOut<JSONObject> echo(String word);
}

接下来层层递进,为大家演示以下3种RPC远程调用客户端:

(1)简单的RPC客户端实现类。

(2)静态代理模式的RPC客户端实现类。

(3)动态代理模式的RPC客户端实现类。

最后的动态代理模式的RPC客户端实现类在实现原理上已经非常接近Feign的RPC客户端实现类。

简单的RPC客户端实现类

简单的RPC客户端实现类的主要工作如下:

(1)组装REST接口URL。

(2)通过HttpClient组件调用REST接口并获得响应结果。

(3)解析REST接口的响应结果,封装成JSON对象,并且返回给调用者。

简单的RPC客户端实现类的参考代码如下:

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
typescript复制代码package com.crazymaker.demo.proxy.basic;
//省略import
@AllArgsConstructor
@Slf4j
class RealRpcDemoClientImpl implements MockDemoClient
{
final String contextPath = TestConstants.DEMO_CLIENT_PATH;
//完成对REST接口api/demo/hello/v1的调用
public RestOut<JSONObject> hello()
{
/**
*远程调用接口的方法,完成demo-provider的REST API远程调用
*REST API功能:返回hello world
*/
String uri = "api/demo/hello/v1";
/**
*组装REST接口URL
*/
String restUrl = contextPath + uri;
log.info("restUrl={}", restUrl);
/**
*通过HttpClient组件调用REST接口
*/
String responseData = null;
try
{
responseData = HttpRequestUtil.simpleGet(restUrl);
} catch (IOException e)
{
e.printStackTrace();
}
/**
*解析REST接口的响应结果,解析成JSON对象并且返回给调用者
*/
RestOut<JSONObject> result = JsonUtil.jsonToPojo(responseData,
new TypeReference<RestOut<JSONObject>>() {});
return result;
}
//完成对REST接口api/demo/echo/{0}/v1的调用
public RestOut<JSONObject> echo(String word)
{
/**
*远程调用接口的方法,完成demo-provider的REST API远程调用
*REST API功能:回显输入的信息
*/
String uri = "api/demo/echo/{0}/v1";
/**
*组装REST接口URL
*/
String restUrl = contextPath + MessageFormat.format(uri, word);
log.info("restUrl={}", restUrl);
/**
*通过HttpClient组件调用REST接口
*/
String responseData = null;
try
{
responseData = HttpRequestUtil.simpleGet(restUrl);
} catch (IOException e)
{
e.printStackTrace();
}
/**
解析
接
的响应结果
解析成
对象
并且返回给调用者 *解析REST接口的响应结果,解析成JSON对象,并且返回给调用者
*/
RestOut<JSONObject> result = JsonUtil.jsonToPojo(responseData,
new TypeReference<RestOut<JSONObject>>() { });
return result;
}
}

以上简单的RPC实现类RealRpcDemoClientImpl的测试用例如下:

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
typescript复制代码package com.crazymaker.demo.proxy.basic;
...
/**
*测试用例
*/
@Slf4j
public class ProxyTester
{
/**
*不用代理,进行简单的远程调用
*/
@Test
public void simpleRPCTest()
{
/**
*简单的RPC调用类
*/
MockDemoClient realObject = new RealRpcDemoClientImpl();
/**
*调用demo-provider的REST接口api/demo/hello/v1
*/
RestOut<JSONObject> result1 = realObject.hello();
log.info("result1={}", result1.toString());
/**
*调用demo-provider的REST接口api/demo/echo/{0}/v1
*/
RestOut<JSONObject> result2 = realObject.echo("回显内容");
log.info("result2={}", result2.toString());
}
}

运行测试用例之前,需要提前启动demo-provider微服务实例,然后将主机名称crazydemo.com通过hosts文件绑定到demo-provider实例所在机器的IP地址(这里为127.0.0.1),并且需要确保两个REST接口/api/demo/hello/v1、/api/demo/echo/{word}/v1可以正常访问。

运行测试用例,部分输出结果如下:

1
2
3
4
ini复制代码[main] INFO c.c.d.p.b.RealRpcDemoClientImpl - restUrl=http://crazydemo.com:7700/demo-provider/ api/demo/hello/v1
[main] INFO c.c.d.proxy.basic.ProxyTester - result1=RestOut{datas={"hello":"world"}, respCode=0, respMsg='操作成功}
[main] INFO c.c.d.p.b.RealRpcDemoClientImpl - restUrl=http://crazydemo.com:7700/demo-provider/ api/demo/echo/回显内容/v1
[main] INFO c.c.d.proxy.basic.ProxyTester - result2=RestOut{datas={"echo":"回显内容"}, respCode=0, respMsg='操作成功}

以上的RPC客户端实现类很简单,但是实际开发中不可能为每一个远程调用Java接口都编写一个RPC客户端实现类。如何自动生成RPC客户端实现类呢?这就需要用到代理模式。接下来为大家介绍简单一点的代理模式实现类——静态代理模式的RPC客户端实现类。

从基础原理讲起:代理模式与RPC客户端实现类

首先来看一下代理模式的基本概念。代理模式的定义:为委托对象提供一种代理,以控制对委托对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个目标对象,而代理对象可以作为目标对象的委托,在客户端和目标对象之间起到中介的作用。

代理模式包含3个角色:抽象角色、委托角色和代理角色,如图3-3所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-3 代理模式角色之间的关系图

(1)抽象角色:通过接口或抽象类的方式声明委托角色所提供的业务方法。

(2)代理角色:实现抽象角色的接口,通过调用委托角色的业务逻辑方法来实现抽象方法,并且可以附加自己的操作。

(3)委托角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。

代理模式分为静态代理和动态代理。

(1)静态代理:在代码编写阶段由工程师提供代理类的源码,再编译成代理类。所谓静态,就是在程序运行前就已经存在代理类的字节码文件,代理类和被委托类的关系在运行前就确定了。

(2)动态代理:在代码编写阶段不用关心具体的代理实现类,而是在运行阶段直接获取具体的代理对象,代理实现类由JDK负责生成。

静态代理模式的实现主要涉及3个组件:(1)抽象接口类(Abstract Subject):该类的主要职责是声明目标类与代理类的共同接口方法。该类既可以是一个抽象类,又可以是一个接口。

(2)真实目标类(Real Subject):该类也称为被委托类或被代理类,该类定义了代理所表示的真实对象,由其执行具体业务逻辑方法,而客户端通过代理类间接地调用真实目标类中定义的方法。

(3)代理类(Proxy Subject):该类也称为委托类或代理类,该类持有一个对真实目标类的引用,在其抽象接口方法的实现中需要调用真实目标类中相应的接口实现方法,以此起到代理的作用。

使用静态代理模式实现RPC远程接口调用大致涉及以下3个类:

(1)一个远程接口,比如前面介绍的模拟远程调用Java接口MockDemoClient。

(2)一个真实被委托类,比如前面介绍的RealRpcDemoClientImpl,负责完成真正的RPC调用。

(3)一个代理类,比如本小节介绍的DemoClientStaticProxy,通过调用真实目标类(委托类)负责完成RPC调用。

通过静态代理模式实现MockDemoClient接口的RPC调用实现类,类之间的关系如图3-4所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-4 静态代理模式的RPC调用UML类图

静态代理模式的RPC实现类DemoClientStaticProxy的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码package com.crazymaker.demo.proxy.basic;
//省略import
@AllArgsConstructor
@Slf4j
class DemoClientStaticProxy implements DemoClient
{
/**
*被代理的真正实例
*/
private MockDemoClient realClient; @Override
public RestOut<JSONObject> hello()
{
log.info("hello方法被调用" );
return realClient.hello();
}
@Override
public RestOut<JSONObject> echo(String word)
{
log.info("echo方法被调用" );
return realClient.echo(word);
}
}

在静态代理类DemoClientStaticProxy的hello()和echo()两个方法中,调用真实委托类实例realClient的两个对应的委托方法,完成对远程REST接口的请求。

以上静态代理类DemoClientStaticProxy的使用代码(测试用例)大致如下:

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
typescript复制代码package com.crazymaker.demo.proxy.basic;
//省略import
/**
*静态代理和动态代理,测试用例
*/
@Slf4j
public class ProxyTester
{
/**
*静态代理测试
*/
@Test
public void staticProxyTest()
{
/**
*被代理的真实RPC调用类
*/
MockDemoClient realObject = new RealRpcDemoClientImpl();
/**
*静态的代理类
*/
DemoClient proxy = new DemoClientStaticProxy(realObject);
RestOut<JSONObject> result1 = proxy.hello();
log.info("result1={}", result1.toString());
RestOut<JSONObject> result2 = proxy.echo("回显内容");
log.info("result2={}", result2.toString());
}
}

运行测试用例前,需要提前启动demo-provider微服务实例,并且需要将主机名称crazydemo.com通过hosts文件绑定到demo-provider实例所在机器的IP地址(这里为127.0.0.1),并且需要确保两个REST接口/api/demo/hello/v1、/api/demo/echo/{word}/v1可以正常访问。

一切准备妥当,运行测试用例,输出如下结果:

1
2
3
4
5
6
css复制代码[main] INFO c.c.d.p.b.DemoClientStaticProxy - hello方法被调用
[main] INFO c.c.d.p.b.RealRpcDemoClientImpl - restUrl= http://crazydemo.com:7700/demo-provider/ api/demo/hello/v1
[main] INFO c.c.d.proxy.basic.ProxyTester - result1=RestOut{datas={"hello":"world"}, respCode=0, respMsg='操作成功}
[main] INFO c.c.d.p.b.DemoClientStaticProxy - echo方法被调用
[main] INFO c.c.d.p.b.RealRpcDemoClientImpl - restUrl=http://crazydemo.com:7700/demo-provider/ api/demo/echo/回显内容/v1
[main] INFO c.c.d.proxy.basic.ProxyTester - result2=RestOut{datas={"echo":"回显内容"}, respCode=0, respMsg='操作成功}

静态代理的RPC实现类看上去是一堆冗余代码,发挥不了什么作用。为什么在这里一定要先介绍静态代理模式的RPC实现类呢?原因有以下两点:

(1)上面的RPC实现类是出于演示目的而做了简化,对委托类并没有做任何扩展。而实际的远程调用代理类会对委托类进行很多扩展,比如远程调用时的负载均衡、熔断、重试等。

(2)上面的RPC实现类是动态代理实现类的学习铺垫。Feign的RPC客户端实现类是一个JDK动态代理类,是在运行过程中动态生成的。大家知道,动态代理的知识对于很多读者来说不是太好理解,所以先介绍一下代理模式和静态代理的基础知识,作为下一步的学习铺垫。

使用动态代理模式实现RPC客户端类

为什么需要动态代理呢?需要从静态代理的缺陷开始介绍。静态代理实现类在编译期就已经写好了,代码清晰可读,缺点也很明显:

(1)手工编写代理实现类会占用时间,如果需要实现代理的类很多,那么代理类一个一个地手工编码根本写不过来。

(2)如果更改了抽象接口,那么还得去维护这些代理类,维护上容易出纰漏。

动态代理与静态代理相反,不需要手工实现代理类,而是由JDK通过反射技术在执行阶段动态生成代理类,所以也叫动态代理。使用的时候可以直接获取动态代理的实例,获取动态代理实例大致需要如下3步:

(1)需要明确代理类和被委托类共同的抽象接口,JDK生成的动态代理类会实现该接口。

(2)构造一个调用处理器对象,该调用处理器要实现InvocationHandler接口,实现其唯一的抽象方法invoke(…)。而InvocationHandler接口由JDK定义,位于java.lang.reflect包中。

(3)通过java.lang.reflect.Proxy类的newProxyInstance(…)方法在运行阶段获取JDK生成的动态代理类的实例。注意,这一步获取的是对象而不是类。该方法需要三个参数,其中的第一个参数为类装载器,第二个参数为抽象接口的class对象,第三个参数为调用处理器对象。

举一个例子,创建抽象接口MockDemoClient的一个动态代理实例,大致的代码如下:

1
2
3
4
5
6
7
8
9
10
ini复制代码//参数1:类装载器
ClassLoader classLoader = ProxyTester.class.getClassLoader();
//参数2:代理类和被委托类共同的抽象接口
Class[] clazz = new Class[]{MockDemoClient.class};
//参数3:动态代理的调用处理器
InvocationHandler invocationHandler = new DemoClientInocationHandler (realObject);
/**
*使用以上3个参数创建JDK动态代理类
*/
MockDemoClient proxy = (MockDemoClient)Proxy.newProxyInstance(classLoader, clazz, invocationHandler);

创建动态代理实例的核心是创建一个JDK调用处理器InvocationHandler的实现类。该实现类需要实现其唯一的抽象方法invoke(…),并且在该方法中调用被委托类的方法。一般情况下,调用处理器需要能够访问到被委托类,一般的做法是将被委托类实例作为其内部的成员。

例子中所获取的动态代理实例涉及3个类,具体如下:

(1)一个远程接口,使用前面介绍的模拟远程调用Java接口MockDemoClient。

(2)一个真实目标类,使用前面介绍的RealRpcDemoClientImpl类,该类负责完成真正的RPC调用,作为动态代理的被委托类。

(3)一个InvocationHandler的实现类,本小节将实现
DemoClientInocationHandler调用处理器类,该类通过调用内部成员被委托类的对应方法完成RPC调用。模拟远程接口MockDemoClient的RPC动态代理模式实现,类之间的关系如图3-5所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-5 动态代理模式实现RPC远程调用UML类图

通过动态代理模式实现模拟远程接口MockDemoClient的RPC调用,关键的类为调用处理器,调用处理器
DemoClientInocationHandler的代码如下:

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
arduino复制代码package com.crazymaker.demo.proxy.basic;
//省略import
/**
*动态代理的调用处理器
*/
@Slf4j
public class DemoClientInocationHandler implements InvocationHandler
{
/**
*被代理的被委托类实例
*/
private MockDemoClient realClient;
public DemoClientInocationHandler(MockDemoClient realClient)
{
this.realClient = realClient;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
String name = method.getName();
log.info("{} 方法被调用", method.getName());
/**
*直接调用被委托类的方法:调用其hello方法
*/
if (name.equals("hello"))
{
return realClient.hello();
}
/**
*通过Java反射调用被委托类的方法:调用其echo方法
*/
if (name.equals("echo"))
{
return method.invoke(realClient, args);
}
/**
*通过Java反射调用被委托类的方法
*/
Object result = method.invoke(realClient, args);
return result;
}
}

调用处理器
DemoClientInocationHandler既实现了InvocationHandler接口,又拥有一个内部被委托类成员,负责完成实际的RPC请求。调用处理器有点儿像静态代理模式中的代理角色,但是在这里却不是,仅仅是JDK所生成的代理类的内部成员。

以上调用处理器
DemoClientInocationHandler的代码(测试用例)如下:

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
typescript复制代码package com.crazymaker.demo.proxy.basic;
//省略import
@Slf4j
public class StaticProxyTester {
/**
*动态代理测试
*/
@Test
public void dynamicProxyTest() {
DemoClient client = new DemoClientImpl();
//参数1:类装载器
ClassLoader classLoader = StaticProxyTester.class.getClassLoader();
//参数2:被代理的实例类型
Class[] clazz = new Class[]{DemoClient.class};
//参数3:调用处理器
InvocationHandler invocationHandler =
new DemoClientInocationHandler(client);
//获取动态代理实例
DemoClient proxy = (DemoClient)
Proxy.newProxyInstance(classLoader, clazz, invocationHandler);
//执行RPC远程调用方法
Result<JSONObject> result1 = proxy.hello();
log.info("result1={}", result1.toString());
Result<JSONObject> result2 = proxy.echo("回显内容");
log.info("result2={}", result2.toString());
}
}

运行测试用例前需要提前启动demo-provider微服务实例,并且需要确保其两个REST接口/api/demo/hello/v1、/api/demo/echo/{word}/v1可以正常访问。

一切准备妥当,运行测试用例,输出的结果如下:

1
2
3
4
ini复制代码18:36:32.499 [main] INFO c.c.d.p.b.DemoClientInocationHandler - hello方法被调用
18:36:32.621 [main] INFO c.c.d.p.b.StaticProxyTester - result1=Result{data={"hello":"world"}, status=200, msg='操作成功, reques
18:36:32.622 [main] INFO c.c.d.p.b.DemoClientInocationHandler - echo方法被调用
18:36:32.622 [main] INFO c.c.d.p.b.StaticProxyTester - result2=Result{data={"echo":"回显内容"}, status=200, msg='操作成功, reques

JDK动态代理机制的原理

动态代理的实质是通过java.lang.reflect.Proxy的newProxyInstance(…)方法生成一个动态代理类的实例,该方法比较重要,下面对该方法进行详细介绍,其定义如下:

1
2
3
4
5
6
7
swift复制代码public static Object newProxyInstance(ClassLoader loader,//类加载器
Class<?>[] interfaces,//动态代理类需要实现的接口
InvocationHandler h) //调用处理器
throws IllegalArgumentException
{
...
}

此方法的三个参数介绍如下:

第一个参数为ClassLoader类加载器类型,此处的类加载器和被委托类的类加载器相同即可。

第二个参数为Class[]类型,代表动态代理类将会实现的抽象接口,此接口是被委托类所实现的接口。

第三个参数为InvocationHandler类型,它的调用处理器实例将作为JDK生成的动态代理对象的内部成员,在对动态代理对象进行方法调用时,该处理器的invoke(…)方法会被执行。

InvocationHandler处理器的invoke(…)方法如何实现由大家自己决定。对被委托类(真实目标类)的扩展或者定制逻辑一般都会定义在此InvocationHandler处理器的invoke(…)方法中。

JVM在调用Proxy.newProxyInstance(…)方法时会自动为动态代理对象生成一个内部的代理类,那么是否能看到该动态代理类的class字节码呢?

答案是肯定的,可以通过如下方式获取其字节码,并且保存到文件中:

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码 /**
*获取动态代理类的class字节码
*/
byte[] classFile = ProxyGenerator.generateProxyClass("Proxy0",
RealRpcDemoClientImpl.class.getInterfaces());
/**
*在当前的工程目录下保存文件
*/
FileOutputStream fos =new FileOutputStream(new File("Proxy0.class"));
fos.write(classFile);
fos.flush();
fos.close();

运行3.1.4节的dynamicProxyTest()测试用例,在demo-provider模块的根路径可以发现被新创建的Proxy0.class字节码文件。如果IDE有反编译的能力,就可以在IDE中打开该文件,然后可以看到其反编译的源码:

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
java复制代码import com.crazymaker.demo.proxy.MockDemoClient;
import com.crazymaker.springcloud.common.result.RestOut;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class Proxy0 extends Proxy implements MockDemoClient {
private static Method m1;
private static Method m4;
private static Method m3;
private static Method m2;
private static Method m0;
public Proxy0(InvocationHandler var1) throws {
super(var1);
}
...
public final RestOut echo(String var1) throws {
try {
return (RestOut)super.h.invoke(this, m4, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final RestOut hello() throws {
try {
return (RestOut)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
...
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m4 = Class.forName("com.crazymaker.demo.proxy.MockDemoClient")
.getMethod("echo", Class.forName("java.lang.String"));
m3 = Class.forName("com.crazymaker.demo.proxy.MockDemoClient")
.getMethod("hello");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

通过代码可以看出,这个动态代理类其实只做了两件简单的事情:

(1)该动态代理类实现了接口类的抽象方法。动态代理类Proxy0实现了MockDemoClient接口的echo(String)、hello()两个方法。此外,Proxy0还继承了java.lang.Object的equals()、hashCode()、toString()方法。

(2)该动态代理类将对自己的方法调用委托给了InvocationHandler调用处理器内部成员。以上代理类Proxy0的每一个方法实现的代码其实非常简单,并且逻辑大致一样:将方法自己的Method反射对象和调用参数进行二次委托,委托给内部成员InvocationHandler调用处理器的invoke(…)方法。至于该内部InvocationHandler调用处理器的实例,则由大家自己编写,在通过java.lang.reflect.Proxy的newProxyInstance(…)创建动态代理对象时作为第三个参数传入。

至此,JDK动态代理机制的核心原理和动态代理类的神秘面纱已经彻底地揭开了。

Feign的RPC客户端正是通过JDK的动态代理机制来实现的,Feign对RPC调用的各种增强处理主要是通过调用处理器InvocationHandler来实现的。

本文转载自: 掘金

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

MySQL 千万数据量深分页优化,拒绝线上故障!

发表于 2021-05-18

优化项目代码过程中发现一个千万级数据深分页问题,缘由是这样的

库里有一张耗材 MCS_PROD 表,通过同步外部数据中台多维度数据,在系统内部组装为单一耗材产品,最终同步到 ES 搜索引擎

MySQL 同步 ES 流程如下:

  1. 通过定时任务的形式触发同步,比如间隔半天或一天的时间频率
  2. 同步的形式为增量同步,根据更新时间的机制,比如第一次同步查询 >= 1970-01-01 00:00:00.0
  3. 记录最大的更新时间进行存储,下次更新同步以此为条件
  4. 以分页的形式获取数据,当前页数量加一,循环到最后一页

在这里问题也就出现了,MySQL 查询分页 OFFSET 越深入,性能越差,初步估计线上 MCS_PROD 表中记录在 1000w 左右

如果按照每页 10 条,OFFSET 值会拖垮查询性能,进而形成一个 “性能深渊”

同步类代码针对此问题有两种优化方式:

  1. 采用游标、流式方案进行优化
  2. 优化深分页性能,文章围绕这个题目展开

一、软硬件说明

MySQL VERSION

1
2
3
4
5
6
7
sql复制代码mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.30 |
+-----------+
1 row in set (0.01 sec)

表结构说明

借鉴公司表结构,字段、长度以及名称均已删减

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> DESC MCS_PROD;
+-----------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------------+--------------+------+-----+---------+----------------+
| MCS_PROD_ID | int(11) | NO | PRI | NULL | auto_increment |
| MCS_CODE | varchar(100) | YES | | | |
| MCS_NAME | varchar(500) | YES | | | |
| UPDT_TIME | datetime | NO | MUL | NULL | |
+-----------------------+--------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

通过测试同学帮忙造了 500w 左右数据量

1
2
3
4
5
6
7
sql复制代码mysql> SELECT COUNT(*) FROM MCS_PROD;
+----------+
| count(*) |
+----------+
| 5100000 |
+----------+
1 row in set (1.43 sec)

SQL 语句如下

因为功能需要满足 增量拉取的方式,所以会有数据更新时间的条件查询,以及相关 查询排序(此处有坑)

1
2
3
4
5
6
7
8
9
10
sql复制代码SELECT
MCS_PROD_ID,
MCS_CODE,
MCS_NAME,
UPDT_TIME
FROM
MCS_PROD
WHERE
UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY UPDT_TIME
LIMIT xx, xx

个人整理了一些资料,有需要的朋友可以直接点击领取。

25大Java面试专题(附解析)

从0到1Java学习路线和资料

Java核心知识集

MySQL王者晋级之路

二、重新认识 MySQL 分页

LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接收一个或两个数字参数,参数必须是一个整数常量

如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数

举个简单的例子,分析下 SQL 查询过程,掌握深分页性能为什么差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 100000, 1;
+-------------+-------------------------+------------------+---------------------+
| MCS_PROD_ID | MCS_CODE | MCS_NAME | UPDT_TIME |
+-------------+-------------------------+------------------+---------------------+
| 181789 | XA601709733186213015031 | 尺、桡骨LC-DCP骨板 | 2020-10-19 16:22:19 |
+-------------+-------------------------+------------------+---------------------+
1 row in set (3.66 sec)

mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 100000, 1;
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+
| 1 | SIMPLE | MCS_PROD | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using index condition |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)

简单说明下上面 SQL 执行过程:

  1. 首先查询了表 MCS_PROD,进行过滤 UPDT_TIME 条件,查询出展示列(涉及回表操作)进行排序以及 LIMIT
  2. LIMIT 100000, 1 的意思是扫描满足条件的 100001 行,然后扔掉前 100000 行

MySQL 耗费了 大量随机 I/O 在回表查询聚簇索引的数据上,而这 100000 次随机 I/O 查询数据不会出现在结果集中

如果系统并发量稍微高一点,每次查询扫描超过 100000 行,性能肯定堪忧,另外 LIMIT 分页 OFFSET 越深,性能越差(多次强调)

图1 数据仅供参考

三、深分页优化

关于 MySQL 深分页优化常见的大概有以下三种策略:

  1. 子查询优化
  2. 延迟关联
  3. 书签记录

上面三点都能大大地提升查询效率,核心思想就是让 MySQL 尽可能扫描更少的页面,获取需要访问的记录后再根据关联列回原表查询所需要的列

3.1 子查询优化

子查询深分页优化语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID >= ( SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) LIMIT 1;
+-------------+-------------------------+------------------------+
| MCS_PROD_ID | MCS_CODE | MCS_NAME |
+-------------+-------------------------+------------------------+
| 3021401 | XA892010009391491861476 | 金属解剖型接骨板T型接骨板A |
+-------------+-------------------------+------------------------+
1 row in set (0.76 sec)

mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID >= ( SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) LIMIT 1;
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+--------------------------+
| 1 | PRIMARY | MCS_PROD | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 2296653 | 100.00 | Using where |
| 2 | SUBQUERY | m1 | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using where; Using index |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+--------------------------+
2 rows in set, 1 warning (0.77 sec)

根据执行计划得知,子查询 table m1 查询是用到了索引。首先在 索引上拿到了聚集索引的主键 ID 省去了回表操作,然后第二查询直接根据第一个查询的 ID 往后再去查 10 个就可以了

图2 数据仅供参考

3.2 延迟关联

“延迟关联” 深分页优化语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sql复制代码mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD INNER JOIN (SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) AS  MCS_PROD2 USING(MCS_PROD_ID);
+-------------+-------------------------+------------------------+
| MCS_PROD_ID | MCS_CODE | MCS_NAME |
+-------------+-------------------------+------------------------+
| 3021401 | XA892010009391491861476 | 金属解剖型接骨板T型接骨板A |
+-------------+-------------------------+------------------------+
1 row in set (0.75 sec)

mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD INNER JOIN (SELECT m1.MCS_PROD_ID FROM MCS_PROD m1 WHERE m1.UPDT_TIME >= '1970-01-01 00:00:00.0' ORDER BY m1.UPDT_TIME LIMIT 3000000, 1) AS MCS_PROD2 USING(MCS_PROD_ID);
+----+-------------+------------+------------+--------+---------------+------------+---------+-----------------------+---------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+--------+---------------+------------+---------+-----------------------+---------+----------+--------------------------+
| 1 | PRIMARY | <derived2> | NULL | ALL | NULL | NULL | NULL | NULL | 2296653 | 100.00 | NULL |
| 1 | PRIMARY | MCS_PROD | NULL | eq_ref | PRIMARY | PRIMARY | 4 | MCS_PROD2.MCS_PROD_ID | 1 | 100.00 | NULL |
| 2 | DERIVED | m1 | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using where; Using index |
+----+-------------+------------+------------+--------+---------------+------------+---------+-----------------------+---------+----------+--------------------------+
3 rows in set, 1 warning (0.00 sec)

思路以及性能与子查询优化一致,只不过采用了 JOIN 的形式执行

3.3 书签记录

关于 LIMIT 深分页问题,核心在于 OFFSET 值,它会 导致 MySQL 扫描大量不需要的记录行然后抛弃掉

我们可以先使用书签 记录获取上次取数据的位置,下次就可以直接从该位置开始扫描,这样可以 避免使用 OFFEST

假设需要查询 3000000 行数据后的第 1 条记录,查询可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码mysql> SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID < 3000000 ORDER BY UPDT_TIME LIMIT 1;
+-------------+-------------------------+---------------------------------+
| MCS_PROD_ID | MCS_CODE | MCS_NAME |
+-------------+-------------------------+---------------------------------+
| 127 | XA683240878449276581799 | 股骨近端-1螺纹孔锁定板(纯钛)YJBL01 |
+-------------+-------------------------+---------------------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME FROM MCS_PROD WHERE MCS_PROD_ID < 3000000 ORDER BY UPDT_TIME LIMIT 1;
+----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | MCS_PROD | NULL | index | PRIMARY | MCS_PROD_1 | 5 | NULL | 2 | 50.00 | Using where |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

好处是很明显的,查询速度超级快,性能都会稳定在毫秒级,从性能上考虑碾压其它方式

不过这种方式局限性也比较大,需要一种类似连续自增的字段,以及业务所能包容的连续概念,视情况而定

上图是阿里云 OSS Bucket 桶内文件列表,大胆猜测是不是可以采用书签记录的形式完成

四、ORDER BY 巨坑, 慎踩

以下言论可能会打破你对 order by 所有 美好 YY

先说结论吧,当 LIMIT OFFSET 过深时,会使 ORDER BY 普通索引失效(联合、唯一这些索引没有测试)

1
2
3
4
5
6
7
sql复制代码mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME,UPDT_TIME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 100000, 1;
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+
| 1 | SIMPLE | MCS_PROD | NULL | range | MCS_PROD_1 | MCS_PROD_1 | 5 | NULL | 2296653 | 100.00 | Using index condition |
+----+-------------+----------+------------+-------+---------------+------------+---------+------+---------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

先来说一下这个 ORDER BY 执行过程:

  1. 初始化 SORT_BUFFER,放入 MCS_PROD_ID,MCS_CODE,MCS_NAME,UPDT_TIME 四个字段
  2. 从索引 UPDT_TIME 找到满足条件的主键 ID,回表查询出四个字段值存入 SORT_BUFFER
  3. 从索引处继续查询满足 UPDT_TIME 条件记录,继续执行步骤 2
  4. 对 SORT_BUFFER 中的数据按照 UPDT_TIME 排序
  5. 排序成功后取出符合 LIMIT 条件的记录返回客户端

按照 UPDT_TIME 排序可能在内存中完成,也可能需要使用外部排序,取决于排序所需的内存和参数 SORT_BUFFER_SIZE

SORT_BUFFER_SIZE 是 MySQL 为排序开辟的内存。如果排序数据量小于 SORT_BUFFER_SIZE,排序会在内存中完成。如果数据量过大,内存放不下,则会利用磁盘临时文件排序

针对 SORT_BUFFER_SIZE 这个参数在网上查询到有用资料比较少,大家如果测试过程中存在问题,可以加微信一起沟通

4.1 ORDER BY 索引失效举例

OFFSET 100000 时,通过 key Extra 得知,没有使用磁盘临时文件排序,这个时候把 OFFSET 调整到 500000

凉凉夜色为你思念成河,化作春泥呵护着你… 一首凉凉送给写这个 SQL 的同学,发现了 Using filesort

1
2
3
4
5
6
7
sql复制代码mysql> EXPLAIN SELECT MCS_PROD_ID,MCS_CODE,MCS_NAME,UPDT_TIME FROM MCS_PROD WHERE (UPDT_TIME >= '1970-01-01 00:00:00.0') ORDER BY UPDT_TIME LIMIT 500000, 1;
+----+-------------+----------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
| 1 | SIMPLE | MCS_PROD | NULL | ALL | MCS_PROD_1 | NULL | NULL | NULL | 4593306 | 50.00 | Using where; Using filesort |
+----+-------------+----------+------------+------+---------------+------+---------+------+---------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

Using filesort 表示在索引之外,需要额外进行外部的排序动作,性能必将受到严重影响

所以我们应该 结合相对应的业务逻辑避免常规 LIMIT OFFSET,采用 # 深分页优化 章节进行修改对应业务

结言

最后有一点需要声明下,MySQL 本身并不适合单表大数据量业务

因为 MySQL 应用在企业级项目时,针对库表查询并非简单的条件,可能会有更复杂的联合查询,亦或者是大数据量时存在频繁新增或更新操作,维护索引或者数据 ACID 特性上必然存在性能牺牲

如果设计初期能够预料到库表的数据增长,理应构思合理的重构优化方式,比如 ES 配合查询、分库分表、TiDB 等解决方式

本文转载自: 掘金

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

如何设计出优秀的API

发表于 2021-05-18

目前,后台接口开发可以用RESTFull风格,也可以用Web Service、SOAP协议、RPC协议,也可以用HTTP协议;可以用短连接,也可以使用长连接。如果我们希望继续进行划分,还可以分为同步或异步、单个或批量、是否有SDK包、内部接口还是开放接口平台等。

评判维度

1.API接口应该是需要定义一定的规范,例如简短易记,通俗易懂等,从程序的角度来说,API应该遵循行业规范,在调用时不需要做特殊化处理,有利于复用已有的代码或工具。

2.API发布上线之后,要根据真实用户的反馈或者业务发展的需要进行调整,这些调整必须尽量不影响用户。要么提供多版本支持,要么给用户提供切实可行的更新策略等等。

3.对外公开的API存在被攻击的风险,以及无法准确预估的访问量等,一个好的API必须要有防注入、防篡改、防重放等安全机制,还要在访问量急剧上涨时避免服务挂掉。

URL规范

1.便于输入的URI,简短不冗余。每个WEB API都是一个服务,那下面反例当中的“service”就是冗余的,而且“api”也重复出现了两次,这种冗余都不利于记忆和输入。

1
2
ruby复制代码反例:http://api.example.com/api/service/users
正例:http://api.example.com/users

2.没有大小写混用的URI。HTTP协议(RFC7230)规定:除了模式(schema)和主机名以外,URI的其他信息都要区分字母的大小写。下述两个反例大小写混用,不方便记忆。

1
2
bash复制代码反例:http://api.example.com/Users/12345
反例:http://example.com/API/getUserName

3.规则统一的URI,确保采用统一的规则和风格,方便用户记忆和使用,易于修改的URI,命名存在可预见的规律,下述正例我们可以很容易猜测改变最后的ID就可以访问其他内容的信息。

1
bash复制代码正例:http://api.example.com/v1/users/123456

规范总结

1.URI最好由名词组成。URI的全称是统一资源定位符(Uniform Resource Identifier),用于标识资源在互联网上的位置,类似于邮寄地址,而地址都是由名词组成的。在名词使用上也有一些需要注意的事项:其一,使用名词复数形式;其二,尽量采用多数API中使用的表示相同含义的单词;其三,通过尽可能少的单词来表示;其四,尽可能不用奇怪的缩略语等。

2.不使用空格及需要编码的字符,例如在URI中使用中文等。

3.使用连接符(-)来连接多个单词,推荐脊柱法:首先,URI里的主机名(域名)允许使用连字符而禁止使用下划线,且不区分大小写。其次,点字符具有特殊含义,为了与主机名的规则保持一致。

1
2
3
bash复制代码脊柱法:http://api.example.com/v1/users/12345/profile-image
蛇形法:http://api.example.com/v1/users/12345/profile_image
驼峰法:http://api.example.com/v1/users/12345/profileImage

版本控制

1.咱们在开发的过程中对于版本迭代是无法避免的,但是怎么去做到在各个版本之间做到好的兼容性,不去破坏原有的API避免影响老版本的正常使用,尤其是程序已经上线并且别很多人使用的情况下.

2.您也可以对准备抛弃的老版本用户进行建议升级,不过不提倡直接关闭某个版本或者不对某个版本进行维护,而是在提示用户在未来的某一天这个版本的接口将停止维护与关闭,尽量升级至最新的版本。(可以参考近期Windows对Win7的公告)

3.好的RESTFull会在URL上显示具体的版本号。比较常见的方案是把版本号放在请求的Header中和放在URL上,我比较建议使用第二种方案,这样更加直观与方便

1
ruby复制代码示例:http://api.example.com/v1/*

4.版本编号也存在业界规范:语义化版本控制(Semantic Versioning)规范,网站地址:semver.org。版本编号由点号连接的3个数字组成,例如:1.2.3,分别表示主版本编号、次版本编号、补丁版本编号,版本编号的增加遵循下述规则:

  1. 在对软件进行不向下兼容的变更时,增加主版本编号;
  2. 在对软件进行向下兼容的变更或废除某些特定的功能时,增加次版本编号;
  3. 如果软件的API没有发生变更,只是修正了部分bug,则增加补丁版本编号。

状态码

1.当客户端通过API向服务器发出请求时,客户端应该知道反馈,无论是失败,成功还是请求错误。HTTP状态代码是一系列标准化代码,针对HTTP请求的可能会发生的各种情况。服务器应始终返回正确的状态代码。

2.很多人喜欢把错误信息放在返回值中,典型的Code和Message,其实并不好,下面是Http状态码,可以合理利用处理各种请求反馈,将http自身的错误和服务器内部的错误,有一个很好的区分。

接口文档

1.一个程序员的老笑话,程序员最讨厌不写注释的代码和写代码加注释。写文档是无聊的,但必不可少,良好的文档会保存你的理智思考,会避免api消费的很多问题。一个好的文档包括枚举值字典表,所有API接口的请求参数,请求方法,请求路径,返回参数,正确示例与错误示例不可偷工减料。每个请求参数和返回参数必须加注释,不要让别人去猜,文档必须是演进的,一旦进行修改必须及时同步更新文档。

2.尽量不用使用自动生成的文档,但如果你真的这么做了,那一定要保证你检查和校验过这个文档。不要缩减请求和响应的内容,最好就全部展求。在你的文档中有些重点的要加粗。

结尾

今天先分享到这里,如果你觉得有价值,麻烦动动手指点下 「 分享 」按钮,让更多小伙伴可以看到,我也会更加有动力坚持分享。另外,同学我后续还会分享职业规划、应聘面试、技能提升、影响力打造等经验,欢迎 关注 本掘金号 「 _我思故我在 」!

本文转载自: 掘金

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

妈妈,我要做游戏, 一文告诉你怎么入行游戏开发

发表于 2021-05-18

目录

一、程序工种的分类

二、程序的技能点

三、程序如何入门

四、怎么入行


点赞再看,养成赞美的习惯,微信搜一搜【香菜聊游戏】关注我。

图片

经常在csdn 上看到我用java 做了什么游戏,我用python做了什么游戏,而且很多人阅读,我只想说你们都是弟弟,**都是外行**,你们写的那张根本就不是游戏行业的生产方式,连个玩具都不算,自嗨还行,没有一点专业性,专业的还是要看我的,正儿八经的做游戏的。

王者荣耀这款游戏我想几乎没有人不知道,看看身边有多少人在玩,你想想你每天有多少时间在玩这款游戏。有流量就有流水,王者荣耀卖一款皮肤一次活动都是上亿的流水,这样的吸金能力堪比毒品,不服不行。成功的产品自然离不开项目组成员的努力,腾讯自然也不会亏待开发人员,据传王者荣耀项目组的程序年终奖有几十个月,并且游戏行业的工资也算比较高的,这样的福利我想在其他行业真的不多见,看着都眼馋,有人说选择比努力更重要,游戏行业的待遇普遍比其他行业好一些,但是也确实比较苦。

抛开福利不谈,多少人都有做游戏的梦想,游戏这个行业现在也是工业化的,每个成功的项目都是一批人倾力合作的结果,个人英雄主义已经很难取得成功,因此你可以参与游戏的制作,让自己完成的游戏运行在每个玩家的手机上,也可以自豪的说,这个功能是我做的,后台是我实现的,具体的流程是什么样的,那也是一种荣耀,是不错的简历,如果好巧不巧你的游戏能成为经典,作为这种艺术保存下来,流传下来,是不是也是值得自豪的。

在游戏行业也不少年了,总有人问我作为一个程序怎么进入游戏行业,作为一个不怎么懂程序的小白怎么做游戏,不管是为了梦想还是因为待遇,真的很欢迎大家进入游戏行业,这个行业需要新鲜的血液,需要创意,不过今天大概聊下怎么成为游戏的程序,毕竟我是程序。

一、程序工种的分类

程序分为客户端和服务器

客户端的职责就是负责玩家端的展现的所有事情,控制美术资源的表现,完成策划的流程设计,负责和服务器的数据对接等等,每个客户端相当于是游戏世界的一个人。

服务端的职责就是负责整个游戏流程的控制,负责玩家数据的存储和安全,是整个游戏世界的管理者。

二、程序的技能点

  • 客户端:

工具:现在商业化的手游市面上的用的工具最多的是unity,cocos creator和UE4 ,其他的可能也有一些自研引擎,但是都产品不多,流行的引擎的技能是通用的,并且也是在不断的迭代下最方面的,开发效率最高的工具,值得学习。

编程语言方面,不同的工具的编程语言也不同,比如cocos creator的编程语言是JavaScript和Typescript,unity的标配语言是C#,但是很多公司和项目为了热更新,基本上编程语言是lua,Ue4 的编程语言是C++,Ue4 也是未来的趋势,值得学习。

  • 服务端

服务端的工具和编程语言实在是五花八门,

如果你用C++,一般最常用的是Visual studio

如果你用Java,一把最常用的是Idea,

如果用Node.js,可以选择用webstorm或者vs code

如果你要用python ,一般选择用pycharm

三、程序如何入门

1.在学校学

在学校学的最大的弊端在于学的时候学了,但是没有机会用,学完就忘了,但是学校是最好的入门方式,不懂就问,不懂就查,奉劝各位珍惜学校的时间,多学,认真学,时光不负你。

2.上培训班

多少毕业生找不到工作被骗去培训班,培训班的好处就是有人带着你学,也会教你怎么用,但是要花钱,花好多钱。而且有的公司不喜欢培训班出来的学生,因为公司认为这样的同学自学能力差,编程功底弱,很难培养,不推荐这种学习方式。

3.自学看视频,看书

现在网络太发达了,各种视频都有,不管是小白入门还是大神深入都有,只要你愿意去找,基本上都可以找到视频,给Java 系的同学推荐一个B站的视频,带你一步一步入门,看着还不错,可以试试。

传送门 :www.bilibili.com/video/BV1Rx…

4.找朋友带

现在还记得当年的时候得亏有学姐带着我学习,不懂的地方都可以问,也是积累了一些编程的基础,最终如愿的找到工作,如果你想学程序,可以关注我,我会一直分享和游戏相关的技术,也可以加我微信我们聊人生,我愿意分享我的经验。

四、怎么入行

有了基本的技能点,怎么才能入行,有哪些渠道进入游戏公司呐,说实在话基本的招聘方式都是大同小异的,入行的途径主要有校招,社招,内推,参与创业等等。但是圈子这种东西确实是存在的,不在圈子内很难接触到资源,很难了解到行业的信息,如果你想入行,你想进入游戏行业可以关注我公众号【香菜聊游戏】,可以带你了解游戏,可以帮你内推到苏州和上海的游戏公司,可以带你入行程序(毕竟这么多年的程序)。

最后做个总结:

图片

原创打字不容易,点赞,转发,关注三连,关注我公众号:【香菜聊游戏】有更多福利哦

本文转载自: 掘金

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

如何快速实现一个聊天室?

发表于 2021-05-18

前些天做了一个网站:modubox.cn 其中有个群聊插件,许多人问如何实现的。这里简单说下,为了快速完成群聊功能,我选择从最简单的 WebSocket 开始。

什么是WebSocket ?

既然要使用它,就需要了解一下它吧。WebSocket其实也是一种基于TCP的网络协议,它与HTTP协议最大的不同是:是一种双向通信协议,在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,而HTTP协议只能客户端主动发起通信。

所以WebSocket能够用于聊天,当然其他地方也能应用,如果做客服系统或推送消息都可以从这里开始。

如何实现单聊/群聊?

群聊:所有客户端的消息发送到服务器,服务端将消息发送给所有客户端。

单聊:WebSocket客户端之间是无法直接通信的,想要通信,必须由服务端转发。

群聊单聊

群聊单聊

实现

1. 引入WebSocket的支持

我们使用当前最流行的Spring Boot框架构建项目,然后引入Spring Boot 对 WebSocket 的支持:

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

2. 开启WebSocket

1
2
3
4
5
6
7
8
typescript复制代码@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

3. 服务端

这里主要有以下几点:

  1. 声明服务端点路径
  2. 存储所有连接用户,等待匹配用户
  3. 连接 onOpen,消息OnMessage,关闭onClose,错误onError 方法
  4. 发送消息给特定连接者

@ServerEndpoint(value = “/websocket/random/“)
@Component
public class ChatRandomServer {
//所有连接
public static ConcurrentHashMap<String, ChatRandomServer> webSocketSet = new ConcurrentHashMap<>();
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//所有在配对的ID
private static List webSocketLiveList = new CopyOnWriteArrayList();
//自己的id标识
private String id = “”;
//连接对象的id标识
private String toUser = “”;

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
scss复制代码/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
session.setMaxIdleTimeout(3600000);
this.session = session;
//获取用户ip
String ip = IpUtil.getRemoteAddress(session);
this.id = ip;
ChatRandomServer put = webSocketSet.put(this.id, this);
//如果已经在队里,就不去找对象
if (put == null) {
try {
if (pair()) {
sendMessage("匹配成功");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
sendMessage("匹配失败");
webSocketSet.remove(this.id);
session.close();
} catch (IOException e) {
e.printStackTrace();
}

}
log.info("用户{}加入!当前在线人数为: {}", this.id, webSocketSet.size());

}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
ChatRandomServer UserId = webSocketSet.get(toUser);
webSocketLiveList.remove(this.id);
if (UserId != null) {
try {
sendToUser(session, "对方已离开", toUser);
} catch (IOException e) {
e.printStackTrace();
}
}
webSocketSet.remove(this.id);
log.info("{}连接关闭!当前在线人数:{}, 当前在匹配的人数:{}" ,this.id,webSocketSet.size(), webSocketLiveList.size());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("来自 {} 的消息: {}", this.id, message);
try {
ChatRandomServer.sendToUser(session, message, toUser, 2);
} catch (IOException e) {
e.printStackTrace();
}

}

@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
try {
SendSelf(session,"服务器出现错误");
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 发送消息给自己
*/
public void sendMessage(String message) throws IOException {
SendSelf(this.session, message);
}
private static void SendSelf(Session session, String message) throws IOException {
session.getBasicRemote().sendText(message);
}

/**
* 发送信息给指定ID用户
*/
public static void sendToUser(Session session, String message, String sendUserId) throws IOException {
ChatRandomServer UserId = webSocketSet.get(sendUserId);
if (UserId != null) {
UserId.sendMessage(message);
} else {
SendSelf(session, "发送失败");
}
}

/**
* 通知除了自己之外的所有人
*/
private void sendOnlineCount(String message) {
for (String key : webSocketSet.keySet()) {
try {
if (key.equals(id)) {
continue;
}
webSocketSet.get(key).sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 发送信息给所有人
*/
public void sendToAll(String message) throws IOException {
for (String key : webSocketSet.keySet()) {
try {
webSocketSet.get(key).sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

public synchronized boolean pair() throws IOException {
//是否存在等待匹配的用户
if (webSocketLiveList.size() > 0) {
//随机匹配一个
Random ra = new Random();
int nextInt = ra.nextInt(webSocketLiveList.size());
toUser = webSocketLiveList.get(nextInt);

try {
ChatRandomServer UserId = webSocketSet.get(toUser);
UserId.setToUser(id);
sendToUser(session, "配对成功", toUser);
} catch (IOException e) {
e.printStackTrace();
}
webSocketLiveList.remove(nextInt);
return true;
}
//没有匹配的,则将自己加入等待匹配队列
webSocketLiveList.add(id);
return false;
}

}

4. 前端支持

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
kotlin复制代码 start: function () {
if (typeof (WebSocket) === "undefined") {
alert("您的浏览器不支持socket")
} else {
// 实例化socket
this.socket = new WebSocket(`ws://localhost:8082/websocket/room`);
// 监听socket连接
this.socket.onopen = this.open
// 监听socket错误信息
this.socket.onerror = this.error
// 监听socket消息
this.socket.onmessage = this.getMessage
this.socket.onclose = this.close
}
},
open: function () {
},
error: function () {
},
getMessage: function (obj) {
//接收信息后根据不同情况不同处理方式
let data = JSON.parse(obj.data);
if (data.code === 1) {
} else if (data.code === 2) {
} else {
}
},
close: function (e) {
},

doSend: function () {
if (that.sendData === '') {
return;
}
this.socket.send(that.sendData);
that.sendData = '';
},

以上代码不完整,如果需要看下完整代码,联系我。

本文转载自: 掘金

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

再见 Jenkins !几行脚本搞定自动化部署,这款神器有点

发表于 2021-05-18

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

在开发或生产环境中,我们经常会搞一套自动化部署方案(俗称一键部署)。比较流行的一种就是Gitlab+Jenkins实现方案,不过这种方案占用内存比较大,没有个8G内存,很难流畅运行,而且部署起来也不快。最近发现一款神器Drone,轻量级CI/DI工具,结合Gogs使用内存占用不到1G,几行脚本就能实现自动化部署,推荐给大家!

Drone简介

Drone是一款基于容器技术的持续集成工具,使用简单的YAML配置文件即可完成复杂的自动化构建、测试、部署任务,在Github上已经有22K+Star。

Gogs安装

我们将使用轻量级的Gogs来搭建Git仓库,这里只是简单说下安装步骤,具体使用可以参考《Github标星34K+Star,这款开源项目助你秒建Git服务!》。

  • 首先需要下载Gogs的Docker镜像;
1
bash复制代码docker pull gogs/gogs
  • 下载完成后在Docker容器中运行Gogs;
1
2
3
4
bash复制代码docker run -p 10022:22 -p 10080:3000 --name=gogs \
-e TZ="Asia/Shanghai" \
-v /mydata/gogs:/data \
-d gogs/gogs
  • Gogs运行成功后,访问Web页面地址并注册账号:http://192.168.5.78:10080

  • 然后将我们的SpringBoot项目mall-tiny-drone的源码上传上去即可,项目地址:github.com/macrozheng/…

Drone安装

接下来我们安装下Drone,不愧是基于容器的CI/DI工具,使用Docker安装很方便!

  • 首先下载Drone的Server和Runner的镜像;
1
2
3
4
bash复制代码# Drone的Server
docker pull drone/drone:1
# Drone的Runner
docker pull drone-runner-docker:1
  • 这里有个Server和Runner的概念,我们先来理解下;
    • Server:为Drone的管理提供了Web页面,用于管理从Git上获取的仓库中的流水线任务。
    • Runner:一个单独的守护进程,会轮询Server,获取需要执行的流水线任务,之后执行。
  • 接下来我们来安装drone-server,使用如下命令即可;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码docker run \
-v /mydata/drone:/data \
-e DRONE_AGENTS_ENABLED=true \
-e DRONE_GOGS_SERVER=http://192.168.5.78:10080 \
-e DRONE_RPC_SECRET=dronerpc666 \
-e DRONE_SERVER_HOST=192.168.5.78:3080 \
-e DRONE_SERVER_PROTO=http \
-e DRONE_USER_CREATE=username:macro,admin:true \
-e TZ="Asia/Shanghai" \
-p 3080:80 \
--restart=always \
--detach=true \
--name=drone \
drone/drone:1
  • 这里的配置参数比较多,下面统一解释下;
+ DRONE\_GOGS\_SERVER:用于配置Gogs服务地址。
+ DRONE\_RPC\_SECRET:Drone的共享秘钥,用于验证连接到server的rpc连接,server和runner需要提供同样的秘钥。
+ DRONE\_SERVER\_HOST:用于配置Drone server外部可访问的地址。
+ DRONE\_SERVER\_PROTO:用于配置Drone server外部可访问的协议,必须是http或https。
+ DRONE\_USER\_CREATE:创建一个管理员账号,该账号需要在Gogs中注册好。
  • 接下来安装drone-runner-docker,当有需要执行的任务时,会启动临时的容器来执行流水线任务;
1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码docker run -d \
-v /var/run/docker.sock:/var/run/docker.sock \
-e DRONE_RPC_PROTO=http \
-e DRONE_RPC_HOST=192.168.5.78:3080 \
-e DRONE_RPC_SECRET=dronerpc666 \
-e DRONE_RUNNER_CAPACITY=2 \
-e DRONE_RUNNER_NAME=runner-docker \
-e TZ="Asia/Shanghai" \
-p 3000:3000 \
--restart always \
--name runner-docker \
drone/drone-runner-docker:1
  • 这里的配置参数比较多,下面统一解释下。
    • DRONE_RPC_PROTO:用于配置连接到Drone server的协议,必须是http或https。
    • DRONE_RPC_HOST:用于配置Drone server的访问地址,runner会连接到server获取流水线任务并执行。
    • DRONE_RPC_SECRET:用于配置连接到Drone server的共享秘钥。
    • DRONE_RUNNER_CAPACITY:限制runner并发执行的流水线任务数量。
    • DRONE_RUNNER_NAME:自定义runner的名称。

Drone使用

  • 让我们来访问下Drone的控制台页面,第一次登录需要输入账号密码(在Gogs中注册的账号),访问地址:http://192.168.5.78:3080/

  • 此时我们在Gogs中的项目会现在在列表中,如果没有的话可以点下SYNC按钮;

  • 接下来我们需要对仓库进行设置,将仓库设置为Trusted(否则Drone创建的容器无法挂载目录到宿主机),最后点击SAVE按钮保存;

  • 保存成功后会在Gogs中自动配置一个Web钩子,当我们推送代码到Gogs中去时,会触发这个钩子,然后执行在Drone中的流水线任务;

  • 拉到最下面,我们可以发送一个测试推送,推送成功会显示绿色的√;

  • 此时我们在Drone中发现其实流水线执行失败了,那是因为我们在脚本中引用了Secret中的ssh_password;

  • 在仓库的设置中添加一个Secret即可,Secret是专门用来存储密码的,此密码只能被使用或删除,无法被查看;

  • 在ACTIVITY FEED中使用RESTART可以重新执行该流水线,发现已经成功执行。

编写脚本

当我们向Git仓库Push代码时,会自动触发Web钩子,然后Drone就会从Git仓库Clone代码,再通过项目目录下的.drone.yml配置,执行相应的流水线,接下来我们来看看这个脚本是如何写的。

  • 首先我们来了解下在.drone.yml中配置的工作流都有哪些操作,看下流程图就知道了;

  • 再来一个完整的.drone.yml,配上详细的注解,看下就基本懂了!
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
yaml复制代码kind: pipeline # 定义对象类型,还有secret和signature两种类型
type: docker # 定义流水线类型,还有kubernetes、exec、ssh等类型
name: mall-tiny-drone # 定义流水线名称

steps: # 定义流水线执行步骤,这些步骤将顺序执行
- name: package # 流水线名称
image: maven:3-jdk-8 # 定义创建容器的Docker镜像
volumes: # 将容器内目录挂载到宿主机,仓库需要开启Trusted设置
- name: maven-cache
path: /root/.m2 # 将maven下载依赖的目录挂载出来,防止重复下载
- name: maven-build
path: /app/build # 将应用打包好的Jar和执行脚本挂载出来
commands: # 定义在Docker容器中执行的shell命令
- mvn clean package # 应用打包命令
- cp target/mall-tiny-drone-1.0-SNAPSHOT.jar /app/build/mall-tiny-drone-1.0-SNAPSHOT.jar
- cp Dockerfile /app/build/Dockerfile
- cp run.sh /app/build/run.sh

- name: build-start
image: appleboy/drone-ssh # SSH工具镜像
settings:
host: 192.168.5.78 # 远程连接地址
username: root # 远程连接账号
password:
from_secret: ssh_password # 从Secret中读取SSH密码
port: 22 # 远程连接端口
command_timeout: 5m # 远程执行命令超时时间
script:
- cd /mydata/maven/build # 进入宿主机构建目录
- chmod +x run.sh # 更改为可执行脚本
- ./run.sh # 运行脚本打包应用镜像并运行

volumes: # 定义流水线挂载目录,用于共享数据
- name: maven-build
host:
path: /mydata/maven/build # 从宿主机中挂载的目录
- name: maven-cache
host:
path: /mydata/maven/cache
  • run.sh执行脚本可以实现打包应用和运行容器镜像,之前讲过这里就不再赘述了,具体可以参考《我常用的自动化部署技巧,贼好用,推荐给大家!》,运行成功效果如下。

总结

对比Jenkins复杂的图形化界面操作,Drone使用脚本来定义流水线任务无疑更简单、更直观。Drone更加轻量级,内存占用少且响应速度快!自动化部署要啥Jenkins?直接给Git整个CI/DI功能难道不香么?

参考资料

  • 官方文档:docs.drone.io/
  • 结合Maven使用:docs.drone.io/pipeline/ku…
  • 结合SSH使用:plugins.drone.io/appleboy/dr…
  • 将容器目录挂载到宿主机:docs.drone.io/pipeline/do…

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

1…666667668…956

开发者博客

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