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

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


  • 首页

  • 归档

  • 搜索

线上问题定位分析解决

发表于 2021-07-30

定位问题的先决条件

需要有详细的日志记录,提前告警的监控平台,事发现场保留

日志 :业务日志,中间件日志

监控 :CPU、内存、磁盘、网络,类加载、GC、线程等

快照 :-XX:+HeapDumpOnOutOfMemoryError 和 -XX:HeapDumpPath

分析问题,解决问题的思路

经验+直觉,快速定位 > 逐一排查,传输链路 > 寻找规律
不要轻易怀疑监控。考虑资源。优先保证系统能正常运行。保留现场,事后排查定位问题。

逐一排查,传输链路,通过日志或工具逐一排查

  1. 内部原因,是否是客户端或者前端问题,程序发布后的Bug,回滚后可以立即解决
  2. 外部原因,比如服务,第三方服务,主机、组件的问题。
    1. 服务:错误日志邮件提醒或elk快速定位问题,查看gc日志
    2. 第三方服务:单独调用测试,联系第三方加急解决
    3. 主机: CPU相关问题,可以使用 top、vmstat、pidstat、ps 等工具排查; 内存相关问题,可以使用 free、top、ps、vmstat、cachestat、sar 等工具排查;IO 相关问题,可以使用 lsof、iostat、pidstat、sar、iotop、df、du 等工具排查;网络相关问题,可以使用 ifconfig、ip、nslookup、dig、ping、tcpdump、iptables等工具排查。
    4. 组件:查看日志输出,使用命令查看运行情况
  3. 因为系统资源不够造成系统假死的问题,通常需要先通过重启和扩容解决问题,之后再进行分析,系统资源不够,一般体现在 CPU 使用高、 内存泄漏或OOM 的问题、IO问题、网络相关问题这四个方面

分析问题的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码jps -v 查看java进程
jinfo -flags pid 查看运行参数
jstat -gc 8544 5000 100,将每隔5s采样一次pid为8544的gc,输出100次

jmap -dump:live,format=b,file=dump.hprof 29170
#生成虚拟机的内存转储快照 注意线上可能会触发线上gc
jmap -heap 29170
jmap -histo:live 29170 | more
jmap -permstat 29170

jstack -l 29170 |more 显示虚拟机的线程快照

df -h # 磁盘
free -m / -h # 内存
top cpu # cpu

线上cpu100%报警(找出最耗时CPU进程-线程-堆栈-代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
perl复制代码方法1:原生工具,慢
top -c #显示完整信息 P:cpu使用排序 M:内存使用排序
top -Hp 10765 ,#显示一个进程的线程运行信息列表 -H 显示线程信息,-p指定pid & P
printf "%x\n" 10804 转16进制 2f71
jstack 12084 | grep '0x2f71' -C5 --color 查看堆栈,找到线程在干嘛

方法2:
使用提前准备好的sh脚本,可以一条命名查看当前出事的线程代码,快,推荐
sh show-busy-java-threads.sh > a.txt #查询java耗时线程前5个
sh show-busy-java-threads.sh -p > a.txt #查询指定进程

方法3:
使用arthas,工具内置很多功能,比如可以查看源码,判断是否发布成功,可以用来排查疑难问题
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
dashboard
thread -8
jad com.xx.xx.xx.xxximp 查看线上类代码
watch com.xx.xx.xx.xxximp doTask '{params}' '#cost>100' -x 2
#观察会慢在什么入参上,监控耗时超过100毫秒的 doTask方法的入参,并且输出入参,展开2层入参参数
ognl #查询某静态字段的值

定位到堆栈就可以定位到出问题代码的行号,然后找对应的发布分支代码该行号即可

线上内存OOM

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
bash复制代码某Java服务(假设PID=12084)出现了OOM,最常见的原因为:
1. 有可能是内存分配确实过小,而正常业务使用了大量内存
2. 某一个对象被频繁申请,却没有释放,内存不断泄漏,导致内存耗尽未调用close(),dispose()释放资源,例如:文件io,网络io
3. 某一个资源被频繁申请,系统资源耗尽,例如:不断创建线程(没有用线程池),不断发起网络连接等
总结:本身资源不够,申请资源太多,资源耗尽

分析工具:
jvisualvm(直方图),MAT(优先,直方图,跟踪内存使用的引用关系),JProfiler

线下分析:
服务挂掉之后有保留文件:直接下载dump文件导入mat分析
java -jar -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=

线上分析:
1. 确认是不是内存本身就分配过小
jmap -heap 12084

2. 找到最耗内存的对象
jmap -histo:live 12084 | head -n 10 #该命令会强制执行一次fgc

jmap -dump:format=b,file=/opt/dump.hprof {pid} #以二进制输出档当前内存的堆情况,
然后可以导入MAT等工具进行
tar –czf dump.tar.gz dump.hprof

3. 确认进程创建的线程数,以及网络连接数,如果资源耗尽,也可能出现OOM
ll /proc/17306/fd | wc -l
ll /proc/17306/task | wc -l

如何防止线上问题发生

数据库:上线一个定时监控和杀掉慢SQL的脚本。这个脚本每分钟执行一次,检测上一分钟内,有没有执行时间超过一分钟(这个阈值可以根据实际情况调整)的慢SQL,如果有大事务自己觉得该阈值的合理性,如果发现,直接杀掉这个会话

cpu或者内存的使用率上做报警,大于90%的时候可以dump和jstack一次,甚至jstat也可以做,然后95%的时候也同样执行一次,甚至98或者99的时候也可以做一次,这样不仅可以保留现场,同时还可以对比

完善的服务报错日志监控,可选elfk+日志监控或sentry

完善的流程机制。完善的主机,中间件监控报警机制

遇到过的线上问题以及解决思路

Zuul 网关不响应任何请求,zuul假死

App打不开,请求超时,访问数据库超时,数据库cpu飙升有规律,在某个时间点才飙升,去调度中心找该时间断的的定时任务,排查是异步转账开多了线程导致的

工具汇总

  • 去哪儿bistour
  • mat 分析堆快照
  • arthas arthas.aliyun.com/doc/quick-s…
  • gceasy.io #在线gc日志,dump文件分析
  • fastthread.io #在线gc日志,dump文件分析
  • console.perfma.com #在线生成jvm参数

参考

  • www.bbsmax.com/A/6pdD7b2X5…
  • cloud.tencent.com/developer/a…

本文转载自: 掘金

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

SaaS系统从0到1搭建,07代码生成(逻辑篇) 前言 思路

发表于 2021-07-30

前言

车队管理类似的SaaS平台,从0到1,继续..

上一篇咱撸到代码生成的模板,模板或者说代码生成更加友好、更可配的方式,有一些方案各有优劣吧,那本篇继续模板后的实现逻辑。

(我是后半夜Java,在掘金这分享下经验,那些靠copy的搬运作者,未经允许,不要copy文章了)

思路

通过表名,获取表相关信息,调用模板去生成对应代码

实现

control就不写了,就个表名List 就可以,我们先看Service逻辑。这个是参考人人开源的,目前开源的项目还是蛮多的。
返回byte字节是方便给前端导出下载,,让人你要直接生成放到本地路径也可以

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
typescript复制代码import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipOutputStream;

/**
* 代码生成器
*/
@Service
public class SysGeneratorService {
@Autowired
private GeneratorDao generatorDao;

/**
* 通过表名称去生成代码,支持多表一起
* @param tableNames 表明集合
* @return
*/
public byte[] generatorCode(String[] tableNames) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zip = new ZipOutputStream(outputStream);
for(String tableName : tableNames){
//查询表信息
Map<String, String> table = queryTable(tableName);
//查询列信息
List<Map<String, String>> columns = queryColumns(tableName);
//生成代码
GenUtils.generatorCode(table, columns, zip);
}
IOUtils.closeQuietly(zip);
return outputStream.toByteArray();
}
/**
* 根据表名查询表相关信息
* @param tableName
* @return
*/
public Map<String, String> queryTable(String tableName) {
return generatorDao.queryTable(tableName);
}

/**
* 通过表名获取表的字段信息
* @param tableName
* @return
*/
public List<Map<String, String>> queryColumns(String tableName) {
return generatorDao.queryColumns(tableName);
}
}

