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

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


  • 首页

  • 归档

  • 搜索

冲刺大厂每日算法&面试题,动态规划21天——第十八天 导读

发表于 2021-11-16

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

导读

在这里插入图片描述

肥友们为了更好的去帮助新同学适应算法和面试题,最近我们开始进行专项突击一步一步来。我们先来搞一下让大家最头疼的一类算法题,动态规划我们将进行为时21天的养成计划。还在等什么快来一起肥学进行动态规划21天挑战吧!!

21天动态规划入门

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组
[0,3,1,6,2,2,7] 的子序列。

1
2
3
4
5
java复制代码示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
1
2
3
4
java复制代码示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4
1
2
3
4
java复制代码示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码class Solution {
public int lengthOfLIS(int[] nums) {
if (nums.length == 0) {
return 0;
}
int[] dp = new int[nums.length];
dp[0] = 1;
int maxans = 1;
for (int i = 1; i < nums.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
}

面试题

继续介绍二叉树的面试题:
构建二叉树节点

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

public class node {
public int val;
public node left;//左孩子
public node right;//右孩子
public node() {

}
public node(int val,node left,node right) {
this.val=val;
this.left=left;
this.right=right;
}
public node(int val, node left) {
super();
this.val = val;
this.left = left;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class kTierNums {
//求第k层节点的个数
public int kNums(node root,int k) {
if(root==null||k<1)return 0;
if(k==1)return 1;
int leftNum=kNums(root.left,k-1);
int rightNum=kNums(root.right,k-1);
return leftNum+rightNum;
}


}

问:怎么判断是否为平衡二叉树

我先来说一下什么是平衡二叉树

平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码package tree;

public class AVL {
//判断是不是平衡二叉树
public int BalancedTree(node root) {
if(root==null)return 0;
int left=BalancedTree(root.left);
int right=BalancedTree(root.right);
if(left==-1||right==-1||Math.abs(left-right)>1) {
return -1;
}
return Math.max(left, right)+1;
}
public boolean isBalanced(node root) {
return BalancedTree(root)!=1;
}
}

本文转载自: 掘金

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

SQL优化及多数据库支持分享(六)

发表于 2021-11-16

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

子查询优化

​ MySQL 从 4.1 版本开始支持子查询,使用子查询进行 select嵌套查询,可以一次完成很多逻辑上需要多个步骤的 SQL 操作。但是子查询虽然很灵活,其执行效率并不高。因为执行子查询时,会创建临时表,查询SQL执行完毕后再删除这些临时表。所以,子查询的速度会受到一定的影响。我们可以使用连接查询来代替子查询,因为连接查询不会建立临时表,其速度比子查询快,然后我们可以通过where条件过滤掉多余数据。

索引与其优化

​ 索引的本质也是一种数据结构,我们可以将其理解为数据表的“目录”,在MySQL中索引是通过B+树实现的,建立索引后会显著提升查询效率。当然并不是所有的索引都会生效,同时索引也不是百利而无一害。下面我们就详细介绍下:

1、索引的分类

​ 普通索引 - index :加速数据查询效率;

​ 主键索引 - primary key :加速查找+约束(不为空且唯一);

​ 唯一索引 - unique:加速查找+约束 (唯一);

​ 联合索引:

  • primary key:联合主键索引
  • unique:联合唯一索引
  • index:联合普通索引

全文索引 fulltext :用于搜索很长一篇文章的时候,效果最好。

2、索引的优点

  • 保证数据唯一性:可以通过建立唯一索引或者主键索引,保证数据库表中每一行数据的唯一;
  • 提高数据检索效率:建立索引可以减少表的检索行数,大大提高检索的数据效率;
  • 在表连接的连接条件上建立索引,可以加速表与表直接的相连;
  • 在分组和排序字句进行数据检索,可以减少查询时间中分组和排序时消耗的时间(PS:数据库的记录会重新排序);

3、索引的缺点

  • 在创建索引和维护索引会耗费大量时间,且随着数据量的增加而增加;
  • 索引文件会占用物理空间,除了数据表需要占用物理空间之外,每一个索引还会占用一定的物理空间;
  • 当对表的数据进行增删改操作的时候,索引也要进行动态的维护,就会降低数据的维护速度(PS:注意 -> 如果在一个大表上创建了多种组合索引,索引文件的会膨胀很快);

4、适合加索引的情况

  • 在经常需要作为查询条件的列上加索引,可以加快查询的速度;
  • 主键上:可以确保列的唯一性;
  • 在表与表的连接条件上加索引,可以加快连接查询的速度;
  • 在经常需要排序、分组和去重的字段上加索引,可以加快排序查询的时间;

5、不适合加索引的情况

  • 查询中很少使用到的字段不应该创建索引,如果建立了反而会降低 mysql 的性能,同时页增大了空间需求(因为维护索引也会消耗数据库性能);
  • 数据差异性很少的的字段也不适合建立索引,比如性别字段,只有男或者女
  • 数据类型为 text 或者 image、bit 的字段不应加索引;
  • 当表的增删改操作远远大于查询操作时不应创建索引,因为这两个操作是互斥的;

6、索引失效的情况

  • 查询条件中有 or,即使加了索引也不会生效;
  • 加索引字段,其值不能有 null 值,有 null 值会使该列索引失效;
  • 对于联合索引,要遵循最左原则,保证查询条件中字段的顺序与索引中的顺序一致;
  • 模糊查询以%开头的,索引不生效;
  • 在索引的列上使用表达式或者函数,索引会失效
    e.g. select * from com_user where YEAR(add_date) < 2021,这样在每一行上都会进行运算,进而全表扫描导致索引失效,可以改成:select * from com_user where add_date < ’2021-01-01′;

本文转载自: 掘金

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

Mysql基础篇:必知必会(下)

发表于 2021-11-16

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

示例表

1
2
3
4
5
6
7
8
9
10
mysql复制代码mysql> DESC one_piece;
+---------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| id | char(10) | NO | | NULL | |
| pirates | char(10) | NO | | NULL | |
| name | char(10) | NO | | NULL | |
| age | int(11) | YES | | NULL | |
| post | varchar(10) | YES | | NULL | |
+---------+-------------+------+-----+---------+-------+

接着上篇继续!

一、插入数据

插入完整行

使用 INSERT 插入完整行它要求指定 表名和插入到新行中的值。

1
2
3
4
5
6
7
sql复制代码mysql> INSERT INTO one_piece
-> VALUES('1',
-> '草帽海贼团',
-> '路飞',
-> 'age',
-> '团长',
-> '1500000000');

注意

  • 必须每列提供一个值,空值使用 NULL
  • 各列必须以它们在表定义中出现的次序填充

插入部分行

INSERT 推荐的插入方法是明确给出表的列名。这样还可以省略列,即只给某些列提供值,其他列不提供值。

省略的列必须满足以下某个条件:

  • 该列定义为允许 NULL 值(无值或空值)。
  • 在表定义中给出默认值(如果不给出值,将使用默认值)。

如果表中不允许有 NULL 值或者默认值,这时却省略了表中的值, DBMS 就会产生错误消息,相应的行不能成功插入。

现在同样在 one_piece 表中插入一行。

1
2
3
4
5
6
sql复制代码mysql> INSERT INTO one_piece(id,
-> pirates,
-> name)
-> VALUES('1',
-> '草帽海贼团',
-> '路飞');

不管使用哪种INSERT 语法,VALUES 的数目都必须正确。如果不提供列名,则必须给每个表列提供一个值;如果提供列名,则必须给列出的每个列一个值。否则,就会产生一条错误消息,相应的行不能成功插入。

从一个表复制到另一个表

有一种数据插入不使用 INSERT 语句。要将一个表的内容复制到一个全新的表(运行中创建的表)。

1
2
sql复制代码mysql> CREATE TABLE one_pieceCopy AS
-> SELECT * FROM one_piece;
  • 任何 SELECT 选项和子句都可以使用,包括 WHERE 和 GROUP BY。
  • 可利用联结从多个表插入数据。
  • 不管从多少个表中检索数据,数据都只能插入到一个表中。

主要用途:它是试验新 SQL 语句前进行表复制的很好工具。先进行复制,可在复制的数据上测试 SQL 代码,而不会影响实际的数据。

二、更新数据

使用 UPDATE 语句,更新(修改)表中的数据。

有两种使用 UPDATE 的方式:

  • 更新表中的特定行
  • 更新表中的所有行

使用时要明确是 更新特定行 还是 更新所有行。

UPDATE 语句中可以使用子查询,使得能用 SELECT 语句检索出的数据 更新列数据。

更新单行单列

将 路飞 的赏金更新为 10000000000

1
2
3
sql复制代码mysql> UPDATE one_piece
-> SET bounty = 10000000000
-> WHERE name = '路飞';

更新单行多列

在更新多个列时,只需要使用一条 SET 命令,每个“列=值”对之间用逗号分隔(最后一列之后不用逗号)。

1
2
3
4
sql复制代码mysql> UPDATE one_piece
-> SET bounty = 10000000000,
-> age = '19'
-> WHERE name = '路飞';

更新所有行

不使用 WHERE 限制条件,即更新表中所有行。

1
2
3
sql复制代码mysql> UPDATE one_piece
-> SET bounty = 10000000000,
-> age = '19'

删除列中的值

假如表定义允许 NULL 值,要删除某个列的值,可设置它为 NULL。(要注意删除列值(保留列结构)和删除列(完全删除)的区别)

1
2
3
sql复制代码mysql> UPDATE one_piece
-> SET bounty = NULL
-> WHERE name = '路飞';

三、删除数据

使用 DELETE 语句,删除表中的数据。

有两种使用 DELETE 的方式:

  • 删除表中的特定行
  • 删除表中的所有行

使用时要明确是 删除特定行 还是 删除所有行。

删除单行

删除 one_piece 表中 name 为 路飞 的行。

1
2
sql复制代码mysql> DELETE FROM one_piece
-> WHERE name = '路飞';

删除所有行

删除 Customers 中的所有行。不删除表本身。

1
sql复制代码mysql> DELETE FROM one_piece;

如果想从表中删除所有行,推荐使用 TRUNCATE TABLE 语句,它完成相同的工作,而速度更快(因为不记录数据的变动)。

但要注意: TRUNCATE 属于数据定义语言( DDL ),且 TRUNCATE 命令执行后无法回滚,使用 TRUNCATE 命令之前最好对当前表中的数据做备份。

1
sql复制代码mysql> TRUNCATE TABLE one_piece;

四、约束

约束

DBMS 通过在数据库表上施加约束来实施引用完整性。大多数约束是在表定义中定义的,用 CREATE TABLE 或是 ALTER TABLE 语句。

主键

主键是一种特殊的约束,用来保证一列(或 一组列)中的值是唯一的,而且永不改动。没有主键,要安全地 UPDATE 或 DELETE 特定行而不影响其他行会 非常困难。

主键的条件:

  • 任意两行的主键值都不相同。
  • 每行都具有一个主键值(即列中不允许 NULL 值)。

创建表时定义主键。

1
2
3
4
5
sql复制代码CREATE TABLE teacher
(
id INT(11) PRIMARY KEY,
teacher_name VARCHAR(10)
);

使用 ALTER TABLE 添加主键。

1
2
sql复制代码ALTER TABLE teacher
ADD CONSTRAINT PRIMARY KEY(id);

删除主键约束。

1
sql复制代码ALTER TABLE teacher DROP PRIMARY KEY;

外键

外键是表中的一列,其值必须列在另一表的主键中。外键是保证引用完 整性的极其重要部分。

下面新建 student 表并添加外键 teacher_id 与 teacher 表中的主键 id 进行关联。

在创建表的时定义外键。

1
2
3
4
5
6
sql复制代码CREATE TABLE student
(
stu_id INT(11) PRIMARY KEY,
teacher_id INT(11) REFERENCES teacher(id),
stu_name VARCHAR(10)
);

使用 ALTER TABLE 添加外键。

1
2
3
sql复制代码ALTER TABLE student
ADD CONSTRAINT teacher_id_id
FOREIGN KEY (teacher_id) REFERENCES teacher(id);

使用外键可以有效地防止意外删除,比如在上面两表中如果删除 teacher 表中的信息,如果该 id 在 student 表中也有出现,那么 Mysql 会防止删除操作。当然也可以启用级联删除的特性,那么在删除时就会删除所有相关信息。

删除外键

1
sql复制代码ALTER TABLE student DROP FOREIGN KEY teacher_id_id;

唯一约束

唯一约束用来保证一列(或一组列)中的数据是唯一的。它们类似于主 键,但存在以下重要区别。

  • 表可包含多个唯一约束,但每个表只允许一个主键。
  • 唯一约束列可包含 NULL 值。
  • 与主键不一样,唯一约束不能用来定义外键。

在创建表的时定义唯一约束。

1
2
3
4
5
6
sql复制代码CREATE TABLE student
(
stu_id INT(11) PRIMARY KEY,
teacher_id INT(11) REFERENCES teacher(id),
stu_name VARCHAR(10)
);

使用 ALTER TABLE 添加唯一约束。

1
2
sql复制代码ALTER TABLE student
ADD CONSTRAINT unique_id UNIQUE(stu_id);

删除唯一性约束。

1
sql复制代码ALTER TABLE student DROP INDEX unique_id;

检查约束

检查约束用来保证一列(或一组列)中的数据满足一组指定的条件。

常见用途:

  • 检查最小或最大值。
  • 指定范围。
  • 只允许特定的值。

下面创建一个检查约束来限制性别列只能输入男、女。

在创建表的时定义检查约束。

1
2
3
4
5
sql复制代码CREATE TABLE student
(
stu_id INT(11) PRIMARY KEY,
gender VARCHAR(1) CHECK(gender IN('男', '女'))
);

使用 ALTER TABLE 添加检查约束。

1
sql复制代码ALTER TABLE student ADD CONSTRAINT check_gender CHECK(gender in ('男', '女'));

删除检查约束。

1
sql复制代码ALTER TABLE student DROP CHECK check_gender;

基础篇到今日就完结了,内容比较基础,进阶篇抽空搞起!

这就是今天要分享的内容,微信搜 Python新视野,每天带你了解更多有用的知识。更有整理的近千套简历模板,几百册电子书等你来领取哦!

本文转载自: 掘金

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

五分钟学会用 Jmeter+ant+jenkins 实现接口

发表于 2021-11-16

一、安装并配置环境

安装jmeter+ant+jenkins并且配置环境

1.下载安装jdk1.8并且配置环境变量,自行配置

2.下载ant包,解压并且配置环境变量

window中设置ant环境变量:

ANT_HOME C:/ apache-ant-1.9.7

path C:/ apache-ant-1.9.7/bin

classpath C:/apache-ant-1.9.7/lib

cmd打开 输入ant执行

在这里插入图片描述
说明ant安装成功!因为ant默认运行build.xml文件,build.xml 需要我们自己建立,等哈后面 我们再说这个

为了确定真的安装成功了 我们执行ant -version

在这里插入图片描述

3.将\apache-jmeter-3.2\extras下面的ant-jmeter-1.1.1的jar包到ant的lib目录,如图所示:

在这里插入图片描述

二、建立build.xml

在这里插入图片描述

从上图可以知道 我们创建了一个文件夹demo,然后把build.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
53
54
55
xml复制代码<?xml version="1.0" encoding="UTF-8"?>

<project name="ant-jmeter-test" default="run" basedir=".">
<tstamp>
<format property="time" pattern="yyyyMMddhhmm" />
</tstamp>
<!-- 需要改成自己本地的 Jmeter 目录-->
<property name="jmeter.home" value="D:\study\apache-jmeter-3.2" />
<!-- jmeter生成jtl格式的结果报告的路径-->
<property name="jmeter.result.jtl.dir" value="D:\study\apache-jmeter-3.2\demo\report\jtl" />
<!-- jmeter生成html格式的结果报告的路径-->
<property name="jmeter.result.html.dir" value="D:\study\apache-jmeter-3.2\demo\report\html" />
<!-- 生成的报告的前缀-->
<property name="ReportName" value="TestReport" />
<property name="jmeter.result.jtlName" value="${jmeter.result.jtl.dir}/${ReportName}${time}.jtl" />
<property name="jmeter.result.htmlName" value="${jmeter.result.html.dir}/${ReportName}${time}.html" />

<target name="run">
<antcall target="test" />
<antcall target="report" />
</target>

<target name="test">
<taskdef name="jmeter" classname="org.programmerplanet.ant.taskdefs.jmeter.JMeterTask" />
<jmeter jmeterhome="${jmeter.home}" resultlog="${jmeter.result.jtlName}">
<!-- 声明要运行的脚本。"*.jmx"指包含此目录下的所有jmeter脚本-->
<testplans dir="D:\study\apache-jmeter-3.2\demo" includes="*.jmx" />
<property name="jmeter.save.saveservice.output_format" value="xml"/>
</jmeter>
</target>

<path id="xslt.classpath">
<fileset dir="${jmeter.home}/lib" includes="xalan*.jar"/>
<fileset dir="${jmeter.home}/lib" includes="serializer*.jar"/>
</path>

<target name="report">
<tstamp><format property="report.datestamp" pattern="yyyy/MM/dd HH:mm"/></tstamp>
<xslt
classpathref="xslt.classpath"
force="true"
in="${jmeter.result.jtlName}"
out="${jmeter.result.htmlName}"
style="${jmeter.home}/extras/jmeter-results-report-loadtest.xsl">
<param name="dateReport" expression="${report.datestamp}"/>
</xslt>
<!-- 因为上面生成报告的时候,不会将相关的图片也一起拷贝至目标目录,所以,需要手动拷贝 -->
<copy todir="${jmeter.result.html.dir}">
<fileset dir="${jmeter.home}/extras">
<include name="collapse.png" />
<include name="expand.png" />
</fileset>
</copy>
</target>
</project>

上图注释已经很清晰了,只需要配置下jmeter路径以及报告样式就可以使用我这个build.xml文件了

三、创建一个jmx文件 然后运行下

在这里插入图片描述

进入到该目录,并cmd执行ant

在这里插入图片描述
上图的BUILD SUCCESSFUL 证明已经成功了,而且报告在report/html下面。现在我们打开报告看看
在这里插入图片描述
报告是不是很好看,其实就是\apache-jmeter-3.2\extras这个目录下jmeter-results-report-loadtest.xsl这个样式生成的报告

四、集成jenkins

废话不多说 ,启动jenkins

1、java -jar jenkins.war (自己去下载jenkins.war) 也可以在群里问我要

2、下载jenkins.tar 并解压 将解压之后的文件丢在tomcat下面的webapps下面

在这里插入图片描述
请添加图片描述

双击C:\apache-tomcat-7.0.72\bin下面的startup.bat 启动

在这里插入图片描述

这就证明jenkins已经启动了

然后随意打开一个浏览器 输入:http://localhost:8080/jenkins 默认端口号为8080

在这里插入图片描述
我自己改成8888的,你们随意

创建一个项目。

在这里插入图片描述

选择第一个自由风格,然后点击OK按钮

在这里插入图片描述

然后配置ant,增加build路径

在这里插入图片描述
在这里插入图片描述
点击保存之后,我们继续配置测试报告

我们需要下载一个插件Public HTML reports

系统管理—-》管理插件—-》可选插件 输入Public HTML reports 进行查询并进行安装

接下来继续配置报告

点击增加构建后操作步骤 并点击Public HTML reports
在这里插入图片描述

并点击保存按钮 好了 现在我们报告也配置好了

在这里插入图片描述
点击之后 出现

在这里插入图片描述
证明jenkins在构建了

在这里插入图片描述
好了到了这一步 已经构建成功了,Success !

我们在jenkins下面看下报告吧

在这里插入图片描述)在这里插入图片描述)在这里插入图片描述)在这里插入图片描述

报告已经ok了,其实文章篇幅很长,配置很简单

🎈程序人生专栏🎈

《月薪3万的大厂测试工程师裸辞3个月,送外卖谋生背后的真实感悟》

《“我转行做测试开发的这一年多,月薪5K变成了24K”,中文系萌妹的自白》

《深圳不是说很缺软件测试吗?为什么我找了两个月还是没找到工作?》

《技术选型都做不好,难怪自动化做得这么费力…》

本文转载自: 掘金

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

力扣第九十五题-不同的二叉搜索树 II 前言 一、思路 二、

发表于 2021-11-16

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

前言

力扣第九十五题 不同的二叉搜索树 II 如下所示:

给你一个整数 n ,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 **。可以按 任意顺序 返回答案。

示例 1:

1
2
ini复制代码输入: n = 3
输出: [[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]

一、思路

题目意思很简单,就是需要我们返回所有不同的从 1 ~ n 组成的二叉搜索树。

那么什么是 二叉搜索树 呢? 它的定义如下所示:

二叉搜索树(Binary Search Tree)可能是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

其实二叉搜索树的定义是没必要特意去记的,因为 二叉搜索树 它的作用就是用于快速搜素的。举个例子:我们如果发现根节点的值小于目标值,则可以直接去右孩子上检索。

我们可以先选一个根节点,这样左孩子和右孩子的元素都确定了。例如对于 n = 3 来说,假设我们选择了 1 作为根节点。

则左孩子一定为 null,而右孩子的元素一定为 2 和 3。

而对于 2 和 3 来说,它两也需要保证具有二叉树的性质 左子树值小于根节点,右子树节点的值大于根节点。则一定只有下面两种组成子树的方式:

image.png

最后我们再将根节点为 1 的左孩子和它的两个右孩子做组合,只能有如下的两种结果:

image.png

综上所述,大致的步骤如下所示:

  1. 先选择一个根节点 x
  2. 根据左孩子上面的元素 1 ~ x-1,组成所有可能的子树
  3. 根据右孩子上面的元素 x+1 ~ n,组成所有可能的子树
  4. 将得到的左孩子和右孩子组合,返回结果即可

二、实现

实现代码

实现代码与思路中保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码public List<TreeNode> generateTrees(int n) {
if (n == 0) {
return new LinkedList<>();
}
return dfs(1, n);
}

public List<TreeNode> dfs(int start, int end) {
List<TreeNode> ret = new LinkedList<>();
if (start > end) {
ret.add(null);
return ret;
}
// 枚举可行根节点
for (int i = start; i <= end; i++) {
// 找到所有的左子树
List<TreeNode> leftTrees = dfs(start, i - 1);

// 找到所有的右子树
List<TreeNode> rightTrees = dfs(i + 1, end);

// 组合
for (TreeNode left : leftTrees) {
for (TreeNode right : rightTrees) {
TreeNode currTree = new TreeNode(i);
currTree.left = left;
currTree.right = right;
ret.add(currTree);
}
}
}
return ret;
}

测试代码

1
2
3
java复制代码public static void main(String[] args) {
new Number95().generateTrees(3);
}

结果

image.png

三、总结

感谢看到最后,非常荣幸能够帮助到你~♥

如果你觉得我写的还不错的话,不妨给我点个赞吧!如有疑问,也可评论区见~

本文转载自: 掘金

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

基于Netty实现简化的RPC框架

发表于 2021-11-16

基于Netty实现简化的RPC

一些理论

  • 当你的项目太大,业务越来越多的时候,需要将服务拆分,RPC就可以用于服务于服务之间的调用问题。系统中的内部服务之间的调用用RPC。
  • RPC的架构主要包括三个部分:
  1. Register注册中心:将本地服务发布成远程服务,管理远程服务,提供给服务消费者使用。
  2. Server服务提供者:提供服务接口的定义和实现类。
  3. Client服务消费者:通过远程代理对象调用远程服务。
  • RPC就是将以下这些步骤封装起来,使得客户端能够像调用本地服务一样调用远程的服务。
  1. 接收调用
  2. 将方法参数等封装成能够进行网络传输的消息体序列化后发送到服务端
  3. 将服务端处理的结果反序列化后返回给客户端。

实践

服务端代码

  1. 服务端在Handler部分根据传过来的RPC请求体进行解析,调用相应的方法,返回RPC相应消息体。
  2. 上一步的解析部分通过Spring的反射获取类名和方法名。
RPC请求消息体和RPC响应消息体
  1. 调用的接口全限定名
  2. 调用接口中的方法名
  3. 方法返回类型
  4. 方法参数类型数组
  5. 方法参数值数组
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
scala复制代码/**
* RPC请求体
*/
@Data
public class RpcRequestMessage extends Message{

//调用的接口全限定名
private String className;

//调用接口中的方法名
private String methodName;

//方法返回类型
private Class<?> returnType;

//方法参数类型数组
private Class[] parameterTypes;

//方法参数值数组
private Object[] parameters;

public RpcRequestMessage(int sequenceId, String className, String methodName, Class<?> returnType, Class[] parameterTypes, Object[] parameters) {
super(sequenceId);
this.className = className;
this.methodName = methodName;
this.returnType = returnType;
this.parameterTypes = parameterTypes;
this.parameters = parameters;
}

@Override
public int getMessageType() {
return MessageConstant.RPC_REQUEST_MESSAGE;
}
}
处理Rpc请求的Handler
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
scala复制代码/**
* 处理Rpc请求的Handler,通过反射的机制来创建对象,调用方法,最后返回数据。
*/
public class RpcRequestHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage msg) throws Exception {

RpcResponseMessage rpcResponseMessage = new RpcResponseMessage();
try {

//Class.forName获取类对象本身的.class属性
Class<?> aClass = Class.forName(msg.getClassName());

//获取方法对象
Method method = aClass.getMethod(msg.getMethodName(), msg.getParameterTypes());

//调用方法 .newInstance()调用构造函数生成对象
Object invoke = method.invoke(aClass.newInstance(), msg.getParameters());

rpcResponseMessage.setReturnValue(invoke);
} catch (Exception e){
e.printStackTrace();
rpcResponseMessage.setException(new Exception(e.getMessage()));
}

ctx.writeAndFlush(rpcResponseMessage);

}
}

