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

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


  • 首页

  • 归档

  • 搜索

自从用完Gradle后,有点嫌弃Maven了!速度贼快! 一

发表于 2021-08-25

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

相信使用Java的同学都用过Maven,这是一个非常经典好用的项目构建工具。但是如果你经常使用Maven,可能会发现Maven有一些地方用的让人不太舒服:

  1. Maven的配置文件是XML格式的,假如你的项目依赖的包比较多,那么XML文件就会变得非常非常长;
  1. XML文件不太灵活,假如你需要在构建过程中添加一些自定义逻辑,搞起来非常麻烦;
  1. Maven非常的稳定,但是相对的就是对新版java支持不足,哪怕就是为了编译java11,也需要更新内置的Maven插件。如果你对Maven的这些缺点也有所感触,准备尝试其他的构建工具,那么你可以试试gradle,这是一个全新的java构建工具,解决了Maven的一些痛点。

一、安装Gradle

最传统的安装方法就是去gradle官网下载二进制包,解压,然后将路径添加到环境变量中。如果你没什么其他需求,可以使用这种安装方式。但是,gradle是一个非常新潮的项目,每隔几个月就会发布一个新版本,这种方式可能跟不上gradle的更新速度。

所以我更加推荐使用包管理器来安装gradle。如果你使用linux系统,那么不必多说。如果你使用Windows系统,我推荐使用scoop包管理器来安装gradle。它安装方便,而且使用SHIM目录来管理环境变量,在各种工具中配置gradle也很方便。

当然,如果你完全不喜欢安装这么多乱七八糟的东西,那也可以使用gradle。gradle提供了一个名为gradle wrapper的工具,可以在没有安装gradle的情况下使用gradle。

好吧,其实它就是个脚本文件,当你运行wrapper脚本的时候,如果脚本发现你电脑里没有gradle,就会自动替你下载安装一个。现在甚至还出现了Maven wrapper,也是个脚本文件,可以自动安装Maven。

之前相信一些朋友听说过gradle,然后尝试使用它,结果因为速度太慢,最后放弃了。之前我也因为gradle的速度,放弃了它一段时间。不过现在使用gradle的话会方便很多。gradle官方在中国开设了,CDN,使用gradle wrapper的时候下载速度非常快。可以说现在是一个学习使用gradle的好时候。

二、使用gradle wrapper

这里我使用的IDEA来创建和使用gradle项目。

)​

IDEA默认就会使用gradle wrapper来创建项目,所以无需安装gradle也可以正常运行。这时候项目结构应该类似下图所示,使用Maven的同学应该比较熟悉,因为这和Maven的项目结构几乎完全一致。

gradle文件夹和gradlew那几个文件就是gradle wrapper的文件,而.gradle后缀名的文件正是gradle的配置文件,对应于Maven的pom.xml。

)​

gradle wrapper的优点之一就是可以自定义下载的gradle的版本,如果是团队协作的话,这个功能就非常方便,简单设置即可统一团队的构建工具版本。

这里我就设定成目前最新的gradle 6.4.默认下载安装的是bin版,仅包含二进制。如果你使用IDEA的话,它会推荐下载all版,包含源代码,这样IDEA就可以分析源代码,提供更加精确的gradle脚本支持。

)​

三、依赖管理

下面来看看gradle的依赖管理功能,这也算是我们使用构建工具的主要目的之一了。这点也是gradle相较maven的优势之一了。相较于maven一大串的XML配置,gradle的依赖项仅需一行。

dependencies {

testImplementation ‘junit:junit:4.13’

implementation ‘com.google.code.gson:gson:2.8.6’

}这里推荐一下Jetbrains的package search网站,是寻找maven和gradle依赖包的最佳网站,可以非常轻松的搜索和使用依赖项。

)​

gradle依赖的粒度控制相较于Maven也更加精细,maven只有compile、provided、test、runtime四种scope,而gradle有以下几种scope:

1.implementation,默认的scope。implementation的作用域会让依赖在编译和运行时均包含在内,但是不会暴露在类库使用者的编译时。举例,如果我们的类库包含了gson,那么其他人使用我们的类库时,编译时不会出现gson的依赖。

2.api,和implementation类似,都是编译和运行时都可见的依赖。但是api允许我们将自己类库的依赖暴露给我们类库的使用者。

3.compileOnly和runtimeOnly,这两种顾名思义,一种只在编译时可见,一种只在运行时可见。而runtimeOnly和Maven的provided比较接近。

4.testImplementation,这种依赖在测试编译时和运行时可见,类似于Maven的test作用域。

5.testCompileOnly和testRuntimeOnly,这两种类似于compileOnly和runtimeOnly,但是作用于测试编译时和运行时。通过简短精悍的依赖配置和多种多样的作用与选择,Gradle可以为我们提供比Maven更加优秀的依赖管理功能。

四、gradle的任务和插件

gradle的配置文件是一个groovy脚本文件,在其中我们可以以编程方式自定义一些构建任务。因为使用了编程方式,所以这带给了我们极大的灵活性和便捷性。

打个比方,现在有个需求,要在打包出jar的时候顺便看看jar文件的大小。在gradle中仅需在构建脚本中编写几行代码即可。而在Maven中则需要编写Maven插件,复杂程度完全不在一个水平。

当然,Maven发展到现在,已经存在了大量的插件,提供了各式各样的功能可以使用。但是在灵活性方面还是无法和Gradle相比。

而且Gradle也有插件功能,现在发展也十分迅猛,存在了大量非常好用的插件,例如gretty插件。gretty原来是社区插件,后来被官方吸收为官方插件,可以在Tomcat和jetty服务器上运行web项目,比Maven的相关插件功能都强大。

虽然gradle可以非常灵活的编写自定义脚本任务,但是其实一般情况下我们不需要编写构建脚本,利用现有的插件和任务即可完成相关功能。在IDEA里,也可以轻松的查看当前gradle项目中有多少任务,基本任务如build、test等Maven和Gradle都是相通的。

)​

五、配置镜像

Maven官方仓库的下载速度非常慢,所以一般我们要配置国内的镜像源。gradle在这方面和Maven完全兼容,因此只需稍微配置一下镜像源,即可使用Maven的镜像。

如果你用gradle构建过项目,应该就可以在用户目录的.gradle文件夹下看到gradle的相关配置和缓存。之前wrapper下载的gradle也存放在该文件夹下,位置是wrapper/dists。

)​

而依赖的本地缓存在caches\modules-2\files-2.1文件夹下。目录结构和Maven的本地缓存类似,都是包名+版本号的方式,但是gradle的目录结构最后一层和Maven不同,这导致它们无法共用本地缓存。

)​

言归正传,在gradle中配置下载镜像需要在.gradle文件夹中直接新建一个init.gradle初始化脚本,脚本文件内容如下。这样一来,gradle下载镜像的时候就会使用这里配置的镜像源下载,速度会快很多。再加上gradle wrapper在中国设置了CDN,现在使用gradle的速度应该会很快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
arduino复制代码allprojects {
repositories {
maven {
url "https://maven.aliyun.com/repository/public"
}
maven {
url "https://maven.aliyun.com/repository/jcenter"
}
maven {
url "https://maven.aliyun.com/repository/spring"
}
maven {
url "https://maven.aliyun.com/repository/spring-plugin"
}
maven {
url "https://maven.aliyun.com/repository/gradle-plugin"
}
maven {
url "https://maven.aliyun.com/repository/google"
}
maven {
url "https://maven.aliyun.com/repository/grails-core"
}
maven {
url "https://maven.aliyun.com/repository/apache-snapshots"
}
}
}

当然,如果你有代理的话,其实我推荐你直接为gradle设置全局代理。因为gradle脚本实在是太灵活了,有些脚本中可能依赖了github或者其他地方的远程脚本。这时候上面设置的下载镜像源就不管用了。

所以有条件还是干脆直接使用全局代理比较好。设置方式很简单,在.gradle文件夹中新建gradle.properties文件,内容如下。中间几行即是设置代理的配置项。

当然其他几行我也建议你设置一下,把gradle运行时的文件编码设置为UTF8,增加跨平台兼容性。

1
2
3
4
5
6
7
ini复制代码org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=10800
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=10800
systemProp.file.encoding=UTF-8
org.gradle.warning.mode=all

六、为什么使用gradle?

看到这里,你应该对gradle有了基本的了解, 也可以将其用于你的项目之中。但是如果你Maven已经非常熟悉了,可能不太愿意使用gradle,因为貌似没有必要。但是既然gradle出现了,就说明有很多人对Maven还是有一定的意见。

因此在这里我来总结一下gradle相比maven的优势。

  1. 速度,gradle使用构建缓存、守护进程等方式提高编译速度。结果就是gradle的编译速度要远超maven,平均编译速度比Maven快好几倍,而且项目越大,这个差距就越明显。

)​

  1. 灵活性,gradle要比Maven灵活太多,虽然有时候灵活并不是一件好事情。但是大部分情况下,灵活一点可以极大的方便我们。Maven死板的XML文件方式做起事情来非常麻烦。

很多Maven项目都通过执行外部脚本的方式来完成一些需要灵活性的工作。而在gradle中配置文件就是构建脚本,构建脚本就是编程语言(groovy编程语言),完全可以自给自足,无需外部脚本。

  1. 简洁性,完成同样的功能,gradle脚本的长度要远远短于maven配置文件的长度。虽然很多人都说XML维护起来不麻烦,但是我觉得,维护一个光是依赖就有几百行的XML文件,不见得就比gradle脚本简单。

也许是因为我上面说的原因,也许有其他原因,不得不承认的一件事情就是gradle作为一个新兴的工具已经有了广泛的应用。spring等项目已经从Maven切换到了gradle。

开发安卓程序也只支持gradle了。因此不管是否现在需要将项目从maven切换到gradle,但是至少学习gradle是一件必要的事情。​

本文转载自: 掘金

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

如何正确创建线程池

发表于 2021-08-25

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

WangScaler: 一个用心创作的作者。

声明:才疏学浅,如有错误,恳请指正。
上篇文章线程池的基本介绍,我们简单的介绍了线程池的执行过程,并提到了线程池的错误用法,这篇文章将告诉你,如何正确的创建线程池。

