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

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


  • 首页

  • 归档

  • 搜索

第八十八章 SQL命令 WHERE(一) 第八十八章 SQL

发表于 2021-11-27

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

第八十八章 SQL命令 WHERE(一)

指定一个或多个限制性条件的SELECT子句。

大纲

1
2
3
sql复制代码SELECT fields
FROM table
WHERE condition-expression

参数

  • condition-expression - 由一个或多个布尔谓词组成的表达式,该谓词控制要检索的数据值。

描述

可选的WHERE子句可以用于以下目的:

  • 指定限制要返回哪些数据值的谓词。
  • 指定两个表之间的显式连接。
  • 指定基表和另一个表中的字段之间的隐式连接。

WHERE子句最常用于指定一个或多个谓词,这些谓词用于限制SELECT查询或子查询检索到的数据(过滤出行)。
还可以在UPDATE命令、DELETE命令或INSERT(或INSERT or UPDATE)命令的结果集中使用WHERE子句。

WHERE子句限定或取消查询选择中的特定行。
符合条件的行是那些条件表达式为真的行。
条件表达式可以是一个或多个逻辑测试(谓词)。
多个谓词可以通过AND和OR逻辑操作符链接。

如果谓词包含除法,并且数据库中有任何值可以生成值为零或NULL的除法,则不能依赖求值顺序来避免被零除法。
相反,使用CASE语句来抑制风险。

WHERE子句可以指定包含子查询的条件表达式。子查询必须用圆括号括起来。

WHERE子句可以使用=(内部连接)符号连接操作符指定两个表之间的显式连接。

WHERE子句可以使用箭头语法(- >)操作符在基表和来自另一个表的字段之间指定隐式连接。

指定字段

WHERE子句最简单的形式是指定一个比较字段和值的谓词,例如WHERE Age > 21。
有效的字段值包括以下:列名(WHERE Age > 21);
%ID, %TABLENAME,或%CLASSNAME;
标量函数指定列名(WHERE ROUND(Age,-1)=60),一个排序规则函数指定列名(WHERE %SQLUPPER(name) %STARTSWITH ' AB')。

不能按列号指定字段。

因为重新编译表时RowID字段的名称可能会改变,WHERE子句应该避免通过名称引用RowID(例如,WHERE ID=22)。
相反,使用%ID伪列名来引用RowID(例如,WHERE %ID=22)。

不能通过列别名指定字段;
尝试这样做会产生SQLCODE -29错误。
但是,可以使用子查询来定义列别名,然后在WHERE子句中使用该别名。
例如:

1
2
3
sql复制代码SELECT Interns FROM 
(SELECT Name AS Interns FROM Sample.Employee WHERE Age<21)
WHERE Interns %STARTSWITH 'A'

不能指定聚合字段;
尝试这样做将生成SQLCODE -19错误。
但是,可以通过使用子查询向WHERE子句提供聚合函数值。
例如:

1
2
3
4
sql复制代码SELECT Name,Age,AvgAge
FROM (SELECT Name,Age,AVG(Age) AS AvgAge FROM Sample.Person)
WHERE Age < AvgAge
ORDER BY Age

整型和字符串

如果将定义为整数数据类型的字段与数值进行比较,则在执行比较之前将数值转换为规范形式。
例如,WHERE Age=007.00解析为WHERE Age=7。
这种转换发生在所有模式中。

