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

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


  • 首页

  • 归档

  • 搜索

Python开发基础总结(七)数据库+FTP+字符编码+源码

发表于 2021-11-15

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

一、数据库的使用

1、数据库中的字段使用的utf8格式编码,但是读取出来却是问号。这个问题的解决可以通过在查询的时候指定编码方式来解决,只要执行sql语句:Query_Execsql(pdb, “SET NAMES ‘utf8’”);

注意,这个需要在连接后马上进行。并且,在其他的操作中,会一直使用这种编码。除非再次更改。

2、fetchone():返回一条记录。fetchall():返回所有的记录。

3、可以使用一个简单的方法获取所有的记录:

cur.execute(sql)

for tel, name, pwd in cur:

print tel, name, pwd

二、FTP 的使用

Python的标准模块ftplib就可以支持FTP。

几个函数:

FTP(host= ‘’ , user= ‘’ , passwd= ‘’ , acct= ‘’ , timeout=_GLOBAL_DEFAULT_TIMEOUT):如果参数中有user,则Connect();如果同时也有user,则login()。如果没用这些参数,后要自己调用connect和login。

connect(self, host=’’, port=0, timeout=-999):如果端口不是标准端口,则要手动调用connect。

login(user = ‘’, passwd = ‘’, acct = ‘’):登陆。

pwd():获得当前的工作路径。

cwd(path):更改当前的工作路径。

dir(path,cb):显示目录中的内容。cb为文件的处理函数。会传递给retrlines。这个函数可以获取一个目录下的所有的内容。

retrlines(self, cmd, callback = None):下载文本文件。cmd的形式为“RETR FILENAME”,callback是一个函数,要处理文本文件的每一个行。这里一个问题,如果直接用file的write方法,则会丢失换行符。而又没有writeline函数。

retrbinary(self, cmd, callback, blocksize=8192, rest=None):下载二进制文件,cmd的形式为“RETR FILENAME”,callback是一个函数,要处理文本文件的每一个块。默认大小事8k,但是可以更改。

storlines(self, cmd, fp, callback=None):上传文本文件。cmd的形式为“STOR FILENAME”。fp是一个文件对象,必须有readline方法。callback:每传送一行,就会调用这个函数。

storbinary(self, cmd, fp, blocksize=8192, callback=None, rest=None): 上传二进制文件。cmd的形式为“STOR FILENAME”。fp是一个文件对象,必须有read(num_bytes)方法。默认大小事8k,但是可以更改。

quit():退出。

三、字符编码的使用

encode是将Unicode转化为str,decode是将字符串转化为Unicode。所以,一个字符串要转化为另一种格式可以:

1
2
3
ini复制代码s = ‘中文’

s.decode(fromcodec).encode(tocodec)

也可以直接使用:s.encode(tocodec)。这个时候,相当于默认调用了decode,并且使用的是默认的编码方式。

四、源码安全

  1. Python代码如果直接发布,可能会暴露源码。
  2. 一个方法是利用c扩展Python,来代替核心模块。
  3. 另一个折中的方法就是对源码进行编译,生成pyc或者pyo文件。这些事字节码文件。可能会被反编译。所以,可能需要研究一下Python的pyo生成和加载方式,来生成更安全的Python字节码。网上说可以修改Python源码的opcode。没有研究过。
  4. 命令:python -m compileall 。
  5. 也可以在Python中使用:
1
2
3
4
5
6
7
python复制代码import compileall

compileall._dir('Lib/', force=True)

# Perform same compilation, excluding files in .svn directories.
import re
compileall._dir('Lib/', rx=re.compile('/[.]svn'), force=True)

本文转载自: 掘金

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

MySQL之like操作符

发表于 2021-11-15

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

1、简介

当对未知或者说知道某一部分的值进行过滤时,可以使用like操作符;like操作符用于模糊匹配。

like支持两个通配符,它们分别是:

  • %通配符,用于匹配多个字符
  • _通配符,用于匹配单个字符

通配符根据其所处未知又分为六种匹配方式:

匹配方式 作用
%xx 表示右匹配,右边的xx字符需要完全相等,左边可以是任意字符,也可以没有字符
_xx 表示右匹配,右边的xx字符需要完全相等,左边可以是任意一个字符,必须是一个不能没有字符
xx% 表示左匹配,右边的xx字符需要完全相等,右边可以是任意字符,也可以没有字符
xx_ 表示左匹配,左边的xx字符需要完全相等,右边可以是任意一个字符,必须是一个不能没有字符
%xx% 表示中间匹配,中间必须完全相等,左右两边可以是任意字符,左右两边可以没有其他字符
xx 表示中间匹配,中间必须完全相等,左右两边可以是任意一个字符,左右两边必须是一个不能没有字符

2、正文

首先准备一张User表,DDL和表数据如下所示,可以直接复制使用。

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
sql复制代码SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`age` int(11) NOT NULL COMMENT '年龄',
`sex` smallint(6) NOT NULL COMMENT '性别',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '李子捌', 18, 1);
INSERT INTO `user` VALUES (2, '张三', 22, 1);
INSERT INTO `user` VALUES (3, '李四', 38, 1);
INSERT INTO `user` VALUES (4, '王五', 25, 1);
INSERT INTO `user` VALUES (5, '六麻子', 13, 0);
INSERT INTO `user` VALUES (6, '田七', 37, 1);
INSERT INTO `user` VALUES (7, '谢礼', 18, 1);

SET FOREIGN_KEY_CHECKS = 1;

数据的初始顺序如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码mysql> select * from user;
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
| 2 | 张三 | 22 | 1 |
| 3 | 李四 | 38 | 1 |
| 4 | 王五 | 25 | 1 |
| 5 | 六麻子 | 13 | 0 |
| 6 | 田七 | 37 | 1 |
| 7 | 谢礼 | 18 | 1 |
+----+--------+-----+-----+
7 rows in set (0.00 sec)

2.1 %通配符

%通配符有三种匹配方式,分别是%xx、xx%、%xx%,接下来演示三者的简单用法。

需求:

查询user表中姓氏为张的用户

语句:

1
2
3
4
5
6
7
sql复制代码mysql> select * from user where name like '张%';
+----+------+-----+-----+
| id | name | age | sex |
+----+------+-----+-----+
| 2 | 张三 | 22 | 1 |
+----+------+-----+-----+
1 row in set (0.00 sec)

需求:

查询user表中姓名以七结尾的用户

语句:

1
2
3
4
5
6
7
sql复制代码mysql> select * from user where name like '%七';
+----+------+-----+-----+
| id | name | age | sex |
+----+------+-----+-----+
| 6 | 田七 | 37 | 1 |
+----+------+-----+-----+
1 row in set (0.00 sec)

需求:

查询user表中姓名中包含李字符的用户

语句:

1
2
3
4
5
6
7
8
sql复制代码mysql> select * from user where name like '%李%';
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 1 | 李子捌 | 18 | 1 |
| 3 | 李四 | 38 | 1 |
+----+--------+-----+-----+
2 rows in set (0.00 sec)

2.2 _通配符

_通配符和%通配符的区别在于 _只匹配一个字符,并且必须匹配一个字符;而%可以匹配多个字符,甚至0个字符。


需求:

查询user表中姓氏为李,并且名字只有两个中文的用户

语句:

1
2
3
4
5
6
7
sql复制代码mysql> select * from user where name like '李_';
+----+------+-----+-----+
| id | name | age | sex |
+----+------+-----+-----+
| 3 | 李四 | 38 | 1 |
+----+------+-----+-----+
1 row in set (0.00 sec)

需求:

查询user表中名为三的用户

语句:

1
2
3
4
5
6
7
sql复制代码mysql> select * from user where name like '_三';
+----+------+-----+-----+
| id | name | age | sex |
+----+------+-----+-----+
| 2 | 张三 | 22 | 1 |
+----+------+-----+-----+
1 row in set (0.00 sec)

需求:

查询user表中姓名为三个子,并且第二个子为麻的用户

语句:

1
2
3
4
5
6
7
sql复制代码mysql> select * from user where name like '_麻_';
+----+--------+-----+-----+
| id | name | age | sex |
+----+--------+-----+-----+
| 5 | 六麻子 | 13 | 0 |
+----+--------+-----+-----+
1 row in set (0.00 sec)

2.3 通配符使用注意事项

通配符非常强大,我相信很多人都经常使用通配符,但是字符串匹配往往并不是一件性能特别快的事情。因此我们在使用通配符的时候有一些注意事项需要时刻记住。

  • 能不用则不用的原则,不用能避免通配符带来的全部问题,所以如果其他操作符能查询出来,就不要使用like
  • 在使用通配符的地方,尽量缩小查询范围,如果有多个查询条件,应该考虑能否将通配符放置到其他过滤条件的后面
  • 特别注意通配符的选择,以及通配符的位置,可以参考六种匹配方式选择自己合适的

本文转载自: 掘金

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

Java基础学习14之Date类及比较器Comparable

发表于 2021-11-15

Java基础学习14之Date类及比较器Comparable

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

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。


日期处理类—Date类

java.util.data类是在整个程序处理之中唯一可以取得日期当前日期实例化对象的操作方法,也就是说我们要取出当前日期输出Date类对象即可。

public class Date extends Object implements Serializable, Cloneable, Comparable<Date>
实现了Serializable、Serializable、Comparable接口。

Date (Java Platform SE 8 )

  • Date课程以Date级的Date表示特定的时间。

在JDK 1.1之前, Date有两个附加功能。 它允许将日期解释为年,月,日,小时,分钟和第二个值。 它还允许格式化和解析日期字符串。 不幸的是,这些功能的API不适合国际化。 从JDK 1.1开始, Calendar类应该用于在日期和时间字段之间进行转换,并且DateFormat类应用于格式化和解析日期字符串。 在相应的方法Date被弃用。

尽管Date类旨在反映协调的世界时间(UTC),但根据Java虚拟机的主机环境的不同,可能不会这样做。 几乎所有的现代操作系统都假设在所有情况下1天= 24×60×60 = 86400秒。 然而,在UTC的时候,大概每一两年会有一秒钟的时间,叫做“闰秒”。 闰秒总是作为一天的最后一秒,总是在12月31日或6月30日。例如,1995年的最后一分钟是61秒,由于增加了闰秒。 大多数计算机时钟不够准确,不能反映出闰秒的区别。

一些计算机标准是根据格林尼治标准时间(GMT)定义的,相当于世界时间(UT)。 GMT是标准的“民用”名称; UT是同一标准的“科学”名称。 UTC和UT之间的区别是UTC是基于原子钟,UT是基于天文观测,对于所有的实际目的来说,这是一个看不见的细毛。 因为地球的旋转不均匀(减速并以复杂的方式加速),UT并不总是均匀地流动。 根据需要,将时差引入到UTC中,以使UT在UT1的0.9秒内保持UTC,这是UT的版本,并应用了某些修正。 还有其他的时间和日期系统; 例如,基于卫星的全球定位系统(GPS)使用的时间尺度与UTC同步,但不对闰秒进行调整。

进一步信息的一个有趣的来源是美国海军天文台,特别是时间局在:http://tycho.usno.navy.mil

及其“时间系统”的定义如下:

http://tycho.usno.navy.mil/systime.html

在类的所有方法Date接受或返回年,月,日,小时,分钟和秒值,以下表述中使用:

+ *y*年代表整数*y* `- 1900` 。
+ 一个月由0到11的整数表示; 0是1月,1是2月,等等; 11月12日。
+ 日期(月的一天)以通常的方式从1到31的整数表示。
+ 一小时由0到23之间的整数表示。因此,从午夜到凌晨1点的时间是小时0,从中午到下午1点的小时是12小时。
+ 一般以0〜59的整数表示。
+ 第二个由0到61的整数表示; 值60和61仅发生在闰秒上,甚至仅在实际上正确跟踪闰秒的Java实现中发生。 由于目前引入闰秒的方式,在同一分钟内不会发生两个闰秒,但是本规范遵循ISO C的日期和时间约定。在所有情况下,为这些目的而提供的方法的论证不必在指定范围内; 例如,可以将日期指定为1月32日,并将其解释为2月1日。
1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.day13.demo;

import java.util.Date;

public class DateDemo1 {
public static void main(String[] args) {
Date date = new Date();
//Tue Aug 17 17:01:50 CST 2021
System.out.println(date);
}
}

在Date类中最需要关心的一个核心问题:long可以描述日期,看了一通过Date类中提供的方法来进行观察。

方法名称 类型 描述
public Date(long date) 普通 将long类型变为Date类型数据
public long getTime() 普通 将Date类型变为long类型数据

观察转化

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.day13.demo;

import java.util.Date;

public class DateDemo1 {
public static void main(String[] args) {
long num = System.currentTimeMillis();
System.out.println(new Date(num));
System.out.println(new Date(num).getTime());
}
}

这中简单的转换在以后的程序开发经常会使用。

日期格式化—SimpleDateFormat类(核心)

虽然Date可以取得当前的日期时间,但是取出的结构不是我们所喜欢的格式,这时候就需要我们进行格式的转化,使用的是java.text包

image-20210817171959845

但是日期格式里面需要设置一些日期标记:年(YYYY)、月(MM)、日(dd)、时(HH)、分(mm)、秒(ss)、毫秒(SS);

实现日期格式化处理(日期格式化之后是字符串)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.day13.demo;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateDemo1 {
public static void main(String[] args) {
Date date = new Date();
String str = "YYYY-MM-dd HH:mm:ss";
SimpleDateFormat sdf = new SimpleDateFormat(str);
String dateFromat = sdf.format(date);
System.out.println(dateFromat);
}
}

将字符串变为Date类型

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

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateDemo2 {
public static void main(String[] args) throws ParseException{
Date date = new Date();
System.out.println(date);
String str = "yyyy-MM-dd HH:mm:ss";
SimpleDateFormat sdf = new SimpleDateFormat(str);
//将Date类型转化为字符串类型
String newdateStirng = sdf.format(date);
System.out.println(newdateStirng);
//将字符串转化为Date类型
Date newdate = sdf.parse(newdateStirng);
System.out.println(newdate);
}
}

数字操作类—Math类

在Java.lang.Math类之中定义了所有的于数学有关的基本公式,在这个类之中所有的方法都是static型的方法,强调一个方法:round(),public static long round(double a),表示四舍五入。

1
2
3
4
5
6
7
8
9
10
11
java复制代码package com.day13.demo;

public class MathDemo {
public static void main(String[] args) {
System.out.println(Math.round(13.51));
System.out.println(Math.round(13.5));
//如果负数小数,没大于0.5都不进位
System.out.println(Math.round(-13.51));
System.out.println(Math.round(-13.5));//-13
}
}

希望可以准确的保存小数位进行处理。

需要保留几位小数

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day13.demo;
class MyMath{
public static double round(double num, int scale){
return Math.round(num * Math.pow(10, scale)) / Math.pow(10, scale);
}
}
public class MathDemo {
public static void main(String[] args) {
//1234.457
System.out.println(MyMath.round(1234.4567, 3));
}
}

随机数—Random()

Java .util.Random的主要主要作用就是产生随机数,下面通过一个代码来观察就行。
Random (Java Platform SE 8 )

public class Random extends Object implements Serializable

Random (Java Platform SE 8 )

  • 该类的实例用于生成伪随机数的流。 该类使用48位种子,其使用线性同余公式进行修改。 (见Donald Knuth, “计算机编程艺术”,第2卷 ,第3.2.1节)

如果使用相同的种子创建两个Random Random ,并且对每个实例进行相同的方法调用序列,则它们将生成并返回相同的数字序列。 为了保证此属性,为Random类Random 。 为了Java代码的绝对可移植性,Java实现必须使用这里所示的所有算法为Random类。 然而,Random类的子类Random使用其他算法,只要它们遵守所有方法的一般合同。

Random类实现的Random使用protected实用程序方法,每次调用可以提供多达32个伪随机生成位。