客户端代码

  1. writeAndFlush方法的调试(出现了没有报错的难以查明的问题可以用这个方法):writeAndFlush返回的是ChannelFuture对象,有sync同步和addListener异步两种方法,通过异步和promise可以进行两个线程之间的通信。
1
2
3
4
5
arduino复制代码channelFuture.addListener(promise -> {
if (!promise.isSuccess()){
System.out.println(promise.cause());
}
});
  1. 客户端应该生成一个单例模式的channel对象,可以供其他方法来一起调用。
  2. 关闭channel的方法应该设置为异步,而不是同步等待。否则初始化channel的过程中会一直阻塞住,导致无法获取到channel对象。
  3. 单例模式采用双重检查锁+volatite。
  4. 使用代理模式对请求的参数进行封装,并且将数据发送出去,使之能够像调用本地方法一样调用远程方法。
  5. 通过代理模式调用方法获取返回数据是在主线程中操作的,但是数据的处理是在NIO线程中,也就是在RpcResponseHandler执行,线程之间需要使用Promise进行通信。Promise就是一个容器,可以在多个线程中交换结果。
  6. 一次方法的调用对应一个Promise,通过方法请求时带的序列号作为Key,将Promise存入到Map当中。消息接收完毕再把Promise去掉。
  7. 通过一个全局的id变量来作为消息的序列号。