如果将定义为整数数据类型的字段与Display模式下的字符串值进行比较,则将该字符串解析为数值。
例如,与任何非数字字符串一样,空字符串(")被解析为数字0。
这种解析遵循将字符串处理为数字的ObjectScript规则。
例如,WHERE Age=’twenty’解析为WHERE Age=0; WHERE Age=’20something’解析为WHERE Age=20。 SQL只在Display模式下执行解析; 在逻辑或ODBC模式下,将整数与字符串值进行比较将返回null`。

要比较字符串字段和包含单引号的字符串,请使用双引号。
例如,WHERE Name %STARTSWITH 'O''',返回的是 O’Neil and O’Connor, 而不是 Obama.

日期和时间

SQL日期和时间使用逻辑模式内部表示进行比较和存储。
它们可以以逻辑模式、显示模式或ODBC模式返回。
例如,1944年9月28日表示为:逻辑模式37891,显示模式09/28/1944,ODBC模式194409-28。
在条件表达式中指定日期或时间时,可能由于SQL模式与日期或时间格式不匹配,或由于无效的日期或时间值而发生错误。

WHERE子句条件表达式必须使用与当前模式相对应的日期或时间格式。
例如,在逻辑模式下,要返回出生日期为2005年的记录,WHERE子句将出现如下:WHERE DOB BETWEEN 59901 AND 60265
当在显示模式下,同样的WHERE子句会出现如下:WHERE DOB BETWEEN '01/01/2005' AND '12/31/2005'

如果条件表达式的日期或时间格式与显示模式不匹配,将导致错误:

  • 在显示模式或ODBC模式下,以不正确的格式指定日期数据将产生SQLCODE -146错误。
    以不正确的格式指定时间数据将产生SQLCODE -147错误。
  • 在逻辑模式下,以不正确的格式指定日期或时间数据不会产生错误,但要么不返回数据,要么返回非预期的数据。
    这是因为逻辑模式不会将显示或ODBC格式的日期或时间解析为日期或时间值。
    WHERE DOB BETWEEN 37500 AND 38000 AND DOB <> '1944-09-28' '返回一系列DOB值,包括DOB=37891(1944年9月28日),这是<>谓词试图忽略的。

无效的日期或时间值还会生成SQLCODE -146或-147错误。
无效日期是可以在显示模式/ODBC模式中指定的日期,但 IRIS不能转换为等效的逻辑模式。
例如,在ODBC模式下,以下命令会产生SQLCODE -146错误:WHERE DOB > '1830-01-01',因为 IRIS无法处理1840年12月31日之前的日期值。
以下在ODBC模式下也会产生SQLCODE -146错误:WHERE DOB BETWEEN '2005-01-01' AND '2005-02-29',因为2005不是闰年。

在逻辑模式下,Display模式或ODBC模式值不会被解析为日期或时间值,因此不会对其值进行验证。
因此,在逻辑模式下,WHERE子句(例如WHERE DOB > '1830-01-01')不会返回错误。

流字段

在大多数情况下,不能在WHERE子句谓词中使用流字段。
这样做将导致SQLCODE -313错误。
但是,在WHERE子句中允许使用流字段:

  • 流空测试:可以指定流字段IS null或流字段IS NOT null。
  • 流长度测试:可以在WHERE子句谓词中指定CHARACTER_LENGTH(流字段)、CHAR_LENGTH(流字段)或DATALENGTH(流字段)函数。
  • 流子串测试:可以在WHERE子句谓词中指定substring (streamfield,start,length)函数。

List结构

IRIS支持列表结构数据类型%list(数据类型类%Library.List)。
这是一种压缩的二进制格式,并不映射到 SQL的相应本机数据类型。
它对应的数据类型为VARBINARY,默认MAXLEN为32749。
因此,动态SQL不能在WHERE子句比较中使用%List数据。

要引用结构化列表数据,请使用%INLIST谓词或FOR SOME %ELEMENT谓词。

要在条件表达式中使用列表字段的数据值,可以使用%EXTERNAL将列表值与谓词进行比较。
例如,要返回FavoriteColors列表字段值由单个元素'Red'组成的所有记录:

1
2
sql复制代码SELECT Name,FavoriteColors FROM Sample.Person
WHERE %EXTERNAL(FavoriteColors)='Red'

当%EXTERNAL将列表转换为DISPLAY格式时,显示的列表项似乎由一个空格分隔。
这个“空格”实际上是两个非显示字符CHAR(13)和CHAR(10)。
要对列表中的多个元素使用条件表达式,必须指定这些字符。
例如,要返回FavoriteColors列表字段值由两个元素'Orange'和'Black'(按顺序)组成的所有记录:

1
2
sql复制代码SELECT Name,FavoriteColors FROM Sample.Person
WHERE %EXTERNAL(FavoriteColors)='Orange'||CHAR(13)||CHAR(10)||'Black'

变量

WHERE子句谓词可以指定:

%TABLENAME或%CLASSNAME伪字段变量关键字。
%TABLENAME返回当前表名。
%CLASSNAME返回当前表对应的类名。
如果查询引用多个表,可以在关键字前加上表别名。
例如,t1.%TABLENAME。

一个或多个ObjectScript特殊变量(或它们的缩写):$HOROLOG, $JOB, $NAMESPACE, $TLEVEL, $USERNAME, $ZHOROLOG, $ZJOB, $ZNSPACE, $ZPI, $ZTIMESTAMP, $ZTIMEZONE, $ZVERSION。

谓词列表

SQL谓词可分为以下几类:

  • Equality Comparison 谓词
  • BETWEEN 谓词
  • IN and %INLIST 谓词
  • %STARTSWITH Predicate and Contains Operator
  • NULL Predicate
  • EXISTS Predicate
  • FOR SOME Predicate
  • FOR SOME %ELEMENT Predicate
  • LIKE, %MATCHES, and %PATTERN Predicates
  • %INSET and %FIND Predicates

谓词区分大小写

谓词使用为字段定义的排序规则类型。
默认情况下,字符串数据类型字段是用SQLUPPER排序规则定义的,它不区分大小写。

%INLIST、Contains操作符([)、%MATCHES和%PATTERN谓词不使用字段的默认排序规则。
它们总是使用区分大小写的EXACT排序法。

两个字面值字符串的谓词比较总是区分大小写的。

谓词条件和%NOINDEX

可以使用%NOINDEX关键字作为谓词条件的前缀,以防止查询优化器在该条件上使用索引。
这在指定绝大多数行都满足的范围条件时非常有用。
例如,WHERE %NOINDEX Age >= 1。

离群值的谓词条件

如果动态SQL查询中的WHERE子句选择了一个非空的离群值,可以通过将离群值文字括在双括号中来显著提高性能。
这些双括号导致动态SQL在优化时使用离群值选择性。
例如,如果企业位于马萨诸塞州,那么很大一部分员工将居住在马萨诸塞州。
对于Employees表Home_State字段,'MA'是离群值。
要最优地选择这个值,应该指定WHERE Home_State=(('MA'))。

在嵌入式SQL或视图定义中不应使用此语法。
在嵌入式SQL或视图定义中,总是使用离群值选择,不需要特殊编码。

动态SQL查询中的WHERE子句会自动针对空离群值进行优化。
例如,WHERE FavoriteColors IS NULL这样的子句。
当NULL是离群值时,is NULL和is NOT NULL谓词不需要特殊编码。

离群值选择性由运行调优表实用程序决定。

本文转载自: 掘金

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

spark调优(二):UDF减少JOIN和判断 1 起因

发表于 2021-11-27

「这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」

大家好,我是怀瑾握瑜,一只大数据萌新,家有两只吞金兽,嘉与嘉,上能code下能teach的全能奶爸

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~


  1. 起因

平时写sql语句的时候经常会有大表与小标做关联查询,然后再进行group by等逻辑分组处理,或者是有很多判断条件,sql里有很多if语句,一些区间类的结构查询,这种sql语句直接放到spark上执行,会有大量的shuffle,而且执行时间巨慢

尤其是大表和小标数据差距特别大,大表作为主要处理对象,进行shuffle和map的时候花费大量时间

  1. 优化开始

2.1 改成java代码编写程序

首先的一个方法是用java代码编写spark程序,把所有的条件全部打散,或者小表做广播变量,然后每次处理数据时候在进行取值和判断

但这么会让代码可读性比较差,而且如果是用一些工具直接跑sql出计算结果,破坏程序整体性

2.2 使用UDF

UDF(User-Defined Functions)即是用户定义的hive函数。hive自带的函数并不能完全满足业务需求,这时就需要我们自定义函数了

我们这里只做最简单的UDF,就是制作一个hive函数,然后在大表中查询的时候,直接去调用方法把当初需要关联才能获得数据直接返回

首先可以定义一个udf类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码public class UDF implements UDF2<Long, Long, Long> {

Map<Long, TreeMap<Long, Long>> map;

public TripUDF(Broadcast<Map<Long, TreeMap<Long, Long>>> bmap) {
this.map = bmap.getValue();
}

@Override
public Long call(Long id, Long time) throws Exception {
if (map.containsKey(terminalId)) {
Map.Entry<Long, Long> a = map.get(id).floorEntry(time);
Map.Entry<Long, Long> b = map.get(id).ceilingEntry(time);
if (null != a && null != b) {
if (a.getValue().equals(b.getValue())) {
return a.getValue();
}
}
}
return -1L;
}
}

这个UDF方法就是先把小表的数据查询出来,做成TreeMap,然后把范围都放进去,广播出去,再每次查询的时候,都用大表到这里去用id和time进行匹配,匹配成功就是要获得的结果

如果用sql去表达,大概就是,大表的time需要去匹配小表的时间段

1
2
3
4
csharp复制代码tablea join tableb 
on tablea.id=tableb.id and
tablea.time >= tableb.timeStart and
tablea.time <= tableb.timeEnd

然后spark去注册UDF方法

1
2
ini复制代码String udfMethod = "structureMap";
spark.udf().register(udfMethod, new UDF(broadcast1), DataTypes.StringType);

这样直接去查询大表,然后在特定字段使用udf方法,就可以直接获取相应的结果

1
python复制代码select id,time,structureMap(id,time) as tag from tablea

这样tag的最终结果就和直接关联tableb然后再获取其中的值是一样的结果,但具体执行的内容都交给spark去优化


结束语

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~

可关注公众号【怀瑾握瑜的嘉与嘉】,获取资源下载方式

本文转载自: 掘金

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

SpringBoot 实战:优雅的使用枚举参数(原理篇)

发表于 2021-11-27

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战」

本文被《Spring Boot 实战》专栏收录。

你好,我是看山。

SpringBoot 实战:优雅的使用枚举参数 中聊了怎么优雅的使用枚举参数,本文就来扒一扒 Spring 是如何找到对应转换器 Converter 的。

找入口

对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方法中的ha.handle(processedRequest, response, mappedHandler.getHandler())逻辑。我们从这里出发,一层一层向里扒。

跟着代码深入,我们会找到org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest的逻辑:

1
2
3
4
5
6
7
8
9
java复制代码public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

可以看出,这里面通过getMethodArgumentValues方法处理参数,然后调用doInvoke方法获取返回值。

继续深入,能够找到org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveArgument方法,这个方法就是解析参数的逻辑。

试想一下,如果是我们自己实现这段逻辑,会怎么做呢?

  1. 获取输入参数
  2. 找到目标参数
  3. 检查是否需要特殊转换逻辑
  4. 如果需要,进行转换
  5. 如果不需要,直接返回

图片

获取输入参数的逻辑在org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName,单参数返回的是 String 类型,多参数返回 String 数组。核心代码如下:

1
2
3
4
java复制代码String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
    arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}

所以说,无论我们的目标参数是什么,输入参数都是 String 类型或 String 数组,然后 Spring 把它们转换为我们期望的类型。

找到目标参数的逻辑在DispatcherServlet中,根据 uri 找到对应的 Controller 处理方法,找到方法就找到了目标参数类型。

接下来就是检查是否需要转换逻辑,也就是org.springframework.validation.DataBinder#convertIfNecessary,顾名思义,如果需要就转换,将字符串类型转换为目标类型。在我们的例子中,就是将 String 转换为枚举值。

查找转换器

继续深扒,会在org.springframework.beans.TypeConverterDelegate#convertIfNecessary方法中找到这么一段逻辑:

1
2
3
4
5
6
7
8
9
java复制代码if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
    try {
        return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
    }
    catch (ConversionFailedException ex) {
        // fallback to default conversion logic below
        conversionAttemptEx = ex;
    }
}

这段逻辑中,调用了org.springframework.core.convert.support.GenericConversionService#canConvert方法,检查是否可转换,如果可以转换,将会执行类型转换逻辑。

检查是否可转换的本质就是检查是否能够找到对应的转换器。如果能找到,就用找到的转换器开始转换逻辑,如果找不到,那就是不能转换,走其他逻辑。

我们可以看看查找转换器的代码org.springframework.core.convert.support.GenericConversionService#getConverter,可以对我们自己写代码有一些启发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);

protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
    ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
    GenericConverter converter = this.converterCache.get(key);
    if (converter != null) {
        return (converter != NO_MATCH ? converter : null);
    }

    converter = this.converters.find(sourceType, targetType);
    if (converter == null) {
        converter = getDefaultConverter(sourceType, targetType);
    }

    if (converter != null) {
        this.converterCache.put(key, converter);
        return converter;
    }

    this.converterCache.put(key, NO_MATCH);
    return null;
}

转换为伪代码就是:

  1. 根据参数类型和目标类型,构造缓存 key
  2. 根据缓存 key,从缓存中查询转换器
  3. 如果能找到且不是 NO_MATCH,返回转换器;如果是 NO_MATCH,返回 null;如果未找到,继续
  4. 通过org.springframework.core.convert.support.GenericConversionService.Converters#find查询转换器
  5. 如果未找到,检查源类型和目标类型是否可以强转,也就是类型一致。如果是,返回 NoOpConverter,如果否,返回 null。
  6. 检查找到的转换器是否为 null,如果不是,将转换器加入到缓存中,返回该转换器
  7. 如果否,在缓存中添加 NO_MATCH 标识,返回 null

图片

Spring 内部使用Map作为缓存,用来存储通用转换器接口GenericConverter,这个接口会是我们自定义转换器的包装类。我们还可以看到,转换器缓存用的是ConcurrentReferenceHashMap,这个类是线程安全的,可以保证并发情况下,不会出现异常存储。但是getConverter方法没有使用同步逻辑。换句话说,并发请求时,可能存在性能损耗。不过,对于 web 请求场景,并发损耗好过阻塞等待。

我们在看下 Spring 是如何查找转换器的,在org.springframework.core.convert.support.GenericConversionService.Converters#find中就是找到对应转换器的核心逻辑:

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
java复制代码private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);

@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
    // Search the full type hierarchy
    List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
    List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
    for (Class<?> sourceCandidate : sourceCandidates) {
        for (Class<?> targetCandidate : targetCandidates) {
            ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
            GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
            if (converter != null) {
                return converter;
            }
        }
    }
    return null;
}

@Nullable
private GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
        TypeDescriptor targetType, ConvertiblePair convertiblePair) {

    // Check specifically registered converters
    ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
    if (convertersForPair != null) {
        GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
        if (converter != null) {
            return converter;
        }
    }
    // Check ConditionalConverters for a dynamic match
    for (GenericConverter globalConverter : this.globalConverters) {
        if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
            return globalConverter;
        }
    }
    return null;
}

我们可以看到,Spring 是通过源类型和目标类型组合起来,查找对应的转换器。而且,Spring 还通过getClassHierarchy方法,将源类型和目标类型的家族族谱全部列出来,用双层 for 循环遍历查找。

上面的代码中,还有一个matches方法,在这个方法里面,调用了ConverterFactory#getConverter方法,也就是用这个工厂方法,创建了指定类型的转换器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private final ConverterFactory<Object, Object> converterFactory;

public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
    boolean matches = true;
    if (this.converterFactory instanceof ConditionalConverter) {
        matches = ((ConditionalConverter) this.converterFactory).matches(sourceType, targetType);
    }
    if (matches) {
        Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
        if (converter instanceof ConditionalConverter) {
            matches = ((ConditionalConverter) converter).matches(sourceType, targetType);
        }
    }
    return matches;
}

类型转换

经过上面的逻辑,已经找到判断可以进行转换。其核心逻辑就是已经找到对应的转换器了,下面就是转换逻辑,在org.springframework.core.convert.support.GenericConversionService#convert中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
    Assert.notNull(targetType, "Target type to convert to cannot be null");
    if (sourceType == null) {
        Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
        return handleResult(null, targetType, convertNullSource(null, targetType));
    }
    if (source != null && !sourceType.getObjectType().isInstance(source)) {
        throw new IllegalArgumentException("Source to convert from must be an instance of [" +
                sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
    }
    GenericConverter converter = getConverter(sourceType, targetType);
    if (converter != null) {
        Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
        return handleResult(sourceType, targetType, result);
    }
    return handleConverterNotFound(source, sourceType, targetType);
}

其中的GenericConverter converter = getConverter(sourceType, targetType)就是前文中getConverter方法。此处还是可以给我们编码上的一些借鉴的:getConverter方法在canConvert中调用了一次,然后在后续真正转换的时候又调用一次,这是参数转换逻辑,我们该怎么优化这种同一请求内多次调用相同逻辑或者请求相同参数呢?那就是使用缓存。为了保持一次请求中前后两次数据的一致性和请求的高效,推荐使用内存缓存。

执行到这里,直接调用ConversionUtils.invokeConverter(converter, source, sourceType, targetType)转换,其内部是使用org.springframework.core.convert.support.GenericConversionService.ConverterFactoryAdapter#convert方法,代码如下:

1
2
3
4
5
6
java复制代码public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    if (source == null) {
        return convertNullSource(sourceType, targetType);
    }
    return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
}

这里就是调用ConverterFactory工厂类构建转换器(即IdCodeToEnumConverterFactory类的getConverter方法),然后调用转换器的conver方法(即IdCodeToEnumConverter类的convert方法),将输入参数转换为目标类型。具体实现可以看一下实战篇中的代码,这里不做赘述。

至此,我们把整个路程通了下来。

文末总结

在本文中,我们跟随源码找到自定义转换器工厂类和转换器类的实现逻辑。这里需要强调一下的是,由于实战篇中我们用到的例子是简单参数的方式,也就是Controller的方法参数都是直接参数,没有包装成对象。这样的话,Spring 是通过RequestParamMethodArgumentResolver处理参数。如果是包装成对象,会使用ModelAttributeMethodProcessor处理参数。这两个处理类中查找类型转换器逻辑都是相同的。

无论是GET请求,还是传参式的POST请求(即Form模式),都可以使用上面这种方式,实现枚举参数的类型转换。但是是 HTTP Body 方式却不行,为什么呢?

Spring 对于 body 参数是通过RequestResponseBodyMethodProcessor处理的,其内部使用了MappingJackson2HttpMessageConverter转换器,逻辑完全不同。所以,想要实现 body 的类型转换,还需要走另外一种方式。将在下一篇中给出。

推荐阅读

  • SpringBoot 实战:一招实现结果的优雅响应
  • SpringBoot 实战:如何优雅的处理异常
  • SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
  • SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
  • SpringBoot 实战:优雅的使用枚举参数
  • SpringBoot 实战:优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
  • SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
  • SpringBoot 实战:JUnit5+MockMvc+Mockito 做好单元测试
  • SpringBoot 实战:加载和读取资源文件内容

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

关于函数的学习(二) — 传递参数

发表于 2021-11-27

传递实参

鉴于函数定义中可能包含多个形参,因此函数调用中也可能包含多个实参。向函数传递实参的方式很多,可使用位置实参,这要求实参的顺序与形参的顺序相同;也可使用关键字实参,其中每个实参都由变量名和值组成;还可使用列表和字典。

1.位置实参

你调用函数时,Python必须将函数调用中的每个实参都关联到函数定义中的一个形参。

最简单的关联方式是基于实参的顺序。这种关联方式被称为位置实参。

例如:

1
2
3
4
5
handlebars复制代码def describe_pet(animal_type, pet_name):
"""显示宠物的信息"""
print("\nI have a "+animal_type+".")
print("My "+animal_type+"'s name is "+pet_name.title()+".")
describe_pet('hamster', 'harry')

这个函数的定义表明,它需要一种动物类型和一个名字。

调用describe_pet()时,需要按顺序提供一种动物类型和一个名字。

输出结果:

1
2
handlebars复制代码I have a hamster.
My hamster's name is Harry.

1.1调用函数多次

你可以根据需要调用函数任意次。

例如:

1
2
3
4
5
6
handlebars复制代码def describe_pet(animal_type, pet_name):
"""显示宠物的信息"""
print("\nI have a "+animal_type+".")
print("My "+animal_type+"'s name is "+pet_name.title()+".")
describe_pet('hamster', 'harry')
describe_pet('dog', 'willie')

打印结果为:

1
2
3
4
handlebars复制代码I have a hamster.
My hamster's name is Harry.
I have a dog.
My dog's name is Willie.

调用函数多次是一种效率极高的工作方式。我们只需在函数中编写描述的代码一次,然后每当需要时,都可调用这个函数,并向它提供新宠物的信息。

在函数中,可根据需要使用任意数量的位置实参,Python将按顺序将函数调用中的实参关联到函数定义中相应的形参。

1.2位置实参的顺序很重要

使用位置实参来调用函数时,如果实参的顺序不正确,结果可能出乎意料:

1
2
3
4
5
handlebars复制代码def describe_pet(animal_type, pet_name):
"""显示宠物的信息"""
print("\nI have a "+animal_type+".")
print("My "+animal_type+"'s name is "+pet_name.title()+".")
describe_pet('harry', 'hamster')

打印结果:

1
2
handlebars复制代码I have a harry.
My harry's name is Hamster.

顺序不正确,结果不一致。所以要确认函数调用中实参的顺序与函数定义中形参的顺序一致。   

2.关键字实参

关键字实参是传递给函数的名称—值对。

你直接在实参中将名称和值关联起来了,因此向函数传递实参时不会混淆。

关键字实参让你无需考虑函数调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。

接着上面的例子:

1
2
3
4
5
handlebars复制代码def describe_pet(animal_type, pet_name):
"""显示宠物的信息"""
print("\nI have a "+animal_type+".")
print("My "+animal_type+"'s name is "+pet_name.title()+".")
describe_pet(animal_type='hamster', pet_name='harry')

打印结果:

1
2
handlebars复制代码I have a hamster.
My hamster's name is Harry.

关键字实参的顺序无关紧要,使用关键字实参时,务必准确地指定函数定义中的形参名。   

3.默认值

编写函数时,可给每个形参指定默认值。在调用函数中给形参提供了实参时,Python将使用指定的实参值;否则,将使用形参的默认值。

给形参指定默认值后,可在函数调用中省略相应的实参。

使用默认值可简化函数调用,还可清楚地指出函数的典型用法。

例如:

1
2
3
4
5
handlebars复制代码def describe_pet(pet_name, animal_type='dog'):
"""显示宠物的信息"""
print("\nI have a "+animal_type+".")
print("My "+animal_type+"'s name is "+pet_name.title()+".")
describe_pet(pet_name='willie')

这里修改了函数describe_pet()的定义,在其中给形参animal_type指定了默认值’dog’。

调用这个函数时,如果没有给animal_type指定值,Python将把这个形参设置为’dog’:

1
2
handlebars复制代码I have a dog.
My dog's name is Willie.

请注意,在这个函数的定义中,修改了形参的排列顺序。

由于给animal_type指定了默认值,无需通过实参来指定动物类型,因此在函数调用中只包含一个实参——名字。

现在,使用这个函数的最简单的方式是,在函数调用中只提供名字:

1
handlebars复制代码describe_pet('willie')

这个函数调用的输出与前一个示例相同。只提供了一个实参——‘willie’,这个实参将关联到函数定义中的第一个形参——pet_name。由于没有给animal_type提供实参,因此Python使用其默认值’dog’。

如果要描述的动物不是小狗,可使用类似于下面的函数调用:

1
handlebars复制代码describe_pet(pet_name='harry', animal_type='hamster')

由于显式地给animal_type提供了实参,因此Python将忽略这个形参的默认值。

注意:使用默认值时,在形参列表中必须先列出没有默认值的形参,再列出有默认值的形参。

这让Python依然能够正确地解读位置实参。   

4.等效的函数调用

鉴于可混合使用位置实参、关键字实参和默认值,通常有多种等效的函数调用方式:

1
handlebars复制代码def describe_pet(pet_name, animal_type='dog'):

基于这种定义,在任何情况下都必须给pet_name提供实参;

指定该实参时可以使用位置方式,也可以使用关键字方式。

如果要描述的动物不是小狗,还必须在函数调用中给animal_type提供实参。

下面对这个函数的所有调用都可行:

1
2
3
4
5
6
7
handlebars复制代码# 一条名为Willie的小狗
describe_pet('willie')
describe_pet(pet_name='willie')
# 一只名为Harry的仓鼠
describe_pet('harry', 'hamster')
describe_pet(pet_name='harry', animal_type='hamster')
describe_pet(animal_type='hamster', pet_name='harry')

这些函数调用的输出与前面的示例相同。

注意:使用哪种调用方式无关紧要,只要函数调用能生成你希望的输出就行。使用对你来说最容易理解的调用方式即可。   

5.避免实参错误

等你开始使用函数后,你提供的实参多于或少于函数完成其工作所需的信息时,将出现实参不匹配错误。

例如,如果调用函数describe_pet()时没有指定任何实参,结果将如何呢?

1
2
3
4
5
6
7
8
9
10
11
handlebars复制代码def describe_pet(animal_type, pet_name):
"""显示宠物的信息"""
print("\nI have a "+animal_type+".")
print("My "+animal_type+"'s name is "+pet_name.title()+".")
describe_pet()
Python发现该函数调用缺少必要的信息,而traceback指出了这一点:
Traceback (most recent call last):
File "pets.py", line 6, in <module>
describe_pet()
TypeError: describe_pet() missing 2 required positional arguments: 'animal_
type' and 'pet_name'

traceback指出了问题出在什么地方,让我们能够回过头去找出函数调用中的错误。

还会指出了导致问题的函数调用。

traceback指出该函数调用少两个实参,并指出了相应形参的名称。

如果这个函数存储在一个独立的文件中,我们也许无需打开这个文件并查看函数的代码,就能重新正确地编写函数调用。

Python读取函数的代码,并指出我们需要为哪些形参提供实参,这提供了极大的帮助。这也是应该给变量和函数指定描述性名称的另一个原因;如果你这样做了,那么无论对于你,还是可能使用你编写的代码的其他任何人来说,Python提供的错误消息都将更有帮助。

如果提供的实参太多,将出现类似的traceback,帮助你确保函数调用和函数定义匹配。

本文转载自: 掘金

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

leetcode 1616 Split Two Strin

发表于 2021-11-27

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」

描述

You are given two strings a and b of the same length. Choose an index and split both strings at the same index, splitting a into two strings: aprefix and asuffix where a = aprefix + asuffix, and splitting b into two strings: bprefix and bsuffix where b = bprefix + bsuffix. Check if aprefix + bsuffix or bprefix + asuffix forms a palindrome.

When you split a string s into sprefix and ssuffix, either ssuffix or sprefix is allowed to be empty. For example, if s = “abc”, then “” + “abc”, “a” + “bc”, “ab” + “c” , and “abc” + “” are valid splits.

Return true if it is possible to form a palindrome string, otherwise return false.

Notice that x + y denotes the concatenation of strings x and y.

Example 1:

  • Input: a = “x”, b = “y”
  • Output: true
  • Explaination: If either a or b are palindromes the answer is true since you can split in the following way:
  • aprefix = “”, asuffix = “x”
  • bprefix = “”, bsuffix = “y”
  • Then, aprefix + bsuffix = “” + “y” = “y”, which is a palindrome.

Example 2:

  • Input: a = “abdef”, b = “fecab”
  • Output: true

Example 3:

  • Input: a = “ulacfd”, b = “jizalu”
  • Output: true
  • Explaination: Split them at index 3:
  • aprefix = “ula”, asuffix = “cfd”
  • bprefix = “jiz”, bsuffix = “alu”
  • Then, aprefix + bsuffix = “ula” + “alu” = “ulaalu”, which is a palindrome.

Example 4:

  • Input: a = “xbdef”, b = “xecab”
  • Output: false

Note:

1
2
3
css复制代码1 <= a.length, b.length <= 10^5
a.length == b.length
a and b consist of lowercase English letters

解析

根据题意,给定两个长度相同的字符串 a 和 b。 选择一个索引并在同一索引处拆分两个字符串,将 a 拆分为两个字符串:aprefix 和 asuffix,其中 a = aprefix + asuffix,将 b 拆分为两个字符串:bprefix 和 bsuffix,其中 b = bprefix + bsuffix。 检查 aprefix + bsuffix 或 bprefix + asuffix 是否形成回文。

将字符串 s 拆分为 sprefix 和 ssuffix 时,允许 ssuffix 或 sprefix 为空。 例如,如果 s = “abc”,则 “” + “abc”、”a” + “bc”、”ab” + “c” 和 “abc” + “” 是有效的拆分。如果可以形成回文字符串则返回 True ,否则返回 False。请注意,x + y 表示字符串 x 和 y 的串联。

其实这道题使用贪心的思想还是比较容易的,加入我们有两个字符串 a 和 b ,并且用中线假定划分的位置,如下:

  • a:AB | CD | EF
  • b:GH | KJ | LM

如果 a 的前缀和 b 的后缀能组成回文,那么 AB 一定和 LM 能组成回文,那么只需要判断并且 CD 或者 KJ 是否是回文即可,即:AB+CD+LM 或者 AB+KJ+LM 这两种情况可以组成回文。将 b 的前缀和 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
python复制代码class Solution(object):
def checkPalindromeFormation(self, a, b):
"""
:type a: str
:type b: str
:rtype: bool
"""
return self.check(a, b) or self.check(b,a)

def check(self, a, b):
if len(a) == 1 : return True
i = 0
j = len(a) - 1
while i<=j and a[i]==b[j]:
i+=1
j-=1
if i>j:return True
return self.isPalindrome(a[i:j+1]) or self.isPalindrome(b[i:j+1])

def isPalindrome(self, s):
i = 0
j = len(s)-1
while i<=j and s[i]==s[j]:
i+=1
j-=1
return i>j

运行结果

1
2
erlang复制代码Runtime: 119 ms, faster than 66.67% of Python online submissions for Split Two Strings to Make Palindrome.
Memory Usage: 15.6 MB, less than 38.10% of Python online submissions for Split Two Strings to Make Palindrome.

原题链接:leetcode.com/problems/sp…

您的支持是我最大的动力

本文转载自: 掘金

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

面试官:说一下final关键字和final的4种用法?

发表于 2021-11-27

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

重要说明:本篇为博主《面试题精选-基础篇》系列中的一篇,查看系列面试文章请关注我。
Gitee 开源地址:gitee.com/mydb/interv…

final 定义

final 翻译成中文是“最终”的意思,它是 Java 中一个常见关键字,使用 final 修饰的对象不允许修改或替换其原始值或定义。
image.png
比如类被 final 修饰之后,就不能被其他类继承了,如下图所示:
image.png

final 的 4 种用法

final 的用法有以下 4 种:

  1. 修饰类
  2. 修饰方法
  3. 修饰变量
  4. 修饰参数

1.修饰类

1
2
3
java复制代码final class Animal {

}

image.png
说明:被 final 修饰的类不允许被继承,表示此类设计的很完美,不需要被修改和扩展。

2.修饰方法

1
2
3
4
5
java复制代码public class FinalExample {
public final void sayHi() {
System.out.println("Hi~");
}
}

image.png
说明:被 final 修饰的方法表示此方法提供的功能已经满足当前要求,不需要进行扩展,并且也不允许任何从此类继承的类来重写此方法。

3.修饰变量

1
2
3
4
java复制代码public class FinalExample {
private static final String MSG = "hello";
//......
}

image.png
说明:当 final 修饰变量时,表示该属性一旦被初始化便不可以被修改。

4.修饰参数

1
2
3
4
5
java复制代码public class FinalExample {
public void sayHi(final String name) {
System.out.println("Hi," + name);
}
}

image.png
说明:当 final 修饰参数时,表示此参数在整个方法内不允许被修改。

final 作用

使用 final 修饰类可以防止被其他类继承,如 JDK 代码中 String 类就是被 final 修饰的,从而防止被其他类继承,导致内部逻辑被破坏。
​

String 类部分源码如下:
image.png

总结

final 是 Java 中常见的一个关键字,被它修饰的对象不允许修改、替换其原始值或定义。final 有 4 种用法,可以用来修饰类、方法、变量或参数。

关注公众号「Java中文社群」查看面试系列文章。

本文转载自: 掘金

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

『Netty核心』Netty断线重连

发表于 2021-11-27

「这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战」。

点赞再看,养成习惯👏👏

一、前言

由于在通信层的网络连接的不可靠性,比如:网络闪断,网络抖动等,经常会出现连接断开。这样对于使用长连接的应用而言,当突然高流量冲击势必会造成进行网络连接,从而产生网络堵塞,应用响应速度下降,延迟上升,用户体验较差。

在通信层的高可用设计中,需要保活长连接的网络,保证通信能够正常。一般有两种设计方式:

  1. 利用TCP提供的连接保活特性
  2. 应用层做连接保活

二、TCP连接保活性的局限

TCP协议层面提供了KeepAlive的机制保证连接的活跃,但是其有很多劣势:

  • 该保活机制非TCP协议的标准,默认是关闭
  • 该机制依赖操作系统,需要进行系统级配置,不够灵活方便
  • 当应用底层传输协议变更时,将无法适用

由于以上的原因,绝大多数的框架、应用处理连接的保活性都是在应用层处理。目前的主流方案是心跳检测,断线重连。

在上一篇文章《『Netty核心』Netty心跳机制》已经介绍Netty是如何实现心跳检测机制的。

三、断线重连

断线重连是指由于网络波动造成用户间歇性的断开与服务器的连接,待网络恢复之后服务器尝试将用户连接到上次断开时的状态和数据。

当心跳检测发现连接断开后,为了保证通信层的可用性,仍然需要重新连接,保证通信的可靠。对于短线重连一般有两种设计方式比较常见:

  1. 通过额外的线程定时轮循所有的连接的活跃性,如果发现其中有死连接,则执行重连
  2. 监听连接上发送的断开事件,如果发送则执行重连操作

四、Netty断线自动重连实现

  1. 客户端启动连接服务端时,如果网络或服务端有问题,客户端连接失败,可以重连,重连的逻辑加在客户端。
  2. 系统运行过程中网络故障或服务端故障,导致客户端与服务端断开连接了也需要重连,可以在客户端处理数据的Handler的 channelInactive 方法中进行重连。
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
java复制代码public class NettyClient {

private String host;
private int port;
private Bootstrap bootstrap;
private EventLoopGroup group;

public static void main(String[] args) throws Exception {
NettyClient nettyClient = new NettyClient("localhost", 9000);
nettyClient.connect();
}

public NettyClient(String host, int port) {
this.host = host;
this.port = port;
init();
}

private void init() {
//客户端需要一个事件循环组
group = new NioEventLoopGroup();
//创建客户端启动对象
// bootstrap 可重用, 只需在NettyClient实例化的时候初始化即可.
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//加入处理器
ch.pipeline().addLast(new NettyClientHandler(NettyClient.this));
}
});
}

public void connect() throws Exception {
System.out.println("netty client start。。");
//启动客户端去连接服务器端
ChannelFuture cf = bootstrap.connect(host, port);
cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
//重连交给后端线程执行
future.channel().eventLoop().schedule(() -> {
System.err.println("重连服务端...");
try {
connect();
} catch (Exception e) {
e.printStackTrace();
}
}, 3000, TimeUnit.MILLISECONDS);
} else {
System.out.println("服务端连接成功...");
}
}
});
//对通道关闭进行监听
cf.channel().closeFuture().sync();
}
}

实现自定义的ChannelInboundHandlerAdapter

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复制代码public class NettyClientHandler extends ChannelInboundHandlerAdapter {
private NettyClient nettyClient;

public NettyClientHandler(NettyClient nettyClient) {
this.nettyClient = nettyClient;
}

/**
* 当客户端连接服务器完成就会触发该方法
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("HelloServer".getBytes(CharsetUtil.UTF_8));
ctx.writeAndFlush(buf);
}

//当通道有读取事件时会触发,即服务端发送数据给客户端
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("收到服务端的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务端的地址: " + ctx.channel().remoteAddress());
}

// channel 处于不活动状态时调用
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.err.println("运行中断开重连。。。");
nettyClient.connect();
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

本文转载自: 掘金

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

抽象类与接口

发表于 2021-11-27

一.抽象类

1.抽象类概述

在java中,一个没有方法体的方法的方法定义为抽象方法,而类中如果有抽象方法,该类必须定义为抽象类。

2. 抽象类的特点

  • 抽象类和抽象方法必须使用abstract关键字进行修饰

public abstract class 类名{}

public abstract void 方法名()

  • 抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
  • 抽象类只能参照多态的方式,通过子类对象实例化,这叫抽象类多态
  • 抽象类的子类只有两种可能,要么重写抽象类中的抽象方法,要么是抽象类。

3.抽象类的成员特点

  • 成员变量
    成员变量可以是变量,也可以是常量。
  • 构造方法
    抽象类有构造方法,但是不能实例化。而构造方法则主要用于子类访问父类数据的初始化。
  • 成员方法
    可以有抽象方法:限定子类必须完成某些动作

也可以有非抽象方法:提高代码复用性

二.接口

1.接口概述

接口就是一种公共的规范标准,只要符合规范标准,大家都可以通用。而Java中的接口更多的体现在对行为的抽象

2.接口的特点

  • 接口用关键字interface修饰

public interface 接口名{}

  • 类实现接口用implements表示

public class 类名 implements 接口名{}

  • 接口的实例化
    接口不能直接实例化,只能参照多态的方式,通过实现类对象实例化,这叫接口多态。
  • 接口的实现类
    和抽象类的子类一样,接口的实现类要么重写接口中的所有抽象方法,要么是抽象类。

3.接口的成员特点

  • 成员变量
    只能是常量,且具有默认的修饰符:public static fianal
  • 构造方法
    接口没有构造方法,因为接口主要是对行为进行抽象的,没有具体存在。一个类没有父类,则默认继承自object类。
  • 成员方法
    接口中的成员方法只能是抽象方法,因为其具有默认修饰符:public abstract

三.抽象类和接口的区别

1.语法区别

  • 成员区别
    成员区别
    抽象类 变量,常量;有构造方法;有抽象方法,也有非抽象方法
    接口 常量,抽象方法
  • 关系区别
关系区别
类与类 继承,单继承
类与接口 实现,可以单实现,也可以多实现
接口与接口 继承,单继承,多继承

2.设计理念区别

接口的设计目的,是对类的行为进行“约束”,也就是提供一种机制,可以强制要求不同的类也能具有相同的行为。接口只约束了行为的有无,而不对如何实现行为进行限制,所以接口大多都是对行为进行抽象。而抽象类的设计目的,则是为了代码复用。分析对象,提炼其内部共性形成抽象类,用以表示对象本质,所以抽象类是对类的抽象,包括类的属性和行为。因此,开发者在使用时,使用动机也不同。开发者继承抽象类是为了使用抽象类的属性和行为,开发者实现接口只是为了使用接口的行为。

总之,抽象类是对事物的抽象,而接口是对行为的抽象。

本文转载自: 掘金

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

RPC架构设计及在dubbo中的实现分析

发表于 2021-11-27
  1. 什么是RPC

RPC(Remote Procedure Call)是远程过程调用的简写,所谓远程过程调用是指应用通过网络像调用本地方法一样同步调用运行在其他计算机进程中的方法,也就是RPC是一种进程间通信的方式,RPC架构采用C/S默认架构,调用方就是客户端,服务提供方就是服务端。主要应用在分布式系统服务调用,分布式计算等场景。

  1. RPC框架设计

由上面的RPC介绍可以看出,RPC的核心组件有

  • 客户端(Client):服务调用方。
  • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
  • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
  • 服务端(Server):服务的真正提供者。
  • Network Service:底层传输,可以是 TCP 或 HTTP。

由这些组件相互协作完成跨进程的服务调用,基本流程如下
image1.png

  1. 客户端(client)调用以本地调用方式调用服务;
  2. 客户端stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
  3. 客户端stub找到服务地址,并将消息发送到服务端;
  4. 服务端stub收到消息后进行解码;
  5. 服务端stub根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给服务端stub;
  7. 服务端stub将返回结果打包成消息并发送至消费方;
  8. 客户端stub接收到消息,并进行解码;
  9. 客户端Stub得到服务端返回的解决。
  10. 客户端收到最终结果。

RPC框架的目标就是要把2~8这些步骤都封装起来,让用户对这些细节透明。
要实现上面的整个流程,基于java语言,我们解决的技术问题有:

1. 序列化

序列化是指在上面的步骤中,客户端Stub,服务端Stub在收到上层返回的数据之后,需要通过我网络模块传输到对方进程时,数据出于性能,及安全的考虑,需要转化成方便网络传输的数据格式。从应用数据到网络传输数据的转化为序列化,网络传输数据到应用数据的转化成为反序列化。序列化在java里有默认实现,但是在目前大部分应用高并发的性能要求下,我一般采用其他高性能的序列化实现方案,比如,Protobuf、Kryo、Hessian、Jackson 等

2. 网络传输

网络传输我们都是基于OSI网络层的TCP/UDP socket实现,但是IO处理基于目前高并发的性能需求,传统的阻塞式IO已经不适用,java也提供了NIO,以及Java7的NIO.2实现

3. 注册中心

注册中心的作用,就是一个RPC服务的一个管理中心,它有两个主要的职能,第一个就是服务注册,对于服务端对外提供的RPC服务,要想被其他调用知道,并调用,需要把它的调用方法,统一发送给注册中心注册,包括寻址ID,入参回参类型等;第二个就是注册中心需要把注册到注册中心的服务端方法告知客户端,这样客户端才能实现调用。目前应用比较广泛的框架有 zookeeper,zookeeper维护一张服务注册表,让多个服务提供者形成一个集群。客户端通过注册表获得服务端访问地址(IP+端口),实现服务调用

4. 代理模式

RPC框架一个主要的特征就是封装了通信细节,使RPC接口调用像调用本地方法一样,要实现这一目标,就要用到代理模式,由rpc接口代理类实现底层的通信逻辑。java的代理实现主要有两种方法,java5以后的动态代理,还有Cglib字节码代理。

整合上面的RPC核心流程以及基于java spring框架,我们可以方便的实现RPC服务开发,其详细实现流程如下:

rpc_time_chart.png

3.Dubbo RPC框架实现分析

Dubbo 框架简介

Dubbo是阿里开源的一款用于服务治理的RPC通信框架。Dubbo除了基础的RPC功能之外,还提供了服务治理能力,像,服务发现,服务降级,服务路由,集群容错。我们这里主要分析下Dubbo的RPC功能实现。

下面是Dubbo的架构:

image2.png

这是Dubbo中各个角色的交互图,我们可以看到在Dubbo中不同的组件有不同的角色:服务提供者Provider、服务消费者Consumer、注册中心Registry、服务运行容器Container以及监控中心Monitor。这些角色在服务启动过程中会按照如下的过程交互:

  1. 容器Container启动服务提供者。
  2. 服务提供者Provider启动并向注册中心Registry注册服务。
  3. 服务消费者Consumer启动并从注册中心Registry订阅服务。
  4. 服务消费者Consumer向服务提供者Provider发起请求。
  5. 服务提供者和消费者同步监控中心统计信息。

Dubbo RPC模块的实现

image3.png

dubbo-rpc模块中包含了多个包,其中dubbo-rpc-api包定义了一个具体的RPC实现需要实现的接口。通过明确定义RPC实现的接口,为自定义扩展提供了统一的API层。Dubbo的实现高度可扩展。

Dubbo支持协议扩展,这些扩展被放在dubbo-rpc模块的各个包中,比如:dubbp-rpc-dubbo包中包含了 dubbo协议 的扩展,dubbo-rpc-http包是HTTP协议实现的RPC扩展。dubbo-rpc模块中总共包含了13种不同协议实现的RPC扩展

我们可以基于技术栈的要求选用不同的RPC实现。官方建议如果是短报文的请求,dubbo协议是比较推荐的选择。下面,我们就以dubbo协议的RPC实现dubbo-rpc-dubbo为例,来看下Dubbo是如何对远程过程调用进行抽象的。

image4.png

上图是Dubbo RPC 一个完整调用简要流程图,其中主要环节有调用的入口,从一个代理对象开始,然后走负载均衡策略选出调用的Invoker对象,再经过Filter链之后封装请求数据,之后走网络层,服务端获取网络请求中的Invoker对象,再走服务端Filter链,接着调用实际的接口实现代理,处理业务逻辑,然后按原路返回调用结果。

4.Dubbo RPC 关键源码分析

image5.png

上图是dubbo rpc 完成一次RPC 方法调用所流经的主要组件

  1. 首先 服务代理对象Proxy持有了一个Invoker对象。然后触发invoke调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class InvokerInvocationHandler implements InvocationHandler {
   private final Invoker<?> invoker;
​
   public InvokerInvocationHandler(Invoker<?> handler) {
       this.invoker = handler;
  }
​
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      ~~~
       /**
       * 此处为rpc服务调用的开始
       * 此时的invoker 为MockClusterInoker
       * 在服务引用的时候包装
       */
       this.invoker.invoke(new RpcInvocation(method, args)).recreate()
       ~~~
  }
}
  1. 在invoke调用过程中,需要使用Cluster,Cluster负责容错。Cluster在调用之前会通过Directory获取所有可以调用的远程服务Invoker列表。
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
java复制代码public class MockClusterInvoker<T> implements Invoker<T> {
 ···
   public Result invoke(Invocation invocation) throws RpcException {
       Result result = null;
  // 从URL里面获取mock配置
       String value = this.directory.getUrl()
        .getMethodParameter(invocation.getMethodName(), "mock",Boolean.FALSE.toString()).trim();
       if (value.length() != 0 && !value.equalsIgnoreCase("false")) {
           if (value.startsWith("force")) {
             // 强制mock场景调用
               result = this.doMockInvoke(invocation, (RpcException)null);
          } else {
             // 失败进入mock场景
               try {
                   result = this.invoker.invoke(invocation);
              } catch (RpcException var5) {
                 //发生异常进入mock
                   result = this.doMockInvoke(invocation, var5);
              }
          }
      } else {
        //无mock场景直接调用
           result = this.invoker.invoke(invocation);
      }
       return result;
  }
  1. 获取到的 invoker列表 和通过Dubbo SPI获取到的 负载均衡器,Dubbo会调用 FailoverClusterInvoker的 doInvoke 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public abstract class AbstractClusterInvoker<T> implements Invoker<T> {
...
  public Result invoke(Invocation invocation) throws RpcException {
  // 检验节点是否销毁
       this.checkWhetherDestroyed();
       LoadBalance loadbalance = null;
  /**
  * 调用Directory -》 调用Rou
  * 生成invoker对象
  * 获取invoker列表
  ***/
       List<Invoker<T>> invokers = this.list(invocation);
       if (invokers != null && !invokers.isEmpty()) {
        // 获取第一个服务提供者的invoker的负载均衡策略,默认为random
           loadbalance = (LoadBalance)ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(((Invoker)invokers.get(0)).getUrl().getMethodParameter(RpcUtils.getMethodName(invocation), "loadbalance", "random"));
      }
       RpcUtils.attachInvocationIdIfAsync(this.getUrl(), invocation);
       return this.doInvoke(invocation, invokers, loadbalance);
  }
...
}
  1. 调用LoadBalance方法做负载均衡,最终选出一个可以调用的Invoker,调用invoker的invoke方法
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
java复制代码public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
       List<Invoker<T>> copyinvokers = invokers;
  // invoker空校验
       this.checkInvokers(invokers, invocation);
  // 获取重试次数,默认重试一次
       int len = this.getUrl().getMethodParameter(invocation.getMethodName(), "retries", 2) + 1;
       if (len <= 0) {
           len = 1;
      }
       RpcException le = null;
       List<Invoker<T>> invoked = new ArrayList(invokers.size());
       Set<String> providers = new HashSet(len);
​
       for(int i = 0; i < len; ++i) {
           if (i > 0) {
             // 有过一次失败之后,需要重新校验节点是否被销毁,invokkers是否为空
               this.checkWhetherDestroyed();
               copyinvokers = this.list(invocation);
               this.checkInvokers(copyinvokers, invocation);
          }
        // 重新负载均衡
           Invoker<T> invoker = this.select(loadbalance, invocation, copyinvokers, invoked);
           invoked.add(invoker);
           RpcContext.getContext().setInvokers(invoked);
           try {
             // 进行远程调用
               Result result = invoker.invoke(invocation);
               return var12;
          } catch (RpcException var17) {
               if (var17.isBiz()) {
                   throw var17;
              }
               le = var17;
          } catch (Throwable var18) {
               le = new RpcException(var18.getMessage(), var18);
          } finally {
               providers.add(invoker.getUrl().getAddress());
          }
      }
     ···
  }
}
​
  1. 上面的invoker对象实际上是InvokerWrapper类,内部包装了ProtocolFilterWrapper类,在这里会经过filter链和listener链的包装,在默认调用中会调用,ConsumerContextFilter,FutureFilter,MonitorFilter等过滤器,调用万filter链之后会调用listener链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
   Invoker<T> last = invoker;
   // 根据key和group获取自动激活的Filter
   List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
   if (!filters.isEmpty()) {
       // 这里是倒排遍历,因为只有倒排,最外层的Invoker才能使第一个过滤器
       for (int i = filters.size() - 1; i >= 0; i--) {
           final Filter filter = filters.get(i);
           // 会把真实的Invoker(服务对象ref)放到拦截器的末尾
           final Invoker<T> next = last;
           // 为每一个filter生成一个exporter,依次串起来
           last = new Invoker<T>() {
              ...
               @Override
               public Result invoke(Invocation invocation) throws RpcException {
                   // $-- 每次调用都会传递给下一个拦截器
                   return filter.invoke(next, invocation);
              }
              ...
          };
      }
  }
   return last;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class ConsumerContextFilter implements Filter {
   public ConsumerContextFilter() {
  }
​
   public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
   RpcContext.getContext().setInvoker(invoker).setInvocation(invocation).setLocalAddress(NetUtils.getLocalHost(), 0).setRemoteAddress(invoker.getUrl().getHost(), invoker.getUrl().getPort());
       if (invocation instanceof RpcInvocation) {
          ((RpcInvocation)invocation).setInvoker(invoker);
      }
​
       RpcResult var4;
       try {
           RpcResult result = (RpcResult)invoker.invoke(invocation);
           RpcContext.getServerContext().setAttachments(result.getAttachments());
           var4 = result;
      } finally {
           RpcContext.getContext().clearAttachments();
      }
​
       return var4;
  }
}
  1. 包装类结束之后再调用AbstractInvoker的invoke方法,在这个方法里调用了DubboInvoker类的doInvoker方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public abstract class AbstractInvoker<T> implements Invoker<T> {
public Result invoke(Invocation inv) throws RpcException {
      if (this.destroyed.get()) {
          ···
      } else {
          RpcInvocation invocation = (RpcInvocation)inv;
          invocation.setInvoker(this);
        ···
          try {
              return this.doInvoke(invocation);
          } catch (InvocationTargetException var6) {
              ···
          } catch (RpcException var7) {
              ···
          } catch (Throwable var8) {
              return new RpcResult(var8);
          }
      }
  }
}
  1. Dubbo协议相关部分的处理逻辑,包括请求的构建、传输。其下层就是Exchange层的内容
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
java复制代码public class DubboInvoker<T> extends AbstractInvoker<T> {
protected Result doInvoke(Invocation invocation) throws Throwable {
       RpcInvocation inv = (RpcInvocation)invocation;
       String methodName = RpcUtils.getMethodName(invocation);
       inv.setAttachment("path", this.getUrl().getPath());
       inv.setAttachment("version", this.version);
       ExchangeClient currentClient;
       if (this.clients.length == 1) {
           currentClient = this.clients[0];
      } else {
           currentClient = this.clients[this.index.getAndIncrement() % this.clients.length];
      }
​
       try {
           boolean isAsync = RpcUtils.isAsync(this.getUrl(), invocation);
           boolean isOneway = RpcUtils.isOneway(this.getUrl(), invocation);
           int timeout = this.getUrl().getMethodParameter(methodName, "timeout", 1000);
           if (isOneway) {
               boolean isSent = this.getUrl().getMethodParameter(methodName, "sent", false);
               currentClient.send(inv, isSent);
               RpcContext.getContext().setFuture((Future)null);
               return new RpcResult();
          } else if (isAsync) {
               ResponseFuture future = currentClient.request(inv, timeout);
               RpcContext.getContext().setFuture(new FutureAdapter(future));
               return new RpcResult();
          } else {
               RpcContext.getContext().setFuture((Future)null);
               return (Result)currentClient.request(inv, timeout).get();
          }
      } catch (TimeoutException var9) {
         ···
      } catch (RemotingException var10) {
         ···
      }
  }
}
  1. Dubbo的Exchange层和Transport层,主要是和网络传输、协议相关的内容

··· 忽略协议处理层内容

  1. 进入服务端ProtocolFilerWrapper类的invoker方法,开始服务端业务逻辑处理,这里首先还是要经已经包装好的filter链和listener链,最后会到真实方法的调用。
1
2
3
4
5
6
7
8
9
10
java复制代码@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  try {
      Result result = invoker.invoke(invocation);
      // 不会处理GenericService类型invoker的异常
      if (result.hasException() && GenericService.class != invoker.getInterface()) {
      ····
      }
  }
}
  1. 经过InvokerWrapper包装类的方法。

11.在AbstractProxyInvoker.invoke 中会执行doInvoker方法。这里doInvoker的实现一般默认实现有两个JdkProxyFactory,JavassistProxyFactory,这里实际是获取实现类的代理,并执行代理类的业务方法,返回结果。

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
java复制代码/**
* JdkRpcProxyFactory
*/
public class JdkProxyFactory extends AbstractProxyFactory {
​
   @Override
   @SuppressWarnings("unchecked")
   public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
       return (T) Proxy.newProxyInstance(invoker.getInterface().getClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
  }
​
   @Override
   public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
       return new AbstractProxyInvoker<T>(proxy, type, url) {
           @Override
           protected Object doInvoke(T proxy, String methodName,
                                     Class<?>[] parameterTypes,
                                     Object[] arguments) throws Throwable {
               Method method = proxy.getClass().getMethod(methodName, parameterTypes);
               return method.invoke(proxy, arguments);
          }
      };
  }
​
}
​

总结下,整个Dubbo Rpc 服务调用的整个流程,在服务的引用过程中,Dubbo把Invoker层层包装为一个代理,用户使用这个代理对象像调用本地服务一样调用。Dubbo将代理的Invoker层层剥开,每层完成特定的功能,最终协调一致,完成了整个服务的调用。

本文转载自: 掘金

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

几条最常用的git命令

发表于 2021-11-27

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

介绍

git是版本控制工具,平常开发中的使用还是非常普遍的,命令非常多,但是常用的就那些。

image.png

常用命令

本地操作

  • 查看代码修改
    命令:git status

例:

1
2
3
4
5
6
7
8
sh复制代码>git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: src/main/java/com/wqy/TaskOperation.java

查看状态,会在结果中打印出当前分支、修改内容

  • 将变更加入暂存区
    命令:git add .

“.”的目的是将当前目录下内容全部放入暂存区
例:

1
sh复制代码>git add .

分支操作

  • 查看当前分支
    命令:git branch -v

例:

1
2
3
4
5
6
sh复制代码>git branch -v
* dev 8b5310f Merge branch 'feature/name' into dev
feature/wwa b7bee2 feat: <E6><8E><A8><E8><8D><90>-<E5><8F><91><E7><8E><B0><E9><A1><B5> - <E6><A0>
feature/name 1b9c1a4 feat: <E6><8E><A8><E8><8D><90><E5><AE><A1><E6><A0><B8>
master 162d523 1.<E8><AE><B2><E8><82><A1><E5><A0><82><E6><95><B0><E6><8D><AE><E5><90>
pdt a133379 feat: <E5><A2><9E><E5><8A><A0>pdt<E5><88><86><E6><94><AF>

查看分支结果会打印出所有分支以及上次操作记录

  • 切换分支
    命令:git checkout [分支名称]

例:

1
2
3
4
5
6
sh复制代码>git checkout feature/name
Switched to branch 'feature/name'
D src/main/java/com/wqy/service/package-info.java
A src/test/java/com/wqy/mapper/NameMapperTest.java
A src/test/java/com/wqy/util/RedisListTest.java
Your branch is up to date with 'origin/feature/name'.

切换分支,结果会打印出分支改变内容

  • 新建分支
    命令:git branch [新分支名称]

例:

1
sh复制代码>git branch aa

新建分支,默认根据当前分支创建新分支

stash操作

  • 保存当前代码
    命令:git stash

例:

1
2
sh复制代码>git stash
Saved working directory and index state WIP on dev: 8b533kk Merge branch 'feature/name' into dev
  • 查看stash集合
    命令:git stash list

例:

1
2
3
4
sh复制代码>git stash list
stash@{0}: WIP on dev: 8b3330f Merge branch 'feature/name' into dev
stash@{1}: WIP on name: d161368 feat: <E5><8F><91><E7><8E><B0><E9><A1><B5>-<E6>B4><E6><96><B0><E9><95><BF><E7><9F><A2><E5><BC><95>
stash@{2}: On dev: Uncommitted changes before Update at 2021/11/25 10:19

列表会打印出全部分支的stash内容

  • 恢复stash内容
    命令:git stash pop stash@{编号}

恢复stash内容,并删除此stash

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sh复制代码>git stash pop stash@{2}
Auto-merging src/main/resources/generator.properties
Removing src/main/java/com/wqy/service/package-info.java
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: src/test/java/com/wqy/mapper/QQMapperTest.java
new file: src/test/java/com/wqy/mapper/AMapperTest.java
new file: src/test/java/com/wqy/util/RedisListTest.java

Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

deleted: src/main/java/com/wqy/service/package-info.java

Dropped stash@{2} (30feafb08ef0efaa58995311417ecfee86de05)

恢复操作,会在结果打印出改变内容

日常开发常用组合操作

经常会出现在不同分支上面写业务代码,当需要切换分支工作之前,代码还没写完整,不想提交,就要将代码保存到stash内,等再次切换回分支时恢复之前代码,不然一不注意就把代码带到切换后的分支上面了。

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
sh复制代码# 切换前查看分支
>git status
On branch dev
Your branch is up to date with 'origin/dev'.

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: src/main/java/com/wqy/TaskOperation.java
# 将内容放入本地暂存区
>git add .
# 添加到stash
>git stash
Saved working directory and index state WIP on dev: 8b533kk Merge branch 'feature/name' into dev
# 切换分支
>git checkout feature/name
Switched to branch 'feature/name'
D src/main/java/com/wqy/service/package-info.java
A src/test/java/com/wqy/mapper/NameMapperTest.java
A src/test/java/com/wqy/util/RedisListTest.java
Your branch is up to date with 'origin/feature/name'.
# 切换分支,切换回上个分支
>git checkout dev
Switched to branch 'dev'
D src/main/java/com/wqy/service/package-info.java
A src/test/java/com/wqy/mapper/QQMapperTest.java
A src/test/java/com/wqy/mapper/AMapperTest.java
A src/test/java/com/wqy/util/RedisListTest.java
Your branch is up to date with 'origin/dev'.
# 查看stash集合
>git stash list
stash@{0}: WIP on dev: 8b3330f Merge branch 'feature/name' into dev
stash@{1}: WIP on name: d161368 feat: <E5><8F><91><E7><8E><B0><E9><A1><B5>-<E6>B4><E6><96><B0><E9><95><BF><E7><9F><A2><E5><BC><95>
stash@{2}: On dev: Uncommitted changes before Update at 2021/11/25 10:19
# 恢复stash内容
>git stash pop stash@{2}

参考

  • 猴子都能懂的GIT入门
  • 廖雪峰Git教程

本文转载自: 掘金

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

1…158159160…956

开发者博客

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