许多应用程序会发现方法Math.random()使用起来更简单。

java.util.Random的java.util.Random是线程安全的。 但是,跨线程的同时使用java.util.Random实例可能会遇到争用,从而导致性能下降。 在多线程设计中考虑使用ThreadLocalRandom 。

java.util.Random的java.util.Random不是加密安全的。 考虑使用SecureRandom获取一个加密安全的伪随机数生成器,供安全敏感应用程序使用。

网站开发的随机验证码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day13.demo;

import java.util.Random;

public class RandomDemo {
public static void main(String[] args) {
char data [] = new char[]{'a','b','c','d','e'};
for (int i = 0; i < 4; i++) {
System.out.print(data[new Random().nextInt(data.length)]);
}
}
}

大数字操作类

如果说现在有两个非常大的数字要进行数学操作,你们认为要怎么做?这个时候数字已经超过了double的范围,那么只能利用字符串来表示,取出每一个字符串变为数字后进行数学计算,这种方式的难度较高,为了解决这种问题,在Java之中提供了两个大数字操作类:java.math包中BigInteger,BigDecimal,而这两个类是属于Number的子类。

​ 1.大整数操作类:BigIntegr

之前已经强调过了,如果数字较大,肯定按照String来处理,所以这一点可以通过Biginteger的构造方法来观察:

​ 构造:public BigInteger(String val);

而且在BigInteger类之中定义了一些基本的数学计算:

​ 加法:public BigInteger add(BigInteger val);

​ 减法:public BigInteger subtract(BigInteger val);

​ 乘法:public BigInteger multiply(BigInteger val);

​ 除法(不保存余数):public BigInteger divide(BigInteger val);

​ 除法(保存余数):public BigInteger divideAndRemainder(BigInteger val)

大数的四则运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.day13.demo;

import java.math.BigInteger;

public class BigAddDemo {
public static void main(String[] args) {
BigInteger bigA = new BigInteger("123712487812974891274891274128947891");
BigInteger bigB = new BigInteger("43895748395789347589347589398");
System.out.println("加法计算:" + bigA.add(bigB));
System.out.println("减法计算:" + bigA.subtract(bigB));
System.out.println("乘法计算:" + bigA.multiply(bigB));
System.out.println("除法计算:" + bigA.divide(bigB));
BigInteger result[] = bigA.divideAndRemainder(bigB);
System.out.println("除法计算:" + result[0] + "." + result[1]);
}
}

​ 2.大小数操作类:BigDcimal

BigDecimal类表示的是大小数操作类,但是这个类也具备了于之前同样的基本计算方式,而在实际的工作之中,是用这个类最多的情况是进行准确位数的四舍五入操作,如果要完成这一操作需要关心BigDecimal类中的以下定义:

构造:public BigDecimal(double val);

除法:public BigDecimal divide(BigDecimal divisor ,int scale ,int roundingMode);

进位模式:public static final int ROUND_HALF_UP。

四舍五入进位操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.day13.demo;

import java.math.BigDecimal;
//大数进位方法
class MyMath1{
public static double round(double num, int scale){
return new BigDecimal(num).divide(new BigDecimal(1), scale, BigDecimal.ROUND_HALF_DOWN).doubleValue();
}
}
public class BigDecimalDemo {
public static void main(String[] args) {
System.out.println(MyMath1.round(2138845.4567, 3));
}
}

Arrays类

排序操作:java.util.Arrays.sort(数组名称),对于Arrays类一直是进行数组排序的操作,类一直进行数组排序的操作,而Arrays类是定义在java.util包下的一个操作类,在这个类之中定义了所有的与数组有关的基本操作:二分查找,拷贝操作,相等判断,填充,变为字符串输出等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.day13.demo;

import java.util.Arrays;

public class ArraysDemo {
public static void main(String[] args) {
int dataA[] = new int []{1,2,3,4,5,6};
int dataB[] = new int []{1,2,3,4,5,6};
//数组输出
System.out.println(Arrays.toString(dataA));
//两个数组进行比较
System.out.println(Arrays.equals(dataA,dataB));
//数组二分法查找
System.out.println(Arrays.binarySearch(dataA, 4)+1);
}
}

比较器—Comparable

Comparable

对象数组排序:public static void sort(Object[] a)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码package com.day13.demo;

import java.util.Arrays;

class Pers{
private String name;
private int age;
public Pers(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Pers [name=" + name + ", age=" + age + "]";
}
}
public class ComparableDemo {
public static void main(String[] args) {
Pers pers[] = new Pers[]{
new Pers("张三",12),
new Pers("李四",23),
new Pers("刘武",54)//对象数组
};
Arrays.sort(pers);//要进行对象数组的排序处理
System.out.println(Arrays.toString(pers));
}
}

这个时候没有任何的语法错误,即:程序的代码是正确的,但是在程序执行的时候出现了以下的问题:

1
2
3
4
5
java复制代码Exception in thread "main" java.lang.ClassCastException: com.day13.demo.Pers cannot be cast to java.lang.Comparable
at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
at java.util.Arrays.sort(Arrays.java:1246)
at com.day13.demo.ComparableDemo.main(ComparableDemo.java:36)

明确的告诉用户现在发生了“ClassCaseException”,类转换异常,Person类不能变为Comparables实例。

如果要为对象指定比较规则,那么对象所在的类必须实现Comparable接口,下面首先来看一下这个接口的定义:

1
2
3
java复制代码public interface Comaparable<T>{
public int compareTo(T o)
}

Stirng类中的compareTo()就属于覆写Comaparable接口所的来的方法。

image-20210817191154586

实现对象数组的排序

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
java复制代码package com.day13.demo;

import java.util.Arrays;

class Pers implements Comparable<Pers>{
private String name;
private int age;
public Pers(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Pers [name=" + name + ", age=" + age + "]\n";
}
@Override
public int compareTo(Pers o) {
// TODO Auto-generated method stub
//升序排序 如果降序排序将1 和 -1 进行位置调换
if(this.age > o.age){
return -1;
}else if(this.age < o.age){
return 1;
}else{
return 0;
}
}
}
public class ComparableDemo {
public static void main(String[] args) {
Pers pers[] = new Pers[]{
new Pers("张三",12),
new Pers("李四",23),
new Pers("刘武",54)//对象数组
};
Arrays.sort(pers);//要进行对象数组的排序处理
System.out.println(Arrays.toString(pers));
}
}

只要是对象数组排序,就必须有Comparable接口。

二叉树( Binary Tree )

二叉树是一种排序的基本的数据结构,而如果要想为多个对象进行排序,那么就必须可以区分出对象的大小,那么就必须依靠Comparable接口完成。

二叉树的基本原理:取第一个元素作为根节点,之后每一个元素的排列要求:如果比根节点晓得数据放在左子树,如果比根节点大的数据放在右子树,在输出的时候采用中序(左-根-右)遍历的方式完成。

但是不管是何种方式操作,一定要记住,这种数据结构的实现永远都需要依靠节点类,而这个时候的节点类要保存两个节点:左,右。

国际化

在java.util.Locale可以找java提供国际化的相关信息

Locale构造:public Locale(String language, String country)

观察区域和语言代码

1
2
3
4
5
6
7
8
9
10
java复制代码package com.day13.demo;

import java.util.Locale;

public class LocalDemo {
public static void main(String[] args) {
System.out.println(Locale.CHINA);//zh_CN
System.out.println(Locale.CHINESE);//zh
}
}
  • 中国Locale:public static final Locale CHINESE
  • 美国Locale:public static final Locale US
  • 取得当前的Locale对象:public static Locale getDefault()

当我们用eclipse打开Message.properties进行编写后不要慌,我们还有一个非常强大的工具在JDK中,CLASSPATH:C:\Program Files\Java\jdk1.8.0_241\bin 自己安装JDK的环境目录下有一个叫native2ascii.exe可以帮助我们进行转码。这种做法非常麻烦,如果要开发国际版本的软件还是自己安装一个编辑软件比较好。

image-20210817212132969

