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

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


  • 首页

  • 归档

  • 搜索

漫谈企业级SaaS的多租户设计

发表于 2020-04-22

企业级SaaS市场近几年在每个细分领域都涌现出了一批玩家。从技术角度看,不同的领域、不同的SaaS产品,必定有着同样的架构内核,其中最关键的便是对于多租户(Multi-Tenancy)的支持。对广大企业来说,引入SaaS产品本质上就是对互联网服务的租赁,因而多租户便必然是SaaS的天然属性之一,也是其与传统互联网应用架构设计的重要差异之一。在SaaS架构的成熟度演进过程中,其核心路线便是如何实现多租户,也就是说,SaaS成熟度的高低,很大程度上取决于如何实现多租户的支持。一 多租户技术的核心关注点

多租户在技术实现层面目前并没有既定的规范,不仅细节多,每处细节的实现方式也多种多样。如何落地,一方面取决于当前研发团队现有的技术储备、技术选型、团队资本实力、所处行业或客户特点(比如金融行业对数据安全会有更高要求),另一方面也与当前的技术发展息息相关,云厂商的崛起和云原生时代的到来,也深刻影响着包括SaaS在内的软件构建的方法。但常规来说,真正的SaaS应用往往需要满足以下两点:* 单实例

  • 多租户

单实例意味着系统资源层面的共享,多租户意味着应用逻辑层面的隔离。所以如何平衡好这两点,才是SaaS应用多租户设计的核心关注点。经典的分布式服务架构天然解决了互联网应用的三高问题(高并发、高性能、高可用),这也是企业SaaS发展中后期即将面临的问题,下面我们来分析下如何在该架构下去设计与实现多租户SaaS应用。二 多租户的实现

从资源共享的层面看,从share nothing到share everything,在天平的任何一个点上都可以支撑多租户。但正如我们前文所说,SaaS架构首要考虑的目标便是单实例,只有单实例才能将成本尽可能降低,产品才会有规模效应。所以所谓共享和隔离,在经典架构下又会聚焦为一点,即如何对不同租户进行资源层面的隔离。三 关于资源

谈到资源,我们可能会想到CPU、内存、磁盘、网络带宽等,但如此多类型的资源,从其特征上又可以归为两类,即存储资源和计算资源。换句话说,SaaS系统在技术本质上也可以认为就是分布式存储和分布式计算的融合。在多租户的实现中,往往更关键的是对于存储资源的处理,计算资源一般只在必要情况下才会考虑,我认为这主要是和存储的“有状态性”有关。下面我们以一些典型场景为例,具体分析一下多租户的设计该如何着手。四 存储资源的隔离

隔离存储资源概括来说可以用一个词来解决:命名空间。以数据库为例,我们只需要在每条租户的记录上,记下对应租户的标识即可。一般来说,不考虑分库分表的情况下,我们逻辑上会在同一个Schema中,存储所有租户的数据。这就要求每张表都会有一个tenant_id字段,也即每条记录都携带了它的“命名空间”——租户标识。再以常用的NoSQL方案Redis为例,一般来说也是在同一个分布式集群中存储所有租户数据,那么很明显在key上携带租户标识即可。所以无论何种存储,思路都是相通的,而且处理起来相对简单粗暴。但这里我想着重强调的是,在工程层面我们应当将这种约定在底层框架里做统一处理。比如在租户上下文中的所有SQL语句,应当都要携带where tenant_id=?这个条件,才能保证逻辑正确,我们很难想象在代码从零到十万、百万行的过程中,所有人都自始至终都牢记这个规则。那么类似场景下,我们就可以通过AOP技术将多租户相关的逻辑切出来进行统一处理,比如在Java中,我们可以定义@TenantContextAware注解,以声明而非编码的方式在需要的地方做对应的租户信息获取及传递处理。那么又如何保证开发者也牢记这个规则呢,由于多租户是SaaS的天然属性,我们可以反其道而行之,默认支持多租户逻辑,同时定义@TenantContextUnaware注解,在不需要多租户的地方进行例外声明,这就大大降低了开发团队的负担。同理,类似Redis Key的维护,也建议定义统一的KeyGeneratePolicy来维护。五 计算资源的隔离

隔离计算资源的方法也可以用一个词来概括,那就是亲和性,简单来说就是租户与集群计算资源的亲和性设计。)计算与存储除了“状态”方面的差异外,还有一个非常重要的区别,计算的财务成本往往远高于存储,比如我们一台虚拟主机上可能只允许数百个线程同时处理请求。正因为如此,宝贵的计算资源在非必要的情况下一般不会再进行细粒度的隔离,例如我们一般不会在运行时只允许某租户的请求只提交给指定工作线程处理。另外一方面,计算资源发生倾斜的后果,往往比存储要严重的多,如同木桶效应般,直接且显著地影响整个集群的服务能力。但特定场景下较粗粒度的隔离,有时候还是非常必要的。比如为了减少系统故障时租户的影响范围,我们可能会将租户的请求哈希后提交给不同的线程池处理,因为这种情况下,反压将会产生全局的影响。另外我们也可能在特定场景下进行进程、集群层面的隔离。总的来说,对计算资源进行隔离,没有既定的模式与套路,而且往往需要高超的资源操作水平,一般不到万不得已不建议实施。同样地,如果一定要实施,那么也应当以组件化的方式进行,保证业务逻辑的纯粹性。通过上述对存储和计算资源的隔离处理,我们的SaaS架构整体看起来将会是下图这个结构。)在这里用一个表格就一些要点对两种手段做个简单的对比,便于大家更直观地理解。六 单实例架构的扩展

面向企业的SaaS服务往往还有一些特点可能会引出一些高阶需求,而独立的单实例架构有时候并不能完全满足这些高阶需求。此时就需要对原有架构进行扩展,以实例级别的整体隔离,配合租户级的请求分流手段,为SaaS带来资源、软件版本等多方面的隔离。但需要注意的是,对单实例架构的扩展,并没有降低其架构成熟度,与我们文中一直在强调的单实例架构理念并不冲突。比如我们往往会根据企业客户的规模和特点对其保障等级进行分级,那如何进一步合理地隔离资源,保障不同级别客户的使用体验,也是一个无法逃避的问题。这种情况下,我们就可以考虑将这类客户的某些资源实施特殊的保护性隔离,或者干脆将单实例架构扩展成为多实例架构,将客户分流到不同保障级别的资源池。如果有个别客户体量远超其他客户,那么在成本允许的情况下,我们甚至可以考虑为其建设专属资源池,对其进行重点保障,这种级别的保护并不意味着牺牲了小体量客户的体验,相反,往往大体量客户才更容易发生一些影响稳定性的突发事件,所以可以认为是一种多赢的操作。另外,SaaS往往能给客户带来更快的特性交付,但这种快速交付很可能带来不佳的使用体验,比如严重BUG的存在。那么这个时候,如果我们的系统是多实例架构,那么就可以很轻易地实现灰度发布,从而使得特性交付的过程更加稳健,也是对品牌形象的一种保护。七 总结

在实际开发中,我们往往容易忽视早期对类似多租户等基础层面的系统性规划与设计,导致后期研发、维护成本持续增加,甚至在面临一些新的商业机会的时候,无法灵活应对。好的架构则能将这些本质的特征透明化,做到业务层无感,从而提高研发效率。在企业SaaS的多租户架构设计环节,我们无法罗列或预判所有可能,在不同的技术选型下的多租户实现也有很大差异,我们应当着重去发掘其技术本质,从计算与存储资源的隔离层面,系统地规划与架构,做好基础组件的建设与沉淀。只有抛开现象去归纳总结相关本质方法,才能以不变应万变。关于作者

张晋。网易智慧企业架构师,负责旗下多款SaaS产品的架构、基础设施建设等相关工作,有丰富的C端、B端产品研发经验。目前主要关注企业级产品的技术架构、研发管理等方面。更多技术干货,欢迎关注“网易智慧企业技术+”。听网易CTO讲述前沿观察,看最有价值技术干货,学网易最新实践经验。网易智慧企业技术+,陪你从思考者成长为技术专家。

本文转载自: 掘金

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

Java 8 到 Java 14,改变了哪些你写代码的方式?

发表于 2020-04-22

前几天,JDK 14 正式发布了,这次发布的新版本一共包含了16个新的特性。

其实,从Java8 到 Java14 ,真正的改变了程序员写代码的方式的特性并不多,我们这篇文章就来看一下都有哪些。

Lambda表达式

Lambda 表达式是 Java 8 中最重要的一个新特性,Lambda 允许把函数作为一个方法的参数。

lambda 表达式的语法格式如下:

1
2
3
复制代码(parameters) -> expression
或
(parameters) ->{ statements; }

如以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码// 1. 不需要参数,返回值为 5  
() -> 5

// 2. 接收一个参数(数字类型),返回其2倍的值
x -> 2 * x

// 3. 接受2个参数(数字),并返回他们的差值
(x, y) -> x – y

// 4. 接收2个int型整数,返回他们的和
(int x, int y) -> x + y

// 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void)
(String s) -> System.out.print(s)

Lambda表达式具有简洁、容易进行并行计算、是未来的编程趋势等优点,但同时也会带来调试困难,新人理解成本高等缺点。

Streams API

除了Lambda 表达式外,Java 8中还引入了Stream API,这使得Java终于进入到函数式编程的行列中来了。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

如下图,就是通过Stream API对集合进行了一系列的操作:

1
2
3
复制代码List<String> strings = Arrays.asList("Hollis", "HollisChuang", "hollis", "Hello", "HelloWorld", "Hollis");
Stream s = strings.stream().filter(string -> string.length()<= 6).map(String::length).sorted().limit(3)
.distinct();

而且,Stream还支持并行流,在性能上比传统的for循环要好很多。(详细用法:《Java 8中处理集合的优雅姿势——Stream》)

从Lambda表达式和Stream API问世至今,已经有6年的时间了,相信很多人已经在工作中使用过这些特性了。

虽然对于这两种语法的使用,很多人持有不同的看法,但是作者还是认为这个功能是十分好用的,只是在日常写代码的时候不要过分”炫技”使用超长的流式操作,代码可读性不要太低就可以了。

新的日期和时间 API

在Java 8之前,日期时间 API 存在诸多问题,如:Date非线程安全、java.util和java.sql的包中都有日期类、日期类并不提供国际化,没有时区支持。

所以,Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。

新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。

常见操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);

// 时间比较
LocalDate today = LocalDate.now();
LocalDate date1 = LocalDate.of(2014, 01, 14);
if(date1.equals(today)){}

// 时间增加
LocalTime time = LocalTime.now();
LocalTime newTime = time.plusHours(2); // adding two hours

但是说实话,Java8中的时间API作者日常工作中用的比较少,主要是有很多历史代码,还是依赖Date等类型,使用新的API就要面临互相转换问题。

本地变量类型推断

在Java 10之前版本中,我们想定义定义局部变量时。我们需要在赋值的左侧提供显式类型,并在赋值的右边提供实现类型:

1
复制代码MyObject value = new MyObject();

在Java 10中,提供了本地变量类型推断的功能,可以通过var声明变量:

1
复制代码var value = new MyObject();

本地变量类型推断将引入“var”关键字,而不需要显式的规范变量的类型。