拒绝策略

当任务队列排满之后,如果正在运行的线程数小于最大线程数的时候,会创建新的线程执行任务,然而当我们的线程数达到最大线程数之后,就会采取拒绝策略,也就是线程池的最后一个参数handler。jdk的内置拒绝策略均实现了RejectedExecutionHandler接口,有以下几种:

  • AbortPolicy:抛出RejectedExecutionException异常,丢弃任务。也是默认的拒绝策略。
  • CallerRunsPolicy:既不丢弃也不抛出异常,而是将任务回退给调用的线程。
  • DiscardOldestPolicy:丢弃等待最久的任务,将当前任务加入任务队列。
  • DiscardPolicy:直接丢弃任务,也不抛出异常。

正确创建线程池

在上篇文章线程池的基本介绍,我已经说过简单使用的示例不可用于正式开发,如果你安装了阿里的代码检查软件,就能看到控制台提示的错误。

image.png
阿里巴巴的开发手册明确表示了线程资源只允许线程池提供,不允许在应用中自行显示创建。

为什么不能创建呢?

因为如果不自定义线程池,有可能造成系统创建大量的同类线程而导致内存被耗尽或者过度切换的问题。再一个就是像newFixedThreadPool和newSingleThreadExecutor创建线程池的时候,任务队列使用的LinkedBlockingQueue,我们在阻塞队列说过了。虽然LinkedBlockingQueue是有界的,最大为Integer.MAX_VALUE,所以说LinkedBlockingQueue是无界队列也不过分,这么大的数值会将所有的任务加入任务队列。

面试常见的问题,就是实际开发中你使用哪个jdk中创建线程池的方法?

现在知道怎么回答了吧,那就是那个也没用,都是自己手写的。那么接下来就写一个例子。

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复制代码package com.wangscaler.thread;

import java.util.concurrent.*;


/**
* @author WangScaler
* @date 2021/8/25 14:00
*/

public class ThreadPool {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(2, 5, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 0; i < 9; i++) {
pool.execute(() -> System.out.println(Thread.currentThread().getName()));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
}
}

线程数如何设置

可参考文章Java线程池如何合理配置核心线程数,不过部分内容有误。

  • cpu密集型:像加解密、压缩、计算等需要大量消耗cpu资源的任务,一般设置为cpu核数+1。
  • io密集型:通信,数据库交互、读写文件等等。
+ cpu核数\*2。
+ CPU 核数 / (1 - 阻塞系数),阻塞系数一般认为在 0.8 ~ 0.9 之间。比如 8 核 CPU,按照公式就是 8 / ( 1 - 0.9 ) = 80 个线程数。

两个执行参数的区别

我们在多线程的三种写法中提到过,Runnable接口没有返回值,而Callable接口有返回结果。相对应的execute没有返回值,而submit有返回值。当你需要接受线程返回值的时候,应该使用submit。可参考文章# ExecutorService中submit和execute的区别

结语

上面提到了四种拒绝策略,应该根据你的业务场景做出选择。

来都来了,点个赞再走呗!

关注WangScaler,祝你升职、加薪、不提桶!

本文转载自: 掘金

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

使用Egg框架进行server端开发

发表于 2021-08-25

一、server端开发必备知识

针对前端开发者而言,学习node开发的难点,其实不在于学习一个框架,也不在于学习一门语言,而是在于无法从server端开发的全局,系统性的把所有东西串联起来。

node-server开发到底需要哪些知识储备呢?

总的来说,就是语言 + 框架 + mysql + ORM框架。学习成本其实主要是语言+框架。

学习成本

语言是基础,能跑demo的角度上来说,少则一两周,多则一两月;框架跑起来,能写demo,也应该是一两周的时间,mysql用一两天知道最基本的知识就行,ORM框架也用一两天搞定。(这里的时间是最小化的链路跑通时间,个人主张在链路跑通的情况下,在场景中学习具体的东西)

语法+框架

服务端开发,至少要学习一门语言,比如node、Java、go等等;学习完这门语言之后,需要学习这门语言对应的框架,比如针对node的egg框架,比如针对Java的spring框架,语言学习的是语法,框架是用一种更高效的方式用这种语法来完成某项功能;

数据库

学完框架,还需要了解数据库的知识,因为对于后端同学来说,需要和数据打交道,这是我们进行数据存储的地方,我们通过接口,对前端提供的是数据的curd能力,选型不用什么特殊的,就用mysql,无论大小厂,基本都用它,从学习成本上说,我们花一两天时间,学会建库和建表,以及简单的查询所有数据就行,具体的细节等具体业务场景再去学习。

数据库连接

学完数据库,还有一个东西需要学习,就是数据库连接,java的数据库连接,底层提供了jdbc的方案,node提供了mysql包进行数据库的连接,它相当于开发语言和数据库之间的桥梁,遵循一般的语法规则,就能上手业务开发。但是呢,这种方式在使用上不友好,因为前后端是通过对象的方式进行通信,而数据库字段和对象的映射关系我们就需要维护好,很费时费力。这个时候就提出了ORM框架。

ORM

ORM框架解决的问题就是把对象和数据库映射的复杂内部关系,框架来解决,我们仅仅进行简单的使用就好。java的ORM框架就是mybatis、而egg也提供了这样的ORM框架egg-sequelize。

二、技术层面

有了上面的整体的server端开发思维,我们再看egg,就很明确他的定位了。

\

egg就是基于node的一个企业级框架,它提供了开发node服务端的一整套的能力。下面的这张图,左边部分是框架涉及到的一些具体的实体对象,右边是egg框架的业务分层,是一种概念型的东西。具体可以结合egg官方文档通读一遍。

\

1、egg是一种node方式的MVC框架

它由这么几个步骤组成,路由的定义,指向向一个controller;controller为控制器,主要进行参数获取、数据返回处理;service进行处理具体的业务逻辑,比如数据库数据获取等等;

  • 路由定义:app/router.js
1
2
3
4
5
arduino复制代码module.exports = app => {
const { router, controller } = app;

router.get('/', controller.home.index);// 表示在匹配到'/',会定向到controller/home.js里面,这里面的class必然有一个index的方法。通过这个方法,决定返回的结果是什么
};
  • 控制器controller
1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码'use strict';

const Controller = require('egg').Controller;
class HomeController extends Controller {
async index() {
// 通常,在一个方法里面会进行参数获取,this.ctx.request.query(get方式)、this.ctx.request.body(post方式)、this.ctx.request.files(获取文件)
const { ctx } = this;
ctx.body = 'hi, egg';
// 通常,this.ctx.body,就是服务端向前端返回的字段。
}
}

module.exports = HomeController;
  • service处理业务逻辑
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
javascript复制代码'use strict'

const Service = require('egg').Service;
// service层就是具体的业务逻辑的处理。一般而言,通常进行数据库的数据查询、逻辑判断等等。

// 该Service的register的逻辑就是,从数据库查询是否存在当前用户,不存在就创建用户,存在就返回‘账号已存在的’异常
class User extends Service {
async register(user) {
const hasUser = await this.ctx.model.User.findOne({
where: {
username: user.username
}
});

if (!hasUser) {
const userInfo = await this.ctx.model.User.create(user);
if (userInfo) {
return this.ctx.response.ServerResponse.successRes(true)
}
return this.ctx.response.ServerResponse.errorRes('用户注册异常', true)
}

return this.ctx.response.ServerResponse.errorRes('账号已存在', true)
}
}

2、获取请求参数

  • get方式获取请求参数:this.ctx.request.query
  • post方式获取请求参数:this.ctx.request.body
  • file文件的获取:this.ctx.request.files

3、能力拓展-extends

helper,进行工具包扩展。

前端开发中,针对公用函数,一般会在一个utils公用工具文件中。

而在egg中,一般使用helper,进行工具的扩展。

1
2
3
4
5
6
javascript复制代码// app/extend/helper.js
module.exports = {
formatUser(user) {
return only(user, [ 'name', 'phone' ]);
}
};

在使用的时候:this.ctx.helper.formatUser(obj)。

response,针对数据请求的返回response进行扩展

一般而言,在这个部分,我会针对数据的返回格式,进行限制。

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
javascript复制代码'use strict'
// code码定义
const SUCCESS = 0;
const ERROR = 1;

class MyResponseData {
constructor(status, msg, data) {
this.msg = msg;
this.data = data;
this.status = status;
}

static successRes(data) {
return new MyResponseData(SUCCESS, 'success', data);
}

static errorRes(msg, data) {
return new MyResponseData(ERROR, msg, data);
}
}

// 这个事例最终返回的数据格式如下:interface ServerResponse {msg: string, status: number, data: any}

module.exports = {
ServerResponse: MyResponseData
}

其他的extends,还包括 application、request、context等。

4、数据库部分

一方面,这是前端本身技术的壁垒,没了解过后端开发的前端本身就对数据库不了解。

另一方面,通过egg官网,跑出来的demo,并不能直接连接到数据库,仅仅是一个运行示例。很多人在这里就断了。

想要这一部分跑通,在思维上一定要有这么一些概念。

1、一般而言,我们必须在自己的本机上安装了mysql,然后得记住安装的时候的账号和密码,这样我们的本地项目才有可能连接到我们的数据库。所谓的数据库,就是运行在机器上的数据存储系统,这是数据库厂商提供的工具。

2、线上环境,我们的数据库一般会由一个单独的服务器来提供。专业名词叫RDS。正常情况下,我们只需要关注数据库的账号和密码就行。

当你的本地安装了mysql,可以直接在egg中进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码config.sequelize = {
dialect: 'mysql', // support: mysql, mariadb, postgres, mssql
database: 'template-test',// 数据库名
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '*******',// 自己的密码
delegate: 'model',
baseDir: 'model',
define: {
// raw: true,
underscored: true,
freezeTableName: true, //直接查找设置的表名,默认是表名加s或者es
timestamps: false,
createdAt: "CreatedAt", //自定义时间戳
updatedAt: "UpdatedAt", // 自定义时间戳
timezone: '+08:00' // 保存为本地时区
}
};

\

5、如何通过node对数据库进行操作

这一部分,和我们选择的ORM框架有关系