语言配置文件Message.properties

1
java复制代码welcome.info = \u5317\u4EAC\u6B22\u8FCE\u4F60\uFF01

测试文件LocaleDemo.java

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.day13.demo;

import java.util.ResourceBundle;

public class LocaleDemo {
public static void main(String[] args) {
//这个时候设置的baseName没有后缀,而且一定要在CLASSPATH之中
ResourceBundle res = ResourceBundle.getBundle("com.day13.msg.Message");
//北京欢迎你!
System.out.println(res.getString("welcome.info"));
}
}

资源文件的名称就只是 包.名称前缀

本文转载自: 掘金

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

程序员为了讨好大舅子,竟自学自动化编程???

发表于 2021-11-15

hello,大家好,我是帅气的超级饭饭团,最近刚和大舅子约了个晚饭,饭桌上大舅子第一句话便是:

听说你是程序员?可以帮我写个自动化脚本吗?

image-20211113211323638

我发现外行人都觉得程序员是万能的啊,但是其实我们都只是会ctrl c + ctrl v而已

不过,大舅子提需求了,能认怂吗?

当然不能啊,必须做啊

image-20211113211945345

虽然我连自动化脚本怎么做、用什么语言都不知道,但是,为了妹子,不能怂啊

我喝了一两白酒,壮了壮胆子,和大舅子讨论了下需求

酒过三巡菜过五味,最后终于将需求定了下来了

大概是这样的:

  • 自动登录某app
  • 自动搜索指定宝贝
  • 自动评论留言
  • 自定义搜索关键词、自定义留言个数

总结下来就是根据关键词自动评论,然后还要提供一个UI操作

确定了需求了,加上白红啤怼了几杯后,我反问大舅子那你妹就是我的了吧

大舅子说:需求搞出来,她就是你的了

为了妹子,必须拼啊,当晚开始奋斗
image.png

先是搜索相关技术,然后开始查漏补缺,最后便是开始实现

给大家看看需要的技术:

  • autojs,自动化脚本引擎
  • 一点js编程意识,实现语言用的就是js
  • 一点安卓意识,毕竟自动化脚本针对的对象就是安卓手机了

最后给大家看看我奋笔疾书一整晚的效果

10s.gif

大概分为几步:

第一步,UI界面的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
javascript复制代码ui.layout(
<vertical>
<text textSize="18sp" textColor="#000000" margin="20" textStyle="bold">
闲鱼自动评论
</text>
<ScrollView>
<vertical>
<text textSize="16sp" margin="8">1.宝贝标签</text>
<input w="*" text="卫衣" id="target" margin="0 16" />
<text textSize="16sp" margin="8">2. 评论内容</text>
<input w="*" text="你好" id="comment" margin="0 16" />
<text textSize="16sp" margin="8">3. 总处理条数</text>
<input text="2" id="total" inputType="number" margin="0 16" />
<linear gravity="center">
<button margin="16" id="ok">开始执行</button>
</linear>
</vertical>
</ScrollView>
</vertical>
)

最终呈现出来的界面是这样的

image-20211114140618779

虽然丑,但是能用就可以啦

第二步便是点击后的处理

1
2
3
4
5
6
7
8
9
10
javascript复制代码ui.ok.click(() => {
var target = ui.target.text();
var comment = ui.comment.text();
var total = ui.total.text();
let main = new Main();

threads.start(function () {
main.process(target, comment, total);
});
});

也就是监听了按钮的点击处理

image-20211114140737520

第三步就是真正做自动化的地方了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
js复制代码let utils = new AppUtils();
utils.consoleShow();
console.log("处理的参数:" + target + "," + message + "," + limit);
let mainTarget;
auto.waitFor();
utils.openApp("闲鱼");

let search = id("search_bar_layout").untilFind();
utils.clickView(search[0]);

let sousuoInputs = className("android.widget.EditText").indexInParent(1).depth(6).untilFind()
sousuoInputs[0].setText(target)
utils.paste(sousuoInputs[0], target);

utils.sleep(1000);
let results = className("android.view.View").descContains(target).untilFind();
utils.clickView(results[0]);
mainTarget = results[0].desc();

console.log("设置标签:" + mainTarget);

var targetViewMap = new java.util.HashMap();
while (targetViewMap.size() < limit) {
let viewIndex = 0;
while (true) {
let targetViews = className("android.view.View").descContains(target).untilFind().filter(function (w) {
return w.desc().length >= 10;
});
if (targetViews.length <= viewIndex) {
break;
}
let targetView = targetViews[viewIndex++];
let text = targetView.desc();
text = text.substring(0, Math.min(10, text.length));
if (!targetViewMap.containsKey(text)) {
utils.sleep(1000);
utils.clickView(targetView);
utils.sleep(1000);
if (textContains("客服").findOnce() != null) {
utils.tryback(mainTarget);
continue;
}

let btns = className("android.view.View").untilFind()
var leaveMessage;
for (key in btns) {
let btn = btns[key]
try {
if (btn.desc() == null) {
continue;
}

if (btn.desc() == "留言") {
leaveMessage = btn;
break;
}

if (!isNaN(btn.desc())) {
leaveMessage = btn;
break;
}
} catch (error) {

}
}

utils.clickView(leaveMessage);
utils.sleep(1000);
try{
leaveMessage.setText(message)
} catch(error) {
}
try {
let leaveMessage2 = descContains("看对眼就留言").findOnce();
utils.clickView(leaveMessage2);
utils.sleep(1000);
leaveMessage2.setText(message)
} catch (error) {
}
try {
let leaveMessage3 = textContains("看对眼就留言").findOnce();
utils.clickView(leaveMessage3);
utils.sleep(1000);
leaveMessage3.setText(message)
} catch (error) {
}

utils.sleep(1000);
let sendBtn = textContains("发送").findOnce();
if (sendBtn == null) {
console.warn("找不到发送按钮:" + text)
continue;
}
utils.clickView(sendBtn);
console.log("评论成功,避免被监控,停止1秒")
utils.tryback(mainTarget);
utils.sleep(3000);

targetViewMap.put(text, targetView);
console.log("当前成功评论个数:" + targetViewMap.size())

if (targetViewMap.size() >= limit) {
break;
}
}
}
while (true) {
if (scrollDown(0)) {
break;
}
}
}

console.log("执行完毕");

可以看到第3步挺多sleep的地方,都是为了等待界面渲染的时间,因为手机比较卡,所以等待的时间也相对长了些。

最后将脚本打包后发给了大舅子,大舅子喜出望外…….

image-20211114141020001

当然啦,大舅子开心,我也开心,毕竟抱得妹子归啊哈哈哈哈

另外,我将自动化学习教程和实战例子都放到github和码云上了,有兴趣学习的可以拿去看

image-20211113135002203

image-20211113134947735

github地址:脚本仓库

码云地址:脚本仓库

b站视频:教学视频

本文转载自: 掘金

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

编写一个接口压测工具 前言 特性 安装 总结

发表于 2021-11-15

前言

前段时间有个项目即将上线,需要对其中的核心接口进行压测;由于我们的接口是 gRPC 协议,找了一圈发现压测工具并不像 HTTP 那么多。

最终发现了 ghz 这个工具,功能也非常齐全。

事后我在想为啥做 gRPC 压测的工具这么少,是有什么难点嘛?为了验证这个问题于是我准备自己写一个工具。

特性

前前后后大概花了个周末的时间完成了相关功能。

github.com/crossoverJi…

也是一个命令行工具,使用起来效果如上图;完整的命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shell复制代码NAME:
ptg - Performance testing tool (Go)

USAGE:
ptg [global options] command [command options] [arguments...]

COMMANDS:
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--thread value, -t value -t 10 (default: 1 thread)
--Request value, --proto value -proto http/grpc (default: http)
--protocol value, --pf value -pf /file/order.proto
--fully-qualified value, --fqn value -fqn package.Service.Method
--duration value, -d value -d 10s (default: Duration of test in seconds, Default 10s)
--request value, -c value -c 100 (default: 100)
--HTTP value, -M value -m GET (default: GET)
--bodyPath value, --body value -body bodyPath.json
--header value, -H value HTTP header to add to request, e.g. "-H Content-Type: application/json"
--target value, --tg value http://gobyexample.com/grpc:127.0.0.1:5000
--help, -h show help (default: false)