单例模式获取channel
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
csharp复制代码/**
* 用于Rpc的客户端启动器
*/
public class RpcClient {

public static volatile Channel channel = null;

private static final Object LOCK = new Object();

public static Channel getChannel(){
if(channel==null){
synchronized (LOCK){
if(channel==null){
initChannel();
}
}
}
return channel;
}


/**
* 初始化Channel
*/
public static void initChannel(){
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ClientChannelInitializer());
ChannelFuture future = bootstrap.connect("127.0.0.1",8282).sync();
channel = future.channel();


// 以异步的方式去关闭channel,防止线程堵塞。
channel.closeFuture().addListener( e -> {
group.shutdownGracefully();
});
} catch (Exception e){
e.printStackTrace();
}
}
}
代理模式封装请求
1
2
3
4
5
6
7
8
typescript复制代码/**
* 消息序列号和Promise的对应
*/
private final static Map<Integer, Promise<Object>> PROMISES = new ConcurrentHashMap<>();

public static Promise<Object> getPromise(int sequenceId){
return PROMISES.get(sequenceId);
}
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
scss复制代码public static <T> T getProxyService(Class<T> service){
ClassLoader classLoader = service.getClassLoader();
int sequenceId = SequenceIdGenerator.getSequenceId();
// 生成代理对象实例
Object o = Proxy.newProxyInstance(classLoader, service.getInterfaces(), (proxy, method, args) -> {
// proxy 代理对象 method 代理对象执行的方法 args 执行方法的参数列表
// 1. 将方法调用转换为 消息对象
RpcRequestMessage rpcRequestMessage = new RpcRequestMessage(
sequenceId,
service.getName(),
method.getName(),
method.getReturnType(),
method.getParameterTypes(),
args
);
// 2. 将请求发送出去
getChannel().writeAndFlush(rpcRequestMessage).addListener(future -> {
if(!future.isSuccess()){
System.out.println(future.cause());
}
});

// 3. 准备一个Promise对象来接受结果并放入map容器中 指定Promise异步接受结果的线程
DefaultPromise promise = new DefaultPromise(getChannel().eventLoop());
MessageConstant.putPromise(sequenceId,promise);

// 4. 同步等待结果
promise.await();

// 5. 返回数据
if(promise.isSuccess()){
return promise.getNow();
}else{
return promise.cause();
}

});
return (T) o;
}