在我们团队内部的ORM框架中,我们选择了egg-sequelize。

每一种ORM都有它自己的一些语法规则,通过相关文档直接操作即可。

这是egg-sequelize中,个人定义了一个简单的user的model。egg-sequelize通过定义的model,进行数据库的所有的数据的CURD。

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
javascript复制代码// app/model/user.js
const crypto =require('crypto');

module.exports = app => {
const {STRING, INTEGER, DATE} = app.Sequelize;

const User = app.model.define('user', {
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true
},
username: STRING(30),
password: {
type: STRING(100),
set: function(password) {
// let pas = crypto.createHash('md5').update(password).digest('hex');
let pas = crypto.createHash('md5').update(password).digest('hex');
console.log('pas', pas)
this.setDataValue("password", pas);
},
get: function() {
return this.getDataValue('password');
}
},
created_at: DATE,
updated_at: DATE,
})

User.prototype.validPassword = function (psd) {
console.log('psd', psd);
console.log('this.password', this.password);
let md5 = crypto.createHash('md5').update(psd).digest('hex')
console.log('md5', md5);
return this.password === crypto.createHash('md5').update(psd).digest('hex');
}
return User
}

数据查询-findOne

1
2
3
4
5
kotlin复制代码const user = this.ctx.model.User.findOne({
where: {
username: user.username
}
});

数据插入-create

1
csharp复制代码const userInfo = await this.ctx.model.User.create(user);

\

数据分页查询-findAndCountAll

1
csharp复制代码const userList = await this.ctx.model.User.findAndCountAll(query);

通过id查询-findByPk

1
csharp复制代码const user = await this.ctx.model.User.findByPk(id);

数据更新-update

1
kotlin复制代码const user = await this.ctx.model.User.update(data,{where: {id}});

三、业务层面

从开发的角度上来说,我们在具备了基础的技术方面的能力,我们发现作为前端,开发服务端还是会有问题,这些问题可能是经验上的,也有可能是一些常规性的,但是我们前端又比较欠缺的。

比如,我们做一个登陆,密码必须要进行密文存储,如何加密?常规的加密方式有哪些?所以我这边也会针对这些问题,总结我们开发需要了解的一些细节。

1、crypto

node提供的一个包,进行算法加密(md5、sha1等等),作用就是密码的密文存储。

为什么不进行明文存储?防止数据库数据泄露,用户的账号被窃取。

\

2、egg-sequelize

关于egg框架的一个ORM框架,进行数据的CURD。

文档:eggjs.org/zh-cn/tutor…

3、jwt

jwt是一种常用的用户验证的解决方案,这里不进行细讲。

jsonwebtoken,一个node包,用于登陆验证。核心有两个api。

1
2
3
4
5
scss复制代码// 这个是签名,生成的token,是要返回给前端的。后续的接口都会走校验,需要在header中,拿到这个token。
token = jwt.sign(data, key)

// 校验
jwt.verify(token, key)

4、egg-validate

进行参数校验的工具

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码const query = this.ctx.request.body;
const params = {
username: query.username,
password: query.password
};
try {
this.ctx.validate({
username: 'string',
password: 'string'
}, params);
} catch(e) {
this.ctx.body = e;
return
}

文档:www.npmjs.com/package/egg…

5、egg-multipart

文件上传的插件

文档:github.com/eggjs/egg-m…

6、多环境配置

一般来说,会分为三个环境:local、dev、prod

egg启动的时候,默认为local环境。如果想要设置环境,可以在命令行中设置。EGG_SERVER_ENV是对运行环境进行指定的命令行参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
perl复制代码"scripts": {
"start": "egg-scripts start --daemon --title=egg-server-template-node",// prod环境
"stop": "egg-scripts stop --title=egg-server-template-node",
"dev": "egg-bin dev",// local环境
"dev:dev": "EGG_SERVER_ENV=dev npm run dev",// dev环境
"debug": "egg-bin debug",
"test": "npm run lint -- --fix && npm run test-local",
"test-local": "egg-bin test",
"cov": "egg-bin cov",
"lint": "eslint .",
"ci": "npm run lint && npm run cov",
"autod": "autod"
},

不同环境的config,需要进行配置。

  • config.default.js,默认环境local。
  • config.dev.js,dev环境下会使用该配置,会和default合并。
  • config.prod.js,prod的配置。

这一块不同的配置,其实大多数情况下,主要是数据库的不同配置。

总的来说,对于新手,egg官方文档不能完全解决大家对于node server端开发的疑惑,本篇文章更多的是对于egg官方文档的一个补充,在整个大的思维框架下,帮助新手上手server端开发。

以上,关于egg的介绍就基本完毕。

本文转载自: 掘金

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

50行Python代码爬取黑丝美眉纯欲高清图 一、技术路线

发表于 2021-08-25

要说最美好的欲望莫过于看黑丝美眉。

一、技术路线

requests:网页请求

BeautifulSoup:解析html网页

re:正则表达式,提取html网页信息

os:保存文件

1
2
3
4
javascript复制代码import re
import requests
import os
from bs4 import BeautifulSoup

二、获取网页信息

获取网页信息的固定格式,返回的字符串格式的网页内容,其中headers参数可模拟人为的操作,‘欺骗’网站不被发现

1
2
3
4
5
6
7
8
9
10
11
python复制代码def getHtml(url):  #固定格式,获取html内容
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
} #模拟用户操作
try:
r = requests.get(url, headers=headers)
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except:
print('网络状态错误')

三、网页爬取分析

右键单击图片区域,选择 审查元素 ,可以查看当前网页图片详情链接,我就满心欢喜的复制链接打开保存,看看效果,结果一张图片只有60几kb,这就是缩略图啊,不清晰,果断舍弃。。。

50行Python代码爬取“黑丝”美眉纯欲性感高清图片

50行Python代码爬取“黑丝”美眉纯欲性感高清图片

没有办法,只有点击找到详情页链接,再进行单独爬取。

空白右键,查看页面源代码,把刚刚复制的缩略图链接复制查找快速定位,分析所有图片详情页链接存在div标签,并且class=‘list’ 唯一,因此可以使用BeautifulSoup提取此标签。并且发现图片详情页链接在herf=后面(同时我们注意到有部分无效链接也在div标签中,观察它们异同,发现无效链接存在’https’字样,因此可在代码中依据此排出无效链接,对应第4条中的函数代码),只需提取出来再在前面加上网页首页链接即可打开,并且右键图片,‘审查元素’,复制链接下载的图片接近1M,表示是高清图片了,到这一步我们只需调用下载保存函数即可保存图片

50行Python代码爬取“黑丝”美眉纯欲性感高清图片

50行Python代码爬取“黑丝”美眉纯欲性感高清图片

四、网页详情页链接获取

首要目标是将每页的每个图片的详情页链接给爬取下来,为后续的高清图片爬取做准备,这里直接使用定义函数def getUrlList(url)

1
2
3
4
5
6
7
8
9
10
11
python复制代码def getUrlList(url):  # 获取图片链接
url_list = [] #存储每张图片的url,用于后续内容爬取
demo = getHtml(url)
soup = BeautifulSoup(demo,'html.parser')
sp = soup.find_all('div', class_="list") #class='list'在全文唯一,因此作为锚,获取唯一的div标签;注意,这里的网页源代码是class,但是python为了和class(类)做区分,在最后面添加了_
nls = re.findall(r'a href="(.*?)" rel="external nofollow" rel="external nofollow" ', str(sp)) #用正则表达式提取链接
for i in nls:
if 'https' in i: #因所有无效链接中均含有'https'字符串,因此直接剔除无效链接(对应第3条的分析)
continue
url_list.append('http://www.netbian.com' + i) #在获取的链接中添加前缀,形成完整的有效链接
return url_list

五、依据图片链接保存图片

通过上面获取了每个图片的详情页链接后,打开,右键图片审查元素,复制链接即可快速定位,然后保存图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码def fillPic(url,page):
pic_url = getUrlList(url) #调用函数,获取当前页的所有图片详情页链接
path = './美女' # 保存路径
for p in range(len(pic_url)):
pic = getHtml(pic_url[p])
soup = BeautifulSoup(pic, 'html.parser')
psoup = soup.find('div', class_="pic") #class_="pic"作为锚,获取唯一div标签;注意,这里的网页源代码是class,但是python为了和class(类)做区分,在最后面添加了_
picUrl = re.findall(r'src="(.*?)"', str(psoup))[0] #利用正则表达式获取详情图片链接,因为这里返回的是列表形式,所以取第一个元素(只有一个元素,就不用遍历的方式了)
pic = requests.get(picUrl).content #打开图片链接,并以二进制形式返回(图片,声音,视频等要以二进制形式打开)
image_name ='美女' + '第{}页'.format(page) + str(p+1) + '.jpg' #给图片预定名字
image_path = path + '/' + image_name #定义图片保存的地址
with open(image_path, 'wb') as f: #保存图片
f.write(pic)
print(image_name, '下载完毕!!!')

六、main()函数

经过前面的主体框架搭建完毕之后,对整个程序做一个前置化,直接上代码

在这里第1页的链接是

www.netbian.com/meinv/

第2页的链接是

www.netbian.com/meinv/index…

并且后续页面是在第2页的基础上仅改变最后的数字,因此在写代码的时候要注意区分第1页和后续页面的链接,分别做处理;同时在main()函数还增加了自定义爬取页数的功能,详见代码

50行Python代码爬取“黑丝”美眉纯欲性感高清图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码def main():
n = input('请输入要爬取的页数:')
url = 'http://www.netbian.com/meinv/' # 资源的首页,可根据自己的需求查看不同分类,自定义改变目录,爬取相应资源
if not os.path.exists('./美女'): # 如果不存在,创建文件目录
os.mkdir('./美女/')
page = 1
fillPic(url, page) # 爬取第一页,因为第1页和后续页的链接的区别,单独处理第一页的爬取
if int(n) >= 2: #爬取第2页之后的资源
ls = list(range(2, 1 + int(n)))
url = 'http://www.netbian.com/meinv/'
for i in ls: #用遍历的方法对输入的需求爬取的页面做分别爬取处理
page = str(i)
url_page = 'http://www.netbian.com/meinv/'
url_page += 'index_' + page + '.htm' #获取第2页后的每页的详情链接
fillPic(url, page) #调用fillPic()函数