考虑到受众,所以同时支持 HTTP 与 gRPC 接口的压测。

做 gRPC 压测时所需的参数要多一些:

1
shell复制代码ptg -t 10 -c 100 -proto grpc  -pf /xx/xx.proto -fqn hello.Hi.Say -body test.json  -tg "127.0.0.1:5000"

比如需要提供 proto 文件的路径、具体的请求参数还有请求接口的全路径名称。

目前只支持最常见的 unary call 调用,后续如果有需要的话也可以 stream。

同时也支持压测时间、次数两种压测方式。

安装

想体验度朋友如果本地有 go 环境那直接运行:

1
shell复制代码go get github.com/crossoverJie/ptg

没有环境也没关系,可以再 release 页面下载与自己环境对应的版本解压使用。


github.com/crossoverJi…

设计模式

整个开发过程中还是有几个点想和大家分享,首先是设计模式。

因为一开始设计时就考虑到需要支持不同的压测模式(次数、时间;后续也可以新增其他的模式)。

所以我便根据压测的生命周期定义了一套接口:

1
2
3
4
5
6
7
8
9
go复制代码type (
Model interface {
Init()
Run()
Finish()
PrintSate()
Shutdown()
}
)

从名字也能看出来,分别对应:

  • 压测初始化
  • 运行压测
  • 停止压测
  • 打印压测信息
  • 关闭程序、释放资源


然后在两个不同的模式中进行实现。

这其实就是一个典型的依赖倒置原则。

程序员要依赖于抽象接口编程、不要依赖具体的实现。

其实大白话就是咱们 Java 里常说的面向接口编程;这个编程技巧在开发框架、SDK或是多种实现的业务中常用。

好处当然是显而易见:
当接口定义好之后,不同的业务只需要根据接口实现自己的业务就好,完全不会互相影响;维护、扩展都很方便。

支持 HTTP 和 gRPC 也是同理实现的:

1
2
3
4
5
go复制代码type (
Client interface {
Request() (*Response, error)
}
)


当然前提得是前期的接口定义需要考虑周全、不能之后频繁修改接口定义,这样的接口就没有意义了。

goroutine

另外一点则是不得不感叹 goroutine+select+channel 这套并发编程模型真的好用,并且也非常容易理解。

很容易就能写出一套并发代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func (c *CountModel) Init() {
c.wait.Add(c.count)
c.workCh = make(chan *Job, c.count)
for i := 0; i < c.count; i++ {
go func() {
c.workCh <- &Job{
thread: thread,
duration: duration,
count: c.count,
target: target,
}
}()
}
}

比如这里需要初始化 N 个 goroutine 执行任务,只需要使用 go 关键字,然后利用 channel 将任务写入。

当然在使用 goroutine+channel 配合使用时也得小心 goroutine 泄露的问题;简单来说就是在程序员退出时还有 goroutine 没有退出。

比较常见的例子就是向一个无缓冲的 channel 中写数据,当没有其他 goroutine 来读取数时,写入的 goroutine 就会被一直阻塞,最终导致泄露。

总结

有 gRPC 接口压测需求的朋友欢迎试用,提出宝贵意见;当然 HTTP 接口也可以。

源码地址:
github.com/crossoverJi…

本文转载自: 掘金

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

本地复现不了问题,那就线上(非正式)debug

发表于 2021-11-15

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

这里所说的线上不单单指正式环境,还可以是开发环境,测试环境。

代码出问题是很正常的,关键是怎么快速找到问题并解决问题。

一些简单的问题我们根据经验可以直接看出来,但是对于复杂的或者直接看不出的问题我们就需要debug了,debug的能力也是程序员必须的。

通常出现问题,本地启动程序debug,打断点然后一步步的跟着流程执行基本就可以解决问题了,但是偏偏就有一些诡异的问题,很难搞哦,本地怎么测都没问题,就是部署到服务器上会有问题,而且并不能简单的看出问题是什么。

图片
(开发与测试的对话)

这种对话是不是很熟悉,线上的问题就是无法在本地复现出来,既然本地无法复现,那我们就线上debug,很简单很实用(工作几年才知道还有这种操作)

下面介绍下利用idea debug线上环境

1. configurations中配置remote

Add new configuration 中选中Remote

图片

2. 配置IP和端口

主要就是配置线上服务器的IP和端口

Name: 随意,就是一个名字

Host: 所要调试的程序所在的服务器IP地址

Port:debug的端口,默认5005,可以任意修改(此端口非程序的端口)

然后Apply和OK 即可,至此本地idea配置完成。

图片

3. 用debug命令重新启动线上的服务

1
2
bash复制代码# debug的启动命令
nohup java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Xms512m -Xmx1024m app,jar > app.log &

dt_socket:使用的通信方式(不需要改)

server:是主动连接调试器还是作为服务器等待调试器连接(不需要改)

suspend:是否在启动JVM时就暂停,并等待调试器连接(不需要改)

address:地址和端口,地址省略,两者用冒号分隔(取自上一步配置的端口)

4. 开始debug

idea选择之前配置的configuration,然后选择debug启动,如下图

图片

然后选中刚刚配置的remote,然后debug模式启动,出现下面的信息代表启动成功。

图片

记得在需要调试的位置打上断点,然后直接请求线上的API ,就可以在本地愉快的debug了。

5. 注意!注意!注意!

重要是事情说三遍

不要在正式环境这样搞,线上debug是会阻塞的,请求都跑到你本地了,对线上环境有影响的。

不要在正式环境这样搞,线上debug是会阻塞的,请求都跑到你本地了,对线上环境有影响的。

不要在正式环境这样搞,线上debug是会阻塞的,请求都跑到你本地了,对线上环境有影响的。

你有没有什么快速定位问题或者更好的debug技术,欢迎沟通交流!!!


我是纪先生,用输出倒逼输入而持续学习,持续分享技术系列文章,以及全网值得收藏好文,欢迎关注或者关注公众号,做一个持续成长的技术人。

实际问题系列的历史文章

1. 好用工具:慢SQL分析pt-query-digest

2. 好用工具: pt-online-schame-change

3. 怎么防止短信被白嫖;

4. 巧用双索引避免es出现索引不存在的问题;

本文转载自: 掘金

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

抽象语法树(AST)入门

发表于 2021-11-15

参考:

  • 刘伟老师【软件工程研究】模块:blog.csdn.net/LoveLion
    • 【Eclipse AST】AST与ASTView简介
    • 【Eclipse AST】AST的获取与访问
    • 【Eclipse AST】AST的创建
    • 【Eclipse AST】AST的修改
  • Redy语法分析--抽象语法树简介
  • 详解AST抽象语法树

AST简介

抽象语法树(abstract syntax tree,AST) 是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。
抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。
抽象语法树在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。

实例

  1. 四则运算表达式

表达式: 1+3*(4-1)+2

image.png

  1. xml
1
2
3
4
5
6
7
8
9
xml复制代码<letter>
<address>
<city>ShiChuang</city>
</address>
<people>
<id>12478</id>
<name>Nosic</name>
</people>
</letter>

image.png

  1. 程序1
1
2
3
4
5
6
7
8
java复制代码while b != 0
{
if a > b
a = a-b
else
b = b-a
}
return a

image.png

  1. 程序2
1
2
3
4
java复制代码sum=0
for i in range(0,100)
sum=sum+i
end

image.png

使用JDT ASTView浏览抽象语法树

Eclipse JDT 提供了可以可视化AST的插件:JDT AstView,该插件可以在IDEA上使用。
image.png
如果下载失败,多半是因为DNS的原因,可以

  • 从官网获取对应插件的下载链接+迅雷 或者
  • 修改hosts文件,修改plugins.jetbrains.com/ 域名地址ip
    详细自行百度

以下是ASTView的测试代码

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