遇到的问题

  1. 堆栈溢出

本文转载自: 掘金

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

一个基于DPoS共识算法的区块链案例解析

发表于 2021-11-16

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

一个基于DPoS共识算法的区块链案例解析

本文收录于我的专栏:细讲区块链

本专栏会讲述区块链共识算法以及以太坊智能合约、超级账本智能合约、EOS智能合约相关知识,还会详细的介绍几个实战项目。如果有可能的话,我们还能一起来阅读以太坊的源码。有兴趣的话我们一起来学习区块链技术吧~

一、前言

前面我们介绍了PoW以及PoS的案例,我们会发现它们都有一些缺点,比如PoW耗费能源比较多,而PoS是持有的币越多,成功挖矿的几率越大,这会造成贫富差距越来越大,并且人们都不太愿意消耗自己的币。

而我们的DPoS,全名为Delegated Proof of Stake,也就是股份授权证明就解决了这些不足。

DPoS就是大家投票选出一定数量的节点来挖矿,用户拥有的票的数量和他持有的币数量有关。这就和股份制公司很像了,大家投票选出董事会成员。

这些被选出来的拥有挖矿权的节点的挖矿权力是一模一样的。

如果某个节点挖到了矿,那么他就要将获得的币分一些给投票给他的人。

一、定义区块、区块链

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码type Node struct {
Name string
Votes int
}
​
type Block struct {
Index     int
Timestamp string
Prehash   string
Hash      string
Data     []byte
delegate *Node
}