代码生成

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
ini复制代码private static final String COLUMN_NAME = "columnName";
private static final String TABLE_NAME = "tableName";
private static final String TABLE_COMMENT = "tableComment";
private static final String TABLE_PREFIX = "tablePrefix";
private static final String DATA_TYPE = "dataType";
private static final String COLUMN_COMMENT = "columnComment";
private static final String EXTRA = "extra";
private static final String UNKNOW_TYPE = "unknowType";
private static final String BIGDECIMAL = "BigDecimal";
private static final String COLUMN_KEY = "columnKey";
private static final String PRI = "PRI";
/**
* 生成代码
*/
public static void generatorCode(Map<String, String> table,
List<Map<String, String>> columns, ZipOutputStream zip){
//配置信息
Configuration config = getConfig();
boolean hasBigDecimal = false;
//表信息
TableEntity tableEntity = new TableEntity();
tableEntity.setTableName(table.get(TABLE_NAME));
tableEntity.setComments(table.get(TABLE_COMMENT));
//表名转换成Java类名
String className = tableToJava(tableEntity.getTableName(), config.getString(TABLE_PREFIX));
tableEntity.setClassName(className);
tableEntity.setClassname(StringUtils.uncapitalize(className));

//列信息
List<ColumnEntity> columsList = new ArrayList<>();
for(Map<String, String> column : columns){
ColumnEntity entity = new ColumnEntity();
entity.setColumnName(column.get(COLUMN_NAME));
entity.setDataType(column.get(DATA_TYPE));
entity.setComments(column.get(COLUMN_COMMENT));
entity.setExtra(column.get(EXTRA));
String attrName = columnToJava(entity.getColumnName());
entity.setAttrName(attrName);
entity.setAttrname(StringUtils.uncapitalize(attrName));
String attrType = config.getString(entity.getDataType(), UNKNOW_TYPE);
entity.setAttrType(attrType);
if (!hasBigDecimal && attrType.equals(BIGDECIMAL)) {
hasBigDecimal = true;
}
//是否主键
if(PRI.equalsIgnoreCase(column.get(COLUMN_KEY)) && tableEntity.getPk() == null){
tableEntity.setPk(entity);
}
columsList.add(entity);
}
tableEntity.setColumns(columsList);

//没主键,则第一个字段为主键
if(tableEntity.getPk() == null){
tableEntity.setPk(tableEntity.getColumns().get(0));
}

总结

代码生成,有很多案例,没有哪个最好,先这样吧,感觉这个大家手头估计都有几个。

SaaS系统从0到1搭建,未完待续….

本文转载自: 掘金

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

SaaS系统从0到1搭建,06代码生成(模板篇) 前言 代码

发表于 2021-07-30

前言

车队管理类似的SaaS平台,从0到1,继续..

上一篇咱撸到租户入驻,已经是实际业务场景了,然后在设计下入驻的租户主要信息,已经租户的状态历史表。本篇本来是编写下关于SaaS计费规则的,但这块还是得按需求来设计,就先不要了。
那就从代码生成器,做个分享吧。

(我是后半夜Java,在掘金这分享下经验,那些靠copy的搬运作者,未经允许,不要copy文章了)

代码生成

程序猿子都知道,平白无期的copy代码是浪费时间的,一直CtrlC CtrlV 浪费头发,主要的精力应该用在业务逻辑上,架构完善上面,那么有一套代码生成的工具,就灰常的适合,或者已经就是程序猿的必备工具了。

场景

比如新增个业务表,那么表已经有了,如果咱用的是Mybatis 那么那些基础的SQL、表对应的Bean对象,CRUD的基础方法这些基本都是一样的,所以这类代码就是需要咱自动生成代码的工具来完成了。

做法

估计网上可以搜索出一片的代码生成工具,但从原理上或者思路上,去理解下,对每个程序猿都会有些成长值。

思路

表->获取字段,生成基本TableNameMapping.xml ,及对应的Bean对象,然后生成service、control那些

准备

先了解下Velocity,也就是*.vm 后缀的文件,Velocity是一个基于java的模板引擎(template engine)。它允许任何人仅仅简单的使用模板语言(templatelanguage)来引用由java代码定义的对象。

TableNameMapping.xml.vm

mapping文件

1
2
3
4
5
6
7
8
9
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${package}.${moduleName}.dao.${className}Dao">
<resultMap type="${package}.${moduleName}.entity.${className}Entity" id="${classname}Map">
#foreach($column in $columns)
<result property="${column.attrname}" column="${column.columnName}"/>
#end
</resultMap>
</mapper>

TableNameDao.java.vm

Dao内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package ${package}.${moduleName}.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import ${package}.${moduleName}.entity.${className}Entity;

/**
* ${comments}
* @author ${author}
* @date ${datetime}
*/
@Mapper
public interface ${className}Dao extends BaseMapper<${className}Entity> {

}

TableNameEntity.java.vm

Entity表对象,也有叫model的,反正一个意思,就是表对应的bean对象

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
java复制代码package ${package}.${moduleName}.entity;

import lombok.Data;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
#if(${hasBigDecimal})
import java.math.BigDecimal;
#end
import java.io.Serializable;
import java.util.Date;

/**
* 对象说明:${comments}
* @author ${author}
* @date ${datetime}
*/
@TableName("${tableName}")
@Data
public class ${className}Entity implements Serializable {
private static final long serialVersionUID = 1L;
#foreach ($column in $columns)
/**
* $column.comments
*/
#if($column.columnName == $pk.columnName)
@TableId
#end
private $column.attrType $column.attrname;
#end
}

TableNameService.java.vm

表对应的接口类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package ${package}.${moduleName}.service;

import com.baomidou.mybatisplus.extension.service.IService;
import ${mainPath}.common.utils.PageUtils;
import ${package}.${moduleName}.entity.${className}Entity;
import java.util.Map;

/**
* 接口描述:${comments}
* @author ${author}
* @date ${datetime}
*/
public interface ${className}Service extends IService<${className}Entity> {
/**
* 分页查询
* @param params
* @return
*/
PageUtils queryPage(Map<String, Object> params);
}

TableNameServiceImpl.java.vm

接口实现类

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复制代码package ${package}.${moduleName}.service.impl;

import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import ${mainPath}.common.utils.PageUtils;
import ${mainPath}.common.utils.Query;
import ${package}.${moduleName}.dao.${className}Dao;
import ${package}.${moduleName}.entity.${className}Entity;
import ${package}.${moduleName}.service.${className}Service;
/**
* 描述:${comments}
* @author ${author}
* @date ${datetime}
*/
public class ${className}ServiceImpl extends ServiceImpl<${className}Dao, ${className}Entity> implements ${className}Service {
/**
* 分页查询
* @param params
* @return
*/
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<${className}Entity> page = this.page(
new Query<${className}Entity>().getPage(params),
new QueryWrapper<${className}Entity>()
);
return new PageUtils(page);
}

}

TableNameController.java.vm

最后就是control层,一般的增删改查的入口:

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
kotlin复制代码package ${package}.${moduleName}.controller;

import java.util.Arrays;
import java.util.Map;
import io.renren.common.validator.ValidatorUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ${package}.${moduleName}.entity.${className}Entity;
import ${package}.${moduleName}.service.${className}Service;
import ${mainPath}.common.utils.PageUtils;
import ${mainPath}.common.utils.R;

/**
* 描述:${comments}
* @author ${author}
* @date ${datetime}
*/
@RestController
@RequestMapping("${moduleName}/${pathName}")
public class ${className}Controller {
@Autowired
private ${className}Service ${classname}Service;

/**
* 分页查询列表
*/
@RequestMapping("/list")
@RequiresPermissions("${moduleName}:${pathName}:list")
public R list(@RequestParam Map<String, Object> params){
PageUtils page = ${classname}Service.queryPage(params);
return R.ok().put("page", page);
}

/**
* 获取单个对象信息
*/
@RequestMapping("/info/{${pk.attrname}}")
@RequiresPermissions("${moduleName}:${pathName}:info")
public R info(@PathVariable("${pk.attrname}") ${pk.attrType} ${pk.attrname}){
${className}Entity ${classname} = ${classname}Service.getById(${pk.attrname});
return R.ok().put("${classname}", ${classname});
}

/**
* 新增
*/
@RequestMapping("/save")
@RequiresPermissions("${moduleName}:${pathName}:save")
public R save(@RequestBody ${className}Entity ${classname}){
${classname}Service.save(${classname});
return R.ok();
}

/**
* 编辑修改
*/
@RequestMapping("/update")
@RequiresPermissions("${moduleName}:${pathName}:update")
public R update(@RequestBody ${className}Entity ${classname}){
ValidatorUtils.validateEntity(${classname});
${classname}Service.updateById(${classname});
return R.ok();
}

/**
* 删除(物理删除)
*/
@RequestMapping("/delete")
@RequiresPermissions("${moduleName}:${pathName}:delete")
public R delete(@RequestBody ${pk.attrType}[] ${pk.attrname}s){
${classname}Service.removeByIds(Arrays.asList(${pk.attrname}s));
return R.ok();
}

}