50行Python代码爬取“黑丝”美眉纯欲性感高清图片

七、完整代码

最后再调用main(),输入需要爬取的页数,即可开始爬取,完整代码如下

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
python复制代码import re
import requests
import os
from bs4 import BeautifulSoup

def getHtml(url): #固定格式,获取html内容
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36'
} #模拟用户操作
try:
r = requests.get(url, headers=headers)
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except:
print('网络状态错误')

def getUrlList(url): # 获取图片链接
url_list = [] #存储每张图片的url,用于后续内容爬取
demo = getHtml(url)
soup = BeautifulSoup(demo,'html.parser')
sp = soup.find_all('div', class_="list") #class='list'在全文唯一,因此作为锚,获取唯一的div标签;注意,这里的网页源代码是class,但是python为了和class(类)做区分,在最后面添加了_
nls = re.findall(r'a href="(.*?)" rel="external nofollow" rel="external nofollow" ', str(sp)) #用正则表达式提取链接
for i in nls:
if 'https' in i: #因所有无效链接中均含有'https'字符串,因此直接剔除无效链接(对应第3条的分析)
continue
url_list.append('http://www.netbian.com' + i) #在获取的链接中添加前缀,形成完整的有效链接
return url_list

def fillPic(url,page):
pic_url = getUrlList(url) #调用函数,获取当前页的所有图片详情页链接
path = './美女' # 保存路径
for p in range(len(pic_url)):
pic = getHtml(pic_url[p])
soup = BeautifulSoup(pic, 'html.parser')
psoup = soup.find('div', class_="pic") #class_="pic"作为锚,获取唯一div标签;注意,这里的网页源代码是class,但是python为了和class(类)做区分,在最后面添加了_
picUrl = re.findall(r'src="(.*?)"', str(psoup))[0] #利用正则表达式获取详情图片链接,因为这里返回的是列表形式,所以取第一个元素(只有一个元素,就不用遍历的方式了)
pic = requests.get(picUrl).content #打开图片链接,并以二进制形式返回(图片,声音,视频等要以二进制形式打开)
image_name ='美女' + '第{}页'.format(page) + str(p+1) + '.jpg' #给图片预定名字
image_path = path + '/' + image_name #定义图片保存的地址
with open(image_path, 'wb') as f: #保存图片
f.write(pic)
print(image_name, '下载完毕!!!')

def main():
n = input('请输入要爬取的页数:')
url = 'http://www.netbian.com/meinv/' # 资源的首页,可根据自己的需求查看不同分类,自定义改变目录,爬取相应资源
if not os.path.exists('./美女'): # 如果不存在,创建文件目录
os.mkdir('./美女/')
page = 1
fillPic(url, page) # 爬取第一页,因为第1页和后续页的链接的区别,单独处理第一页的爬取
if int(n) >= 2: #爬取第2页之后的资源
ls = list(range(2, 1 + int(n)))
url = 'http://www.netbian.com/meinv/'
for i in ls: #用遍历的方法对输入的需求爬取的页面做分别爬取处理
page = str(i)
url_page = 'http://www.netbian.com/meinv/'
url_page += 'index_' + page + '.htm' #获取第2页后的每页的详情链接
fillPic(url_page, page) #调用fillPic()函数

main()

**①兼职交流,行业咨询、大佬在线专业解答

②Python开发环境安装教程

③Python400集自学视频

④软件开发常用词汇

⑤Python学习路线图

⑥3000多本Python电子书**

如果你用得到的话可以直接拿走,点击领取。

到此这篇关于爬取网络黑丝美女高清图片的文章就介绍到这,感谢观看,希望给想学习的朋友有提供到作用,更多Python精彩内容可以看小编主页。

本文转载自: 掘金

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

SpringCloudAlibaba全网最全讲解2️⃣(建议

发表于 2021-08-25

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

🌈往期回顾

**感谢阅读,希望能对你有所帮助,博文若有瑕疵请在评论区留言或在主页个人介绍中添加我私聊我,感谢每一位小伙伴不吝赐教。我是XiaoLin,既会写bug也会唱rap的男孩**
  • SpringCloudAlibaba全网最全讲解1️⃣(建议收藏)
  • 💖10分钟认识RocketMQ!想进阿里连这个都不会?2️⃣💖
  • 💖10分钟认识RocketMQ!想进阿里连这个都不会?1️⃣💖

二、Spring Cloud

2.1、什么是SpringCloud

SpringCloud是一个含概多个子项目的开发工具集,集合了众多的开源框架,他利用了Spring Boot开发的便利性实现了很多功能,如服务注册,服务注册发现,负载均衡等.SpringCloud在整合过程中主要是针对Netflix(奈飞)开源组件的封装.SpringCloud的出现真正的简化了分布式架构的开发。

NetFlix 是美国的一个在线视频网站,微服务业的翘楚,他是公认的大规模生产级微服务的杰出实践者,NetFlix的开源组件已经在他大规模分布式微服务环境中经过多年的生产实战验证,因此Spring Cloud中很多组件都是基于NetFlix组件的封装。

2.2、核心组件

  1. eurekaserver、consul、nacos:服务注册中心组件。
  2. rabbion & openfeign:服务负载均衡 和 服务调用组件。
  3. hystrix & hystrix dashboard:服务断路器和服务监控组件。
  4. zuul、gateway:服务网关组件。
  5. config:统一配置中心组件。
  6. bus:消息总线组件。

image-20200724161314786

2.3、版本命名

SpringCloud是一个由众多独立子项目组成的大型综合项目,原则每个子项目上有不同的发布节奏,都维护自己发布版本号。为了更好的管理springcloud的版本,通过一个资源清单BOM(Bill of Materials),为避免与子项目的发布号混淆,所以没有采用版本号的方式,而是通过命名的方式。这些名字是按字母顺序排列的。如伦敦地铁站的名称(“天使”是第一个版本,“布里斯顿”是第二个版本,"卡姆登"是第三个版本)。当单个项目的点发布累积到一个临界量,或者其中一个项目中有一个关键缺陷需要每个人都可以使用时,发布序列将推出名称以“.SRX”结尾的“服务发布”,其中“X”是一个数字。


伦敦地铁站的名字大致有如下:Angel、Brixton、Camden、Dalston、Edgware、Finchley、Greenwich、Hoxton。

2.4、版本选择

由于SpringCloud的版本是必须和SpringBoot的版本对应的,所以必须要根据SpringCloud版本来选择SpringBoot的版本。

image-20200709112427684

三、SpringCloud Alibaba

3.1、简介

Spring Cloud Alibaba是Spring Cloud下的一个子项目,Spring Cloud Alibaba为分布式应用程序开发提供了一站式解决方案,它包含开发分布式应用程序所需的所有组件,使您可以轻松地使用Spring Cloud开发应用程序,使用Spring Cloud Alibaba,您只需要添加一些注解和少量配置即可将Spring Cloud应用程序连接到Alibaba的分布式解决方案,并使用Alibaba中间件构建分布式应用程序系统。Spring Cloud Alibaba 是阿里巴巴开源中间件跟 Spring Cloud 体系的融合。

image-20210504210759006

3.2、主要功能

  1. 流量控制和服务降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud、Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  2. 服务注册和发现:实例可以在Alibaba Nacos上注册,客户可以使用Spring管理的bean发现实例,通过Spring Cloud Netflix支持Ribbon客户端负载均衡器。
  3. 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  4. 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  5. 消息总线:使用Spring Cloud Bus RocketMQ链接分布式系统的节点。
  6. 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  7. Dubbo RPC:通过Apache Dubbo RPC扩展Spring Cloud服务到服务调用的通信协议。
  8. 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行。

3.3、组件

  1. Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  2. Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  3. RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠
    的消息发布与订阅服务。
  4. Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
  5. Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  6. Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心
    产品。
  7. Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提
    供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和
    访问任意类型的数据。
  8. Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精
    准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  9. Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速
    搭建客户触达通道。

image-20210504211009062

四、微服务项目搭建

4.1、技术选型

  • 持久层:SpingData Jpa
  • 数据库: MySQL5.7
  • 技术栈:SpringCloud Alibaba 技术栈

4.2、模块设计

我们搭建一个微服务的项目,但是只有简单的代码,没有任何业务逻辑。
  • shop-parent 父工程
  • shop-product-api:商品微服务api ,用于存放商品实体。
  • shop-product-server:商品微服务,他的1端口是808x。
  • shop-order-api 订单微服务api,用于存放订单实体。
  • shop-order-server 订单微服务,他的端口是808x。

4.3、微服务的调用

;在微服务架构中,最常见的场景就是微服务之间的相互调用。我们以电商系统中常见的**用户下单**为例来演示微服务的调用:客户向订单微服务发起一个下单的请求,在进行保存订单之前需要调用商品微服务查询商品的信息。

我们一般把服务的主动调用方称为服务消费者,把服务的被调用方称为服务提供者。

image-20201028144911439

4.4、创建父工程

创建一个maven工程,然后在pom.xml文件中添加下面内容
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.3.RELEASE</version>
<relativePath/>
</parent>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-parent</artifactId>
<packaging>pom</packaging>
<version>1.0.0</version>
<modules>
<module>Shop-order-api</module>
<module>Shop-order-server</module>
<module>Shop-product-api</module>
<module>Shop-product-server</module>
</modules>
<properties>
<java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

4.5、创建商品服务

4.5.1、书写Shop-product-api的依赖

创建Shop-product-api项目,然后在pom.xml文件中添加下面内容

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>Shop-product-api</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>

4.5.2、创建实体

1
2
3
4
5
6
7
8
9
10
11
java复制代码//商品
@Entity(name = "t_shop_product")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long pid;//主键
private String pname;//商品名称
private Double pprice;//商品价格
private Integer stock;//库存
}

4.5.3、书写Shop-product-server的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>Shop-order-server</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-order-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-product-api</artifactId>
<version>1.0.0</version>
</dependency>

</dependencies>
</project>

4.5.4、编写application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码server:
port: 8081
spring:
application:
name: product-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: admin
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect

4.5.5、创建数据库