import java.util.LinkedList;
import java.util.List;

public class ASTTest {
public static void main(String[] args) {
int n = 1;
List<Integer> list = new LinkedList<>();
for (int i = 0; i < 10; i++) {
n++;
list.add(i);
}
System.out.println(n + list.size());
}
}

在代码界面右键 Enable JDT AST View
image.png
点击右侧工具栏的 JDT AST,点击相应的AST节点,光标就会在相应的代码闪烁。使用该插件可以帮助快速感受一下AST。
image.png

根据Eclipse AST的定义,图中每一个节点对应一个节点名,每一个子节点也在其依附的父节点中扮演着一个角色(身份),并且叶子节点基本为名称、操作符和字面量。表1给出了图1中的for循环节点(ForStatement)的四个子节点的节点名和依附于父节点的角色。

image.png

图1 ForSatement的语法树结构

image.png
表1 各个子节点的节点名和角色

文本 -> 抽象语法树的过程

  1. 词法分析:文本 -> token列表
    • 去除空格,对token分类,去除空格,然后对token分类,那些属于语法关键字,那些属于操作符,那些属于语句的截止位置,那些属于数据
  2. 语法分析:token列表 -> 语法二叉树
    • 扫描token流,然后分析它的语法,这一步应该是分析一条以;结尾的语句具体的执行规则,然后使用逆波兰表达式组合,最后形成一个二叉树,二叉树从底部往上一步步合并

第一步:词法分析,也叫扫描scanner
它读取我们的代码,然后把它们按照预定的规则合并成一个个的标识 tokens。同时,它会移除空白符、注释等。最后,整个代码将被分割进一个 tokens 列表(或者说一维数组)。

1
2
3
java复制代码const a = 5;
// 转换成
[{value: 'const', type: 'keyword'}, {value: 'a', type: 'identifier'}, ...]

当词法分析源代码的时候,它会一个一个字母地读取代码,所以很形象地称之为扫描 - scans。当它遇到空格、操作符,或者特殊符号的时候,它会认为一个话已经完成了。
第二步:语法分析,也称解析器
它会将词法分析出来的数组转换成树形的形式,同时,验证语法。语法如果有错的话,抛出语法错误。

1
2
3
4
5
6
7
8
9
10
java复制代码[{value: 'const', type: 'keyword'}, {value: 'a', type: 'identifier'}, ...]
// 语法分析后的树形形式
{
type: "VariableDeclarator",
id: {
type: "Identifier",
name: "a"
},
...
}

当生成树的时候,解析器会删除一些没必要的标识 tokens(比如:不完整的括号),因此 AST 不是 100% 与源码匹配的。
解析器100%覆盖所有代码结构生成树叫做CST(具体语法树)。

使用ASTView浏览抽象语法树

本文转载自: 掘金

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

SpringBoot多环境配置怎么玩? 前言 SpringB

发表于 2021-11-14

文章已收录到我的Github精选,欢迎Star:github.com/yehongzhi/l…

前言

一般来说,在日常开发中都会分多个环境,比如git代码分支会分为dev(开发)、release(测试)、pord(生产)等多个环境。可以说每个环境对应的配置信息(比如数据库、缓存、消息队列MQ等)都不相同。因此不同的环境肯定需要对应不同的配置文件。接下来学习一下怎么配置多环境的配置文件。

SpringBoot多环境配置

因为SpringBoot做多环境配置比较简单,而且现在大部分项目基本都会使用SpringBoot,所以这里就介绍怎么用SpringBoot做多环境配置。

单文件版本