总结

代码生成,有很多案例,这种可以自己修改模板的开源也有一些,这种模板的做法,是相对比较好的实现方式。模板分享,下篇我们继续生成代码逻辑。

SaaS系统从0到1搭建,下篇继续….

本文转载自: 掘金

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

Dapr 在阿里云原生的实践 什么是Service Mesh

发表于 2021-07-30

简介: Faas 场景下,比较吸引用户的是成本和研发效率,成本主要通过按需分配和极致的弹性效率来达成。而应用开发者期望通过 FaaS 提供多语言的编程环境,提升研发效率,包括启动时间、发布时间、开发的效率。​

作者|曹胜利

什么是Service Mesh?

从 2010 年的时候,SOA 架构就已经在中大型互联网公司中开始流行,阿里也在2012 年开源了 Dubbo 。而之后微服务架构开始流行,大量互联网和传统企业都投身到微服务的建设中。在国内逐渐形成了Dubbo 和 Spring Cloud 两大微服务阵营。在2016 年的时候,微服务领域一个更具有前沿性,更加符合容器和 Kubernetes 的微服务方案正在孕育,这个技术被称为 Service Mesh。时至今日,Service Mesh 理念已经得到了大范围普及,很多公司都在 Service Mesh 领域有了落地。

Service Mesh 定义

Service Mesh 是一个基础设施层,主要围绕服务间通信来进行。现在的云原生应用的服务拓扑结构非常复杂,Service Mesh 可以在这种复杂拓扑结构中实现可靠的请求传送。Service Mesh 是以 Sidecar 的方式运行,应用旁边会运行一个独立的 Service Mesh 进程,Service Mesh 负责远程服务的通信。军用三轮摩托车和 Service Mesh 非常相像,军用三轮摩托车上一个士兵负责开车,一个士兵负责对人发起射击。

Service Mesh 解决的痛点

传统的微服务架构大都以 RPC 通信框架为基础,在 RPC SDK 中提供了服务注册/发现,服务路由,负载均衡,全链路跟踪等能力。应用业务逻辑和 RPC SDK 在同一个进程中,这种方式给传统微服务架构带了很多挑战:中间件能力相关代码侵入到了业务代码中,耦合性很高;推动 RPC SDK 的升级成本非常高,进而也导致了 SDK 版本分化非常严重。同时这种方式对应用开发者的要求比较高,需要有丰富的服务治理的运维能力,有中间件的背景知识,使用中间件的门槛偏高。

通过 Service Mesh 方式将一些 RPC 的能力进行下沉,这样可以很好的实现关注点分离、职责边界的明确。随着容器和 Kubernetes 技术的发展,Service Mesh 已经成为云原生的基础设施。

Istio 介绍

在 Service Mesh 领域中, Istio 毫无疑问是当中的王者。Istio 由控制面和数据面构成,在 ServiceMesh 中,不同的 Service 之间,通过 Proxy Sidecar 进行通信。Istio 最核心功能是流量管理,通过数据面和控制面协调完成。Istio 是由 Google 联合IBM,Lyft 一起发起的,是 CNCF 生态版图 Service Mesh 领域的最纯正血统,有望成为Service Mesh事实标准。

Istio 的数据面默认使用 Envoy,Envoy 是社区里默认的最佳数据面。Istio 数据面和控制面的交互协议是 xDS。

Service Mesh 小结

最后,对 Service Mesh 做下小结:

  • Service Mesh 定位就是提供服务间通信的基础设施,社区里主要支持 RPC 和http 。
  • 采用 Sidecar 方式部署,支持部署在 Kubernetes 和虚拟机之上。
  • Service Mesh 采用原协议转发,所以 Service Mesh 也被称为网络代理。正是由于这种方式方式,所以可以做到对应用的零侵入。

什么是Dapr?

Service Mesh 遇到的挑战

用户在云上部署业务的形态主要有普通应用类型和FaaS类型。Faas 场景下,比较吸引用户的是成本和研发效率,成本主要通过按需分配和极致的弹性效率来达成。而应用开发者期望通过 FaaS 提供多语言的编程环境,提升研发效率,包括启动时间、发布时间、开发的效率。

Service Mesh 的实现,本质是原协议转发,原协议转发可以给应用带来零侵入的优势。但是原协议转发也带来了一些问题,应用侧中间件SDK还需要去实现序列化和编解码工作,所以在多语言实现方面还有一定成本;随着开源技术的不断发展,使用的技术也在不断迭代,如果想从 Spring Cloud 迁移到 Dubbo ,要么应用开发者需要切换依赖的 SDK,如果想借助Service Mesh来达到这个效果,Service Mesh 需要进行协议转换,成本较高。

Service Mesh 更加聚焦于服务间的通讯,而对其他形态的 Mesh 的支持上非常少。比如 Envoy, 除了在 RPC 领域比较成功外,在 Redis、消息等领域的尝试都未见成效。蚂蚁的 Mosn 中支持了 RPC 和消息的集成。整体多 Mesh 形态的需求是存在的,但是各个 Mesh 产品各自发展,缺少抽象和标准。如此多形态的 Mesh ,是共用一个进程吗?如果是共用一个进程,那么是共用一个端口吗?许多问题都没有答案。而控制面方面,从功能角度来看的话,大都围绕流量来展开。看过 xDS 协议里的内容,核心是围绕发现服务和路由来展开。其他类型的分布式能力,在 Service Mesh的控制面中基本没有涉及,更谈不上抽象各种类似 xDS 的协议去支持这些分布式能力。

因为成本和研发效率等原因,FaaS 受到了越来越多客户的选择,FaaS 对多语言和编程 API 的友好性上有了更多诉求,那么 Service Mesh 在这两块还是不能给客户带来额外的的价值。

分布式应用的需求

Bilgin Ibryam 是 Kubernetes Patterns 的作者,是 RedHat 的首席中间件架构师,在 Apache 社区里非常活跃。他发表了一篇文章对当前分布式的一些困难和问题进行了抽象,将分布式应用需求分成了 4 个大种类:生命周期、网络、状态、绑定。每种类型下面还有一些子能力,如 Point-to-Point, pub/sub, Caching 等比较经典的中间件能力。应用对分布式能力有如此多的需求,而 Service Mesh 显然不能满足应用的当前的需求。Biligin Ibryam 还在文章中提出了 Multiple Runtime 的理念来解决Service Mesh 的困境。

Multiple Runtime 理念推导

在传统的中间件模式下,应用和分布式能力是在一个进程中,以 SDK 方式进行集成。随着各种基础设施下沉,各种分布式能力从应用中移到了应用外。如 K8s 负责了生命周期相关的需求,Istio、Knative 等都负责一些分布式能力。如果将这些能力都移动到独立的 Runtime 中,那么这种情况无论从运维层面还是资源层面来看,都是没办法接受的。所以这时候肯定需要将部分 Runtime 进行整合,最理想的方式肯定是整合成一个。这种方式被定义成 Mecha ,中文意思是机甲的意思,就像日本动漫里主人公变身穿上机甲,机甲的每个部件就像一个分布式能力,机甲里的人对应的是主应用,也叫 Micrologic Runtime 。 这两个 Runtime 可以是一对一的 Sidecar 方式,这种非常适合传统的应用;也可以是多对一的 Node 模式,适合边缘场景或者网管模式下。

那么对于将各种分布式能力进行整合的 Mecha Runtime 这一目标本身问题不大,那么怎么整合呢?对 Mecha 有什么要求呢?

  1. Mecha 的组件能力是抽象的,任何一个开源产品可以快速进行扩展和集成。
  2. Mecha 需要有一定的可配置能力,可以通过 yaml/json 进行配置和激活。这些文件格式最好能和主流的云原生方式对齐。
  3. Mecha 提供标准的 API ,和主应用之间的交互的网络通信基于此 API 来完成,不再是原协议转发,这样对于组件扩展和 SDK 的维护都能带来极大的便利性。
  4. 分布式能力中的生命周期,可以将部分能力交接过底层的基础设施,比如 K8s。当然有些复杂的场景,可能需要 K8s、APP、Mecha Runtime 一起来完成。