由于我们使用的是JPA,所以我们需要创建数据库,但是不需要创建表,因为JPA会在对应的数据库中自动创建表。

4.5.6、创建DAO接口

1
2
3
4
java复制代码// 第一个参数是实体类,第二个参数是实体类对象的主键的类型
public interface ProductDao extends JpaRepository<Product,Long> {

}

4.5.7、创建Service接口及其实现类

1
2
3
java复制代码public interface ProductService {
Product findById(Long productId);
}
1
2
3
4
5
6
7
8
9
10
java复制代码@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductDao productDao;

@Override
public Product findById(Long productId) {
return productDao.findById(productId).get();
}
}

4.5.8、书写Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RestController
@Slf4j
public class ProductController {
@Autowired
private ProductService productService;

//商品信息查询
@RequestMapping("/product")
public Product findByPid(@RequestParam("pid") Long pid) {
Product product = productService.findByPid(pid);
return product;
}
}

4.5.9、加入测试数据

我们在启动项目的时候,可以发现表已经默认帮我们自动创建好了,我们需要导入测试数据。

1
2
3
4
mysql复制代码INSERT INTO t_shop_product VALUE(NULL,'小米','1000','5000'); 
INSERT INTO t_shop_product VALUE(NULL,'华为','2000','5000');
INSERT INTO t_shop_product VALUE(NULL,'苹果','3000','5000');
INSERT INTO t_shop_product VALUE(NULL,'OPPO','4000','5000');

4.5.10、测试

image-20210505140802977

4.6、创建订单微服务

4.6.1、书写Shop-order-api的依赖

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>Shop-order-api</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
</project>

4.6.2、创建订单实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码//订单
@Entity(name = "t_shop_order")
@Data
@JsonIgnoreProperties(value = { "hibernateLazyInitializer"})
public class Order implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long oid;//订单id

//用户
private Long uid;//用户id
private String username;//用户名
//商品
private Long pid;//商品id
private String pname;//商品名称
private Double pprice;//商品单价
//数量
private Integer number;//购买数量
}

4.6.3、创建shop-order-server项目

在pom.xml中添加依赖。

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>Shop-parent</artifactId>
<groupId>cn.linstudy</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>Shop-order-server</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-order-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>cn.linstudy</groupId>
<artifactId>Shop-product-api</artifactId>
<version>1.0.0</version>
</dependency>
</project>

4.6.4、编写application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
yaml复制代码server:
port: 8082
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 1101121833
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect

4.6.5、编写Service及其实现类

我们写的Service接口及其实现类不具备任何的业务逻辑,仅仅只是为了测试而用。
1
2
3
java复制代码public interface OrderService {
Order getById(Long oid,Long pid);
}
1
2
3
4
5
6
7
8
9
10
11
java复制代码@Service
public class OrderServiceImpl implements OrderService {

@Autowired
OrderDao orderDao;

@Override
public Order getById(Long oid, Long pid) {
return orderDao.getOne(oid);
}
}

4.6.6、创建Controller

1
2
3
4
5
6
7
8
9
java复制代码@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("getById")
public Order getById(Long oid,Long pid){
return orderService.getById(oid, pid);
}

4.7、服务之间如何进行调用

假设我们在订单的服务里面需要调用到商品服务,先查询出id为1的商品,然后再查询出他的订单,这个1时候就涉及到服务之间的调用问题了。
服务之间的1调用本质上是通过Java代码去发起一个Http请求,我们可以使用RestTemplate来进行调用。

4.7.1、在shop-order-server的启动类中添加注解

我们既然需要RestTemplate类,就需要将RestTemplate类注入到容器中。
1
2
3
4
5
6
7
8
9
10
java复制代码@SpringBootApplication
public class OrderServer {
public static void main(String[] args) {
SpringApplication.run(OrderServer.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

4.7.2、修改Controller代码

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

@Autowired
RestTemplate restTemplate;

@Autowired
OrderService orderService;
@RequestMapping("getById")
public Order getById(Long oid,Long pid){
Product product = restTemplate.getForObject(
"http://localhost:8083/product?productId="+pid,Product.class); // 这里通过restTemplate去发起http请求,请求地址是http://localhost:8083/product,携带的参数是productId
Order order = orderService.getById(oid, product.getPid());
order.setUsername(product.getPname());
return order;
}
}

这样我们就完成了服务之间的互相调用。

4.8、存在的问题

虽然我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:
  • 一旦服务提供者地址变化,就需要手工修改代码。
  • 一旦是多个服务提供者,无法实现负载均衡功能。
  • 一旦服务变得越来越多,人工维护调用关系困难。
那么应该怎么解决呢?这时候就需要通过**注册中心**动态的实现**服务治理**。

本文转载自: 掘金

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

go(gin)签名踩坑记

发表于 2021-08-25

前言

最近在接入第三方接口的时候,要验证参数里的签名,签名采用SHA256withRSA (RSA2),以确认数据是不是被修改了。具体SHA256withRSA的原理不在这里讲解,本文主要记录在go(gin框架)验签时,踩到的一些坑,加以总结和记录。

ps: 以下的代码有些没有做错误处理,实际开发中不可取。

SHA256withRSA

待签字符串

接口参数以x-www-form-urlencoded的形式传入, 如下的形式

1
2
3
4
5
vbnet复制代码utc_timestamp:1624864579690
sign:LPyc1kQNle9fTNfPi7zDz77eZFG0XD0YBXsRQNw/cCq00YE2dISzZIizi5S30ssHfVS2uuQsOyYYZoI8BgT1VR3vcf3CdOY8rkPPdqhBgcEJyKNRvQ3z+3VnM33gP84J5Ntg/LS8ZAlGpGjL9xTWtKVUbHZk0oy1qJwt3Da+wqchk5oh/cYeQnTyyUheQBf2WwPeNYCoauUS6R3KCtF3X8d2qUjx2ZEMkAMQhqGG9DwapWdTdoStjDZt+/Uz2wNT/4ctTa0iTvKPh5Zn1fBhBEKiflXlC32tRjS5hC2RfXR/1JR/AF+u937THwZmWv4xDPAQwRNcNwIH+a6mafygKg==
sign_type:RSA2
app_id:20210701
content:{"page":1,"size":20}

组装待签名字符串:

  1. 获取全部参数,剔除sign与sign_type参数
  2. 将筛选的参数按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推
  3. 将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串

按照要求,则获取的待签字符串为:app_id=20210701&content={“page”:1,”size”:20}&utc_timestamp=1624864579690

签名与验签

虽然是作为验签方,但是为了方便测试,也实现了签名方法,先准备公钥与私钥,私钥签名,公钥验签。

1
2
3
4
5
6
7
8
vbnet复制代码-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCyPWejY7A+stkupI5Ow1aqlDgQ8g04gByyuyOiqw/wl8j8maerG1e7YKiF5qGOKr+Jw83HPdMFLCZDZebS63taPA2aIA+2x1CpIVfss5jSRQNsVzez9eDW7HTI+Nplx95BLl8OVE724hCgWFEjpwZ4GzORQMzmIXxxw67sdo9iuwIDAQAB
-----END PUBLIC KEY-----


-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALI9Z6NjsD6y2S6kjk7DVqqUOBDyDTiAHLK7I6KrD/CXyPyZp6sbV7tgqIXmoY4qv4nDzcc90wUsJkNl5tLre1o8DZogD7bHUKkhV+yzmNJFA2xXN7P14NbsdMj42mXH3kEuXw5UTvbiEKBYUSOnBngbM5FAzOYhfHHDrux2j2K7AgMBAAECgYBXtQGfk/lxEN7wJcdlGJg3/hGMvR8mU1xL0uyZKiYA1R/wtMed2imUqd6jbTbIV17DMte6mECThgMaHTW1Smz6yrXYwPLmorkZmDxC4ggpvriH7sDgvBL++lOlLfRQqL7XLx72ZDaFWC0qFokKc5vviXBqWnTVMf/SQenSZGkgEQJBAN5z1x9Dyv2XyYwyJqXzEHWmvx7jjwqGQx6nFWnIVfeXQyJSSY7tqT6J4fGHe9eq5nbnqQo964RrR91Q+2iRGMkCQQDNHqjvgoT/skAXy80BP2Mt5W5pFjjeVlaCoaf006mTngkfB24ZmvxoxX5NfNBEGB/iS2KCsU5/h1ykpU3Lj+VjAkA9MwVl9pKr/cxXI5z6XsqSc5N0/gnmTVW94x3DAniUKysvEBBon/3F1M0yU6HAjaXl5Ine5XYb8h/NRXBFLlXxAkEAub1muqOU7bmqoiGxPMz6cWgNh+lQi7zgz5+06FT2fK6hkdB3mYYnxHP5wA8ixFaYIKGkzbXi4EZh1NG/VXKzAwJAFp+hcKz9oRO1LodExpdmATTd031g53X+3MMKG+PJREjAnC9wQL4RsmbzYP5NZ2dORIpNgRWawF2b1KJxWiiCsg==
-----END PRIVATE KEY-----

实现签名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码func RsaSignWithSha256(data []byte, keyBytes []byte) ([]byte, error) {
h := sha256.New()
h.Write(data)
hashed := h.Sum(nil)
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, errors.New("private key error")
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}

signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey.(*rsa.PrivateKey), crypto.SHA256, hashed)
if err != nil {
return nil, err

}

return signature, nil
}

最终返回的结果是一个字节切片,但是参数是以字符串的形式传递的,因此需要把这个字节切片转为字符串,有两种形式 :

  • hex hex.EncodeToString
  • base64 base64.StdEncoding.EncodeToString
    编码之后的数据,也会有明显的差异
1
2
3
4
5
arduino复制代码// base64
AezhDSynfsTMrU517zHK12e2SzczNczm+yRht+Dr+I0K7VE+TLeUbpB1SiMbxLIdT2SsunIm0h5vaeHAyf9QwAFvjlcPG6JhJBOo58AtXx2moVVuu2pAEtO/tJw61VKbT4j5nAIiC1Ac2i1+u5BdbYoAV6Fc+HtfAJBS1iWinwQ=