单文件在实际中使用得并不多,不过也可以实现多环境配置,这里简单介绍一下。以application.yml配置文件举例,你要在一个配置文件里面配置多个环境的配置,肯定需要分割线将其隔开,所以SpringBoot就规定了使用---进行隔开每个环境。

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
yaml复制代码spring:
application:
name: mydemo
profiles:
active: prod # 选择prod环境配置
#整合mybatis
mybatis-plus:
mapper-locations: classpath:mapper/*Mapper.xml
type-aliases-package: com.yehongzhi.mydemo.model
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
---
# 开发环境
server:
port: 8080
spring:
profiles: dev
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://DEV_IP:3306/user?createDatabaseIfNotExist=true
username: root
password: 123456
---
# 测试环境
server:
port: 8090
spring:
profiles: release
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://RELEASE_IP:3306/user?createDatabaseIfNotExist=true
username: root
password: 123456
---
# 生产环境
server:
port: 8888
spring:
profiles: prod
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://PROD_IP:3306/user?createDatabaseIfNotExist=true
username: root
password: 123456

单文件配置多环境的缺点很明显,就是会导致这个application.yml文件非常大,不够清晰。最好是一个环境单独一个文件,这样就清晰很多。于是乎就有了多文件版本。

多文件版本

一般SpringBoot的配置文件都是叫application.yml或者application.properties,这里用application.yml举例,配置多环境配置文件,文件名需要满足这样的格式:application-{profile}.yml。看下图就明白了。

换而言之,dev环境的配置文件就叫做application-dev.yml,那么怎么选择哪个环境的配置文件呢,其实很简单,只需要在application.yml加上如下配置:

1
2
3
yaml复制代码spring:
profiles:
active: dev

这就表示选择加载application-dev.yml文件,何以见得?

一般在启动完成之后,我们可以在控制台搜索关键字profiles找到对应的环境。

所以我们就可以在application.yml里面,通过spring.profiles.active切换不同的环境。这就是多文件版本。

但是我们在平时开发时发现,这个配置要经常改来改去,非常麻烦,有没有不用改这个配置就可以切换的方法呢?当然有。

首先在pom.xml文件增加以下环境变量的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<profiles>
<profile><!-- 开发环境 -->
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
</profile>
<profile><!-- 测试环境 -->
<id>release</id>
<properties>
<profiles.active>release</profiles.active>
</properties>
</profile>
<profile><!-- 生产环境 -->
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
</properties>
</profile>
</profiles>

接着在application.yml配置文件中使用@profiles.active@来配置环境变量。

1
2
3
yaml复制代码spring:
profiles:
active: '@profiles.active@'

接着刷新Maven,可以在IDEA右侧中选择对应的环境,如下图:

当需要切换环境时,就不需要改配置文件的内容,只需要勾选对应的环境即可,就方便很多。

结合Nacos配置中心

一般在项目开发中,都需要配置信息能够在运行时更改配置,于是乎就有了配置中心的概念。配置中心当然也有多环境的配置。

在Nacos配置中心就有命名空间的概念,我们可以使用命名空间来实现多环境配置。首先引入Maven依赖:

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
xml复制代码<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
</profile>
<profile>
<id>release</id>
<properties>
<profiles.active>release</profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
</properties>
</profile>
</profiles>

第二步,启动Nacos,然后在创建对应的命名空间和配置文件。

第三步,在项目中增加bootstrap.yml文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码spring:
application:
name: mydemo
profiles:
active: '@profiles.active@'
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
group: DEFAULT_GROUP
namespace: a4a33d52-371b-451a-a3c1-d01c1d343331 #dev命名空间的ID
enabled: true
prefix: ${spring.application.name}
refresh-enabled: true

在IDEA配置项目启动时设置环境变量。

这样就完成了,启动项目,就可以读到Nacos配置中心的dev命名空间的mydemo-dev.yaml文件。

因为DataId的定义规则是${prefix}-${spring.profiles.active}.${file-extension}。

prefix默认规则是获取${spring.application.name}的值。可以通过spring.cloud.nacos.config.prefix进行配置。

spring.profiles.active即为当前环境对应的profile。可以通过spring.profiles.active进行配置。

file-extension为配置文件的数据格式。可以通过spring.cloud.nacos.config.file-extension进行配置。

总结

以上就是多环境配置的三种方式,多环境配置基本上是创建新项目的基本操作,所以掌握多环境配置还是很有必要的。感谢大家的阅读,希望看完之后能对你有所收获。

觉得有用就点个赞吧,你的点赞是我创作的最大动力~

我是一个努力让大家记住的程序员。我们下期再见!!!

能力有限,如果有什么错误或者不当之处,请大家批评指正,一起学习交流!

本文转载自: 掘金

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

聊一下最近的故障——MySQL优化器的那点事 背景 问题 分

发表于 2021-11-14

背景

最近在做一个资产迁移合并的工作,原先旧资产在A系统当中,要将这部分资产迁移至B系统。那么简单的做法就是B系统起一个任务,从A系统当中分页查询,得到结果然后插入到B系统当中。当然,双方系统背后的MySQL都是分库分表的。

问题

当然,这里我们不讨论数据丢失,事务一致性的问题。那么发生了什么问题呢?
在实际运行当中发现,部分用户的资产,查询会发生超时,导致这部分数据查询不出来。登上堡垒机,看到堆栈日志:

image.png
这个就跟明显,就是查询超时引起的。

分析

SQL查询超时问题,按照我的经验来看,一般都有三种思路:

  1. 数据库表数据量过大,导致SQL运行超时;
  2. 数据源配置文件配置的超时时间过短;
  3. SQL没有执行到索引;
    来分析分析:
  • 检查了一下库表,这个表的量级也就几百万,SQL本身也是比较简单,所以可以排除;
  • 数据源配置文件的超时时间是5s,鉴于其他业务查询倒是正常,那么也可以排除;
    这样一来,那么很明显,就是索引的缘故了。打开explain之后,发现走了另外一个索引!但是sql的写法是按照我们预期的索引来写的!

那么索引为什么会失效了?传统的排查方式,那就是看下SQL本身,是否符合最左匹配原则,另外就是查看下SQL索引字段是不是用了函数操作;当然,既然我在这里说了,肯定是排除了以上的情况。那么是什么原因呢?这也是要进入到我们这次的话题。

实验

各位发现,在上面的描述中,我都没有贴出相关的SQL以及库表结构,这个还是为了保护公司相关的业务以及代码吧。但是这个不妨碍我继续给各位准备下面的实验数据(MySQL是基于5.7版本)。
以下是我的步骤:

  1. 首先创建一张表
1
2
3
4
5
6
7
8
9
10
sql复制代码CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT '',
`sex` int(11) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`stuNo` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`),
KEY `idx_stuNo` (`stuNo`)
) ENGINE=InnoDB AUTO_INCREMENT=20001 DEFAULT CHARSET=utf8;

这里呢,创建了一张student的表,里面建立了两个索引idx_name和idx_stuNo

  1. 为这张表插入数据,这里我插入了20000条数据。(这里我就不演示怎么插入数据了,显得有点蠢)
  2. 准备一条sql
1
sql复制代码select stuNo from student  where stuNo > '100' and stuNo < '30000000'

那么我们来看下他的explain执行计划

1
sql复制代码explain select stuNo from student  where stuNo > '100' and stuNo < '30000000';

结果如下:

image.png
可以发现,这个sql是执行到idx_stuNo这个索引,好像也没有什么大毛病。接下来我们修改一下sql

1
sql复制代码select * from student  where stuNo > '100' and stuNo < '30000000'

我们再来看下他的explain执行计划

1
sql复制代码explain select * from student  where stuNo > '100' and stuNo < '30000000';

image.png
我的天,这里竟然走了全表的查询,没有走索引!

成本模型

索引的作用,就是用来帮我们提高查询效率的。那么当一张表当中有多个索引的时候,MySQL是如何选择正确的搜索路径呢?这里就会引入一个成本模型的概念,这里只探讨一下SQL查询的情况。

SQL查询成本 = IO成本 + CPU成本

全表扫描

首先我们从最简单的情况,全表扫描开始说起。为什么说全表扫描最简单呢,这是因为全表扫描就是基于源表依次遍历,那么这里的IO成本就是计算获取了多少页,CPU成本就是读取了多少行。
于是可得

IO成本 = 查询的页数 * 1.0 + 1.1

CPU成本 = 行数 * 0.2 + 1.0

这里的0.2和1.0都是计算常数(怎么决定的这个肯定是经验值),这里的+1.0和+1.1也是微调值。
第二条SQL,因为是走了全表扫描,说明全表扫描的成本最低,那我们来计算一下:

  1. 首选是获取查询的页数,这个可以根据查询表自身信息可得

show table status like ‘student’

image.png
可以看到Data_length为1589248,那么1589248 / 16 / 1024 = 97(因为默认页大小就是16k),可以得到总页数为97页,需要执行97次IO。总行数就是19980。于是可以得到

IO成本 = 97 * 1.0 + 1.1 = 98.1

CPU成本 = 19980 * 0.2 + 1 = 3997

总成本 = 98.1 + 3997 = 4095.1

索引扫描

索引扫描表查询的时候,它的成本计算,总的来说也是按照IO成本+CPU成本,只是可以分为两个阶段:索引成本 + 数据成本。索引成本指的是获取索引所付出的成本,而数据成本,是在需要回表的时候才需要涉及。我们先来探讨下索引成本:

IO成本 = 索引的扫描区间 * 1.0

CPU成本 = 索引记录数 * 0.2 + 0.01

由于是范围查找,MySQL会认为读取索引的一个扫描区间的I/O成本和读取一个页面的I/O成本是相同的,所以这里的IO成本就是1 * 1.0 = 1;那么这里怎么计算出索引记录数的呢?

  1. 先根据stuNo > 100这个条件访问一下idx_stuNo对应的B+树索引,找到满足stuNo > 100这个条件的第一条记录(我们把这条记录称之为区间最左记录)。在B+数树中定位一条记录是非常快的,是常数级别的,这个过程的性能消耗是可以忽略不计。
  2. 再根据stuNo < 30000000这个条件继续从idx_stuNo对应的B+树索引中找出最后一条满足这个条件的记录(我们把这条记录称之为区间最右记录),这个过程的性能消耗同样可以忽略不计的。
  3. 如果区间最左记录和区间最右记录相隔不太远(在MySQL 5.7.22这个版本里,只要相隔不大于10个页面即可),就可以精确统计出满足100 < stuNo < 30000000条件的二级索引记录条数。否则需要沿着区间最左记录向右读10个页面,计算每个页面平均包含多少记录,然后用这个平均值乘以区间最左记录和区间最右记录之间的页面数量就可以了。

根据上面的操作可以知道这里的索引记录数为5119。那么关于索引成本为

IO成本 = 1 * 1.0 = 1

CPU成本 = 5119 * 0.2 + 0.01 = 1023.81

总成本 = 1 + 1023.81 = 1024.81

我们在使用索引的时候经常会听到一种说法——回表,没错。上面的成本是只索引的成本,当我们从索引当中获取数据的时候并不能满足SQL查询的要求,就会进行回表操作,而这里的回表操作就有点类似于上面的全表扫描

IO成本 = 索引对应数据条数 * 1.0

CPU成本 = 索引对应数据条数 * 0.2

MySQL评估回表操作的I/O成本依旧很粗略,MySQL认为每次回表操作都相当于访问一个页面。所以这里可以计算出成本为:

IO成本 = 5119 * 1.0 = 5119

CPU成本 = 5119 * 0.2 = 1023.8

总成本 = 5119 + 1023.8 = 6142.8

最后,两部分加起来的总成本为:1024.81 + 6142.8 = 7167.61。

小结

由此可以看出,在这种情况下,全表扫描的成本更低。当然我们从这里也可以看到,回表操作带来的代价还是比较高的。各位也可以打开optimizer_trace查看最详细的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
sql复制代码mysql> SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE \G;
*************************** 1. row ***************************
QUERY: select * from student where stuNo > '100' and stuNo < '30000000'
TRACE: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `student`.`id` AS `id`,`student`.`name` AS `name`,`student`.`sex` AS `sex`,`student`.`age` AS `age`,`student`.`stuNo` AS `stuNo` from `student` where ((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))"
}
]
}
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))"
},
{
"transformation": "constant_propagation",
"resulting_condition": "((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))"
}
]
}
},
{
"substitute_generated_columns": {
}
},
{
"table_dependencies": [
{
"table": "`student`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
]
}
]
},
{
"ref_optimizer_key_uses": [
]
},
{
"rows_estimation": [
{
"table": "`student`",
"range_analysis": {
"table_scan": {
"rows": 19980,
"cost": 4095.1
},
"potential_range_indexes": [
{
"index": "PRIMARY",
"usable": false,
"cause": "not_applicable"
},
{
"index": "idx_name",
"usable": false,
"cause": "not_applicable"
},
{
"index": "idx_stuNo",
"usable": true,
"key_parts": [
"stuNo",
"id"
]
}
],
"setup_range_conditions": [
],
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
},
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "idx_stuNo",
"ranges": [
"100 < stuNo < 30000000"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 5119,
"cost": 6143.8, -- 这里是索引成本
"chosen": false,
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
}
}
}
]
},
{
"considered_execution_plans": [
{
"plan_prefix": [
],
"table": "`student`",
"best_access_path": {
"considered_access_paths": [
{
"rows_to_scan": 19980,
"access_type": "scan",
"resulting_rows": 19980,
"cost": 4093,
"chosen": true
}
]
},
"condition_filtering_pct": 100,
"rows_for_plan": 19980,
"cost_for_plan": 4093,
"chosen": true
}
]
},
{
"attaching_conditions_to_tables": {
"original_condition": "((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))",
"attached_conditions_computation": [
],
"attached_conditions_summary": [
{
"table": "`student`",
"attached": "((`student`.`stuNo` > '100') and (`student`.`stuNo` < '30000000'))"
}
]
}
},
{
"refine_plan": [
{
"table": "`student`"
}
]
}
]
}
},
{
"join_execution": {
"select#": 1,
"steps": [
]
}
}
]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)

最后

尽管MySQL的优化器给我们还是带来极大的帮助,但是在某些情况下,计算模型不一定会覆盖到我们的真实效果,所以在这类事情发生的时候,也多了一种排查思路。某些情况下,也可以使用force index的方式来实现~

本文转载自: 掘金

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

我修复的印象最深的一个bug——排查修复问题事件BEX引发的

发表于 2021-11-14

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

前言

  • 最近,我们部门负责项目运维的小王频频接到甲方的反馈,运行的项目使用谷歌浏览器登录后,每次点击处理2秒后,浏览器自动闪退崩溃.小王同学折腾了一个星期,还没找到问题的原因.甲方客户都把问题反馈给项目经理了.项目经理给小王撂下狠话,“明天客户再给我打电话,你以后再也没机会穿拖鞋上班了..”
  • 小王扰了扰头上剩在中间的头发,一脸委屈的看向我,无奈中透着一点深情
  • “Chova大哥哥,你来帮我看看嘛~以后晚上陪你一起健身!”
  • 看着他期待的目光,我心目不免一紧,哆哆嗦嗦地打开了他电脑上的谷歌浏览器……

问题一:问题事件BEX浏览器停止工作

  • 点击项目中问题处置页面跳转后,光标出现转圈 ,2秒后弹框提示Google Chrome已停止工作
    在这里插入图片描述

原因

  • 软件中dll文件和浏览器发生冲突

解决

删除造成冲突的dll文件

  • 点击查看问题详细信息,查看浏览器崩溃的问题签名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码问题签名:
问题事件名称: BEX
应用程序名: chrome.exe
应用程序版本: 69.0.3497.100
应用程序时间戳: 5b9cbd4f
故障模块名称: BrowserUrl.dll
故障模块版本: 0.0.0.0
故障模块时间戳: 5f4b9830
异常偏移: 00004138
异常代码: c0000409
异常数据: 00000000
OS 版本: 6.1.7601.2.1.0.256.48
区域设置 ID: 2052
其他信息 1: 031a
其他信息 2: 031ac9a5aca2c7bab1c2347d68169e05
其他信息 3: e951
其他信息 4: e951aede12191034f862a087b85a801a
  • 问题签名中的故障模块名称就是造成浏览器崩溃的dll文件
  • 通过在浏览器导航栏中输入以下路径查看加载的dll文件位置:
1
url复制代码chrome://conflicts/
  • 将原来的dll备份到其余文件夹,用于后续恢复
  • 在软件文件夹中删除软件中造成冲突的dll文件

修复造成冲突的软件

  • 删除造成冲突的dll文件只是暂时解决浏览器停止工作的问题
  • 因为dll文件时软件的运行依赖,如果随便删除会导致软件本身运行异常
  • 如果想要彻底解决BEX问题事件,需要修复造成冲突的软件,大部分是软件本身存在问题,建议下载官方软件

问题二:谷歌浏览器闪退

  • 点击项目中问题处置页面跳转后,光标出现转圈 ,2秒后浏览器闪退消失
  • 项目部署在同一个服务器上,使用不同的计算机进行测试,问题只是在部分计算机中出现

原因

  • 由于问题仅在部分电脑上出现,初步判定不是项目的问题,从系统方向排查问题
  • 对无问题计算机和问题计算机,初步判定为问题计算机中的安全策略配置导致浏览器访问发生闪退崩溃的问题

解决

删除安全策略

  • 进入控制面板点击用户账户查看当前用户为是否为管理员账户
  • 进入C盘修改策略文件弹出框提示需要管理员权限
  • 打开运行输入gpedit.msc打开本地策略组
  • 进入到本地策略组中的用户账户控制以管理员批准模式控制其余账户
  • 禁用用户账户控制, 重启计算机
  • 开机后进入C盘直接删除操作文件

增加用户权限

  • 在系统中的账户中查看当前用户账户
  • 点击此电脑,选择属性中的安全选项卡,选中高级
  • 更改当前用户账户为所有者并替换子容器和对象的所有者
  • 在权限中添加当前用户账户的完全控制权限并继承

问题三:错误码STATUS_INVALID_IMAGE_HASH浏览器崩溃

  • 谷歌浏览器崩溃,错误码为STATUS_INVALID_IMAGE_HASH
  • 谷歌浏览器插件报错,并弹出错误提示框

原因

  • Google Chrome在79版本中重新启用了渲染器代码完整性保护Renderer Code Integrity Protection. 这个会导致签名不是谷歌或者微软的模块被阻止加载

解决

禁用渲染器代码完整性保护功能

  • 通过文件禁用渲染器代码完整性保护功能 (推荐) :
    • 打开运行输入regedit进入注册表编辑器
    • 进入文件夹 HKEY_LOCAL_MACHINE \ SOFTWARE \ Policies \ Google \ Chrome 中
    • 在右侧的窗口中,右键单击新建, 选择DWORD(32位)值创建新的密钥
    • 双击新建的密钥,将值名称修改为RendererCodeIntegrityEnabled, 并将值数据输入为0
    • 重启谷歌浏览器
  • 使用命令禁用渲染器代码完整性保护功能:
    • 打开运行,输入以下命令可以禁用渲染器代码完整性保护功能,其余功能禁用方式类似
      1
      shell复制代码chrome.exe --disable-features=RendererCodeIntegrity

总结

Windows检查事件日志

  • 右键开始图标,打开事件查看器
  • 点击应用程序和服务, 进入Microsoft => Windows => CodeIntegrity => 可操作
  • 查找问题事件ID为3033的事件
  • 点击问题事件的详细信息,会显示导致浏览器崩溃的dll文件的名称和位置

Google Chrome显示模块加载列表

  • 在导航栏输入以下路径,可以显示加载的各个模块的信息:
1
url复制代码chrome://conflicts/

在这里插入图片描述

一点思考

  • 这个BUG是我目前修复的千千万万个项目的BUG中印象最深的一次BUG,由于问题事件BEX引发的谷歌浏览器闪退崩溃的异常问题.这个BUG因为其不可复现性导致特别难以发现和解决,正是由于这一次的BUG解决过程,让我了解到了一位攻城狮在项目开发维护过程中实际经验的重要性,多思考,多实践,多多积累经验,才是一位攻城狮的成长之路.

本文转载自: 掘金

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

1…342343344…956

开发者博客

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