相信关注这个专栏前几篇文章的老朋友应该都知道区块内的信息代表什么,这里简单说一下Index是区块高度,TimeStamp是时间戳,Data是块保存的一些数据,Hash是当前区块的哈希值,PrevHash是先前区块的哈希值,delegate是区块的挖掘者。

而这里的节点信息就是之前没有介绍的了,Name是节点名称,Votes是被投的票数。

二、生成创世区块

1
2
3
4
5
6
go复制代码func firstBlock() Block {
gene := Block{0, time.Now().String(),
"", "", []byte("first block"), nil}
gene.Hash = string(blockHash(gene))
return gene
}

创世区块就是第一个区块,这里需要我们手写一个。哈希值的计算下面会讲述。

三、计算哈希值

1
2
3
4
5
6
7
8
go复制代码func blockHash(block Block) []byte {
hash := strconv.Itoa(block.Index) + block.Timestamp +
block.Prehash + hex.EncodeToString(block.Data)
h := sha256.New()
h.Write([]byte(hash))
hashed := h.Sum(nil)
return hashed
}

这里是将所有数据拼接在一起,然后计算拼接后的数据的哈希值。

四、生成新的模块

1
2
3
4
5
6
7
scss复制代码func (node *Node) GenerateNewBlock(lastBlock Block, data []byte) Block {
var newBlock = Block{lastBlock.Index + 1,
time.Now().String(), lastBlock.Hash, "", data, nil}
newBlock.Hash = hex.EncodeToString(blockHash(newBlock))
newBlock.delegate = node
return newBlock
}