// hex
01ece10d2ca77ec4ccad4e75ef31cad767b64b373335cce6fb2461b7e0ebf88d0aed513e4cb7946e90754a231bc4b21d4f64acba7226d21e6f69e1c0c9ff50c0016f8e570f1ba2612413a8e7c02d5f1da6a1556ebb6a4012d3bfb49c3ad5529b4f88f99c02220b501cda2d7ebb905d6d8a0057a15cf87b5f009052d625a29f04

不管采用哪种编码,在验证签名的时候都要先解码转成字节切片,否则验签不会通过,以下是验签函数,采用hex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码func RsaVerySignWithSha256(data, signData, keyBytes []byte) bool {
block, _ := pem.Decode(keyBytes)
if block == nil {
panic(errors.New("public key error"))
}
pubKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
panic(err)
}

hashed := sha256.Sum256(data)

// 注意这里签名字符串要解码
//sig, _ := base64.StdEncoding.DecodeString(string(signData)) base64解码
sig, _ := hex.DecodeString(string(signData)) // hex解码

err = rsa.VerifyPKCS1v15(pubKey.(*rsa.PublicKey), crypto.SHA256, hashed[:], sig)
if err != nil {
panic(err)
}
return true
}

最后整个签名与验签的过程就完成了

1
2
3
4
5
6
go复制代码func main (){
s := `app_id=20210701&content={"page":1,"size":20}&utc_timestamp=1624864579690`
sign, _ := RsaSignWithSha256([]byte(s), prvKey)
sigs := hex.EncodeToString(sign)
fmt.Println(RsaVerySignWithSha256([]byte(s), []byte(sigs), pubKey)) // true
}

既然验签过程完成了,接下就是应用在项目中了,这里使用的是gin框架,而接下来的这部分,踩了不少的坑 Orz

go(Gin)踩坑

从参数到待签字符串

在使用gin中,我都会使用ShouldBind将参数与结构体绑定,这次也自然而然的这么使用。将参数绑定到结构体之后,想拼接待签字符串,那就要遍历结构体,要遍历结构体,就要靠反射

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
go复制代码func GetPendingSign(p interface{}) []byte {
var typeInfo = reflect.TypeOf(p)
var valInfo = reflect.ValueOf(p)

num := typeInfo.NumField()
var keys = make([]string, 0, num)
var field = make([]string, 0, num)
for i := 0; i < num; i++ {
key := typeInfo.Field(i).Tag.Get("form") // 结构体的form tag才是待签字符串的key
if key != "sign" && key != "sign_type" {
keys = append(keys, key)
field[key] = typeInfo.Field(i).Name // 找不通过tagName获取值的,因此做了一个form tag和属性名的对应
}
}

sort.Strings(keys)

s := ""
for i, k := range keys {
temp := valInfo.FieldByName(field[i]).Interface() // 通过上面的对应,获取值
if k == "content" {
// 因为content被反序列化了,所以这里重新序列化成json,以拼接字符串
b, _ := json.Marshal(temp)
s = fmt.Sprintf("%s%s=%s&", s, k, string(b))
} else {
s = fmt.Sprintf("%s%s=%v&", s, k, temp)
}
}
s = s[:len(s)-1] //待签名字符串
return []byte(s)
}

上面的方法中,有几个部分做了注释,这几个地方也是比较关键的。接着用postman进行测试,将最开始的参数传入,会得到和预期一样的待签字符串。

1
2
3
4
5
6
7
go复制代码  r := gin.Default()
r.POST("/test", func(c *gin.Context) {
var body Body
c.ShouldBind(&body)
fmt.Println(string(GetPendingSign(body)))
})
r.Run(":8081")

然而就这样结束了吗?不!这种方式,有一个很大的问题!如果把content:{"page":1,"size":20}改成content:{"size":20,"page":1}入参,会发现签名验证失败了!是的!就是调换了size和page的位置,这种方式的问题就暴露出来了。

结构体/map和json

为什么会验签失败呢?第一个想法就是待签字符串是否一致?再次请求,打印拼接出来的字符串,会发现仍然是content={"page":1,"size":20}而不是传入的content:{"size":20,"page":1} 是不是很神奇?居然换位置了?来看下结构体的定义

1
2
3
4
go复制代码type Content struct {
Page int `json:"page" form:"page"`
Size int `json:"size" form:"size"`
}

明显的发现,序列化之后key的顺序和定义结构体属性的顺序保持了一致,而不是以最开始的json为准了,即:结构体序列化成json时,json的key值顺序以定义结构体时,属性的顺序为准

既然说到了结构体,再来看看map

1
2
3
4
5
6
go复制代码	s := `{"size":20,"page":1}`
m := make(map[string]int)
json.Unmarshal([]byte(s), &m)
b, _ := json.Marshal(&m)
fmt.Println(s)
fmt.Println(string(b)) // {"page":1,"size":20}

key的顺序也是被调整了,那么map又是以什么规则来调整key的顺序呢?

从源码的 encoding/json/encode.go 第793行中看到这行代码

1
go复制代码sort.Slice(sv, func(i, j int) bool { return sv[i].s < sv[j].s })

即:map转json是有序的,按照ASCII码升序排列key。

好吧,既然两个方式都会改变key的顺序,那么这种先绑定结构体再遍历拼接的方式就不可取了。

解决方案

既然不能先反序列化,那么就要采取其他的方案了。不以ShouldBind的形式获取参数,那么就用ioutil.ReadAll的方式来获取参数,打印看看获取到的参数

1
perl复制代码utc_timestamp=1624864579690&sign=LPyc1kQNle9fTNfPi7zDz77eZFG0XD0YBXsRQNw%2FcCq00YE2dISzZIizi5S30ssHfVS2uuQsOyYYZoI8BgT1VR3vcf3CdOY8rkPPdqhBgcEJyKNRvQ3z%2B3VnM33gP84J5Ntg%2FLS8ZAlGpGjL9xTWtKVUbHZk0oy1qJwt3Da%2Bwqchk5oh%2FcYeQnTyyUheQBf2WwPeNYCoauUS6R3KCtF3X8d2qUjx2ZEMkAMQhqGG9DwapWdTdoStjDZt%2B%2FUz2wNT%2F4ctTa0iTvKPh5Zn1fBhBEKiflXlC32tRjS5hC2RfXR%2F1JR%2FAF%2Bu937THwZmWv4xDPAQwRNcNwIH%2Ba6mafygKg%3D%3D&sign_type=RSA2&app_id=20210701&content=%7B%22size%22%3A20%2C%22page%22%3A1%7D

x-www-form-urlencoded的参数形式,和query的参形式类似,都是用&和=来拼接,既然都是字符串,那么就手动切割,然后拼成map,最后遍历map,拼接成待签字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码	bodyArray := strings.Split(string(body), "&") //1、先按&切割
data := make(map[string]string)
for _, v := range bodyArray {
// 2、按照 = 切分组装map
vs := strings.Split(v, "=")
if len(vs) == 2 {
value, err := url.QueryUnescape(vs[1]) // 从上面打印的字符,可以看出被urlescape过,因此要Unescape
if err != nil {
c.Abort()
return
}
data[vs[0]] = value
}
}

按照上面的形式,就可以得到一个map,而content的值,因为只是个字符串,没有被反序列化之后再序列化。因此就不会再出现key顺序不一致的问题。接着只需要遍历这个map,按照要求组装待签字符串即可。

最后把这些步骤都封装成一个中间件使用,验签功能完成。

总结

来看看最终都有哪些知识:

  • 签名有base64和hex的编码方式,验签的时候,要对应解码
  • 使用反射遍历结构体
  • 结构体序列化成json时,json key按照结构体的属性顺序重新排序
  • map序列化成json时,json key按照ASCII码升序排列
  • x-www-form-urlencoded 的参数形式,以&和=拼接,并且会被urlescape,处理的时候要unescape
  • 最后一个小知识点, 在中间件中用ioutil.ReadAll读完body,记得重新把body写回去 c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)),否则后面的路由就读不到body了

go路漫漫~

Thanks!

本文转载自: 掘金

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

02 网络面经:一个TCP连接可以发送多少个HTTP请求?

发表于 2021-08-25

一个TCP连接可以发送多少个HTTP请求?就这这个问题,我们聊聊TCP、HTTP以及浏览器之间的关系和对请求处理的优化。

TCP与HTTP的渊源

我们知道TCP协议对应于传输层,HTTP协议对应于应用层。WEB项目中,HTTP协议是建立在TCP的基础上的。

最初浏览器从服务器加载一个网页,会发起一个HTTP请求,这时需要先建立一个TCP连接。当本次数据请求完毕之后,会立刻断开TCP连接。

但随着时间的推理,HTML网页内容越来越复杂,不仅有内容,还有JS、CSS和图片资源,每个资源的请求都建立一次TCP连接,效率就会很低。

这时,Keep-Alive就被提出用来了,专门用于解决效率低的问题。

本文关于TCP连接能够发送多少个HTTP请求,本质上就是围绕着解决通信的低效问题的。

下面我们通过几个常见的面试问题,来逐步揭开这其中包含的知识点。

问题一:浏览器建立TCP连接之后,完成一次HTTP请求,是否会断开?

HTTP协议Header中的Connection属性决定了连接是否持久,不同HTTP协议版本有所不同。

HTTP/1.0中Connection默认为close,即每次请求都会重新建立和断开TCP连接。缺点:建立和断开TCP连接,代价过大。

HTTP/1.1中Connection默认为keep-alive,即连接可以复用,不用每次都重新建立和断开TCP连接。超时之后没有连接则主动断开。可以通过声明Connection为close进行关闭。

优点:TCP连接可被重复利用,减少建立连接的损耗,SSL的开销也可以避免。刷新页面时也可以复用,从而不再建立SSL连接等。

结论:默认情况下(HTTP/1.1)建立TCP连接不会断开,只有在请求报头中声明Connection: close才会请求完成之后关闭连接。不断开的最终目的是减少建立连接所导致的性能损耗。

问题二:一个TCP连接可以对应几个HTTP请求?

如果Connection为close,则一个TCP连接只对应一个HTTP请求。

如果Connection为Keep-alive,则一个TCP连接可对应一个到多个HTTP请求。