其实,所谓的本地变量类型推断,也是Java 10提供给开发者的语法糖。虽然我们在代码中使用var进行了定义,但是对于虚拟机来说他是不认识这个var的,在java文件编译成class文件的过程中,会进行解糖,使用变量真正的类型来替代var(我反编译了Java 10的本地变量类型推断)

Switch 表达式

在JDK 12中引入了Switch表达式作为预览特性。并在Java 13中修改了这个特性,引入了yield语句,用于返回值。

而在之后的Java 14中,这一功能正式作为标准功能提供出来。

在以前,我们想要在switch中返回内容,还是比较麻烦的,一般语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码int i;
switch (x) {
case "1":
i=1;
break;
case "2":
i=2;
break;
default:
i = x.length();
break;
}

在JDK13中使用以下语法:

1
2
3
4
5
6
7
8
复制代码int i = switch (x) {
case "1" -> 1;
case "2" -> 2;
default -> {
int len = args[1].length();
yield len;
}
};

或者

1
2
3
4
5
6
7
8
复制代码int i = switch (x) {
case "1": yield 1;
case "2": yield 2;
default: {
int len = args[1].length();
yield len;
}
};

在这之后,switch中就多了一个关键字用于跳出switch块了,那就是yield,他用于返回一个值。和return的区别在于:return会直接跳出当前循环或者方法,而yield只会跳出当前switch块。

Text Blocks

Java 13中提供了一个Text Blocks的预览特性,并且在Java 14中提供了第二个版本的预览。

text block,文本块,是一个多行字符串文字,它避免了对大多数转义序列的需要,以可预测的方式自动格式化字符串,并在需要时让开发人员控制格式。

我们以前从外部copy一段文本串到Java中,会被自动转义,如有一段以下字符串:

1
2
3
4
5
复制代码 <html>
<body>
<p>Hello, world</p>
</body>
</html>

将其复制到Java的字符串中,会展示成以下内容:

1
2
3
4
5
复制代码"<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";

即被自动进行了转义,这样的字符串看起来不是很直观,在JDK 13中,就可以使用以下语法了:

1
2
3
4
5
6
7
复制代码"""
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";

使用”””作为文本块的开始符合结束符,在其中就可以放置多行的字符串,不需要进行任何转义。看起来就十分清爽了。

如常见的SQL语句:

1
2
3
4
5
复制代码String query = """
SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
WHERE `CITY` = 'INDIANAPOLIS'
ORDER BY `EMP_ID`, `LAST_NAME`;
""";

看起来就比较直观,清爽了。

Records

Java 14 中便包含了一个新特性:EP 359: Records,

Records的目标是扩展Java语言语法,Records为声明类提供了一种紧凑的语法,用于创建一种类中是“字段,只是字段,除了字段什么都没有”的类。通过对类做这样的声明,编译器可以通过自动创建所有方法并让所有字段参与hashCode()等方法。这是JDK 14中的一个预览特性。

使用record关键字可以定义一个记录:

1
复制代码record Person (String firstName, String lastName) {}

record 解决了使用类作为数据包装器的一个常见问题。纯数据类从几行代码显著地简化为一行代码。(详见:Java 14 发布了,不使用”class”也能定义类了?还顺手要干掉Lombok!)

总结

以上,就是从Java 8 到 Java 14中,新推出的可能会影响开发人员写代码的方式的一些主要特性。

不知道大家有没有发现,最近几个版本中推出的一些功能,使得Java和Kotlin等语言越来越像了…

新的这些功能,确实在一定程度上可以简化一些代码,使得开发过程中更加高效,但是说实话,还没有好到足够吸引广大开发者抛弃Java 8进行大规模迁移!

还是那句话:版本任你发,我用Java 8;但是新特性我们还是要去了解下的。

本文转载自: 掘金

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

在java 8 stream表达式中实现if/else逻辑

发表于 2020-04-22

简介

在Stream处理中,我们通常会遇到if/else的判断情况,对于这样的问题我们怎么处理呢?

还记得我们在上一篇文章lambda最佳实践中提到,lambda表达式应该越简洁越好,不要在其中写臃肿的业务逻辑。

接下来我们看一个具体的例子。

传统写法

假如我们有一个1 to 10的list,我们想要分别挑选出奇数和偶数出来,传统的写法,我们会这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    public void inForEach(){
List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

ints.stream()
.forEach(i -> {
if (i.intValue() % 2 == 0) {
System.out.println("i is even");
} else {
System.out.println("i is old");
}
});
}

上面的例子中,我们把if/else的逻辑放到了forEach中,虽然没有任何问题,但是代码显得非常臃肿。

接下来看看怎么对其进行改写。

使用filter

我们可以把if/else的逻辑改写为两个filter:

1
2
3
4
5
6
复制代码List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

Stream<Integer> evenIntegers = ints.stream()
.filter(i -> i.intValue() % 2 == 0);
Stream<Integer> oddIntegers = ints.stream()
.filter(i -> i.intValue() % 2 != 0);

有了这两个filter,再在filter过后的stream中使用for each:

1
2
复制代码        evenIntegers.forEach(i -> System.out.println("i is even"));
oddIntegers.forEach(i -> System.out.println("i is old"));

怎么样,代码是不是非常简洁明了。

总结

lambda表达式需要尽可能的简洁,我们可以用stream的filter来替代if/else业务逻辑。

本文的例子github.com/ddean2009/l…

欢迎关注我的公众号:程序那些事,更多精彩等着您!
更多内容请访问 www.flydean.com

本文转载自: 掘金

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

Argo 项目入驻 CNCF,一文解析 Kubernetes

发表于 2020-04-21

1.png

作者 | 遥鹭、郡宝

**导读:**近期,CNCF 技术监督委员会(Technical Oversight Committee,TOC)投票决定接受 Argo 作为孵化级别的托管项目。作为一个新加入的项目,Argo 主要关注于 Kubernetes 原生的工作流,持续部署等方面。

Argo 项目是一组 Kubernetes 原生工具集合,用于运行和管理 Kubernetes 上的作业和应用程序。它提供了一种在 Kubernetes 上创建工作和应用程序的三种计算模式 – 服务模式、工作流模式和基于事件的模式 – 的简单组合方式。所有的 Argo 工具都实现为控制器和自定义资源。

2.png

阿里云容器服务是国内早期使用 argo workflow 的团队之一。在落地生产过程中,解决了大量性能瓶颈,并且开发了较多功能回馈给社区,团队成员也是 Argo 项目 Maintainer 之一。

Argo 项目:面向 K8s 的工作流

DAG (Directed acyclic graph,有向无环图)是一个典型计算机图论问题,可以用来模拟有相互依赖关系的数据处理任务,比如音视频转码,机器学习数据流,大数据分析等。

Argo 最早是通过 workflow 在社区闻名。Argo Workflow 的项目名称就是 Argo, 是 Argo 组织最初的项目。Argo Workflow 专注于 Kubernetes Native Workflow 设计,拥有声明式工作流机制,能够通过 CRD 的模式完全兼容 Kubernetes 集群,每个任务通过 Pod 的形式运行,Workflow 提供 DAG 等依赖拓扑,并且能够通过 Workflow Template CRD 实现多个 Workflow 之间的组合与拼接。

3.png

上图就是一个典型的 DAG 结构,Argo Workflow 可以根据用户提交的编排模板,很容易的构建出一个有相互依赖关系的工作流。Argo Workflow 就可以处理这些依赖关系,并且按照用户设定的顺序依次运行。

Argo CD 是另一个最近比较知名的项目。 Argo CD 主要面向 Gitops 流程,解决了通过 Git 一键部署到 Kubernetes 的需求,并且能够根据版本标识快速跟踪,回滚。Argo CD 还提供了多集群部署功能,能够打通多个集群之间同一应用部署问题。

4.png

Argo Event 提供基于事件依赖关系的声明式管理,以及基于各种事件源的 Kubernetes 资源触发器。 Argo Events 的常见用法是触发 Argo 工作流并为使用 Argo CD 部署的长期服务生成事件。

Argo Rollout 是为了解决多种部署形式而诞生的项目。Argo Rollout 能实现多种灰度发布方式,同时结合 Ingress, Service Mesh 等方式完成流量管理与灰度测试。

Argo 各个子项目既可以单独使用,也可以结合使用。一般而言,结合使用多个子项目能够发挥 Argo 更大的能力,并且实现更多的功能。

使用 Argo 中遇到的问题与解决方法

阿里云最早落地的是 Argo Workflow,在使用 Argo Workflow 时第一个问题就是权限管理。Argo Workflow 每一个具体的任务都是通过 Pod 来执行,同时有一个 sidecar 容器来监听主任务的进行。这里的 sidecar 监听方式是通过 mount docker.sock 来实现,这就绕过了 Kubernetes APIServer RBAC 机制,无法实现对于用户权限的精确控制。我们与社区一起合作开发,实现了 Argo Kubernetes APIServer Native Executor 功能,sidecar 能够通过 service account 监听 APIServer 来获取到主容器的动态与信息,实现了 Kubernetes RBAC 的支持与权限收敛。

Argo Workflow 在 DAG 解析过程中,每一步都会根据 Workflow label 来扫描所有的 Pod 状态,以此来决定是否需要进行下一步的动作。但是每一次扫描都是串行执行,当集群中 Workflow 较多的时候,就会出现扫描速度缓慢,工作流的任务长时间等待的现象。基于此我们开发了并行扫描功能,将所有的扫描动作使用 goroutine 并行化,极大的加速了工作流执行效率。将原有需要 20 小时运行的任务,降低到 4 小时完成。此功能已经回馈给社区,并且在 Argo Workflow v2.4 版本发布。

在实际生产中,Argo Workflow 执行的步数越多,占用的空间越多。所有的执行步骤均记录在 CRD Status 字段里面。当任务数量超过 1000 步的时候,就会出现单个对象过大,无法存储进入 ETCD,或者会因为流量过大,拖垮 APIServer。我们与社区合作开发了状态压缩技术,能够将 Status 进行字符串压缩。压缩后的 Status 字段大小仅为原来大小的 20 分之一,实现了 5000 步以上的大型工作流运行。

Argo 在基因数据处理场景的落地实践

AGS(阿里云基因计算服务)主要应用于基因组测序二级分析,通过 AGS 加速 API 只需要 15 分钟即可完成一个 30X WGS 的基因比对、排序、去重、变异检测全流程,相比经典流程可加速 120 倍,比目前全球最快的 FPGA/GPU 方案仍能提速 2-4 倍。

通过分析个体基因序列的突变机制,可为遗传病检测、肿瘤筛查等提供有力支撑,未来将在临床医学和基因诊断方面发挥巨大作用。人类全基因组有约 30 亿个碱基对,一个 30X 的 WGS 测序数据量大约在 100GB。AGS 在计算速度、精准度、成本、易用性、与上游测序仪的整合度上具有极大优势,同时适用于 DNA 的 SNP/INDEL 以及 CNV 结构变异检测,以及 DNA/RNA 病毒检测等场景。

5.png

AGS 工作流是基于 argo 实现的,为 Kubernetes 提供容器化的本地工作流程。工作流程中的每个步骤都定义为容器。

工作流引擎是作为 Kubernetes CRD(自定义资源定义)实现的。 因此,可以使用 kubectl 管理工作流,并与其它 Kubernetes 服务本地集成,例如 Volumes、Secrets 和 RBAC。 工作流控制器提供完整的工作流程功能,包括参数替换,存储,循环和递归工作流程。

阿里云在基因计算场景下使用 Argo Workflow 在 Kubernetes 集群上运行数据处理分析业务,能够支持超过 5000 步以上的大型工作流,且能够比传统数据处理方式加速百倍。通过定制化的 Workflow 引擎,极大的便捷了基因数据处理的效率。

作者简介

陈显鹭,阿里云技术专家,深耕 Docker&Kubernetes 多年,是 Docker 多个项目的 Contributor, Kubernetes Group Member,《自己动手写 Docker》作者。 专注于容器技术的编排与基础环境研究。爱好折腾源代码、热爱开源文化并积极参与社区开源项目的研发。

郡宝,Kubernetes 项目贡献者,Kubernetes 和 Kubernetes-sigs 社区成员。在容器、K8s 领域有多年的实践经验,目前就职于阿里巴巴云计算容器服务团队,主要研究方向有容器存储、容器编排 、 AGS 产品等领域.

AGS 试用链接:help.aliyun.com/document_de…

“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术圈。”

本文转载自: 掘金

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

Go 每日一库之 gron

发表于 2020-04-21

简介

gron是一个比较小巧、灵活的定时任务库,可以执行定时的、周期性的任务。gron提供简洁的、并发安全的接口。我们先介绍gron库的使用,然后简单分析一下源码。

快速使用

先安装:

1
复制代码$ go get github.com/roylee0704/gron

后使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package main

import (
"fmt"
"sync"
"time"

"github.com/roylee0704/gron"
)

func main() {
var wg sync.WaitGroup
wg.Add(1)

c := gron.New()
c.AddFunc(gron.Every(5*time.Second), func() {
fmt.Println("runs every 5 seconds.")
})
c.Start()

wg.Wait()
}

gron的使用比较简单:

  • 首先调用gron.New()创建一个管理器,这是一个定时任务的管理器;
  • 然后调用管理器的AddFunc()或Add()方法向它添加任务,在启动时添加也是可以的,见下文分析;
  • 最后调用管理器的Start()方法启动它。

gron支持两种添加任务的方式,一种是使用无参数的函数,另一种是实现任务接口。上面例子中使用的是前一种方式,实现接口的方式我们后面会介绍。添加任务时通过gron.Every()指定周期任务的间隔,上面添加了一个 5s 的周期任务,每隔 5s 输出一行文字。

需要注意的是,我们使用sync.WaitGroup保证主 goroutine 不退出。因为c.Start()中只是启动了一个 goroutine,如果主 goroutine 退出了,整个程序就停止了。

运行程序,每隔 5s 输出:

1
2
3
复制代码runs every 5 seconds.
runs every 5 seconds.
runs every 5 seconds.

该程序需要按下ctrl + c停止!

时间格式

gron接受time.Duration类型的时间间隔,除了time包中定义的基础Second/Minute/Hour,gron中的xtime子包还提供了Day/Week单位的时间。有一点需要注意,gron支持的时间精度为 1s,小于 1s 的间隔是不支持的。除了单位时间间隔,我们还可以使用4m10s这样的时间:

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
复制代码func main() {
var wg sync.WaitGroup
wg.Add(1)

c := gron.New()
c.AddFunc(gron.Every(1*time.Second), func() {
fmt.Println("runs every second.")
})
c.AddFunc(gron.Every(1*time.Minute), func() {
fmt.Println("runs every minute.")
})
c.AddFunc(gron.Every(1*time.Hour), func() {
fmt.Println("runs every hour.")
})
c.AddFunc(gron.Every(1*xtime.Day), func() {
fmt.Println("runs every day.")
})
c.AddFunc(gron.Every(1*xtime.Week), func() {
fmt.Println("runs every week.")
})
t, _ := time.ParseDuration("4m10s")
c.AddFunc(gron.Every(t), func() {
fmt.Println("runs every 4 minutes 10 seconds.")
})
c.Start()

wg.Wait()
}

通过gron.Every()设置每隔多长时间执行一次任务。对于大于 1 天的时间间隔,我们还可以使用gron.Every().At()指定其在某个时间点执行。例如下面的程序,从第二天的22:00开始,每隔一天触发一次,即每天的22:00触发:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码func main() {
var wg sync.WaitGroup
wg.Add(1)

c := gron.New()
c.AddFunc(gron.Every(1*xtime.Day).At("22:00"), func() {
fmt.Println("runs every second.")
})
c.Start()

wg.Wait()
}

自定义任务

实现自定义任务也很简单,只需要实现gron.Job接口即可:

1
2
3
4
复制代码// src/github.com/roylee0704/gron/cron.go
type Job interface {
Run()
}

我们需要调用调度器的Add()方法向管理器添加自定义任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码type GreetingJob struct {
Name string
}

func (g GreetingJob) Run() {
fmt.Println("Hello ", g.Name)
}

func main() {
var wg sync.WaitGroup
wg.Add(1)

g1 := GreetingJob{Name: "dj"}
g2 := GreetingJob{Name: "dajun"}

c := gron.New()
c.Add(gron.Every(5*time.Second), g1)
c.Add(gron.Every(10*time.Second), g2)
c.Start()

wg.Wait()
}

上面我们编写了一个GreetingJob结构,实现gron.Job接口,然后创建两个对象g1/g2,一个 5s 触发一次,一个 10s 触发一次。使用自定义任务的方式可以比较好地处理携带状态的任务,如上面的Name字段。

实际上,AddFunc()方法内部也是通过Add()实现的:

1
2
3
4
5
6
7
8
9
10
复制代码// src/github.com/roylee0704/gron/cron.go
func (c *Cron) AddFunc(s Schedule, j func()) {
c.Add(s, JobFunc(j))
}

type JobFunc func()

func (j JobFunc) Run() {
j()
}

在AddFunc()内部,将传入的函数转为JobFunc类型,而gron为JobFunc实现了gron.Job接口。是不是与net/http包中的HandleFunc和Handle很像。如果注意观察的话,在很多 Go 语言的代码中都有此类模式。

一点源码

gron的源码只有两个文件cron.go和schedule.go,cron.go中实现添加任务和调度的方法,schedule.go中是时间策略相关的代码。两个文件算上注释一共才 260 行!我们添加的任务在gron内部都是以Entry结构表示的:

1
2
3
4
5
6
复制代码type Entry struct {
Schedule Schedule
Job Job
Next time.Time
Prev time.Time
}

Next为下次执行时间,Prev为上次执行时间,Job是要执行的任务,Schedule为gron.Schedule接口类型,调用其Next()可计算出下次执行的时间点。

管理器使用gron.Cron结构表示:

1
2
3
4
5
6
复制代码type Cron struct {
entries []*Entry
running bool
add chan *Entry
stop chan struct{}
}

任务的调度在另外一个 goroutine 中。如果调度未开始,添加任务可直接append到entries切片中;如果调度已开始(Start()方法已调用),需要向通道add发送待添加的任务。任务调度的核心逻辑在Run()方法中:

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
复制代码func (c *Cron) run() {
var effective time.Time
now := time.Now().Local()

// to figure next trig time for entries, referenced from now
for _, e := range c.entries {
e.Next = e.Schedule.Next(now)
}

for {
sort.Sort(byTime(c.entries))
if len(c.entries) > 0 {
effective = c.entries[0].Next
} else {
effective = now.AddDate(15, 0, 0) // to prevent phantom jobs.
}

select {
case now = <-after(effective.Sub(now)):
// entries with same time gets run.
for _, entry := range c.entries {
if entry.Next != effective {
break
}
entry.Prev = now
entry.Next = entry.Schedule.Next(now)
go entry.Job.Run()
}
case e := <-c.add:
e.Next = e.Schedule.Next(time.Now())
c.entries = append(c.entries, e)
case <-c.stop:
return // terminate go-routine.
}
}
}

执行流程如下:

  1. 调度器刚启动时,先计算所有任务的下次执行时间;
  2. 然后在一个for循环中,按照执行时间从早到晚排序,取出最近需要执行任务的时间点;
  3. 在select语句中等待到这个时间点,启动新的 goroutine 执行到期的任务,每个任务一个新的 goroutine;
  4. 如果在等待的过程中,又添加了新的任务(通过通道c.add),计算这个新任务的首次执行时间。跳到步骤 2,因为新添加的任务可能最早执行。

有几个细节需要注意一下:

  1. 任务到期判断使用的是本地时间:time.Now().Local();
  2. 如果没有任务,等待时间设置为now.AddDate(15, 0, 0),即 15 年,防止 CPU 空转;
  3. 任务都是在独立的 goroutine 中执行的;
  4. 通过实现sort.Interface接口可以实现自定义排序(代码中的byTime)。

最后,我们来看一下时间策略的代码。我们知道在Entry结构中存储了一个gron.Schedule类型的对象,调用该对象的Next()方法返回下次执行的时间点:

1
2
3
4
复制代码// src/github.com/roylee0704/gron/schedule.go
type Schedule interface {
Next(t time.Time) time.Time
}

gron内置实现了两种Schedule,一种是periodicSchedule,即周期触发,gron.Every()函数返回的就是这个对象:

1
2
3
4
复制代码// src/github.com/roylee0704/gron/schedule.go
type periodicSchedule struct {
period time.Duration
}

一种是固定时刻的周期触发,它实际上也是周期触发,只是固定了时间点:

1
2
3
4
5
复制代码type atSchedule struct {
period time.Duration
hh int
mm int
}

他们的核心逻辑在Next()方法中,periodicSchedule只需要用当前时间加上周期即可得到下次触发时间。这里Truncate()方法截掉了当前时间中小于 1s 的部分:

1
2
3
复制代码func (ps periodicSchedule) Next(t time.Time) time.Time {
return t.Truncate(time.Second).Add(ps.period)
}

atSchedule的Next()方法先计算当天该时间点,再加上周期就是下次触发的时间:

1
2
3
4
5
6
7
8
9
10
11
复制代码func (as atSchedule) reset(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), as.hh, as.mm, 0, 0, time.UTC)
}

func (as atSchedule) Next(t time.Time) time.Time {
next := as.reset(t)
if t.After(next) {
return next.Add(as.period)
}
return next
}

periodicSchedule提供了At()方法可以转为atSchedule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码func (ps periodicSchedule) At(t string) Schedule {
if ps.period < xtime.Day {
panic("period must be at least in days")
}

// parse t naively
h, m, err := parse(t)

if err != nil {
panic(err.Error())
}

return &atSchedule{
period: ps.period,
hh: h,
mm: m,
}
}

自定义时间策略

我们可以很轻松的实现一个自定义的时间策略。例如,我们要实现一个“指数退避”的时间序列,先等待 1s,然后 2s、4s…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码type ExponentialBackOffSchedule struct {
last int
}

func (e *ExponentialBackOffSchedule) Next(t time.Time) time.Time {
interval := time.Duration(math.Pow(2.0, float64(e.last))) * time.Second
e.last += 1
return t.Truncate(time.Second).Add(interval)
}

func main() {
var wg sync.WaitGroup
wg.Add(1)

c := gron.New()
c.AddFunc(&ExponentialBackOffSchedule{}, func() {
fmt.Println(time.Now().Local().Format("2006-01-02 15:04:05"), "hello")
})
c.Start()

wg.Wait()
}

运行结果如下:

1
2
3
4
复制代码2020-04-20 23:47:11 hello
2020-04-20 23:47:13 hello
2020-04-20 23:47:17 hello
2020-04-20 23:47:25 hello

第二次输出与第一次相差 2s,第三次与第二次相差 4s,第4次与第三次相差 8s,完美!

总结

本文介绍了gron这个小巧的定时任务库,如何使用,如何自定义任务和时间策略,顺带分析了一下源码。gron源码实现非常简洁,非常推荐阅读!

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. gron GitHub:github.com/roylee0704/…
  2. Go 每日一库 GitHub:github.com/darjun/go-d…

我

我的博客:darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

本文转载自: 掘金

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

一款精美的后台内容管理系统

发表于 2020-04-20

一、项目简介

上一篇文章手撸的一个快递查询系统,竟然阅读量过1.8w分享了我手撸自助快递单的项目。这周我对这个快递查询想做一下优化。开发一个后台内容管理系统,将快递信息在后台进行统一管理。搜寻了2个小时找到一款比较契合的瀑布内容管理系统项目pb-cms。这个项目适合搭建博客、企业网站,后台是对内容管理。关键一点作者说项目一直在更新。那就开始本地搭建。

二、开发环境搭建

2.1技术栈

  • SpringBoot: 一款微服务框架,用来简化spring应用的初始搭建以及开发过程。
  • Apache Shiro: 一个功能强大且易于使用的Java安全框架,进行身份验证,授权,加密和会话管理
  • Mybatis Plus: 一个MyBatis 的增强工具,在MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
  • Thymeleaf: 一个XML / XHTML / HTML5模板引擎,能够应用于转换模板文件,以显示您的应用程序产生的数据和文本。

2.2部署

下载项目

1
复制代码git clone https://gitee.com/LinZhaoguan/pb-cms.git

修改配置文件

配置文件在项目根目录 resources下,主要配置文件如下:

1
2
3
4
复制代码application-dev.yml #dev(开发版)配置文件
application-prd.yml #prd(生产版)配置文件
application.yml #主配置文件
logback-spring.xml #日志配置文件

这里修改一下application-dev.yml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/pb-cms-base?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: 123456
redis:
host: 127.0.0.1
port: 6379
password: 123456
timeout: 5000
jedis:
pool:
max-idle: 8
min-idle: 0
max-active: 8
max-wait: -1
server:
servlet:
context-path:/cms

这里注意修改一下mysql数据库url(链接)、username(用户名)、password(和密码)。修改一下redis的host(服务器地址)、port(端口)、password(密码),如果相同就不需要修改了。

导入数据库脚本

使用数据库可视化工具Navicat工具,先创建数据库pb-cms-base,如下图:

然后导入数据库脚本,数据库脚本文件在docs\db\pb_cms_base.sql,数据导入如下图:

运行项目

打开com.puboot.SpringbootApplication,运行main方法即可:

开发环境都是用 Debug模式启动的。当然如果你的项目已经开发完成,想部署让他人使用,可以将项目打包成jar包来独立运行。

1
2
复制代码java -jar pb-cms.jar #本地jar包运行
nohup java -jar pb-cms.jar > pb-cms.log & #linux服务器运行

项目运行前端运行效果:

项目运行后端运行效果:

注: 我的idea的主题是Material Theme UI。

三、导入 excel 功能

pom.xml配置

这里引入了hutools工具,因为它封装了很多工具类,直接使用就可以了。项目pom.xml添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码<!-- hutools-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.0</version>
</dependency>

工具类ExcelUtils

创建工具类,代码如下

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
复制代码/**
* @author wangzg
* @date 2020/4/13
*/
public class ExcelUtils {

/**
* 获取第一个sheet页的内容
* @param inputStream
* @return
*/
public static List<KuaiDi> getKuaiDiList(InputStream inputStream) {
List<KuaiDi> kuaiDiList = new ArrayList<>();
if(Objects.nonNull(inputStream)){
ExcelReader excelReader = ExcelUtil.getReader(inputStream);
//表头添加别名,主要是将中文名转为数据库对应的字段
excelReader.addHeaderAlias("快递单号","kuaidiNo");
excelReader.addHeaderAlias("用户名","userName");
excelReader.addHeaderAlias("电话","phone");
excelReader.addHeaderAlias("快递公司","company");
List<Map<String, Object>> rowList = excelReader.readAll();
if(CollectionUtil.isNotEmpty(rowList)){
rowList.stream().forEach(r->{
KuaiDi kuaiDi = new KuaiDi();
try {
populate(kuaiDi,r);
kuaiDi.setCreateTime(new Date());
kuaiDiList.add(kuaiDi);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
return kuaiDiList;
}

public static void main(String[] args) {
List<KuaiDi> kuaiDiList = getKuaiDiList(ResourceUtil.getStream("D:\\快递.xls"));
kuaiDiList.stream().forEach(System.out::println);
}
}

service调用

service层就比较简单了,获取上传的multipartFile文件对象,将输入流传给ExcelUtils.getKuaiDiList,就可以返回对象的集合kuaiDiList,代码如下:

1
2
3
4
5
6
7
8
复制代码   @Override
public void importData(MultipartFile multipartFile) throws Exception {
List<KuaiDi> kuaiDiList = ExcelUtils.getKuaiDiList(multipartFile.getInputStream());
if(CollectionUtil.isNotEmpty(kuaiDiList)){
//批量插入
this.saveBatch(kuaiDiList);
}
}

四、FAQ

3.1 项目集成Mybatis 是否可以不指定 type-aliases-package属性?

答案是肯定的。为什么会单独说这个问题呢?因为我发现项目报配置文件配置文件中配置了下面内容:

1
2
3
4
5
6
复制代码mybatis-plus:
global-config:
db-config:
id-type: auto
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.puboot.model

com.puboot.model包路径项目是没有的,难道是type-aliases-package支持模糊匹配?一直没想过作者写错的问题。最后本地测试,并查了Mybitas Plus,最终的解释是这样的:

typeAliasesPackage:MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名,注册后在 Mapper 对应的 XML 文件中可以直接使用类名,而不用使用全限定的类名(即 XML 中调用的时候不用包含包名。
看了部分的源码也发现一个小知识点:type-aliases-package是支持多个包的别名定义的。

1
2
3
4
复制代码  /**
* Packages to search type aliases. (Package delimiters are ",; \t\n")
*/
private String typeAliasesPackage;

3.2 @RestController 和@Controller的区别?

因为本项目集成了spring-boot-starter-thymeleaf,可以通过thymeleaf开发我们的前端页面。学过Struts2的同步应该知道是怎么开发Web项目的。前端请求进入到后台控制器的业务处理方法,处理方法绑定数据到上下文,然后让方法返回一个字符串,字符串会匹配到返回前端的生成好的页面。但是有时又需要方法直接返回响应数据的json数据。所以两者的区别显而易见。

@RestController: 这个注解相当于 @Controller + @ResponseBody。

3.3 controller 如何注入service?

我们常用的方式可能是这样,使用@Autowired注入我们要到的service,代码如下:

1
2
3
4
5
6
7
复制代码@RequestMapping("kuaidi")
@Controller
public class KuaiDiController {

@Autowired
private KuaiDiService kuaiDiService;
}

在此项目中我发现了一种写法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码@Controller
@RequestMapping("article")
@AllArgsConstructor
public class ArticleController {

private final BizArticleService articleService;
private final BizArticleTagsService articleTagsService;
private final BizCategoryService categoryService;
private final BizTagsService tagsService;
private final SysConfigService configService;
}

这种方式其实就是构造方法注入,@AllArgsConstructor是lombok插件的一个注解,插件会自动生成全参数的构造方法。

五、最后

开源项目让我们很容易就可以获取并学别人的源码。我自己也在优化自助快递查询的功能。不为别的,只为时间不浪费;每完成一个功能都有一定的成就感,让我乐在其中!

参考文章

  • Mybatis3.2不支持Ant通配符: juejin.cn/post/684490…

不安分的猿人
孜孜不断的技术分享!

本文转载自: 掘金

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

字节码编程,Javassist篇一《基于javassist的

发表于 2020-04-20

作者:小傅哥

博客:bugstack.cn

❝
沉淀、分享、成长,让自己和他人都能有所收获!

❞

目录

  • 一、前言
  • 二、开发环境
  • 三、案例目标
  • 四、技术实现
  • 五、测试结果
      1. 使用Javassist生成的类
      1. 输出的测试结果
  • 六、总结

一、前言

在字节码编程方面有三个比较常见的框架;ASM、byte-buddy、Javassist,他们都可以对这字节码进行操作,只是操作方式和控制粒度不同。

其中 「ASM」 更偏向于底层,需要了解 「JVM」 虚拟机中指定规范以及对局部变量以及操作数栈的知识。虽然在编写起来比较麻烦,但是它也是性能最好功能最强的字节码操作框架。常见的会用在 「CGLIB」 动态代理类中,以及一些非入侵的探针监控场景中。

另外两个框架都是有强大的 API,操作使用上更加容易控制。虽然对对比上会比 「ASM」 性能差一些,但不是说性能完全不好。同样在一些监控场景中也用的非常多。如果你细心可以在你的工程 「jar」 包搜索一下。

在这之前我已经编写了 Javaagent全链路监控 和 ASM 的部分文章,虽然这部分技术内容在 「CRUD」 开发中并不常用,但随着自动化测试、非入侵监控的大量使用,还是蛮多人需要这样的技能学习的。同时我也是这样一个技能的学习者,为此后面会陆续编写和完善关于 「字节码编程」 这个专栏。也希望这个专栏在提升自己技术栈的同时也帮助他人成长。

「那么」,小傅哥计划从 Javassist 到 ASM 陆续完成整套专栏学习的文章编写。从简单入门到应用操作,一步步来完成成体系的技术知识栈学习。

「好!」,现在开始第一个Helloworld案例。相关源码可以通过关注 公众号:bugstack虫洞栈 获取

二、开发环境

  1. JDK 1.8.0
  2. javassist 3.12.1.GA
1
2
3
4
5
6
复制代码<dependency>  
    <groupId>javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.12.1.GA</version>
    <type>jar</type>
</dependency>

三、案例目标

不看实现过程的话,我们的案例目标其实很简单,就是使用 javassist 输出一行 Helloworld 。这话像不像产品说的

1
2
3
4
5
6
7
8
复制代码public class HelloWorld {  
    public static void main(String[] args) {
        System.out.println("javassist hi helloworld by 小傅哥(bugstack.cn)");
    }

    public HelloWorld() {
    }
}

以上的这段代码就是我们接下来需要使用字节码编程技术来实现的内容。

四、技术实现

其实输出一个 Helloworld 还是蛮简单的,主要是从这里面去学习一下 Javassist 的基本语法结构,也能为后续的学习有一个基础的概念。

❝
javassist Helloworld

❞

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
复制代码/**  
 * 公众号:bugstack虫洞栈
 * 博客栈:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!
 * 本专栏是小傅哥多年从事一线互联网Java开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程。如果能为您提供帮助,请给予支持(关注、点赞、分享)!
 */
public class GenerateClazzMethod {


    public static void main(String[] args) throws IOException, CannotCompileException, NotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {

        ClassPool pool = ClassPool.getDefault();

        // 创建类 classname:创建类路径和名称
        CtClass ctClass = pool.makeClass("org.itstack.demo.javassist.HelloWorld");

        // 添加方法
        CtMethod mainMethod = new CtMethod(CtClass.voidType, "main", new CtClass[]{pool.get(String[].class.getName())}, ctClass);
        mainMethod.setModifiers(Modifier.PUBLIC + Modifier.STATIC);
        mainMethod.setBody("{System.out.println(\"javassist hi helloworld by 小傅哥(bugstack.cn)\");}");
        ctClass.addMethod(mainMethod);

        // 创建无参数构造方法
        CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
        ctConstructor.setBody("{}");
        ctClass.addConstructor(ctConstructor);

        // 输出类内容
        ctClass.writeFile();

        // 测试调用
        Class clazz = ctClass.toClass();
        Object obj = clazz.newInstance();

        Method main = clazz.getDeclaredMethod("main", String[].class);
        main.invoke(obj, (Object)new String[1]);

    }

}

这段代码分为几块内容来实现功能,分别包括;

  1. 创建 ClassPool,它是一个基于HashMap实现的 CtClass 对象容器。
  2. 使用 CtClass,创建我们的类信息,也就是类的路径和名称。
  3. 接下来就是给类添加方法。包括;方法的属性、类型、名称、入参、出参和方法体的内容。
  4. 在方法创建好后还需要创建一个空的构造函数,每一个类都会在编译后生成这样一个构造函数。
  5. 当方法创建完成后,我们使用 ctClass.writeFile() 进行输出方法的内容信息。也就可以看到通过我们使用 Javassist 生成类的样子。
  6. 最后就是我们的反射调用 main 方法,测试输出结果。

五、测试结果

当我们执行测试的时候会输出类信息到工程文件夹下,同时会输出我们的测试结果;

1. 使用Javassist生成的类

使用Javassist生成的类,在工程文件夹下

使用Javassist生成的类,在工程文件夹下

2. 输出的测试结果

1
2
3
复制代码javassist hi helloworld by 小傅哥(bugstack.cn)  

Process finished with exit code 0

六、总结

  • 关于 Javassist 的使用在完整的且强大的 API 下,确实还是蛮容易使用的。并且代码的使用上并不是很难理解。
  • 后续会陆续推出字节码编程的案例文章,逐步完善这部分技术知识栈的内容。最终尝试使用这样的技术知识完成一个案例级别的质量检测系统。也欢迎喜欢此类内容的小伙伴跟进学习。
  • 后续的文章可能在专栏类的文章里,文章内容上会短一点。尽可能在一篇文章中描述清楚一个详尽的知识点,也方便后续整理成 PDF 书籍,方便学习使用。

bugstack虫洞栈
沉淀、分享、成长,让自己和他人都能有所收获!
作者小傅哥多年从事一线互联网Java开发,从19年开始编写工作和学习历程的技术汇总,旨在为大家提供一个较清晰详细的核心技能学习文档。如果本文能为您提供帮助,请给予支持(关注、点赞、分享)!

本文转载自: 掘金

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

面试 HTTP ,99% 的面试官都爱问这些问题

发表于 2020-04-20

HTTP 和 HTTPS 的区别

HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范

HTTP 主要内容分为三部分,超文本(Hypertext)、传输(Transfer)、协议(Protocol)。

  • 超文本就是不单单只是本文,它还可以传输图片、音频、视频,甚至点击文字或图片能够进行超链接的跳转。
  • 上面这些概念可以统称为数据,传输就是数据需要经过一系列的物理介质从一个端系统传送到另外一个端系统的过程。通常我们把传输数据包的一方称为请求方,把接到二进制数据包的一方称为应答方。
  • 而协议指的就是是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为协议,只不过是网络协议。

说到 HTTP,不得不提的就是 TCP/IP 网络模型,一般是五层模型。如下图所示

但是也可以分为四层,就是把链路层和物理层都表示为网络接口层

还有一种就是 OSI 七层网络模型,它就是在五层协议之上加了表示层和会话层

而 HTTPS 的全称是 Hypertext Transfer Protocol Secure,从名称我们可以看出 HTTPS 要比 HTTPS 多了 secure 安全性这个概念,实际上, HTTPS 并不是一个新的应用层协议,它其实就是 HTTP + TLS/SSL 协议组合而成,而安全性的保证正是 TLS/SSL 所做的工作。

也就是说,HTTPS 就是身披了一层 SSL 的 HTTP。

那么,HTTP 和 HTTPS 的主要区别是什么呢?

  • 最简单的,HTTP 在地址栏上的协议是以 http:// 开头,而 HTTPS 在地址栏上的协议是以 https:// 开头
1
2
复制代码http://www.cxuanblog.com/
https://www.cxuanblog.com/
  • HTTP 是未经安全加密的协议,它的传输过程容易被攻击者监听、数据容易被窃取、发送方和接收方容易被伪造;而 HTTPS 是安全的协议,它通过 密钥交换算法 - 签名算法 - 对称加密算法 - 摘要算法 能够解决上面这些问题。

  • HTTP 的默认端口是 80,而 HTTPS 的默认端口是 443。

HTTP Get 和 Post 区别

HTTP 中包括许多方法,Get 和 Post 是 HTTP 中最常用的两个方法,基本上使用 HTTP 方法中有 99% 都是在使用 Get 方法和 Post 方法,所以有必要我们对这两个方法有更加深刻的认识。

  • get 方法一般用于请求,比如你在浏览器地址栏输入 www.cxuanblog.com 其实就是发送了一个 get 请求,它的主要特征是请求服务器返回资源,而 post 方法一般用于 <form> 表单的提交,相当于是把信息提交给服务器,等待服务器作出响应,get 相当于一个是 pull/拉的操作,而 post 相当于是一个 push/推的操作。
  • get 方法是不安全的,因为你在发送请求的过程中,你的请求参数会拼在 URL 后面,从而导致容易被攻击者窃取,对你的信息造成破坏和伪造;
1
复制代码/test/demo_form.asp?name1=value1&name2=value2

而 post 方法是把参数放在请求体 body 中的,这对用户来说不可见。

1
2
3
复制代码POST /test/demo_form.asp HTTP/1.1
Host: w3schools.com
name1=value1&name2=value2
  • get 请求的 URL 有长度限制,而 post 请求会把参数和值放在消息体中,对数据长度没有要求。
  • get 请求会被浏览器主动 cache,而 post 不会,除非手动设置。
  • get 请求在浏览器反复的 回退/前进 操作是无害的,而 post 操作会再次提交表单请求。
  • get 请求在发送过程中会产生一个 TCP 数据包;post 在发送过程中会产生两个 TCP 数据包。对于 get 方式的请求,浏览器会把 http header 和 data 一并发送出去,服务器响应 200(返回数据);而对于 post,浏览器先发送 header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据)。

什么是无状态协议,HTTP 是无状态协议吗,怎么解决

无状态协议(Stateless Protocol) 就是指浏览器对于事务的处理没有记忆能力。举个例子来说就是比如客户请求获得网页之后关闭浏览器,然后再次启动浏览器,登录该网站,但是服务器并不知道客户关闭了一次浏览器。

HTTP 就是一种无状态的协议,他对用户的操作没有记忆能力。可能大多数用户不相信,他可能觉得每次输入用户名和密码登陆一个网站后,下次登陆就不再重新输入用户名和密码了。这其实不是 HTTP 做的事情,起作用的是一个叫做 小甜饼(Cookie) 的机制。它能够让浏览器具有记忆能力。

如果你的浏览器允许 cookie 的话,查看方式 chrome://settings/content/cookies

也就说明你的记忆芯片通电了…… 当你想服务端发送请求时,服务端会给你发送一个认证信息,服务器第一次接收到请求时,开辟了一块 Session 空间(创建了Session对象),同时生成一个 sessionId ,并通过响应头的 **Set-Cookie:JSESSIONID=XXXXXXX **命令,向客户端发送要求设置 Cookie 的响应; 客户端收到响应后,在本机客户端设置了一个 **JSESSIONID=XXXXXXX **的 Cookie 信息,该 Cookie 的过期时间为浏览器会话结束;

接下来客户端每次向同一个网站发送请求时,请求头都会带上该 Cookie信息(包含 sessionId ), 然后,服务器通过读取请求头中的 Cookie 信息,获取名称为 JSESSIONID 的值,得到此次请求的 sessionId。这样,你的浏览器才具有了记忆能力。

还有一种方式是使用 JWT 机制,它也是能够让你的浏览器具有记忆能力的一种机制。与 Cookie 不同,JWT 是保存在客户端的信息,它广泛的应用于单点登录的情况。JWT 具有两个特点

  • JWT 的 Cookie 信息存储在客户端,而不是服务端内存中。也就是说,JWT 直接本地进行验证就可以,验证完毕后,这个 Token 就会在 Session 中随请求一起发送到服务器,通过这种方式,可以节省服务器资源,并且 token 可以进行多次验证。
  • JWT 支持跨域认证,Cookies 只能用在单个节点的域或者它的子域中有效。如果它们尝试通过第三个节点访问,就会被禁止。使用 JWT 可以解决这个问题,使用 JWT 能够通过多个节点进行用户认证,也就是我们常说的跨域认证。

UDP 和 TCP 的区别

TCP 和 UDP 都位于计算机网络模型中的运输层,它们负责传输应用层产生的数据。下面我们就来聊一聊 TCP 和 UDP 分别的特征和他们的区别

UDP 是什么

UDP 的全称是 User Datagram Protocol,用户数据报协议。它不需要所谓的握手操作,从而加快了通信速度,允许网络上的其他主机在接收方同意通信之前进行数据传输。

数据报是与分组交换网络关联的传输单元。

UDP 的特点主要有

  • UDP 能够支持容忍数据包丢失的带宽密集型应用程序
  • UDP 具有低延迟的特点
  • UDP 能够发送大量的数据包
  • UDP 能够允许 DNS 查找,DNS 是建立在 UDP 之上的应用层协议。

TCP 是什么

TCP 的全称是Transmission Control Protocol ,传输控制协议。它能够帮助你确定计算机连接到 Internet 以及它们之间的数据传输。通过三次握手来建立 TCP 连接,三次握手就是用来启动和确认 TCP 连接的过程。一旦连接建立后,就可以发送数据了,当数据传输完成后,会通过关闭虚拟电路来断开连接。

TCP 的主要特点有

  • TCP 能够确保连接的建立和数据包的发送
  • TCP 支持错误重传机制
  • TCP 支持拥塞控制,能够在网络拥堵的情况下延迟发送
  • TCP 能够提供错误校验和,甄别有害的数据包。

TCP 和 UDP 的不同

下面为你罗列了一些 TCP 和 UDP 的不同点,方便理解,方便记忆。

TCP UDP
TCP 是面向连接的协议 UDP 是无连接的协议
TCP 在发送数据前先需要建立连接,然后再发送数据 UDP 无需建立连接就可以直接发送大量数据
TCP 会按照特定顺序重新排列数据包 UDP 数据包没有固定顺序,所有数据包都相互独立
TCP 传输的速度比较慢 UDP 的传输会更快
TCP 的头部字节有 20 字节 UDP 的头部字节只需要 8 个字节
TCP 是重量级的,在发送任何用户数据之前,TCP需要三次握手建立连接。 UDP 是轻量级的。没有跟踪连接,消息排序等。
TCP 会进行错误校验,并能够进行错误恢复 UDP 也会错误检查,但会丢弃错误的数据包。
TCP 有发送确认 UDP 没有发送确认
TCP 会使用握手协议,例如 SYN,SYN-ACK,ACK 无握手协议
TCP 是可靠的,因为它可以确保将数据传送到路由器。 在 UDP 中不能保证将数据传送到目标。

TCP 三次握手和四次挥手

TCP 三次握手和四次挥手也是面试题的热门考点,它们分别对应 TCP 的连接和释放过程。下面就来简单认识一下这两个过程

TCP 三次握手

在了解具体的流程前,我们需要先认识几个概念

消息类型 描述
SYN 这个消息是用来初始化和建立连接的。
ACK 帮助对方确认收到的 SYN 消息
SYN-ACK 本地的 SYN 消息和较早的 ACK 数据包
FIN 用来断开连接
  • SYN:它的全称是 Synchronize Sequence Numbers,同步序列编号。是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立 TCP 连接时,首先会发送的一个信号。客户端在接受到 SYN 消息时,就会在自己的段内生成一个随机值 X。
  • SYN-ACK:服务器收到 SYN 后,打开客户端连接,发送一个 SYN-ACK 作为答复。确认号设置为比接收到的序列号多一个,即 X + 1,服务器为数据包选择的序列号是另一个随机数 Y。
  • ACK:Acknowledge character, 确认字符,表示发来的数据已确认接收无误。最后,客户端将 ACK 发送给服务器。序列号被设置为所接收的确认值即 Y + 1。

如果用现实生活来举例的话就是

小明 - 客户端 小红 - 服务端

  • 小明给小红打电话,接通了后,小明说喂,能听到吗,这就相当于是连接建立。
  • 小红给小明回应,能听到,你能听到我说的话吗,这就相当于是请求响应。
  • 小明听到小红的回应后,好的,这相当于是连接确认。在这之后小明和小红就可以通话/交换信息了。

TCP 四次挥手

在连接终止阶段使用四次挥手,连接的每一端都会独立的终止。下面我们来描述一下这个过程。

  • 首先,客户端应用程序决定要终止连接(这里服务端也可以选择断开连接)。这会使客户端将 FIN 发送到服务器,并进入 FIN_WAIT_1 状态。当客户端处于 FIN_WAIT_1 状态时,它会等待来自服务器的 ACK 响应。
  • 然后第二步,当服务器收到 FIN 消息时,服务器会立刻向客户端发送 ACK 确认消息。
  • 当客户端收到服务器发送的 ACK 响应后,客户端就进入 FIN_WAIT_2 状态,然后等待来自服务器的 FIN 消息
  • 服务器发送 ACK 确认消息后,一段时间(可以进行关闭后)会发送 FIN 消息给客户端,告知客户端可以进行关闭。
  • 当客户端收到从服务端发送的 FIN 消息时,客户端就会由 FIN_WAIT_2 状态变为 TIME_WAIT 状态。处于 TIME_WAIT 状态的客户端允许重新发送 ACK 到服务器为了防止信息丢失。客户端在 TIME_WAIT 状态下花费的时间取决于它的实现,在等待一段时间后,连接关闭,客户端上所有的资源(包括端口号和缓冲区数据)都被释放。

还是可以用上面那个通话的例子来进行描述

  • 小明对小红说,我所有的东西都说完了,我要挂电话了。
  • 小红说,收到,我这边还有一些东西没说。
  • 经过若干秒后,小红也说完了,小红说,我说完了,现在可以挂断了
  • 小明收到消息后,又等了若干时间后,挂断了电话。

简述 HTTP1.0/1.1/2.0 的区别

HTTP 1.0

HTTP 1.0 是在 1996 年引入的,从那时开始,它的普及率就达到了惊人的效果。

  • HTTP 1.0 仅仅提供了最基本的认证,这时候用户名和密码还未经加密,因此很容易收到窥探。
  • HTTP 1.0 被设计用来使用短链接,即每次发送数据都会经过 TCP 的三次握手和四次挥手,效率比较低。
  • HTTP 1.0 只使用 header 中的 If-Modified-Since 和 Expires 作为缓存失效的标准。
  • HTTP 1.0 不支持断点续传,也就是说,每次都会传送全部的页面和数据。
  • HTTP 1.0 认为每台计算机只能绑定一个 IP,所以请求消息中的 URL 并没有传递主机名(hostname)。

HTTP 1.1

HTTP 1.1 是 HTTP 1.0 开发三年后出现的,也就是 1999 年,它做出了以下方面的变化

  • HTTP 1.1 使用了摘要算法来进行身份验证
  • HTTP 1.1 默认使用长连接,长连接就是只需一次建立就可以传输多次数据,传输完成后,只需要一次切断连接即可。长连接的连接时长可以通过请求头中的 keep-alive 来设置
  • HTTP 1.1 中新增加了 E-tag,If-Unmodified-Since, If-Match, If-None-Match 等缓存控制标头来控制缓存失效。
  • HTTP 1.1 支持断点续传,通过使用请求头中的 Range 来实现。
  • HTTP 1.1 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。

HTTP 2.0

HTTP 2.0 是 2015 年开发出来的标准,它主要做的改变如下

  • 头部压缩,由于 HTTP 1.1 经常会出现 User-Agent、Cookie、Accept、Server、Range 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP 2.0 使用 HPACK 算法进行压缩。
  • 二进制格式,HTTP 2.0 使用了更加靠近 TCP/IP 的二进制格式,而抛弃了 ASCII 码,提升了解析效率
  • 强化安全,由于安全已经成为重中之重,所以 HTTP2.0 一般都跑在 HTTPS 上。
  • 多路复用,即每一个请求都是是用作连接共享。一个请求对应一个id,这样一个连接上可以有多个请求。

请你说一下 HTTP 常见的请求头

这个问题比较开放,因为 HTTP 请求头有很多,这里只简单举出几个例子,具体的可以参考我的另一篇文章

mp.weixin.qq.com/s/XZZR0945I…

HTTP 标头会分为四种,分别是 通用标头、实体标头、请求标头、响应标头。分别介绍一下

通用标头

通用标头主要有三个,分别是 Date、Cache-Control 和 Connection

Date

Date 是一个通用标头,它可以出现在请求标头和响应标头中,它的基本表示如下

1
复制代码Date: Wed, 21 Oct 2015 07:28:00 GMT

表示的是格林威治标准时间,这个时间要比北京时间慢八个小时

Cache-Control

Cache-Control 是一个通用标头,他可以出现在请求标头和响应标头中,Cache-Control 的种类比较多,虽然说这是一个通用标头,但是又一些特性是请求标头具有的,有一些是响应标头才有的。主要大类有 可缓存性、阈值性、 重新验证并重新加载 和其他特性

Connection

Connection 决定当前事务(一次三次握手和四次挥手)完成后,是否会关闭网络连接。Connection 有两种,一种是持久性连接,即一次事务完成后不关闭网络连接

1
复制代码Connection: keep-alive

另一种是非持久性连接,即一次事务完成后关闭网络连接

1
复制代码Connection: close

HTTP1.1 其他通用标头如下

实体标头

实体标头是描述消息正文内容的 HTTP 标头。实体标头用于 HTTP 请求和响应中。头部Content-Length、 Content-Language、 Content-Encoding 是实体头。

  • Content-Length 实体报头指示实体主体的大小,以字节为单位,发送到接收方。
  • Content-Language 实体报头描述了客户端或者服务端能够接受的语言。
  • Content-Encoding 这又是一个比较麻烦的属性,这个实体报头用来压缩媒体类型。Content-Encoding 指示对实体应用了何种编码。

常见的内容编码有这几种: gzip、compress、deflate、identity ,这个属性可以应用在请求报文和响应报文中

1
2
复制代码Accept-Encoding: gzip, deflate //请求头
Content-Encoding: gzip //响应头

下面是一些实体标头字段

请求标头

Host

Host 请求头指明了服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的 TCP 端口号。如果没有给定端口号,会自动使用被请求服务的默认端口(比如请求一个 HTTP 的 URL 会自动使用 80 作为端口)。

1
复制代码Host: developer.mozilla.org

上面的 Accpet、 Accept-Language、Accept-Encoding 都是属于内容协商的请求标头。

Referer

HTTP Referer 属性是请求标头的一部分,当浏览器向 web 服务器发送请求的时候,一般会带上 Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。

1
复制代码Referer: https://developer.mozilla.org/testpage.html

If-Modified-Since

If-Modified-Since 通常会与 If-None-Match 搭配使用,If-Modified-Since 用于确认代理或客户端拥有的本地资源的有效性。获取资源的更新日期时间,可通过确认首部字段 Last-Modified 来确定。

大白话说就是如果在 Last-Modified 之后更新了服务器资源,那么服务器会响应 200,如果在 Last-Modified 之后没有更新过资源,则返回 304。

1
复制代码If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT

If-None-Match

If-None-Match HTTP 请求标头使请求成为条件请求。 对于 GET 和 HEAD 方法,仅当服务器没有与给定资源匹配的 ETag 时,服务器才会以 200 状态发送回请求的资源。 对于其他方法,仅当最终现有资源的ETag与列出的任何值都不匹配时,才会处理请求。

1
复制代码If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a"

Accept

接受请求 HTTP 标头会通告客户端其能够理解的 MIME 类型

Accept-Charset

accept-charset 属性规定服务器处理表单数据所接受的字符集。

常用的字符集有: UTF-8 - Unicode 字符编码 ; ISO-8859-1 - 拉丁字母表的字符编码

Accept-Language

首部字段 Accept-Language 用来告知服务器用户代理能够处理的自然语言集(指中文或英文等),以及自然语言集的相对优先级。可一次指定多种自然语言集。

请求标头我们大概就介绍这几种,后面会有一篇文章详细深挖所有的响应头的,下面是一个响应头的汇总,基于 HTTP 1.1

响应标头

Access-Control-Allow-Origin

一个返回的 HTTP 标头可能会具有 Access-Control-Allow-Origin ,Access-Control-Allow-Origin 指定一个来源,它告诉浏览器允许该来源进行资源访问。

Keep-Alive

Keep-Alive 表示的是 Connection 非持续连接的存活时间,可以进行指定。

Server

服务器标头包含有关原始服务器用来处理请求的软件的信息。

应该避免使用过于冗长和详细的 Server 值,因为它们可能会泄露内部实施细节,这可能会使攻击者容易地发现并利用已知的安全漏洞。例如下面这种写法

1
复制代码Server: Apache/2.4.1 (Unix)

Set-Cookie

Set-Cookie 用于服务器向客户端发送 sessionID。

Transfer-Encoding

首部字段 Transfer-Encoding 规定了传输报文主体时采用的编码方式。

HTTP /1.1 的传输编码方式仅对分块传输编码有效。

X-Frame-Options

HTTP 首部字段是可以自行扩展的。所以在 Web 服务器和浏览器的应用上,会出现各种非标准的首部字段。

首部字段 X-Frame-Options 属于 HTTP 响应首部,用于控制网站内容在其他 Web 网站的 Frame 标签内的显示问题。其主要目的是为了防止点击劫持(clickjacking)攻击。

下面是一个响应头的汇总,基于 HTTP 1.1

地址栏输入 URL 发生了什么

这道题也是一道经常会考的面试题。那么下面我们就来探讨一下从你输入 URL 后到响应,都经历了哪些过程。

  • 首先,你需要在浏览器中的 URL 地址上,输入你想访问的地址,如下

你应该访问不到的,对不对~

  • 然后,浏览器会根据你输入的 URL 地址,去查找域名是否被本地 DNS 缓存,不同浏览器对 DNS 的设置不同,如果浏览器缓存了你想访问的 URL 地址,那就直接返回 ip。如果没有缓存你的 URL 地址,浏览器就会发起系统调用来查询本机 hosts 文件是否有配置 ip 地址,如果找到,直接返回。如果找不到,就向网络中发起一个 DNS 查询。

首先来看一下 DNS 是啥,互联网中识别主机的方式有两种,通过主机名和 IP 地址。我们人喜欢用名字的方式进行记忆,但是通信链路中的路由却喜欢定长、有层次结构的 IP 地址。所以就需要一种能够把主机名到 IP 地址的转换服务,这种服务就是由 DNS 提供的。DNS 的全称是 Domain Name System 域名系统。DNS 是一种由分层的 DNS 服务器实现的分布式数据库。DNS 运行在 UDP 上,使用 53 端口。

DNS 是一种分层数据库,它的主要层次结构如下

一般域名服务器的层次结构主要是以上三种,除此之外,还有另一类重要的 DNS 服务器,它是 本地 DNS 服务器(local DNS server)。严格来说,本地 DNS 服务器并不属于上述层次结构,但是本地 DNS 服务器又是至关重要的。每个 ISP(Internet Service Provider) 比如居民区的 ISP 或者一个机构的 ISP 都有一台本地 DNS 服务器。当主机和 ISP 进行连接时,该 ISP 会提供一台主机的 IP 地址,该主机会具有一台或多台其本地 DNS 服务器的 IP地址。通过访问网络连接,用户能够容易的确定 DNS 服务器的 IP地址。当主机发出 DNS 请求后,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 服务器层次系统中。

首先,查询请求会先找到本地 DNS 服务器来查询是否包含 IP 地址,如果本地 DNS 无法查询到目标 IP 地址,就会向根域名服务器发起一个 DNS 查询。

注意:DNS 涉及两种查询方式:一种是递归查询(Recursive query) ,一种是迭代查询(Iteration query)。《计算机网络:自顶向下方法》竟然没有给出递归查询和迭代查询的区别,找了一下网上的资料大概明白了下。

如果根域名服务器无法告知本地 DNS 服务器下一步需要访问哪个顶级域名服务器,就会使用递归查询;

如果根域名服务器能够告知 DNS 服务器下一步需要访问的顶级域名服务器,就会使用迭代查询。

在由根域名服务器 -> 顶级域名服务器 -> 权威 DNS 服务器后,由权威服务器告诉本地服务器目标 IP 地址,再有本地 DNS 服务器告诉用户需要访问的 IP 地址。

  • 第三步,浏览器需要和目标服务器建立 TCP 连接,需要经过三次握手的过程,具体的握手过程请参考上面的回答。
  • 在建立连接后,浏览器会向目标服务器发起 HTTP-GET 请求,包括其中的 URL,HTTP 1.1 后默认使用长连接,只需要一次握手即可多次传输数据。
  • 如果目标服务器只是一个简单的页面,就会直接返回。但是对于某些大型网站的站点,往往不会直接返回主机名所在的页面,而会直接重定向。返回的状态码就不是 200 ,而是 301,302 以 3 开头的重定向码,浏览器在获取了重定向响应后,在响应报文中 Location 项找到重定向地址,浏览器重新第一步访问即可。
  • 然后浏览器重新发送请求,携带新的 URL,返回状态码 200 OK,表示服务器可以响应请求,返回报文。

HTTPS 的工作原理

我们上面描述了一下 HTTP 的工作原理,下面来讲述一下 HTTPS 的工作原理。因为我们知道 HTTPS 不是一种新出现的协议,而是

所以,我们探讨 HTTPS 的握手过程,其实就是 SSL/TLS 的握手过程。

TLS 旨在为 Internet 提供通信安全的加密协议。TLS 握手是启动和使用 TLS 加密的通信会话的过程。在 TLS 握手期间,Internet 中的通信双方会彼此交换信息,验证密码套件,交换会话密钥。

每当用户通过 HTTPS 导航到具体的网站并发送请求时,就会进行 TLS 握手。除此之外,每当其他任何通信使用HTTPS(包括 API 调用和在 HTTPS 上查询 DNS)时,也会发生 TLS 握手。

TLS 具体的握手过程会根据所使用的密钥交换算法的类型和双方支持的密码套件而不同。 我们以RSA 非对称加密来讨论这个过程。整个 TLS 通信流程图如下

  • 在进行通信前,首先会进行 HTTP 的三次握手,握手完成后,再进行 TLS 的握手过程
  • ClientHello:客户端通过向服务器发送 hello 消息来发起握手过程。这个消息中会夹带着客户端支持的 TLS 版本号(TLS1.0 、TLS1.2、TLS1.3) 、客户端支持的密码套件、以及一串 客户端随机数。
  • ServerHello:在客户端发送 hello 消息后,服务器会发送一条消息,这条消息包含了服务器的 SSL 证书、服务器选择的密码套件和服务器生成的随机数。
  • 认证(Authentication):客户端的证书颁发机构会认证 SSL 证书,然后发送 Certificate 报文,报文中包含公开密钥证书。最后服务器发送 ServerHelloDone 作为 hello 请求的响应。第一部分握手阶段结束。
  • 加密阶段:在第一个阶段握手完成后,客户端会发送 ClientKeyExchange 作为响应,这个响应中包含了一种称为 The premaster secret 的密钥字符串,这个字符串就是使用上面公开密钥证书进行加密的字符串。随后客户端会发送 ChangeCipherSpec,告诉服务端使用私钥解密这个 premaster secret 的字符串,然后客户端发送 Finished 告诉服务端自己发送完成了。

Session key 其实就是用公钥证书加密的公钥。

  • 实现了安全的非对称加密:然后,服务器再发送 ChangeCipherSpec 和 Finished 告诉客户端解密完成,至此实现了 RSA 的非对称加密。

文章参考:

What is a TLS handshake?

Recursive and Iterative DNS Queries

DNS递归查询与迭代查询

TCP三次握手和四次挥手过程

HTTP/1.0 AND 1.1, WHAT ARE THE DIFFERENCES?

TCP Connection Termination

Transmission_Control_Protocol

SYN

TCP 3-Way Handshake (SYN, SYN-ACK,ACK)

HTTP/2 相比 1.0 有哪些重大改进?

TCP vs UDP: What’s the Difference?

计算机网络7层模型

HTTP常见面试题

本文转载自: 掘金

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

如何在项目中封装 Kotlin + Android Data

发表于 2020-04-19

在之前的文章 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用 分析了 Dialog 加载绘制流程、设计模式,以及基于 DataBinding 封装的 DataBindingDialog 的基础库 JDataBinding,这篇文章主要讲基于 DataBinding 封装的基础库 JDataBinding

JDataBinding 源码地址:https://github.com/hi-dhl/JDataBinding

JDataBinding 是基于 DataBinding 封装的 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 基础库,欢迎 start

DataBinding 是什么?查看 Google官网,会有更详细的介绍

DataBinding 是 Google 在 Jetpack 中推出的一款数据绑定的支持库,利用该库可以实现在页面组件中直接绑定应用程序的数据源

利用 Kotlin 的 inline、reified、DSL 等等语法, 结合着 DataBinding,可以设计出更加简洁并利于维护的代码

DataBindingListAdapter

DataBindingListAdapter 是基于 ListAdapter 封装的,使用更少的代码快速实现 RecyclerView adapter and ViewHolder

什么是 ListAdapter?

ListAdapter 是 Google 推出的一个新的类库,相比传统的 Adapter,它能够用较少的代码实现更多的 RecylerView 的动画,并且可以自动存储之前的 list,ListAdapter 还加入了 DiffUtil 的工具类,只有当 items 变化的时候进行刷新,而不用刷新整个 list,大大提高 RecyclerView 的性能

什么是 DiffUtil?

DiffUtil 主要在后台计算 list 是否相同,然后回到回主线程刷新数据,主要用了 Myers Diff Algorithm, 而我们日常使用的 git diff 就用到了该算法

好了介绍完基础概念之后,来看一下 DataBindingListAdapter 是如何使用的,为什么我会说使用更少的代码快速实现 RecyclerView adapter and ViewHolder

Step1: 继承 BaseViewHolder

创建一个自定义的 ViewHolder 类,继承 DataBindingListAdapter,通过 viewHolderBinding 可以快速实现 DataBinding 的绑定

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码class TestViewHolder(view: View) : BaseViewHolder<Model>(view) {

val binding: RecycieItemTestBinding by viewHolderBinding(view)

override fun bindData(data: Model) {
binding.apply {
model = data
executePendingBindings()
}
}

}

Step2: 继承 DataBindingListAdapter

实现带头部和尾部的 Adapter,创建自定义的 Adapter,继承 DataBindingListAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码class TestAdapter : DataBindingListAdapter<Model>(Model.CALLBACK) {

override fun viewHolder(layout: Int, view: View): DataBindingViewHolder<Model> = when (layout) {
R.layout.recycie_item_header -> HeaderViewHolder(view)
else -> TestViewHolder(view)
}

override fun layout(position: Int): Int = when (position) {
0 -> R.layout.recycie_item_header
getItemCount() - 1 -> R.layout.recycie_item_footer
else -> R.layout.recycie_item_test
}

override fun getItemCount(): Int = super.getItemCount() + 2
}

构造方法传入了 Model.CALLBACK,Model.CALLBACK 实现了 DiffUtil.ItemCallback,用于计算 list 的两个非空 item 的不同。具体要写两个抽象方法 areItemsTheSame 和 areContentsTheSame

1
2
3
4
5
6
7
8
kotlin复制代码val CALLBACK: DiffUtil.ItemCallback<Model> = object : DiffUtil.ItemCallback<Model>() {
// 判断两个Objects 是否代表同一个item对象, 一般使用Bean的id比较
override fun areItemsTheSame(oldItem: Model, newItem: Model): Boolean =
oldItem.id == newItem.id

// 判断两个Objects 是否有相同的内容。
override fun areContentsTheSame(oldItem: Model, newItem: Model): Boolean = true
}

Step3: 绑定 RecyclerView 和 Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码<data>

<variable
name="viewModel"
type="com.hi.dhl.jdatabinding.demo.ui.MainViewModel" />

<variable
name="testAdapter"
type="com.hi.dhl.jdatabinding.demo.ui.TestAdapter" />
</data>

<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:adapter="@{testAdapter}"
app:adapterList="@{viewModel.mLiveData}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

这里用到了 DataBinding 的自定义数据绑定部分,可以百度、Google 具体的用法,具体实现可以参考 demo 下面 fragment_test.xml 文件

DataBindingDialog

在 Kotlin 中应该尽量避免使用构建者模式,使用 Kotlin 的具名可选参数构造类,实现构建者模式,代码更加简洁

在 “Effective Java” 书中介绍构建者模式时,是这样子描述它的:本质上 builder 模式模拟了具名的可算参数,就像 Ada和 Python中的一样

幸运的是,Kotlin 是一门拥有具名可选参数的变成语言,DataBindingDialog 在使用 Kotlin 的具名可选参数构造类实现 Dailog 构建者模式的基础上,用 DataBinding 进行二次封装,加上 DataBinding 数据绑定的特性,使 Dialog 变得更加简洁、易用

Step1: 继承 DataBindingDialog

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
kotlin复制代码class AppDialog(
context: Context,
val title: String? = null,
val message: String? = null,
val yes: AppDialog.() -> Unit
) : DataBindingDialog(context, R.style.AppDialog) {
private val mBinding: DialogAppBinding by binding(R.layout.dialog_app)

init {
requireNotNull(message) { "message must be not null" }
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)

mBinding.apply {
setContentView(root)
display.text = message
btnNo.setOnClickListener { dismiss() }
btnYes.setOnClickListener { yes() }
}

}
}

Step2: 简洁的调用方式

1
2
3
4
5
6
less复制代码AppDialog(
context = this@MainActivity,
message = msg,
yes = {
// do something
}).show()

DataBindingActivity

Kotlin 中的函数和构造器都支持具名可选参数,在使用上更加灵活,在 DataBindingActivity 中使用 Kotlin 的 inline、reified 强大的特性,将类型参数实化,初始化 View 更加简洁

继承 DataBindingActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
kotlin复制代码class MainActivity : DataBindingActivity() {
private val mBinding: ActivityMainBinding by binding(R.layout.activity_main)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding.apply {
dialog.setOnClickListener {
val msg = getString(R.string.dialog_msg)
AppDialog(
context = this@MainActivity,
message = msg,
yes = {
Toast.makeText(this@MainActivity, msg, Toast.LENGTH_SHORT).show()
}).show()
}
}
}
}

DataBindingFragment

在 Fragment 当中如何使用 Kotlin 的 inline、reified 初始化 View,可以查看DataBindingFragment

继承自 DataBindingFragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
kotlin复制代码class FragmentTest : DataBindingFragment() {
val testViewModel: MainViewModel by viewModel()

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

return binding<FragmentTestBinding>(
inflater,
R.layout.fragment_test, container
).apply {
viewModel = testViewModel
testAdapter = TestAdapter()
lifecycleOwner = this@FragmentTest
}.root
}
}

关于基于 DataBinding 封装的 DataBindingActivity、DataBindingFragment、DataBindingDialog、DataBindingListAdapter 基础库,点击 JDataBinding 前往查看,欢迎start

JDataBinding 源码地址:https://github.com/hi-dhl/JDataBinding

参考文献

github.com/..BaseRecyc…

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 如何高效获取视频截图
  • 10分钟入门 Shell 脚本编程
  • 如何在项目中封装 Kotlin + Android Databinding

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

Servlet入门看这一篇就够了

发表于 2020-04-18

公众号 《java编程手记》记录JAVA学习日常,分享学习路上点点滴滴,从入门到放弃,欢迎关注

image.png

Servlet是什么

官方解说: Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。

通俗的讲,servlet是运行在web服务器如tomcat,jetty这样应用服务器上的一段程序,他可以响应http协议的请求,并且实现用户自己的逻辑,最终将结果返回到用户的客户端(浏览器)

如何实现一个Servlet

我们先来看看如何实现一个Servlet,通常实现一个servlet需要如下几步,

创建项目

这里我们以IDEA(专业版)为例

  1. File -> new -> Project

在这里插入图片描述

  1. 左侧栏选择Java,右侧Project SDK 选择自己的jdk,没有的话需要自己new添加以下, 框中需要勾选 Web Application ,表示我们创建的是一个Servlet项目

在这里插入图片描述

  1. next 填写 Project name , Project location , 然后 finish

至此项目已经创建完毕,默认创建完毕的目录有 src,用来存放我们的java代码,web用来存放关于web的一些资源,web下面有WEB-INF和默认生成的index.jsp,WEB-INF目录下面有一个web.xml

在这里插入图片描述

下载Tomcat

访问 tomcat.apache.org/download-90… ,下载tomcat9,根据自己的系统下载对应的文件

在这里插入图片描述
下载下来后解压,等待下面配置使用

IDEA中配置Tomcat

点击 Add Configuration

在这里插入图片描述

依次选择 + -> Tomcat Server - > Local

  • ① 配置本地tomcat,配置下上面下载的tomcat
  • ② 默认启动后打开浏览器
  • ③ 默认启动后打开浏览器的地址
  • ④ 设置启动VM参数
  • ⑤ 设置Tomcat所用的JDK
  • ⑥ 设置Tomcat显示的名称

Tomcat基础信息配置完成之后,需要将当前项目配置到Tomcat中,切换到Deployment配置

在这里插入图片描述

切换到Deployment后,点击 右边的 + 号,选择 Artifact 添加当前项目的war包,或者点击下方的Fix添加war包

在这里插入图片描述

添加war包后,下方会有一个Application context 路径配置,用来配置当前项目的路径名,也就是之前讲Tomcat时候webapps目录下的项目名称,所有这个项目下的servlet请求都需要在这个路径下来进行,比如我上面的配置,后续的请求就是 localhost:8080/servlet_demo_war_exploded/* ,必须加上/servlet_demo_war_exploded才能访问到当前项目

编写一个Servlet

终于来到我们的正题了,编写一个servlet程序,按照Servlet规范,我们需要引入servlet-api这个jar包,然后继承HttpServlet这个类,并且重写对应的doGet和doPost方法。

servlet-api包在我们下载的解压tomcat的lib目录里面

在这里插入图片描述

在 web 目录下新建lib目录,将servlet-api.jar复制到新建的lib目录,右键servlet-api.jar ,点击 Add as Library… , OK即可

在这里插入图片描述

在这里插入图片描述

在src目录下新建DemoServlet,继承javax.servlet.http.HttpServlet,并且重写doGet,doPost方法

1
2
3
4
5
java复制代码 PrintWriter writer = resp.getWriter();
PrintWriter writer = resp.getWriter();
writer.print("this is servlet html");
writer.flush();
writer.close();

修改web/WEB-INF/web.xml文件,添加Servlet配置项

1
2
3
4
5
6
7
8
9
xml复制代码 <servlet>
<servlet-name>demoServlet</servlet-name> //定义servletname
<servlet-class >com.muku.servlet.DemoServlet</servlet-class> //上面继承HttpServlet的class
</servlet>

<servlet-mapping>
<servlet-name>demoServlet</servlet-name> //上面servlet-name
<url-pattern>/*</url-pattern> //表示拦截所有请求
</servlet-mapping>
1
2
3
4
5
6
7
java复制代码servlet 	定义一个Servlet
servlet-name 为servlet命名
servlet-class 我们自己实现的servletclass类

servlet-mapping 定义servlet与请求的映射关系
url-pattern 配置拦截的请求路径,比如 /html/1 /html/2 ,/*表示拦截所有请求
servlet-name url-pattern中定义的拦截请求路径,由哪个servlet来处理

总体的修改截图如下

在这里插入图片描述

在这里插入图片描述

启动我们配置好的tomcat,启动成功后会IDEA会自动打开浏览器,并且访问我们设置好的路径,不出意外的话就可以看到 , this is servlet html
在这里插入图片描述

至此,一个Servlet已经编写成功,并且可以成功访问了

基于注解的Servlet

上面编写一个Servlet我们需要修改web.xml,并且继承HttpServlet类才能实现一个Servlet,在Servlet3.0后提供了基于注解的方式来实现Servlet开发,话不多说,直接上代码

1
2
3
4
5
6
7
8
9
10
11
java复制代码@WebServlet(name = "annotation",urlPatterns = "/*")
public class AnnotationServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
writer.print("this is servlet html");
writer.flush();
writer.close();
}
}

直接在我们实现类的上面加入WebServlet这个注解即可

WebServlet注解主要有以下几个重要属性

  • name 表示当前servlet的名称,与之前wem.xml中的servlet-name相对应
  • urlPatterns 表示当前servlet处理的请求路径映射
  • initParams 表示servlet初始化配置

Servlet中重要的几个对象

HttpServletRequest

每当有一个http请求过来,就会将当前http请求的信息封装为一个HttpServletRequest对象,并且通过doGet,doPost等方法当作入参传入,HttpServletRequest有如下几个常用方法

  • getHeader 获取请求头的头部信息
  • getCookies 获取当前请求的cookie信息
  • getMethod 获取当前请求方法类别 Get Post
  • getQueryString 获取当前请求参数的kv串 k1=v1=&k2=v2
  • getRequestURI 获取当前请求路径 /servlet/a
  • getRequestURL 获取当前请求的总路径,包含域名协议 http://localhost:8080/servlet/a
  • getSession 获取当前请求的session信息
  • getParameter 获取请求参数中对应key的value

HttpServletResponse

每当有一个http请求过来,就会将当前http返回的信息封装为一个HttpServletResponse对象,我们可以通过设置resonse的属性来返回相对应的数据,HttpServletRequest有如下几个常用方法

  • addCookie 添加cookie,可以向当前http请求设置cookie,在下次请求的时候会携带回来
  • sendRedirect 重定向到指定请求地址,code会被设置为302
  • addHeader 设置返回的头部数据
  • setStatus 设置相应的状态码,常见的状态码包含200 302 405 500等等
  • setContentType 设置相应数据的格式,比如常见的有text/html,text/javascript , text/css , application/json 等等,浏览器会根据contentType来渲染对应的数据
  • getWriter 重要方法,获取输出流对象,可以通过write方法来向页面输出内容,比如上面写到的 this is servlet html

ServletConfig

ServletConfig是servlet级别的,每个servlet都有一些自己的属性,包括名称,初始化参数等等,这些属性在每个servlet之间是独有,这些属性的集合就是用ServletConfig对象来表示的

  • getServletName 获取当前servlet名称
  • getServletContext 获取ServletContext对象,每个web应用都有且仅有一个ServletContext对象,
  • getInitParameter 获取servlet初始化配置的参数信息
  • getInitParameterNames 获取servlet所有初始化配置信息的名称集合

ServletContext

一个web应用在启动时会创建一个ServletContext对象,表示web应用的上下文,可以用来配置读取当前应用的全局配置,servlet之间通过servletContext对象来进行通信,servletContext对象可以通过servletConfig对象来获取

  • get/set Attribute 设置获取servlet全局属性
  • getAttributeNames 获取所有属性的名称集合
  • addServlet 添加Servlet
  • addFilter 添加过滤器
  • addListener 添加监听器

Servlet的生命周期

Servlet的生命周期设计到servlet从创建到被销毁,主要包括如下过程

  • 调用init方法进行初始化操作
  • 调用service方法处理请求,根据请求方法调用对应的doGet doPost doPut等方法
  • 调用destroy方法销毁

Init

Init方法在servlet的生命周期中只在创建时被调用且只被调用一次,一般用于一些初始化操作,如初始化连接池,连接数据库等等

Service

service方法是在处理http请求时的重要方法,每当新来一个http请求时,服务端就会产生一个新的线程来处理这个请求,然后经过service方法来进行处理,service会根据当前请求的方法类型来调用doGet,doPost等方法,这些方法又会被我们重写,所以每个请求真正被处理的其实还是我们重写的逻辑

destroy

destroy方法只会在Servlet生命周期结束时被调用,主要用来做一些结尾性的操作,比如关闭数据库的连接池,清空内存写入等等

本文转载自: 掘金

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

1…819820821…956

开发者博客

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