既然最理想只剩下一个 Runtime , 那么为什么还叫 Multiple Runtime 呢?因为应用本身其实也是一个 Runtime ,再加上 Mecha Runtime ,所以至少是两个 Runtime 。

Dapr 介绍

前面的 Multiple Runtime 介绍地比较抽象,可以来从 Dapr 来重新理解下 Multiple Runtime 。Dapr 是 Multiple Runtime 的一个很好的践行者,所以 Dapr 肯定和应用共存的,要么是 Sidecar 模式,要么是 Node 模式。Dapr 这个词其实是不是造出来的,而是 Distributed Application Runtime 的首字母拼接而成,Dapr 这个图标可以看出来是一个帽子,这个帽子其实是一个服务生的帽子,表示的含义是要为应用做好服务。

Dapr 是由微软开源的,阿里巴巴深度参与合作。当前的 Dapr 已经发布 1.1 版本,现在已经接近生产的能力。

既然 Dapr 是 Multiple 的最佳实践者,那么 Dapr 的运行机制也是基于 Mulitple Runtime 的理念来构建的。Dapr 对分布式能力进行了抽象,定义了一套分布式能力的 API,而且这些 API 是基于 Http 和 gRPC 来构建的,这种抽象和能力在 Dapr 中被称为 Building Block;Dapr 为了支持开源产品和商业化等不同类型的产品对 Dapr中的分布式能力进行扩展,内部拥有一套 SPI 扩展机制,这种 SPI 机制叫 Components 。应用开发者在使用 Dapr 之后,只需要针对各种分布式能力的 API 来进行编程,而无需过多关注具体的实现,而 Dapr 中根据 Yaml 文件可以自由激活对应的组件。

Dapr 特性

应用开发者使用各种多语言的 Dapr SDK 就可以直接拥有各种分布式能力。当然开发者也可以自己基于 HTTP 和 gRPC 来完成调用。Dapr 可以运行在大部分环境里,包括你自己电脑的环境,或者任何 Kubernetes 环境下,或者边缘计算场景,或者阿里云、AWS、GCP 等云厂商。

Dapr 社区里已经集成了 70+ 的 components 实现,应用开发者可以快速进行选择和使用。相似能力的组件的替换,可以通过 Dapr 里完成,应用侧可以做到无感知。

Dapr 核心模块

我们从 Dapr 产品模块纬度来解析下,看为什么 Dapr 是 Mulitiple Runtime 的一个很好实践。

Component 机制确保了可以快速扩展能力的实现,现在社区已经有的 Components实现已经有 70 个以上,不只包含开源产品,还包含云上的商业化产品。

Building Block 表示的的分布式能力,现在只支持 7 个,后续需要更多的分布式能力能够进来。BuildingBlock 现在支持了 HTTP 和 gRPC 这两种开放,而且普及度已经非常高的协议。而 Dapr 中 Building Block 下具体那些 Components 会被激活,需要依赖 YAML 文件来进行。正因为 Dapr 中采用了 HTTP、gRPC 的方式暴露能力,所以在应用侧想要支持多语言的标准的API编程界面就变得更为容易了。

Dapr 核心:Component & Building Block

Dapr Component 是 Dapr 插件扩展的核心,是 Dapr 的 SPI 。现在支持的 Components 有 Bindings 、Pub/Sub、Middleware、ServiceDiscovery、Secret Stores、State。扩展点里有些是功能纬度的如Bindings,pub/sub,state 等,有些是横向的如 Middleware。假设你想实现Redis的Dapr集成,你只需要去实现 Dapr 的State Component。Dapr Building Block是Dapr提供出来的能力,支持 gRPC 和 HTTP 方式。现在支持的能力有 Service Invocation,State,Pub/Sub 等。

一个 Building Block 由 1 个或多个 Component 组成,Binding的Building Block 包含 Bindings 和 Middleware 两个 Component 。

Dapr 整体架构

Dapr 和 Istio 一样,也有数据面和控制面。控制面有 Actor Placement,Sidecar Injector, Sentry, OPerator。Actor Placement 主要为 Actor 服务,Sentry 做安全和证书相关的工作,Sidecar Injector 主要负责 Dapr Sidecar 的注入。Dapr 里激活某个组件实现是通过 YAML 文件来完成的,YAML 文件可以通过两种方式来指定:一种是本地指定运行时参数,另外一种是通过控制平面 Operator 来完成,将组件激活的文件以 K8s CRD 方式存储并下发到 Dapr的Sidecar 中。控制面的 2 个核心组件都依赖于 K8s 来运行。现在的 Dapr Dashboard 功能还很弱,短期还不到增强的方向,现在各个组件的集成之后,各个组件的运维还需要在原来的控制台里完成,Dapr 控制平面不参与具体组件实现的运维。

Dapr 标准运行形式是和应用在同一个 Pod 中,但分属于两个容器。Dapr 的其他内容,前面已经做了足够的介绍,这里不做介绍了。

Dapr 微软落地场景

Dapr 经历了 2 年左右的发展,在微软内部的落地情况是怎么样的呢?

Dapr 的 github 上有两个项目:workflows 和 Azure Functions Dapr extensions。Azure Logic App 是微软的一个基于云上的自动工作流平台。而 Workflows,就是整合了 Azure Logic App 和 Dapr。Azure Logic App 中有几个关键的概念,Trigger 和 Connector 和 Dapr 非常契合。Trigger 可以使用 Dapr 的 Input Binding 来完成,依赖 Dapr 的 Input Binding 的大量组件实现,可以扩大流量入口的类型。而 Connector 和 Dapr 的 Output Binding 或者 Service Invocation 的能力非常匹配,可以快速访问外部资源。Azure Functions Dapr extensions 则是基于Azure Function extension 做的 Dapr 支持,可以让 Azure Function 快速使用上Dapr 的各种 Building Block 的能力,同时能给函数开发者带来多语言的相对简单一致的编程体验。

Azure API Management Service和上面提到的两个落地场景的角度不太一致,它是前提是应用之间已经通过Dapr Sidecar方式进行访问,应用的提供的服务通过Dapr来进行暴露。这时候如果非K8s的应用或者跨集群的应用想要访问当前集群的服务,就需要一个网关,这个网关可以直接暴露Dapr的能力,在网关中会增加一些安全和权限的控制。当前支持3种Building Block:Service Invocation、pub/sub、resource Bindings。

Dapr 小结

Dapr 提供的面向能力的 API ,能够给开发者带来支持多语言的一致的编程体验,同时这些 API 的SDK相对比较轻量级。这些特性非常适合 FaaS 场景。而随着 Dapr 集成生态的不断完善,开发者面向能力编程的优势将进一步扩大,通过 Dapr 可以更加方便地将 Dapr 组件的实现进行替换,而无需开发者做代码的调整。当然原来的组件和新的组件实现,必须是相同类型的分布式能力。

和 Service Mesh 差异点:

提供能力:Service Mesh 专注服务调用;Dapr 提供的分布式能力范围更广,覆盖多种分布式原语。

工作原理:Service Mesh 采用原协议转发做到零侵入;Dapr 采用多语言SDK + 标准API + 各种分布式能力。

面向领域:Service Mesh 对传统微服务的无侵入升级支持很友好;Dapr 对面向应用的开发者提供了更加友好的编程体验。

阿里在 Dapr 上的探索

阿里在 Dapr 的发展路线

2019 年 10 月,微软开源了 Dapr,发布了 0.1.0 的版本。这时候,阿里和微软正好因为 OAM 已经展开一些合作,了解到了 Dapr 这个项目,所以就开始对其进行评估。在 2020 年初的时候,阿里和微软在阿里巴巴线下做了一轮 Dapr 的沟通,了解到了微软对 Dapr 的看法、投入,以及后续的发展计划。此时阿里已经认定 Dapr 这个项目具有较大的价值。一直到 2020 年中,才开始围绕 Dapr 开始投入工作。到 10 月份,Dapr 在函数计算场景下开始线上灰度部分功能,到今天为止,函数计算相关的 Dapr 的所有功能的灰度已经基本完成,开始开放公测。到 2021 年 2 月份,终于发布了 1.0 版本。

阿里云函数计算集成 Dapr