还是讲过的逻辑,将这些数据放入区块中,便生成了一个新的区块。

五、创建节点

1
2
3
4
5
6
7
8
9
10
css复制代码var NodeAddr = make([]Node, 10)
​
​
func CreateNode() {
for i := 0; i < 10; i++ {
name := fmt.Sprintf("节点 %d 票数", i)
//初始化时票数为0
NodeAddr[i] = Node{name, 0}
}
}

假设我们这个区块链项目有10个节点,然后初始化节点,将节点的名字设为 节点0到节点9,然后初始化票数为0,将初始化的节点放入节点列表中。

六、模拟投票

1
2
3
4
5
6
7
8
9
css复制代码func Vote() {
for i := 0; i < 10; i++ {
rand.Seed(time.Now().UnixNano())
time.Sleep(100000)
vote := rand.Intn(10000)
NodeAddr[i].Votes = vote
fmt.Printf("节点 [%d] 票数 [%d]\n", i, vote)
}
}

我们这里使用随机数来分配节点被投的票数,因为要给10个节点投票,所以遍历10次,每次给节点投范围为0-9999的票数。

七、选拔挖矿节点

1
2
3
4
5
6
7
8
9
10
11
scss复制代码func SortNodes() []Node {
n := NodeAddr
for i := 0; i < len(n); i++ {
for j := 0; j < len(n)-1; j++ {
if n[j].Votes < n[j+1].Votes {
n[j], n[j+1] = n[j+1], n[j]
}
}
}
return n[:3]
}