问题三:一个TCP连接中,可以同时发送多个HTTP请求吗?

HTTP/1.1中单个TCP连接在同一时刻只能处理一个请求。HTTP/1.1在RFC 2616中规定了Pipelining来解决这个问题,但浏览器默认是关闭的。

RFC 2616中规定:一个支持持久连接的客户端可以在一个连接中发送多个请求(不需要等待任意请求的响应)。收到请求的服务器必须按照请求收到的顺序发送响应。

Pipelining本身存在一些问题,比如代理服务器不能正确处理HTTP Pipelining、Head-of-line Blocking连接头阻塞(首个请求耗时过长,阻塞其他请求)。所以,浏览器默认关闭该功能。

HTTP/2.0提供了多路复用技术Multiplexing,一个TCP可以并发多个HTTP请求(理论无上限,但是一般浏览器会有TCP并发数的限制)。

HTTP/1.1中为了提升性能,通常会采用连接复用和同时建立多个TCP连接的方式提升性能。

结论:HTTP/1.1中存在Pipelining技术支持一个连接发送多个请求,但存在弊端,浏览器默认关闭。HTTP/2.0中通过多路复用技术支持一个TCP连接中并发请求HTTP。

问题四:浏览器对同一Host建立TCP连接的数量有没限制?

不同浏览器限制不同,比如Chrome最多允许同一个Host可建立6个TCP连接。

如果服务器只支持HTTP/1.1,浏览器会采用在同一个Host下建立多个TCP连接来进行效率提升。如果是基于HTTPS传输,在SSL握手之后,还会尝试协商是否可以采用HTTP/2.0的Multiplexing功能。

问题五:keep-alive使用场景及优缺点

开启keep-alive对内存要求高,关闭keep-alive对CPU要求高;如果内存和CPU都足够,开启和关闭keep-alive对性能影响不大;如果考虑服务器压力,如果是静态页面,大量的调用js或者图片的话,建议开启keep-alive;如果是动态网页,建议关闭keep-alive。

注意事项:如果需要使用keep-alive功能,服务器端如果使用nginx中keepalive_timeout值要大于0。

小结

通过上面的整体分析,我们不仅了解了TCP与HTTP之间的关系,还明确了现代浏览器基于不同的HTTP协议所作出的网络层面优化。而HTTP2/0的多路复用机制还是一些高性能框架的基础,比如gRPC的实现。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan

本文转载自: 掘金

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

进阶学Python:Python面向对象基础! 八月更文

发表于 2021-08-25

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

📖前言

三毛曾经感慨:

“心之何如,有似万丈迷津,横亘千里,其中并无舟子可以渡人,除了自渡,他人爱莫能助。” ——生命的洪流中,终其一生,我们都在学会怎么做自己的摆渡人。

关于安装和汉化可以观看博主的这篇文章《VsCode下载安装及汉化 》以及Python系列:windows10配置Python3.0开发环境!,安装完毕重启VsCode!


🚀Come on!什么是编程范式?

在了解面向对象编程之前,我们需要了解三大编程范式以及其之间的区别和利弊即:面向过程编程、函数式编程、面向对象编程。

编程:是程序员用特定的语法 +数据结构 + 算法组成的代码来告诉计算机如何执行任务的过程。

如果将编程的过程比喻成练习武功,那么编程范式就是武林中的各种流派,而在编程的武林中最常见的三大流派即为:面向过程编程(武当派)、面向对象编程(少林派)和函数式编程(峨眉派)。

编程范式没有好坏之分:“武功的流派是没有高低之分,只要习武之人采用高低之分。”在编程的世界中也是如此,各种编程方式应用在不同的场景都有其方便和优劣之处,谁更好谁更坏不能一概而论。

一个程序是程序员为了得到一个任务结果而编写的一组指令的集合,正所谓条条大路通罗马,实现一个任务的方式有很多种不同的方法,对这些不同的编程方式的特点进行归纳总结得出来的编程方式类别,即为编程范式,不同的编程范式本质上代表对各种各类的任务采取的不同的解决问题的思路,大多数语言只支持一种编程范式,当然也有一些语言可以同时支持多种编程范式。两种最重要的编程范式分别是面向过程编程和面向对象编程。

✨面向过程编程:

其核心在过程,过程便是解决问题的步骤,即为了解决问题先干嘛后干嘛,面向过程的设计就像一条固定的设计好的流水线,让问题根据这些步骤流程一步步来得到解决。例如:

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
python复制代码#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : loginWeiXin.py
@Time : 2019/10/22 09:23:52
@Author : YongJia Chen
@Version : 1.0
@Contact : chen867647213@163.com
@License : (C)Copyright 2018-2019, Liugroup-NLPR-CASIA
@Desc : None
'''

# here put the import lib

#假设我们让用户登入微信:
username = "Sunny Chen"
password = "12345"
count = 0
while count < 3:
user = input("please enter your username:")
passwd = input("please enter your password:")
if username == user and password == passwd:
print("welcome to wechat...")
break
else:
print("sorry,incorrent username or password..")
count += 1
if count == 3:
print("sorry,you have tried too many times..")

# 在这个例子里:我们让用户登入微信,然而在整个过程中,我们设计让用户输入用户名和密码,如果用户名和密码正常,就登入成功;而密码错误,则显示用户名或者密码错误;当然我们也不可能让用户一直试下去,只有三次机会,如没成功则显示登入次数太多。
#这便是典型的面向过程设计,根据问题一步步的设计解决步骤。

# 输出为
# please enter your username:Sunny Chen
# please enter your password:12345
# welcome to wechat...

优缺点也很明显:

  1. 优:复杂的问题流程化,进而简单化。即:将一个大的复杂的问题,分成一个个小的步骤去实现,毕竟实现小的问题要简单许多。
  2. 劣:一条流水线或流程就是用来解决一个问题,但是很难去解决一个相当大的问题,这样会显得十分繁琐。即生产一个复杂的机器很难用一条流水线去解决,即便是能,也得是大改,改一个组件,牵一发而动全身。

💕函数式编程:

其核心在函数,将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : functionDemo.py
@Time : 2019/10/22 09:27:41
@Author : YongJia Chen
@Version : 1.0
@Contact : chen867647213@163.com
@License : (C)Copyright 2018-2019, Liugroup-NLPR-CASIA
@Desc : None
'''

# here put the import lib


#最纯粹的函数式编程
def cal(x, y):
return x + y


#而在python下的函数,通常是为了实现某些特定功能而组织到一起的代码块。

💋面向对象编程:

其核心在对象:即万物皆对象,而每个对象均有特征和功能,通过设计将把这些整合在一块便是面向对象设计,面向对象更加注重对现实世界的模拟,存在的为对象,不存在的可以创造出来作为对象。

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
python复制代码#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : dxDemo.py
@Time : 2019/10/22 09:28:42
@Author : YongJia Chen
@Version : 1.0
@Contact : chen867647213@163.com
@License : (C)Copyright 2018-2019, Liugroup-NLPR-CASIA
@Desc : None
'''

# here put the import lib


#创建类,该类的特征和功能是相似的
class teacher(object):
def __init__(self, name, age, course):
self.name = name
self.age = age
self.course = course

def intro(self):
print("%s is %s years old." % (self.name, self.age))

def attend_class(self):
print("%s teach %s" % (self.name, self.course))


#创建具体的对象,该对象的具体特征和功能。
t1 = teacher("sunny chen", 21, "python")
t1.attend_class()

#这就是Python中面向对象编程,通过定义类对一类事物的特征和功能进行整合,然后在通过对象具体到某一个事物的具体特征和功能。
#即类就相当于 印钞机 ,而对象就相当于人民币

🤳其优缺点:

  1. 编程的复杂度远高于面向过程,不了解面向对象而立即上手基于它设计程序,极容易出现过度设计的问题。一些扩展性要求低的场景使用面向对象会徒增编程难度,比如管理linux系统的shell脚本就不适合用面向对象去设计,面向过程反而更加适合。
  2. 无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,即便是上帝也无法准确地预测最终结果。于是我们经常看到对战类游戏,新增一个游戏人物,在对战的过程中极容易出现阴霸的技能,一刀砍死3个人,这种情况是无法准确预知的,只有对象之间交互才能准确地知道最终的结果。

🎉面向对象设计和面向对象编程

  1. 面向对象设计其思想为“一切皆对象”,而每个对象均有其特征和功能,将这些特征和功能整合到一块就是面向对象设计,而面向对象编程是通过定义类来整合一类事物的共同属性和函数,并可以通过实例化来创建函数。
  2. 面向对象编程只是实现面向对象设计的一种方式,而面向对象设计也可通过其他方式进行实现。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
python复制代码#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : mianxiangduixiangDemo.py
@Time : 2019/10/22 09:32:07
@Author : YongJia Chen
@Version : 1.0
@Contact : chen867647213@163.com
@License : (C)Copyright 2018-2019, Liugroup-NLPR-CASIA
@Desc : None
'''

# here put the import lib


def dogs(name, age, kind):
def init(name, age, kind):
dog = {
"name": name,
"age": age,
"kind": kind,
"intro": intro,
"yell": yell
}
return dog

def intro(dog):
print("This %s's name is %s,it's %s years old." %
(dog["kind"], dog["name"], dog["age"]))

def yell(dog):
print("The %s is wangwangwang" % (dog["kind"]))

return init(name, age, kind)


d1 = dogs("sunny chen", 21, "Look")
print(d1["name"])
d1["intro"](d1)

从上面这个例子中可以看出:面向对象设计不一定需要用面向对象编程,也可以用其他方式实现面向对象设计,但是面向对象编程一定是面向对象设计实现的。

👏对象的理解:

核心是“对象”二字,面向对象的设计必须从上帝视角进行设计,而在上帝眼中,世间存在的万物皆为对象,同时不存在的也可以创造出来。程序员也可以把自己当成小说家,可以塑造一个个人物,而每个人物均有其自身的特征,也具有自身的功能去完成一些事情。。

故从这些例子中看出:与面向过程机械式的思维方式形成鲜明的对比,面向对象更加专注对现实世界事物而非流程的模拟,是一种“上帝式”的思维方式。