除了极致弹性等运维侧的好处之外,函数计算区别于中台应用的地方还在于,函数计算更加关注能够给开发者带来更好的研发体验,提升整体的研发效率。而 Dapr 能够给函数计算的价值就是提供多语言的统一的面向能力的编程界面,而开发者无需关注具体的产品。像 Java 语言如果要使用阿里云上的 OSS 服务,需要引入 maven 依赖,同时需要写一些 OSS 代码,而通过 Dapr 你只需要调用 Dapr SDK 的 Binding 方法即可以做到,方便编程的同时,整个可运行包也无需引入多余的依赖包,而是可控的。

函数计算英文名是 Function Compute,简称为 FC。FC 的架构包含的系统比较多,和开发者相关的主要包括 Function Compute Gateway和函数运行的环境。FC Gateway主要负责承接流量,同时会根据承接的流量的大小,当前的 CPU、内存使用情况,对当前函数实例进行扩缩容。函数计算运行时环境部署在一个 Pod 中,函数实例在主容器中,dapr 则是在 sidecar 容器中。当有外部流量访问函数计算的服务时,流量会先走到 Gateway ,Gateway 会根据访问的内容将流量转发到提供当前服务的函数实例中,函数实例接收到请求之后如果需要访问外部资源,就可以通过Dapr 的多语言 SDK 来发起调用。这时候 SDK 会向 Dapr实例发起gRPC请求,而在dapr 实例中回根据请求的类型和 body 体,选择对应的能力和组件实现,进而向外部资源发起调用。

在 Service Mesh 场景下,Mesh 以 Sidecar 形式存在,和应用部署在同一个 Pod 的两个容器里,可以很好满足 Service Mesh 的需求。但是在函数计算场景下,Dapr作为独立容器方式运行过于消耗资源,而且多个函数实例本身部署在一个 Pod 中以便节省资源开支和秒级弹性。所以在函数计算场景下,需要将函数实例和Dapr进程部署在同一个容器下,但是以两个进程方式存在。

函数计算场景下,可以设置预留实例数,表示当前函数最小实例数。如果有预留的实例,但是这些实例长久没有流量访问需要进入暂停/休眠状态,这种方式和 AWS 的方式是一致的。进入休眠状态的函数,实例内的进程或者线程需要停止运行。函数运行时中增加了 Extension 结构,来支持 Dapr 生命周期的调度。当函数实例进入休眠状态时,extension 通知 Dapr 进入休眠状态;当函数实例恢复运行之后,extension 通知 Dapr 重新恢复之前运行的状态。Dapr 内部的组件实现需要能支持这种方式的生命周期管理,以 Dubbo 为例,Dubbo 的注册中心 nacos 需要定时往 Nacos server 发送心跳保持了解,同时 Dapr 集成的Dubbo Consumer也需要往Dubbo Provider 发送心跳。当进入暂态之后,心跳都需要退出;当恢复运行之后,整个运行状态需要恢复。

上面讲到的函数计算和 Dapr 结合的点,都是基于对外的流量,那么流入的流量呢?消息的流量是否可以直接流入到 Dapr ,而无需经过 Gateway 呢?要做到这一点,还需要在 Dapr Sidecar 将一些性能数据及时上报给 Gateway ,方便 Gateway 可以做到资源的弹性。

SasS 业务上云

随着阿里内部孵化的SaaS业务越来越多,SaaS业务对外服务的诉求非常强烈。而SaaS业务对多云部署的诉求非常强烈,客户期望SaaS业务能部署在阿里云公有云或者华为专有云上。而且客户期望底层依赖的技术是开源的或者标准的云厂商的商业化产品。

以阿里一个SaaS业务上云来说明,左侧是阿里内部原来的系统,右侧是改造之后的系统,改造的目标是将依赖的阿里内部的系统切换成开源软件,Ali RPC切换到Dubbo,而阿里内部的Cache,Message,Config分别切换到Redis、RocketMq和Nacos。期望通过Dapr来实现最小代价的切换。

既然想用Dapr来完成这个使命,那么最简单粗暴的方法肯定是让应用依赖Dapr的SDK,但是这种方式改造成本太高,所以我们在保持原来API不变的情况下,将底层实现适配到Dapr SDK。通过这种方式,应用就可以直接使用原来的API访问Dapr,只需要升级对应的依赖JAR包版本。改造之后,开发者还是面向原来的SDK进行编程,但是底层已经被替换成了Dapr的面向能力编程,所以在迁移过程中,应用可以使用一套代码,而无需为每个云环境或者不同技术维护不同的分支。集团内部用Dapr Sidecar的时候,会使用rpc.yaml、cache.yaml、msg.yaml、config.yaml来激活组件实现,而在公有云上回通过dubbo.yaml、redis.yaml、rocketmq.yaml、nacos.yaml文件来激活适合阿里云环境的组件实现。这种通过不同yaml文件激活不同组件来屏蔽组件实现的方式给SaaS业务多云部署形态带来了极大的便利。

钉钉是Dapr的重要合作伙伴和推动者,和云原生团队合作推进Dapr在钉钉落地。通过将一些中间件能力下沉到Dapr Sidecar之后,屏蔽了底层相似能力的中间件实现。但是钉钉还有自己的业务痛点,钉钉通用的业务组件是强业务绑定,需要具体的业务进行一些定制,这样同时导致了复用度很低,所以钉钉期望通过将部分业务组件能力下沉到Dapr。这样可以让不同业务有相同的编程体验,而组件维护者只需要维护好Components实现。

Dapr展望

基础设施下沉成为软件发展趋势

软件架构的发展历史及其精彩。回顾阿里巴巴系统架构演进的历史,能让人了解国内甚至全球的软件架构的发展历史。淘宝最开始成立的时候,是单体应用;随着业务规模的发展,系统首先对硬件进行升级这种Scale Up的方式;但是很快发现这种方式遇到了各种各样的问题,所以在2008年开始引入了微服务的解决方案;SOA的解决方案是分布式的,对于稳定性,可观测性等方面,需要引入熔断、隔离、全链路监控等高可用方案;接下来面临的问题是怎么在机房、IDC层面来让业务达到99.99%以上可用的SLA,这时候就有了同城双机房、异地多活等解决方案。而随着云技术的不断发展,阿里巴巴拥抱和引导云原生技术的发展,积极拥抱云原生技术,以K8s为基础,积极开展云原生技术的升级。

从这个历史中,我们可以发现,软件架构新的诉求越来越多,原先底层基础设施无法完成只能交给应用侧富SDK去完成,在K8s和容器逐渐成为标准之后,重新将微服务和一些分布式能力还给基础设施。未来的趋势是以Service Mesh和Dapr为代表的分布式能力下沉,释放云和云原生技术发展的红利。

云原生场景下的应用开发者的诉求

未来的应用开发者,应该期望能够面向能力,无言无关,不和具体云厂商和技术绑定的开发体验,同时通过云技术的红利能够做到极致的弹性带来的成本优势。我相信这个理想还是有可能实现的一天的,从当前的角度出发,怎么样才能完成这个目标呢?

  1. Multiple Runtime理念能够真正落地,并且能够持续发展;
  2. 以Dapr为例,期望能将Dapr面向分布式能力的API推动成为一个行业标准,并且这个标准是需要持续发展的;
  3. K8s和Serverless技术的持续发展,未来可以将弹性做到极致。

Dapr 社区方向

最后来看下Dapr的社区发展:

1.推动 API 标准化,集成更多分布式能力;

2.更多Component集成,Dapr 生态完善;

3.更多公司落地,拓展产品边界和打磨 Dapr 产品, 达到生产可用;

4.进入 CNCF, 成员云原生的 Multiple Runtime 的事实标准。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

Elasticsearch-核心篇(13)-Logstash

发表于 2021-07-30

一、日志文件采集

  1. 参考文档
    • 文件采集:www.elastic.co/guide/en/lo…
    • elasticsearch输出:www.elastic.co/guide/en/lo…
  2. 可以从日志文件中采集数据
  3. 自定义配置文件logstash.conf配置文件,配置文件采集信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
nginx复制代码input {
# 从文件读取日志信息
file{
path => "日志文件路径"
codec => plain {
charset => "UTF-8"
}
}
}

output {
# 输出到es
elasticsearch {
hosts => ["127.0.0.1:9200"]
index => "logstash"
}
}