然后我们根据投票数来选出投票的节点,这里我们使用冒泡排序根据投票数给节点排序,最后从排序后的列表中选出前三个票数最多的节点作为挖矿节点。

八、主逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码func main() {
CreateNode()
fmt.Printf("创建的节点列表: \n")
fmt.Println(NodeAddr)
fmt.Print("节点票数: \n")
   
Vote()
   
nodes := SortNodes()
fmt.Print("获胜者: \n")
fmt.Println(nodes)
​
first := firstBlock()
lastBlock := first
fmt.Print("开始生成区块: \n")
for i := 0; i < len(nodes); i++ {
fmt.Printf("[%s %d] 生成新的区块\n", nodes[i].Name, nodes[i].Votes)
lastBlock = nodes[i].GenerateNewBlock(lastBlock, []byte(fmt.Sprintf("new Block %d", i)))
}
}

主逻辑也是比较简单的,先初始化10个节点,然后投票,再根据票数选出前三名。然后前三名来生成新的区块。

是不是很简单,如果有兴趣的话,还可以看看前面描述的关于PoW和PoS的简单实例。

本文转载自: 掘金

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

OAuth 20(二):OAuth 20 四种授权许可类

发表于 2021-11-16

大家好,我是 橘长,此前我们带来了 OAuth 2.0 的第一篇「 掀开 OAuth 2.0 授权初貌」的解读。

今天继续解读 「 OAuth 2.0 中的授权许可协议」。

一、OAuth 历史

一上来就直接 OAuth 2.0,那自然会有疑问,有 1.0 协议吗?

自然是有的,以前大多数应用都是 Web 端,OAuth 1.0 时期的授权许可类型就一种,它想着用一套协议去应对各种业务场景。

随着IT业务的不断发展,移动端、web端等多元化场景,OAuth 1.0 协议有点应对不过来,以及存在固化攻击等安全问题,因此 OAuth 2.0 应运而生了。

了解一个技术的历史演变,有助于对这项技术未来走势的判断,这是洞见性思维,而非直觉。

二、四种授权许可协议

四种机制.png
OAuth 2.0 官方提供了四种授权协议,分别如图所示。

其中授权码许可机制是最完备,最安全的一种,当我们掌握了最难的那种,自然而言去掌握其他几种就会很快上手。

1、资源拥有者许可机制

简单理解其实就是账号名和密码,通过账号名和密码去换取凭据,再通过凭据去访问业务接口。

2、隐式许可机制

这是 OAuth 2.0 最不安全的一种授权许可机制。

存在的意义是针对于 无 Server 端的 APP 应用架构。

APP 静态页中展示了一系列第三方入口,前端直接用客户端配置等去换取凭据,相当不安全的做法。

3、客户端凭据许可机制

受保护资源没有明确的资源拥有者的时候。

比方说淘宝的 logo,此时第三方软件可以通过“唯一标识 + 密钥”的形式去换取凭据。

4、授权码许可机制

四种角色都存在,引入授权码的概念来做中转,通过授权码去换取凭据,这是 OAuth 2.0 中最安全、最完备的做法。

三、微信授权说明授权码许可机制

微信授权许可流程.png
接下来,橘长用简化版流程说明“微信授权授权码许可机制”:

第一步:微信用户访问第三方软件,第三方软件请求微信授权服务获取授权链接

第二步:第三方软件拿到授权链接后,第一次重定向引导用户到授权页

第三步:用户点击确认授权,授权服务颁发授权码回调第三方软件(第二次重定向)

第四步:第三方软件通过拿到的授权码加上相关颁发的配置去换取凭据

第五步:授权服务颁发凭据,第三方软件拿到凭据去访问受保护资源,进而拿到数据

四、总结

今天橘长带大家分析了 OAuth 的发展历史以及授权许可类型,只需要记住两个点:

1、关注一个技术的发展历史,有助于培养自身的洞见性思维

2、OAuth 2.0 有四种授权协议:客户端凭据许可机制、隐式许可机制、资源拥有者许可机制、授权码许可机制,尤其是授权码许可机制一定要掌握。*

下一篇橘长将给大家带来「 OAuth 2.0 中的令牌机制」的解读,感谢你的关注,可在主页找到我,如果你觉得有所收益,欢迎点赞、转发、评论,感谢认可!

本文转载自: 掘金

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

mysql 查询执行过程 ①

发表于 2021-11-16

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

mysql 查询执行过程 ①

查询执行过程

  1. 客户端发送一条查询给服务器
  2. 服务器先检查查询缓存,如果命中了缓存,立刻返回存储在缓存中的结果。
  3. 如果没有命中缓存,服务端进行sql 解析、预处理,再由优化器生成对应的执行计划。
  4. Mysql 根据优化器生成的执行计划,调用存储引擎的API 来执行查询。
  5. 将结果返回给客户端

Mysql 客户端和服务端之间的通信协议是半双工的,在任何一个时刻,要么由服务器向客户端发送数据,要么由客户端向服务器发送数据,这两个动作不能同时进行。

查询缓存

在解析一个查询语句之前,如果查询缓存是打开的,mysql 会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存中的查询只有一个字节不同也不会匹配缓存结果。

如果当前的查询恰好命中了查询缓存,在返回查询结果之前mysql 会检查一次用户权限。如果权限没有问题,mysql会直接从缓存中拿到结果并返回给客户端。

查询过程

将sql 转换成一个执行计划,mysql 依照这个执行计划和存储引擎进行交互。

解析sql,预处理,优化sql 执行计划。

语法解析和预处理

mysql 通过关键字将sql 语句进行解析,生成一个解析树,mysql 解析器使用mysql 语法规则验证和解析查询。

预处理器根据一些mysql 规则进一步检查解析树是否合法,然后预处理器验证权限