🎎python面向对象编程

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
python复制代码#!/usr/bin/env python
# -*- encoding: utf-8 -*-
'''
@File : pythonMXDXDemo.py
@Time : 2019/10/22 09:34:46
@Author : YongJia Chen
@Version : 1.0
@Contact : chen867647213@163.com
@License : (C)Copyright 2018-2019, Liugroup-NLPR-CASIA
@Desc : None
'''

# here put the import lib


class Dog(object):
def __init__(self, name, age):
self.name = name
self.age = age

def eat(self):
print("%s is eating" % (self.name))

def info(self):
print("The dog's name is %s,it's %d years old." %
(self.name, self.age))


dog1 = Dog("Sunny Chen", 33)
print(dog1.name)

# 首先我们定义了一个Dog类,该类中有一个构造函数和两个方法,当我们实例化时,会自动执行__init__()方法,并且将参数dog1,"alex",33传给该构造函数,
# 注会将实例本身作为参数参入,实例之所以能够调用类方法类属性也是通过这种机制实现的。同时在底部会将这些参数传入该对象的属性字典中,方便调用。

类

  一个类就是对一类事物的特征和功能的整合,也可以说是对具有相同属性事物的一种抽象和模板。而在该类中定义了这些对象的都具备的属性和方法。比如说:动物类、人类、或者说是汽车类等

对象

  通过类实例化得到的具体事物,该事物具有具体的属性和方法。例如:具体的人、具体的动物或者说某一个汽车等,这些具体的事物均有该类的属性共同的功能,同时也还可以有具有自身的独特的属性和功能,比如:某个具体的人有所有人均有的属性–说话、吃饭等,当然每个人都是独特的–相貌特征等;

属性

  一类事物共有的特征即为类的属性,而某个具体事物具有的特征即为对象的属性;比如:人类有身高、年龄、性别等特征。

方法

  人类不止有身高、体重、年龄等这些特征,还能做许多事情,例如:工作、吃饭、唱歌等功能,而这些功能我们也可以称之为方法。

实例化

  由一个类创造得到一个具体的实例(对象)的过程,称之为实例化。

  同时在上述案例中有个特殊的方法 __init__(),这个特殊的函数我们称之为构造函数。

构造函数

  当该类被实例化的时候就会执行该函数。那么我们就可以把要先初始化的属性放到这个函数里面。同时可能我们还会好奇,为什么上述例子中 __init__(self,name,age) ,而我们在默认传参的时候,只传入了一个函数,这是由于 python 在实例化 dog1 = Dog("sunny",21 ) 时,会将实例对象本身作为参数传入即 self = dog1,这种机制才能使得—该实例才能调用类的属性和方法。

快去动手试试吧!


🎉最后

  • 更多参考精彩博文请看这里:陈永佳的博客
  • 喜欢博主的小伙伴可以加个关注、点个赞哦,持续更新嘿嘿!

本文转载自: 掘金

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

sonar入门:全网最全的概念解析与安装

发表于 2021-08-25

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

sonar是一款代码检测工具,如果在开发过程中,想监控组员的代码书写是否正规,可以使用该工具。sonar提供了可视化界面(可以跟领导装x),是一款即实用,又能在公司显摆自己能力的工具。如果小伙伴们在公司内担任leader的角色,同时又想能够上位,可以落地,提升自己的存在感。。。。废话不多说,下面开始介绍。

1.概念解读

sonar是一款代码质量检查的中间件,但是在安装过程中,一度有n多概念,让我懵逼。以下将带大家区别这些概念。

1.sonarqube

sonarqube:是一种自动代码审查工具,用于检测代码中的错误,漏洞和代码格式上的问题。它可以与您现有的工作流程集成,以实现跨项目分支和提取请求的连续代码检查。同时也提供了可视化的管理页面,用于查看检测出的结果。

同时sonarqube提供了一系列的规范(大概有300多条,不建议使用),可以通过勾选从而设置在检测时是否使用。

image.png

也就是说sonarqube只是显示平台,需要与代码连接才能够扫描。

2.sonarlint

sonarlint是idea的插件,sonarlint本身也有检查的功能,可以在idea中检测代码,也可以在idea中配置检测的规范,同时,sonarlint也可以连接sonarqube,只不过只是用sonarqube的前台页面。

测试结果

image.png

检测规范(可以通过勾选控制)

image.png

3.sonar-scanner

sonar-scanner插件:用于扫描代码与连接sonarqube,使用sonar-scanner插件就可以将本地代码连接sonarqube 并检测出结果,并显示在sonarqube中。

sonarqube检测代码java代码有两种方式,一种使用sonar-scanner,一种使用sonarlint,具体可以参照下文。

4.p3c

p3c:阿里巴巴指定的代码格式规范 共50条左右 可以在idea中使用 也可以将配置导入进sonarqube 然后使用sonarqube检测

建议使用p3c规则,sonar规则是在太复杂,什么代码都能给你怼出错误。

2.安装

因为公司仅剩一台windows服务器,所以。。。。。老板你感觉采购内存啊!!!!

1.安装jdk

可以参考前文文档。

2.下载解压

image.png
下载地址:www.sonarqube.org/downloads/,画红圈的为社区版(免费)。

3.安装数据库

根据自己需要安装数据库。

4.准备数据

新建sonar库与sonar用户。
image.png

5.修改配置文件

image.png
image.png

1
2
3
4
5
6
7
8
ini复制代码#sonar用户页面登录账号密码
sonar.login=root
sonar.password=root

#mysql连接与mysql账号密码
sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance&useSSL=false
sonar.jdbc.username=sonar
sonar.jdbc.password=sonar

如有以下报错 请注意mysql版本和mysql的账号密码

1
2
lua复制代码2021.04.06 10:50:09 WARN  app[][o.e.t.n.Netty4Transport] exception caught on transport layer [[id: 0x29e09cd2, L:/127.0.0.1:52892 - R:/127.0.0.1:9001]], closing connection
java.io.IOException: 远程主机强迫关闭了一个现有的连接。

6.启动

选择windows版本。
image.png
按顺序点击bat脚本。
image.png
出现以下字样,为安装成功。
image.png

7.登录

访问地址为http://localhost:9000,点击login并输入上文配置的登录账号与密码。
image.png

8.安装插件

这里选择了汉化插件
下载地址为:github.com/xuhuisheng/…
根据版本下载自己的jar包
image.png
下载好jar包后将jar包复制到sonarqube-7.6\extensions\plugins中并重启,完事手收工。
image.png

本文转载自: 掘金

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

面试必备:布隆过滤器是什么?有什么用?

发表于 2021-08-25

前言

大家好,我是捡田螺的小男孩。今天我们来聊聊一道经典面试题,布隆过滤器是什么?有什么用?

  • 公众号:捡田螺的小男孩

缓存穿透

应对缓存穿透问题,我们可以使用布隆过滤器。我们先来回顾下缓存穿透知识点哈:

一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。

读取缓存

缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

假设我们需要查产品详情,有查询请求进来,我们先根据产品Id直接去缓存中查一下,没有的话,再去查下数据库。如果现在有大量请求进来,而且都在请求一个不存在的产品Id,那么这些请求都会怼到数据库,数据库压力一上来,可能就挂了。我们可以在请求数据库层前,加个中间层,去缓解数据库压力嘛,如果,不存在的话,就不去查数据库啦。

大量数据判断是否存在

这个中间层,是不是用HashMap就好了呢?听起来不错嘛,HashMap时间复杂度可以达到O(1),但是呢因为HashMap数据是在内存里面的,如果大量的数据远超出了服务器的内存呢,那就无法使用HashMap啦,可以使用布隆过滤器来做这个缓冲的事情。

布隆过滤器是什么

布隆过滤器是一种占用空间很小的数据结构,它由一个很长的二进制向量和一组Hash映射函数组成,它用于检索一个元素是否在一个集合中,空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

布隆过滤器原理是?
假设我们有个集合A,A中有n个元素。利用k个哈希散列函数,将A中的每个元素映射到一个长度为a位的数组B中的不同位置上,这些位置上的二进制数均设置为1。如果待检查的元素,经过这k个哈希散列函数的映射后,发现其k个位置上的二进制数全部为1,这个元素很可能属于集合A,反之,一定不属于集合A。

来看个简单例子吧,假设集合A有3个元素,分别为{d1,d2,d3}。有1个哈希函数,为Hash1。现在将A的每个元素映射到长度为16位数组B。

我们现在把d1映射过来,假设Hash1(d1)= 2,我们就把数组B中,下标为2的格子改成1,如下:

我们现在把d2也映射过来,假设Hash1(d2)= 5,我们把数组B中,下标为5的格子也改成1,如下:

接着我们把d3也映射过来,假设Hash1(d3)也等于 2,它也是把下标为2的格子标1:

因此,我们要确认一个元素dn是否在集合A里,我们只要算出Hash1(dn)得到的索引下标,只要是0,那就表示这个元素不在集合A,如果索引下标是1呢?那该元素可能是A中的某一个元素。因为你看,d1和d3得到的下标值,都可能是1,还可能是其他别的数映射的,布隆过滤器是存在这个缺点的:会存在hash碰撞导致的假阳性,判断存在误差。

如何减少这种误差呢?

  • 搞多几个哈希函数映射,降低哈希碰撞的概率
  • 同时增加B数组的bit长度,可以增大hash函数生成的数据的范围,也可以降低哈希碰撞的概率

我们又增加一个Hash2哈希映射函数,假设Hash2(d1)=6,Hash2(d3)=8,它俩不就不冲突了嘛,如下:

即使存在误差,我们可以发现,布隆过滤器并没有存放完整的数据,它只是运用一系列哈希映射函数计算出位置,然后填充二进制向量。如果数量很大的话,布隆过滤器通过极少的错误率,换取了存储空间的极大节省,还是挺划算的。

目前布隆过滤器已经有相应实现的开源类库啦,如Google的Guava类库,Twitter的 Algebird 类库,信手拈来即可,或者基于Redis自带的Bitmaps自行实现设计也是可以的。

最后

谢谢大家,如果觉得有收获的话,求个点赞,感谢。我的公众号:捡田螺的小男孩。有兴趣可以关注下哈

本文转载自: 掘金

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

1…550551552…956

开发者博客

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