二、MySQL数据采集

  1. 可以将MySQL数据导入到ES中,时间es检索的是数据库中的数据信息
  2. 导入mysql的驱动包:mysql-connector-java-8.0.20.jar,并确定数据库服务可用
    • 将jar包上传到**/opt/es/logstash/logstash-7.8.0/logstash-core/lib/jars**
  3. 在logstash目录下修改配置文件内容如下
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
erlang复制代码input {
# 从数据库中读取数据
jdbc {
# 数据库驱动地址
jdbc_driver_library => "./repository/mysql-connector-java-5.1.47.jar"
# 驱动包名称
jdbc_driver_class => "com.mysql.jdbc.Driver"
# 连接地址
jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/logstash"
# 数据库用户名
jdbc_user => "root"
# 数据库密码
jdbc_password => "tianxin"
# 获取数据SQL
statement => "select * from logstash where id > :sql_last_value"
# 可以将SQL定义到文件中
#statement_filepath => "./config/jdbc.sql"
# 同步频率(分 时 天 月 年),默认一分钟执行一次
schedule => "*/1 * * * *"
# 记录上次执行结果,将会保存到last_run_metadata_path配置文件中,不建议配置
# 如果配置为true将会导致磁盘受到两倍IO压力
#record_last_run => false
# 数据库自增字段,用来增量记录,通常是主键
use_column_value => true
# 查询字段类型,此处为数值,时间戳为timestamp
tracking_column_type => "numeric"
# 查询条件的字段
tracking_column => "id"
# 记录最后一次查询的id,初始数值默认0,时间戳为1970后的时间戳
last_run_metadata_path => "./repository/last_run_metadata.txt"
# 是否清除last_run_metadata_path值,如果为true,表示每次都做全量同步
clean_rum => false
}
}

output {
# 输出到es
elasticsearch {
hosts => ["127.0.0.1:9200"]
index => "logstash"
# 文档id与数据库id字段关联
document_id => "%{id}"
# 使用模板,可以提升效率
template_overwrite => true
}
}

三、TCP数据采集

  1. 可以采集TCP端口数据信息
    • 参考文档:www.elastic.co/guide/en/lo…
  2. 在logstash目录下修改配置文件内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nginx复制代码input {
# 监听TCP端口信息
tcp {
port => 4560
codec => json_lines
}
}

output {
# 输出到es
elasticsearch {
hosts => ["127.0.0.1:9200"]
index => "logstash"
}
}

本文转载自: 掘金

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

手把手教你做一个Java贪吃蛇小游戏的exe应用程序—就是玩

发表于 2021-07-29

做一个Java贪吃蛇小游戏的exe应用程序

大家好,我是孙不坚1208,今天给大家分享一下:如何做一个贪吃蛇小游戏的exe应用程序,希望能给需要的朋友带来方便,同时也希望能得到大家的关注与支持。
在这里插入图片描述

思路:

首先你要有一条贪吃蛇,哎,就是玩儿。

1.先用Java敲一个贪吃蛇小游戏的程序。

2.把Java程序打包成jar文件包,把jar包打包成exe应用程序

知道思路以后,接下来按照我的详细步骤走,能少踩坑,别问我怎么知道的!!!

一、Java“敲一个”贪吃蛇小游戏

废话不多说,本篇主要讲从java项目到exe应用程序,Java项目的代码在我上传的资源当中——代码-greedy snack.zip
在这里插入图片描述

二、将项目打成jar包

如何打包成jar包?

1.手动打可直接执行的jar包(本次不使用)

…….

2. 使用intellij idea工具打可直接执行的jar包

1) 打开项目
2)点击 File — 选择 Project Structure,找到“Artifacts” 点击 “+” 选择“JAR” —“Empty”。

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

3) Name栏可以填入自定义的名字,Output ditectory选择jar包生成目标目录,Available Elements里双击需要添加到jar包的文件,即可添加到左边的jar包目录,如图:

在这里插入图片描述

4)点击Create Manifest,选择放置MANIFEST.MF的文件路径,直接默认项目根目录就行,点击OK。

在这里插入图片描述

5) 点击Main Class后面选择按钮, 弹出框中选择需要运行程序入口main函数,点击OK,点击OK。

在这里插入图片描述

6) 点击菜单中“Build” -> “Build Artifacts”, 双击弹出框中待生成jar包下面的build即可,如下图

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

7)至此使用Intellij idea生成可直接执行jar包就完成了。
8)查看生成的jar包,并运行jar包。

​ 命令行运行jar包

1
复制代码java -jar xxx.jar

三、打包成为exe应用程序

这里首先你要有:

  • 上面生成的jar包 greedy snack.jar
  • 工具:exe4j,一个将jar转换成exe的工具

链接:pan.baidu.com/s/1FlgsxJZ5…
提取码:ijdv

1)安装exe4j,打开安装好的exe4j,进行注册

注册码:L-g782dn2d-1f1yqxx1rv1sqd

在这里插入图片描述

2) 点击 Next, 选择JAVA转EXE

在这里插入图片描述

3) Next,填入自定义的名字和生成目标文件目录

在这里插入图片描述

4) 继续Next,选择启动模式、设置程序名称和程序图标

在这里插入图片描述

5) 下方有个选项,需要设置打包后的程序兼容32和64位系统。

在这里插入图片描述

勾选,然后Next。

在这里插入图片描述

6)然后Next,直到出现如下界面,开始选择jar包以及配置

在VM参数配置的地方加上:-Dfile.encoding=utf-8

在这里插入图片描述

添加jar包

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

设置启动类

在这里插入图片描述

Next 配置JRE

在这里插入图片描述

在这里插入图片描述

Next 选择 Client VM

在这里插入图片描述

7)Next 直到如下图界面

在这里插入图片描述

桌面多了一个贪吃蛇小游戏3.0.exe文件,至此我们就成功的做出了一个Java贪吃蛇小游戏的exe应用程序

在这里插入图片描述

在这里插入图片描述

做一个贪吃蛇小游戏exe应用程序儿,哎,就是玩儿,给你们玩!

本文转载自: 掘金

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

rabbitMq高级(ttl过期时间,死信队列,延时队列)一

发表于 2021-07-29

一、过期时间TTL

1、设置队列TTL

过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置。

  • 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
  • 第二种方法是对消息进行单独设置,每条消息TTL可以不同。