语法树校验完成后,由优化器转化为执行计划。一条查询可以有多种执行方式,优化器找到最好的执行计划。

本文转载自: 掘金

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

Java实现图片转字符图片示例demo

发表于 2021-11-16

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

前面介绍了一篇java实现图片灰度化处理的小demo,接下来再介绍一个有意思的东西,将一个图片转换成字符图片

借助前面图片灰度化处理的知识点,若我们希望将一张图片转成字符图片,同样可以遍历每个像素点,然后将像素点由具体的字符来替换,从而实现字符化处理

基于上面这个思路,具体的实现就很清晰了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Test
public void testRender() throws IOException {
String file = "http://i0.download.fd.52shubiao.com/t_960x600/g1/M00/10/17/oYYBAFWvR5-IeXHuAAd5kPb8eSgAACm0QF50xIAB3mo414.jpg";
// 从网络上下载图片
BufferedImage img = ImageIO.read(FileReadUtil.getStreamByFileName(file));


int w = img.getWidth(), h = img.getHeight();
// 创建新的字符图片画板
BufferedImage gray = new BufferedImage(w, h, img.getType());
Graphics2D g2d = gray.createGraphics();
g2d.setColor(null);
g2d.fillRect(0, 0, w, h);

Font font = new Font("宋体", Font.BOLD, 1);
g2d.setFont(font);
for (int x = 0; x < w; x ++) {
for (int y = 0; y < h; y ++) {
g2d.setColor(ColorUtil.int2color(img.getRGB(x, y)));
g2d.drawString("灰", x, y);
}
}
g2d.dispose();
System.out.printf("渲染完成");
}

注意上面的实现,在会字符的时候,先取出源像素点的色彩,然后重新设置给g2d,这个int转color也比较简单,实现如下

1
2
3
4
5
6
7
java复制代码public static Color int2color(int color) {
int a = (0xff000000 & color) >>> 24;
int r = (0x00ff0000 & color) >> 16;
int g = (0x0000ff00 & color) >> 8;
int b = (0x000000ff & color);
return new Color(r, g, b, a);
}

这样就实现了一个基础版的转字符图了,实际跑一下看看效果

image.png
这下尴尬了,输出的并不是我们预期的字符图,那么问题出在哪呢?

仔细看上面的文字大小为1,文字太小,导致即使是有字符组件的图,最终肉眼看起来和原图也没啥区别

那么我们就试一下将这个文字搞大点,将n*n个像素点作为一个文字渲染区域,这样我们需要调整一下遍历的步长;其次就是这个区域的颜色怎么定

  • 直接取均值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码/**
* 求取多个颜色的平均值
*
* @return
*/
Color getAverage(BufferedImage image, int x, int y, int w, int h) {
int red = 0;
int green = 0;
int blue = 0;

int size = 0;
for (int i = y; (i < h + y) && (i < image.getHeight()); i++) {
for (int j = x; (j < w + x) && (j < image.getWidth()); j++) {
int color = image.getRGB(j, i);
red += ((color & 0xff0000) >> 16);
green += ((color & 0xff00) >> 8);
blue += (color & 0x0000ff);
++size;
}
}

red = Math.round(red / (float) size);
green = Math.round(green / (float) size);
blue = Math.round(blue / (float) size);
return new Color(red, green, blue);
}

另外的就是改一下遍历的步长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码@Test
public void testRender() throws IOException {
String file = "http://i0.download.fd.52shubiao.com/t_960x600/g1/M00/10/17/oYYBAFWvR5-IeXHuAAd5kPb8eSgAACm0QF50xIAB3mo414.jpg";
// 从网络上下载图片
BufferedImage img = ImageIO.read(FileReadUtil.getStreamByFileName(file));


int w = img.getWidth(), h = img.getHeight();
// 创建新的灰度图片画板
BufferedImage gray = new BufferedImage(w, h, img.getType());
Graphics2D g2d = gray.createGraphics();
g2d.setColor(null);
g2d.fillRect(0, 0, w, h);

int size = 12;
Font font = new Font("宋体", Font.BOLD, size);
g2d.setFont(font);
for (int x = 0; x < w; x += size) {
for (int y = 0; y < h; y += size) {
Color avgColor = getAverage(img, x, y, size, size);
g2d.setColor(avgColor);
g2d.drawString("灰", x, y);
}
}
g2d.dispose();
System.out.printf("渲染完成");
}

再次执行之后结果如下,实现了我们的预期效果

image.png

最后再介绍一个更好用的姿势,直接使用开源项目 github.com/liuyueyi/qu… 来实现图片字符画

使用这个项目的 image-plugins 之后,生成一个灰度图就很简单了

1
2
3
4
5
6
7
8
9
java复制代码public void testCharImg() throws IOException {
String img = "http://hbimg.b0.upaiyun.com/2b79e7e15883d8f8bbae0b1d1efd6cf2c0c1ed1b10753-cusHEA_fw236";
BufferedImage out = ImgPixelWrapper.build().setSourceImg(img).setBlockSize(2)
.setPixelType(PixelStyleEnum.CHAR_COLOR)
.setChars("小灰灰blog")
.build()
.asBufferedImg();
System.out.println(out);
}

注意这个ImgPixelWrapper封装类,处理基础的字符处理之外,还支持生成灰度图,gif图转字符动画,图片像素化(如马赛克…)

至于quick-media这个项目就更有意思了,java侧若想生成酷炫的二维码,选择它绝对不会让你失望;有兴趣的小伙伴可以瞅一瞅

一灰灰的联系方式

尽信书则不如无书,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 个人站点:blog.hhui.top
  • 微博地址: 小灰灰Blog
  • QQ: 一灰灰/3302797840
  • 微信公众号:一灰灰blog

本文转载自: 掘金

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

1…322323324…956

开发者博客

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