如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Component
public class MyRabbitmqConfig {
//创建队列(my_ttl_queue),投递到该队列的消息如果没有消费都将在6秒之后被删除
@Bean
public Queue my_ttl_queue(){
return QueueBuilder.durable("my_ttl_queue")
.ttl(5000)
.build();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitmqStudy2ApplicationTests {

@Autowired
RabbitTemplate rabbitTemplate;
/**
* 过期队列消息
* 投递到该队列的消息如果没有消费都将在6秒之后被删除
*/
@Test
public void ttlQueueTest(){
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", "发送到过期队列my_ttl_queue,5秒内不消费则不能再被消费。");
}

}

效果如下(红线):
在这里插入图片描述
等待5秒过后,消息过期:
在这里插入图片描述

2、设置消息TTL

为了让效果更加直观,并证明之前说的消息的过期时间以队列ttl和消息ttl两者之间TTL较小的那个数值为准,我们登录客户端删除之前的队列,然后重新配置:

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Component
public class MyRabbitmqConfig {

@Bean
public Queue my_ttl_queue(){
return QueueBuilder.durable("my_ttl_queue")
.ttl(500000)
.build();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码     /**
* 过期消息
* 该消息投递任何交换机或队列中的时候;如果到了过期时间则将从该队列中删除
*/
@Test
public void ttlMessageTest(){
MessageProperties messageProperties = new MessageProperties();
//设置消息的过期时间,30秒
messageProperties.setExpiration("30000");//字符串非负数字
Message message = new Message("测试过期消息,30秒钟过期".getBytes(), messageProperties);
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", message);
}

可以清楚看到,我们预设的在队列ttl为500秒,消息为30秒,由客户端可以清楚看到,消息在队列中躺了30秒,故可以证明前面的两者之间取TTL较小。
在这里插入图片描述

这里还有个思考:

我们也可以在配置中不设置队列的ttl,只设置消息的ttl,这样就能达到跟定时器一样的效果,确实可以这样,因为队列不设置ttl的话,表示在队列中永远存在,通过设置不同消息ttl达到定时消费,但是也可能存在一些情况导致消息不断堆积在队列中消费不了,所以我们是不是也可以设置一个保证业务不受影响的尽可能大的队列ttl?

二、死信队列

DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。

消息变成死信,可能是由于以下的原因:

  • 消息被拒绝
  • 消息过期
  • 队列达到最大长度

DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。

要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange 指定交换机即可。

配置如下(专门画了张图,方便理解)
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
java复制代码    //定义定向交换机中的持久化死信队列
@Bean
public Queue my_dlx_queue(){
return QueueBuilder.durable("my_dlx_queue")
.build();
}

//定义广播类型交换机
@Bean
public DirectExchange my_dlx_exchange(){
return ExchangeBuilder.directExchange("my_dlx_exchange").build();
}

@Bean
public Binding dlx_exchange_to_queue(@Qualifier("my_dlx_queue")Queue my_dlx_queue,
@Qualifier("my_dlx_exchange")DirectExchange my_dlx_exchange){
//绑定路由键my_ttl_dlx,可以将过期的消息转移到my_dlx_queue队列
return BindingBuilder.bind(my_dlx_queue).to(my_dlx_exchange).with("my_ttl_dlx");
}

//定义过期队列及其属性
@Bean
public Queue my_ttl_dlx_queue(){
return QueueBuilder.durable("my_ttl_dlx_queue")
.ttl(6000)//投递到该队列的消息如果没有消费都将在6秒之后被投递到死信交换机
.deadLetterExchange("my_dlx_exchange")//设置当消息过期后投递到对应的死信交换机
.build();
}

//定义定向交换机 根据不同的路由key投递消息
@Bean
public DirectExchange my_normal_exchange(){
return ExchangeBuilder.directExchange("my_normal_exchange").build();
}

@Bean
public Binding normal_exchange_to_queue(@Qualifier("my_ttl_dlx_queue")Queue my_ttl_dlx_queue,
@Qualifier("my_normal_exchange")DirectExchange my_normal_exchange){
//绑定路由键my_ttl_dlx,可以将过期的消息转移到my_dlx_queue队列
return BindingBuilder.bind(my_ttl_dlx_queue).to(my_normal_exchange).with("my_ttl_dlx");
}
1
2
3
4
5
6
7
8
java复制代码    /**
* 过期消息投递到死信队列
* 投递到一个正常的队列,但是该队列有设置过期时间,到过期时间之后消息会被投递到死信交换机(队列)
*/
@Test
public void dlxTTLMessageTest(){
rabbitTemplate.convertAndSend("my_normal_exchange", "my_ttl_dlx", "测试过期消息;6秒过期后会被投递到死信交换机");
}

实例结果也很明显,

1、现在my_ttl_dlx_queue停留了6秒
在这里插入图片描述
2、成为死信,路由到my_dlx_queue
在这里插入图片描述

下一篇,将介绍一下自动确认机制,以及mq在生产应用中会遇到的问题及解决方案

本文转载自: 掘金

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

JVM 调优神器 arthas

发表于 2021-07-29

一、安装

arthas在github上有个page,地址是alibaba.github.io/arthas/。

安装的方式有好几种:

  1. 直接下载一个可以启动的jar包然后用java -jar的方式启动
  2. 用官方提供的as.sh脚本一键安装
  3. 用rpm的方式安装

本篇介绍第一种方式,因为它简单而且想迁移的时候也超级方便(毕竟只需要把下载的jar包拷贝走就行了)。

1
c复制代码curl -O https://alibaba.github.io/arthas/arthas-boot.jar

如果下载速度太慢,可以用gitee上的源

1
c复制代码curl -O https://arthas.gitee.io/arthas-boot.jar

curl命令直接把arthas-boot.jar下载到你想要的目录

1
2
c复制代码[root@localhost ~]# ll -lrt
-rw-r--r--. 1 root root 138880 Jun 22 02:55 arthas-boot.jar

二、启动

用java命令直接启动

1
2
3
4
c复制代码[root@localhost ~]# java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.3.3
[INFO] Can not find java process. Try to pass <pid> in command line.
Please select an available pid.

但是这里启动失败了,这是因为arthas在启动时会检测本机运行的jvm进程,然后让用户选择需要绑定的进程,后面的操作都是针对选定的进程的。

这里我先启动一个java应用,然后再启动arthas。

1
2
3
4
c复制代码[root@localhost ~]# java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.3.3
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1\. Then hit ENTER.
* [1]: 2467 jvm-0.0.1-SNAPSHOT.jar

下面就列出了本机正在运行的java进程,等待用户输入,这里输入1然后回车。如果是第一次启动需要下载一些必要的文件,等待下载完成即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码[root@localhost arthas]# java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.3.3
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1\. Then hit ENTER.
* [1]: 2467 jvm-0.0.1-SNAPSHOT.jar
1
[INFO] arthas home: /usr/local/arthas
[INFO] Try to attach process 2467
[INFO] Attach process 2467 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'

wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.3.3
pid 2467
time 2020-06-22 03:02:31

[arthas@2467]$

如果看到这个界面就表示启动并关联成功了。

三、help命令

在arthas交互环境中,可以输入help命令,然后会出现所有arthas支持的命令

1
2
3
4
5
6
7
8
9
10
c复制代码[arthas@2467]$ help
NAME DESCRIPTION
help Display Arthas Help
keymap Display all the available keymap for the specified connection.
sc Search all the classes loaded by JVM
sm Search the method of classes loaded by JVM
classloader Show classloader info
jad Decompile class
getstatic Show the static field of a class
...

如果不知道命令的用法,可以输入相应的命令后加参数–help,比如可以看一下sc命令的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
c复制代码[arthas@2467]$ sc --help
USAGE:
sc [-c <value>] [-d] [-x <value>] [-f] [-h] [-E] class-pattern
SUMMARY:
Search all the classes loaded by JVM
EXAMPLES:
sc -d org.apache.commons.lang.StringUtils
sc -d org/apache/commons/lang/StringUtils
sc -d *StringUtils
sc -d -f org.apache.commons.lang.StringUtils
sc -E org\\.apache\\.commons\\.lang\\.StringUtils

WIKI:
https://alibaba.github.io/arthas/sc
OPTIONS:
-c, --classloader <value> The hash code of the special class's classLoader
-d, --details Display the details of class
-x, --expand <value> Expand level of object (0 by default)
-f, --field Display all the member variables
-h, --help this help
-E, --regex Enable regular expression to match (wildcard matching by default)
<class-pattern> Class name pattern, use either '.' or '/' as separator

不仅会显示出命令是干嘛用的,命令的完整参数,还很贴心地提供了一些具体的例子,如果英语看不习惯,还可以到WIKI下面那个地址看官方文档,有中文版的。

四、用arthas解决上一篇的问题

(1)cpu占用过高

用thread命令列出线程的信息

1
2
3
4
5
6
7
8
9
10
c复制代码[arthas@2467]$ thread
Threads Total: 28, NEW: 0, RUNNABLE: 11, BLOCKED: 0, WAITING: 14, TIMED_WAITING: 3, TERMINATED: 0
ID NAME GROUP PRIORITY STATE %CPU TIME INTERRUPTE DAEMON
16 http-nio-8080-exec-2 main 5 RUNNABLE 99 0:25 false true
29 Attach Listener system 9 RUNNABLE 0 0:0 false true
11 Catalina-utility-1 main 1 WAITING 0 0:0 false false
12 Catalina-utility-2 main 1 TIMED_WAIT 0 0:0 false false
28 DestroyJavaVM main 5 RUNNABLE 0 0:4 false false
3 Finalizer system 8 WAITING 0 0:0 false true
2 Reference Handler system 10 WAITING 0 0:0 false true

这个命令会把所有线程按照cpu占用率从高到低列出来,如果线程太多,可以通过-n参数指定输出的行数。

上面的输出结果可以看到id为16的这个线程cpu占用率很过,然后再通过thread加线程id输出改线程的栈信息

1
2
3
4
5
6
7
8
c复制代码[arthas@2467]$ thread 16
"http-nio-8080-exec-2" Id=16 RUNNABLE
at com.spareyaya.jvm.service.EndlessLoopService.service(EndlessLoopService.java:19)
at com.spareyaya.jvm.controller.JVMController.endlessLoop(JVMController.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
...

两步就定位到了问题

(2)死锁

还是用thread命令,参数是-b

1
2
3
4
5
6
7
8
c复制代码[arthas@2997]$ thread -b
"Thread-3" Id=29 BLOCKED on java.lang.Object@3f20bf9 owned by "Thread-4" Id=30
at com.spareyaya.jvm.service.DeadLockService.service1(DeadLockService.java:27)
- blocked on java.lang.Object@3f20bf9
- locked java.lang.Object@2fea801a <---- but blocks 1 other threads!
at com.spareyaya.jvm.controller.JVMController.lambda$deadLock$0(JVMController.java:37)
at com.spareyaya.jvm.controller.JVMController$$Lambda$456/748979989.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)

这个命令和jstack工具检测死锁同样简单,不过个人认为jstack工具检测死锁其实要比这个更直观一些。

(3)内存泄漏

这个我们可以用dashboard命令来动态查看内存情况

如果内容使用率在不断上升,而且gc后也不下降,后面还发现gc越来越频繁,很可能就是内存泄漏了。

这个时候我们可以直接用heapdump命令把内存快照dump出来,作用和jmap工具一样

1
2
3
c复制代码[arthas@23581]$ heapdump --live /root/jvm.hprof
Dumping heap to /root/jvm.hprof...
Heap dump file created

然后把得到的dump文件导入eclipse,用MAT插件分析就行了。

五、arthas其它命令

arthas还提供了很多用于监控的命令,比如监控某个方法的执行时间,反编译线上的class文件,甚至在不重启java应用的情况下直接替换某个类。官方的使用文档已经写得太详细了,这里就不再一一介绍了,大家可以自己尝试。

六、再说MAT工具

上一篇和本篇在排查内存泄漏的时候我们都用到了同一个工具来分析——MAT。之前我们是在eclipse中安装了MAT插件,使用的时候只能打开eclipse来用。问题是,现在使用eclipse作为开发工具的移动互联网公司应该很少了,我们也不想每次分析内存快照时都要启动一个eclipse。

所以这里介绍一个MAT的独立工具,它是独立于eclipse的应用,下载地址是www.eclipse.org/mat/downloa…,可以根据自己的系统选择版本。

比如在windows下可以直接双击MemoryAnalyzer.exe启动,启动后可以通过顶部菜单的File->Open Heap Dump…来打开一个快照文件,也可以在welcome界面中点击Open a Heap Dump。如果你的快照文件特别大,需要调整jvm参数,在windows下修改MemoryAnalyzer.ini文件,把-Xmx参数的值设置成适合的值(默认是1024M)。

image.png
在Overview选择卡中,可以选择需要分析的内容。比如可以点击Leak Suspects分析可能的内存泄漏,也可以点击Histogram来查看每个类的实例统计。

image.png
然后重点关注那些实例数目特别多的,或者占用内存特别多的(这个还可以设置正则表达式进行过滤,在大项目时很有用),然后结合自己的代码看看这些对象是不是真正都需要的,还是因为作用域设置得太大了导致没有及时回收造成。

image.png
image.png
总之,分析内存快照其实是一项费时费力的工作,在分析中积累经验其它很重要,工具只是为了提高分析的效率。

至于像JProfile这种商业版专业的jvm分析工具,也可以去多了解。

本文转载自: 掘金

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

【用websocket的注意了!】https下浏览器只允许使

发表于 2021-07-29

问题描述:

https下发起ws连接,连接失败,浏览器报错。

问题排查:

https连接下浏览器不允许ws协议了,只允许wss协议。

报错内容如下:

VM71 index.js:5 Mixed Content: The page at ‘https://10.67.36.75/main.htm?_=1609838620497‘ was loaded over HTTPS, but attempted to connect to the insecure WebSocket endpoint ‘ws://10.67.36.75:24048/‘. This request has been blocked; this endpoint must be available over WSS.

问题解决:

业务(websokcet服务端)新增支持wss协议。

番外番外:

针对web(js)而言,代码无需大改,只需要把websocket的url里的”ws” 改为”wss”即可。。。然而服务端就需要大改来支持wss协议了。。。

经过简单测试,发现chrome浏览器、360安全浏览器等都已经强制https连接下使用wss协议了,ws协议无法使用了。。。看来是大势所趋了。。。

本文转载自: 掘金

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

原神20宅男福利!爬虫实战,爬取原神真人cos图片并保存,

发表于 2021-07-29

激动的心,颤抖的手,老婆你们谁没有?

原神5.jpg

(图片来源于米游社)

7月21号《原神》2.0发布,大家更新了吗?

更新内容一览:

1、稻妻城:稻妻城和六大岛屿相连,目前新的岛屿只是其中三个;

2、家园系统更新:会新增植物,种植系统;

3、新圣遗物:稻妻会上三种新圣遗物;

4、主要登场人物:八重神子、珊瑚宫星海,早柚、神里绫华、托马、巴尔、宵宫、五郎。

激动的我,在逛米游社的时候,看着这些cos美女已经按捺不住了,连夜的给大家爬了cos同人图,保存了!有福同享,下面我们一起来看看这些美女,不对是代码操作,正好给大家一个练手的小项目!

首先,我们来看看效果图:

Sapmle_gif(1).gif

项目介绍

开发环境: Python3.6

模块(库): requests/ json /os/ threading

爬取目标: 爬取的是原神官方网站,米游社。bbs.mihoyo.com/ys/home/49 (米游社.原神)

目的: 爬取COS专区下的图片,并保存

原神.png

在COS专区下的图片排序以最新回复栏目排序,因此所爬取的图片会随着最新的时间而更改。程序运行时自动爬取最新20条最新图片。

原神4.jpg

1、导入库

1
2
3
4
5
6
7
arduino复制代码import requests

import json

import os

import threading

2、初始化URL地址、设置UA代理(注意:这里的url并不是首页,而是一个二级页面)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ruby复制代码class WebSpider(object):

def __init__(self):



self.url = 'https://bbs-api.mihoyo.com/post/wapi/getForumPostList?forum_id=49'

self.headers = {

'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'

' Chrome/92.0.4515.107 Safari/537.36'

}

3、分析数据并储存为字典返回

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
ini复制代码  def parse(self):

img_dict_data = {}

res = requests.get(self.url, headers=self.headers).content.decode('utf-8')

res = json.loads(res)

res = res['data']['list']

subject_name = [i['post']['subject'] for i in res]

cover_url = [i['post']['cover'] for i in res]

# print(cover_url, subject_name)

# 获取对应的标题以及图片地址

for name, url in zip(subject_name, cover_url):

# print(name, url)

img_dict_data[name] = url

return img_dict_data

4、 保存图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码def save_img(self, data):

for k, v in data.items():

img_type = v.split('/')[-1].split('.')[-1]

save_path = os.path.dirname(os.path.join(__file__)) + '/img' # 当前目录下的图片保存路径

if not os.path.exists(save_path):

os.mkdir('img')

with open(f'img/{k}.{img_type}', 'wb') as f:

img = requests.get(v, headers=self.headers).content f.write(img)

print(f'{k}.{img_type} ---图保存成功!'

米游社3.jpg

多的我就不说了,源代码附有详细说明:

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
python复制代码"""
爬取地址:https://bbs-api.mihoyo.com/post/wapi/getForumPostList?forum_id=49
getForumPostList:api返回当前最新回复的列表json数据
forum_id=49:COS栏目ID数据为 49
"""
import requests
import json
import os
import threading

class WebSpider(object):
def __init__(self):
self.url = 'https://bbs-api.mihoyo.com/post/wapi/getForumPostList?forum_id=49'
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)'
' Chrome/92.0.4515.107 Safari/537.36'
}

def parse(self):
img_dict_data = {}
res = requests.get(self.url, headers=self.headers).content.decode('utf-8')
res = json.loads(res)
res = res['data']['list']
subject_name = [i['post']['subject'] for i in res]
cover_url = [i['post']['cover'] for i in res] # 遍历图片的URL地址
# print(cover_url, subject_name)
# 获取对应的标题以及图片地址
for name, url in zip(subject_name, cover_url):
# print(name, url)
img_dict_data[name] = url # 字典增加数据

return img_dict_data # 返回数据

# 保存图片
def save_img(self, data):
for k, v in data.items():
img_type = v.split('/')[-1].split('.')[-1] # 获取图片类型
save_path = os.path.dirname(os.path.join(__file__)) + '/img' # 当前目录下的图片保存路径
if not os.path.exists(save_path): # img文件夹不存在时则创建新文件夹
os.mkdir('img')
with open(f'img/{k}.{img_type}', 'wb') as f:
img = requests.get(v, headers=self.headers).content # 发送请求获取图片内容
f.write(img) # 写入数据
print(f'{k}.{img_type} ---图保存成功!')

def main(self):
data = self.parse()
self.save_img(data)

WPS图片拼图.png

(图片来源于米游社,左一神里同人图,右一博主仧郎的cos图)


有这技术 还要啥自行车?福利已经发布,大家可以留下你们的赞再走!!源码获取看简介!!关键词回复“原神”

本文转载自: 掘金

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

1…587588589…956

开发者博客

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