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

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


  • 首页

  • 归档

  • 搜索

Hands-on Rust 学习之旅(2)—— 基础知识

发表于 2021-10-18

输入输出

和 C++ 一样,都有标准的输入输出:

1
2
3
4
5
6
7
8
9
10
rust复制代码use std::io::stdin;

fn main() {
println!("你好,你叫什么名字?");
let mut your_name = String::new();
stdin()
.read_line(&mut your_name)
.expect("Failed to read line");
println!("你好,{}", your_name)
}

如果有开发经验的,或者对 C++有所了解的,这代码不能理解,一句话:就是利用标准的std::io::stdin方法输入内如,然后把内容存入变量 your_name 中,读出,最后加上一个异常提示 expect。

关于这个链式的写法,文中有说明:

Combining functions like this is called function chaining. Starting from the top, each function passes its results to the next function. It’s common to format a function chain with each step on its line, indented to indicate that the block belongs together.

function

将上面的代码封装成为一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rust复制代码use std::io::stdin;

fn what_is_your_name() -> String {
let mut your_name = String::new();
stdin()
.read_line(&mut your_name)
.expect("Failed to read line");
your_name
}
fn main() {
println!("你好,你叫什么名字?");
let name = what_is_your_name();
println!("你好,{}", name);
}

其中,特别不一样的地方在于:函数中的返回,可以直接写 your_name,省略了 return 和分号,这个有意思。

This line doesn’t end with a semicolon. This is Rust shorthand for return. Any expression may return this way. It’s the same as typing return your_name;. Clippy will complain if you type return when you don’t need it.

Array

定义数组有两个规则:

  1. 数据类型一致;
  2. 数组的长度不变

定义数组和便利数组的方法和形式,和其他语言差不多,不需要怎么去解释,如:

1
2
3
4
5
6
7
8
9
10
11
rust复制代码let visitor_list = ["叶梅树", "叶帅", "叶哥"];

println!("第一种遍历方法");
for i in 0..visitor_list.len() {
println!("{}", visitor_list[i]);
}

println!("第二种遍历方法");
for visitor in &visitor_list {
println!("{}", visitor);
}

Structs

俗语:结构体

其中,上面我们用到的 String 和 StdIn 都是结构体类型。

我们可以定义一个结构体,然后再继承这个结构体,编写对应的结构体方法,有点类似 Swift 的写法。

如,我们定义一个 Visitor 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
rust复制代码// 定义一个结构体
struct Visitor {
name: String,
greeting: String,
}

// 编写继承函数
impl Visitor {
fn new(name: &str, greeting: &str) -> Self {
Self {
name: name.to_lowercase(),
greeting: greeting.to_string(),
}
}

fn greet_visitor(&self) {
println!("{}", self.greeting);
}
}

使用:

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
rust复制代码use std::io::stdin;

fn what_is_your_name() -> String {
let mut your_name = String::new();
stdin()
.read_line(&mut your_name)
.expect("Failed to read line");
your_name
}

// 定义一个结构体
struct Visitor {
name: String,
greeting: String,
}

// 编写继承函数
impl Visitor {
fn new(name: &str, greeting: &str) -> Self {
Self {
name: name.to_string(),
greeting: greeting.to_string(),
}
}

fn greet_visitor(&self) {
println!("{}", self.greeting);
}
}

fn main() {

let visitor_list = [
Visitor::new("bert", "Hello Bert, enjoy your treehouse."),
Visitor::new("steve", "Hi Steve. Your milk is in the fridge."),
Visitor::new("fred", "Wow, who invited Fred?"),
];

println!("你好,你叫什么名字?");
let name = what_is_your_name();

let known_visitor = visitor_list
.iter()
.find(|visitor| visitor.name == name);

match known_visitor {
Some(visitor) => visitor.greet_visitor(),
None => println!("不在列表之内")
}
}

Vectors

相比 Arrays,Vectors 可以动态的调整大小,利用 push 方法增加元素,直到收到系统内存的限制,或者无限增加。

其他使用方法大同小异,这里就不在描述了。

Enumerations

这个使用也差不多,没什么不同的地方,在以后使用过程中去描述使用方法。

1
2
3
4
5
6
arduino复制代码enum VisitorAction {
Accept,
AcceptWithNote { note: String },
Refuse,
Probation,
}

这里的,AcceptWithNodte { note: String } 当你使用到时,可以自定义变量使用。

对于第二章,核心的基本就这些了,第三章我们就可以进入 Game 阶段了,以上的基础知识我们可以有针对的上网查看具体使用方法。

本文转载自: 掘金

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

java使用snmp管理设备

发表于 2021-10-18

最近项目中用到了snmp4j包进行设备拓扑,设备性能监控,本文主要讲解一下SNMP,snmp4j包在Java中的使用。

一、SNMP介绍

SNMP是简单网络管理协议,专门设计用于在 IP 网络管理网络节点(服务器、工作站、路由器、交换机及HUBS等)的一种标准协议,它是一种应用层协议。 SNMP 使网络管理员能够管理网络效能,发现并解决网络问题以及规划网络增长。通过 SNMP 接收随机消息(及事件报告)网络管理系统获知网络出现问题。目前, SNMP 有 3 种: SNMPV1 、 SNMPV2 、 SNMPV3。第 1 版和第 2 版没有太大差距,但 SNMPV2 是增强版本,包含了其它协议操作。与前两种相比, SNMPV3 则包含更多安全和远程配置。

1、基本操作类型

SNMP对外提供了三种用于控制MIB对象的基本操作命令。它们是:Get、Set 和 Trap。Get:管理站读取代理者处对象的值。它是SNMP协议中使用率最高的一个命令,因为该命令是从网络设备中获得管理信息的基本方式。Set:管理站设置代理者处对象的值。Trap: 代理者主动向管理站通报重要事件。Trap 消息可以用来通知管理站线路的故障、连接的终端和恢复、认证失败等消息,管理站可相应的作出处理。

2、snmp消息组成

一条snmp消息由版本识别符、团体名、PDU组成。版本识别符用于说明现在使用的是哪个版本的SNMP协议,确保SNMP代理使用相同的协议,每个SNMP代理都直接抛弃与自己协议版本不同的数据报。团体名是基本的安全机制,用于实现SNMP网络管理员访问SNMP管理代理时的身份验证。PDU (协议数据单元)是SNMP消息中的数据区, 即Snmp通信时报文数据的载体。PDU指明了SNMP的消息类型及其相关参数。

3、MIB(信息管理库)

上文提到了MIB对象,MIB是信息管理库,可以理解成为agent维护的管理对象数据库, MIB数据对象以一种树状分层结构进行组织,这个树状结构中的每个分支都有一个专用的名字和一个数字形式的标识符,可以通过其数字标识符来查找MIB中的数据对象,这个数字标识符号从结构树的顶部(或根部)开始,直到各个叶 子节点(即数据对象)为止。

4、OID

每个管理对象都有自己的OID(Object Identifier),管理对象通过树状结构进行组织,OID由树上的一系列整数组成,也就是从根节点 通向它的路径,整数之间用点( . )分隔开,树的叶子节点才是真正能够被管理的对象。

二、Java实现SNMP

1、搭建环境

首先本地计算机和被管理的设备要开启snmp,然后下载snmp4j包,或者在maven项目pom文件添加依赖。
在这里插入图片描述

2、代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
java复制代码

import org.snmp4j.*;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.mp.MPv1;
import org.snmp4j.mp.MPv2c;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.*;
import org.snmp4j.smi.*;
import org.snmp4j.transport.DefaultTcpTransportMapping;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.MultiThreadedMessageDispatcher;
import org.snmp4j.util.ThreadPool;

import java.io.IOException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.*;

public class SNMPUtil {
private String address="udp:192.160.0.2/161";
private String username="admin";
private String authpassword="123";
private String privpassword="123";
private Snmp snmp;

/**
* 初始化snmp
* @throws IOException
*/
public void initSnmp() throws IOException {
//1、初始化多线程消息转发类
MessageDispatcher messageDispatcher = new MessageDispatcherImpl();
//其中要增加三种处理模型。如果snmp初始化使用的是Snmp(TransportMapping<? extends Address> transportMapping) ,就不需要增加
messageDispatcher.addMessageProcessingModel(new MPv1());
messageDispatcher.addMessageProcessingModel(new MPv2c());
//当要支持snmpV3版本时,需要配置user
OctetString localEngineID=new OctetString(MPv3.createLocalEngineID());
USM usm=new USM(SecurityProtocols.getInstance().addDefaultProtocols(),localEngineID,0);

OctetString userName= new OctetString(username);
OctetString authpass= new OctetString(authpassword);
OctetString privpass= new OctetString(privpassword);
UsmUser user= new UsmUser(userName,AuthMD5.ID,authpass,PrivDES.ID,privpass);
usm.addUser(user.getSecurityName(),user);
messageDispatcher.addMessageProcessingModel(new MPv3(usm));

TransportMapping transportMapping= new DefaultUdpTransportMapping();
snmp = new Snmp(messageDispatcher,transportMapping);
snmp.listen();
}

/**
* 创建目标对象
* @param oid
* @return
*/
public Target createTarget(String oid){
Target target=null;
int version=1;
if(!(version==SnmpConstants.version1||version==SnmpConstants.version2c||version==SnmpConstants.version3)){
return target;
}
if(version==SnmpConstants.version3){
target = new UserTarget();
//snmpV3需要设置安全级别和安全名称,其中安全名称是创建snmp指定user设置的new OctetString("SNMPV3")
target.setSecurityLevel(SecurityLevel.AUTH_PRIV);
target.setSecurityName(new OctetString(this.username));
}else {
target=new CommunityTarget();
//snmpV1和snmpV2需要指定团体名名称
target.setSecurityName(new OctetString(this.username));
if(version==SnmpConstants.version2c){
target.setSecurityModel(SecurityModel.SECURITY_MODEL_SNMPv2c);
}
}
target.setVersion(version);
target.setAddress(GenericAddress.parse(this.address));
target.setRetries(3);
target.setTimeout(2000);
return target;
}
/**
* 配置设备字符串类型的属性,封装成报文添加到PDU中
* @param pdu
* @param oid
* @param var
*/
public static void setStringVar(PDU pdu,String oid,String var){
OID oidStr = new OID();
oidStr.setValue(oid);
VariableBinding ipBind = new VariableBinding(oidStr,new OctetString(var));
pdu.add(ipBind);
}

/**
* 配置设备数字类型的属性,封装成报文添加到PDU中
* @param pdu
* @param oid
* @param var
*/
public static void setIntVar(PDU pdu,String oid,int var){
OID oidStr = new OID();
oidStr.setValue(oid);
VariableBinding ipBind = new VariableBinding(oidStr,new Integer32(var));
pdu.add(ipBind);
}
/**
* 配置设备数字类型的属性,封装成Guage类型报文添加到PDU中
* @param pdu
* @param oid
* @param var
*/
public static void setGuage(PDU pdu,String oid,long var){
OID oidStr = new OID();
oidStr.setValue(oid);
VariableBinding ipBind = new VariableBinding(oidStr,new Gauge32(var));
pdu.add(ipBind);
}
public static void setIpAddress(PDU pdu,String oid,String var){
OID oidStr = new OID();
oidStr.setValue(oid);
VariableBinding ipBind = new VariableBinding(oidStr,new IpAddress(var));
pdu.add(ipBind);
}

/**
* 创建报文
* @param version
* @param type
* @param oid
* @return
*/
private static PDU createPDU(int version,int type,String oid){
PDU pdu=null;
if(version==SnmpConstants.version3){
pdu= new ScopedPDU();
}else {
pdu= new PDUv1();
}
pdu.setType(type);
//可以添加多个变量oid
/*for(String oid:oids){
pdu.add(new VariableBinding(new OID(oid)));
}*/
pdu.add(new VariableBinding(new OID(oid)));
return pdu;
}

/**
* get方式获取属性
* @param oid
* @return
*/
public List<Map> snmpGet(String oid){
try{
List<Map> list= new ArrayList<Map>();
initSnmp();
Target target = this.createTarget(oid);
PDU pdu=createPDU(1,PDU.GET,oid);
ResponseEvent responseEvent = snmp.send(pdu,target);
PDU response=responseEvent.getResponse();
if(null==response){
System.out.println("Timeout.....");
}else {
if(response.getErrorStatus()==PDU.noError){
Vector<? extends VariableBinding> vbs= response.getVariableBindings();
for (VariableBinding vb: vbs
) {
Map map = new HashMap();
map.put("value",vb.getVariable());
list.add(map);
}
return list;
}else {
System.out.println("Error:"+response.getErrorStatusText());
}
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

/**
* 设置属性值
* @param oid
* @return
*/
public boolean setProprety(String oid) {
boolean bool = false;
try{
initSnmp();
Target target = this.createTarget(oid);
PDU pdu=createPDU(1,PDU.SET,oid);
ResponseEvent responseEvent = snmp.send(pdu, target);
PDU result = responseEvent.getResponse();
if(result!=null){
System.out.println("result:"+result.toString());
if (result.getErrorStatus() == result.noError) {
bool = true;
}
}
}catch (IOException e){
e.printStackTrace();
}
return bool;
}

public void snmpwalk(String oid){
try{
List<Map> list= new ArrayList<Map>();
initSnmp();
Target target = this.createTarget(oid);
PDU pdu=createPDU(1,PDU.GETNEXT,oid);
boolean matched=true;
while (matched){
ResponseEvent responseEvent = snmp.send(pdu,target);
if(responseEvent==null||responseEvent.getResponse()==null){
break;
}
PDU response=responseEvent.getResponse();
String nextOid=null;
Vector<? extends VariableBinding> vbs= response.getVariableBindings();
for (int i = 0; i <vbs.size() ; i++) {
Map map = new HashMap();
VariableBinding vb= vbs.elementAt(i);
Variable variable= vb.getVariable();
nextOid=vb.getOid().toDottedString();
if(!nextOid.startsWith(oid)){
matched=false;
break;
}
map.put("oid",nextOid);
map.put("value",variable);
list.add(map);
}
if(!matched){
break;
}
pdu.clear();
pdu.add(new VariableBinding(new OID(nextOid)));
}
}catch (IOException e){

}
}

//trap
class TrapReceiver implements CommandResponder{
//用户名
private String username = "admin";
//鉴权密码
private String authPassword = "123";
//数据加密密码
private String privPassword = "123";
//trap地址
private String address = "udp:192.168.0.15/162";


private MultiThreadedMessageDispatcher dispatcher;
private Snmp snmp = null;
private Address listenAddress;
private ThreadPool threadPool;
private void init() throws UnknownHostException, IOException {
try {
//创建接收SnmpTrap的线程池,参数: 线程名称及线程数
threadPool = ThreadPool.create("Trap", 2);
//创建一个多线程消息分发器,以同时处理传入的消息,该实例将用于分派传入和传出的消息
dispatcher = new MultiThreadedMessageDispatcher(threadPool,
new MessageDispatcherImpl());
//监听端的 ip地址 和 监听端口号
listenAddress = GenericAddress.parse(address);
//在指定的地址上创建UDP传输
TransportMapping<?> transport;
if (listenAddress instanceof UdpAddress) {
//必须是本机地址
transport = new DefaultUdpTransportMapping((UdpAddress) listenAddress);
} else {
transport = new DefaultTcpTransportMapping((TcpAddress) listenAddress);
}
//初始化snmp需要设置messageDispatcher里面的参数和TransportMapping参数
snmp = new Snmp(dispatcher, transport);
//消息分发器添加接收的版本信息
/* v1和v2都具有基本的读、写MIB功能。*
* v2增加了警报、批量数据获取、管理站和管理站通信能力。*
* v3在v2的基础上增加了USM,使用加密的数据和用户验证技术,提高了安全性*/
snmp.getMessageDispatcher().addMessageProcessingModel(new MPv3());
snmp.getMessageDispatcher().addMessageProcessingModel(new MPv2c());
snmp.getMessageDispatcher().addMessageProcessingModel(new MPv1());
//创建具有所提供安全协议支持的USM,//根据本地IP地址和其他四个随机字节创建本地引擎ID
USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0);
SecurityModels.getInstance().addSecurityModel(usm);
// 添加安全协议,如果没有发过来的消息没有身份认证,可以跳过此段代码
SecurityProtocols.getInstance().addDefaultProtocols();
// 创建和添加用户
OctetString userName1 = new OctetString(username);
OctetString authPass = new OctetString(authPassword);
OctetString privPass = new OctetString(privPassword);
UsmUser usmUser1 = new UsmUser(userName1, AuthMD5.ID, authPass, PrivAES128.ID, privPass);
//因为接受的Trap可能来自不同的主机,主机的Snmp v3加密认证密码都不一样,所以根据加密的名称,来添加认证信息UsmUser。
//添加了加密认证信息的便可以接收来自发送端的信息。
UsmUserEntry userEnty1 = new UsmUserEntry(userName1, usmUser1);
UsmUserTable userTable = snmp.getUSM().getUserTable();
// 添加其他用户
userTable.addUser(userEnty1);
//开启Snmp监听,可以接收来自Trap端的信息。
snmp.listen();
snmp.addCommandResponder(this);
}catch (Exception e){
e.printStackTrace();
}
}
public void run() {
try {
init();
snmp.addCommandResponder(this);
System.out.println("开始监听Trap信息!");
} catch (Exception ex) {
ex.printStackTrace();
}
}

/**
* 实现CommandResponder的processPdu方法, 用于处理传入的请求、PDU等信息
* 当接收到trap时,会自动进入这个方法
*
* @param respEvnt
*/
@Override
public void processPdu(CommandResponderEvent respEvnt) {
// 解析Response
System.out.println("trap接受到告警消息,开始对消息进行处理");
try {
if (respEvnt != null && respEvnt.getPDU() != null) {
PDU pdu=respEvnt.getPDU();
Vector<? extends VariableBinding> vbs= pdu.getVariableBindings();
for (int i = 0; i <vbs.size() ; i++) {
System.out.println("消息体oid:"+vbs.elementAt(i).getOid());
System.out.println("消息体oid对应值:"+vbs.elementAt(i).getVariable());
}

}
}catch (Exception e){
e.printStackTrace();
}

}
}
}

实际SNMP报文类型还有getBulk,getNext等类型,有兴趣的小伙伴可以自己尝试一下。还有一款MIB Browser(MIB浏览器)是SNMP开发中必备一种工具,有时间再讲一下它的安装和使用。

本文转载自: 掘金

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

JDK成长记8:HashMap的兄弟姐妹们

发表于 2021-10-18

file

LinkedHashMap的源码底层原理

LinkedHashMap的源码底层原理

LinkedHashMap继承自HashMap,但是它的底层增加了一个链表来维护插入或者访问顺序,使得LinkedHashMap变动有顺序性。如下图所示:

file

上图中可以看出,LinkedHashMap继承了HashMap,多了两个成员变量,tail和head指针,还使用Entry内部类继承了HashMap的Node内部类,在原有基础上增加了before和after指针。

默认情况下,是按照插入顺序的,也就是put的顺序。LiknedHashMap在put元素的时候会记录用指针,将数组元素的插入顺序记录下来。

通过重写HashMap的newNode()方法,在put方法,插入一个Entry后,Entry的before和last指针类似链表会连起来,并且LinkedHashMap的tail和head指针也会记录下首尾元素。将插入顺序用链表记录下来。

至于访问顺序的情况,你可以在一会后面的例子会提到。

TreeMap的源码底层原理

TreeMap的源码底层原理

同理TreeMap也是一个有序的Map,底层是通过红黑树来维持顺序的,并且TreeMap支持自定义排序规则。原理下图所示:

file

TreeMap没有继承HashMap,他自己实现了put方法,主要逻辑是生成树root节点和普通叶子节点。

刚开始树为空,肯定是进入第一步,生成root节点,创建一个Entry就结束了,这个Entry中多了left和partent、right,color等变量。

创建TreeMap的时候可以指定排序规则,默认不指定使用Key值默认的compare方法,进行排序生成一棵排序后的二叉树,之后调整为红黑树。

而且它没有数组的结构,它就是一个纯粹的红黑树,get的时候通过遍历红黑树获取元素。

在遍历的时候,TreeMap通过EntryIterator进行从小到大的遍历,实现有序访问。

HashTable、HashSet、LinkedHashSet、TreeSet底层原理

HashTable、HashSet、LinkedHashSet、TreeSet底层原理

最后我们聊一下HashMap的其他兄弟姐妹。为什么这么说呢?

因为HashTable和HashMap核心区别就是使用synchronized保证线程安全,这个和Vector+ArrayList很像。

**因为HashSet使用了HashMap,只不过add方法时候的value都是new Object()而已,结合map的特性,同一个key值只能存在一个,map在put的时候,会hash寻址到数组的同一个位置去,然后覆盖原来的值,所以Set是去重的。默认是无序的。**核心代码如下:

1
2
3
typescript复制代码public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

因为LinkedHashSet继承了HashSet,此时HashSet通过使用LinkedHashMap是可以进行访问有序的保证。

因为TreeSet也同理,默认是根据key值的compare方法来排序的,可以自定义Comparator,底层使用了TreeMap,add元素时,同样是空的Object,同样去重,但是TreeSet访问是可以有序。

1
2
3
typescript复制代码 public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

它们的源码都极其简单, 没什么好研究的。所以重点是,你懂了之前的HashMap、LinkedHashMap、TreeMap才是关键。**

下面我们来看一些使用这些集合的场景:

LinkedHashMap应用:Storm中的LRUMap

LinkedHashMap应用:Storm中的LRUMap

  1. 之前你应该熟悉了LinkedHashMap的的插入有序性。调用put方法时,通过链表记录插入的顺序。但是LinkedHashMap还可以支持访问有序性。按照get方法的访问顺序,进行排序。比如:

这个底层和插入有序很像,也是通过一个变量叫做accessOrder和HashMap的put方法和get中重写预留的方法做到的。

每访问一次元素,会将元素移动链表的指针,将刚访问的元素移动到链表的尾部。

基于这个机制我们可以实现一个LRU的Map,实现有自动失效LRU内存缓存Map,这样当元素个数超过缓存容量时,通过LRU可以保证最近最少访问的元素被移除掉。

如果你看过storm的源码的话,你会看到有这样一个LRUMap实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import java.util.LinkedHashMap;
import java.util.Map;


public class LRUMap<A, B> extends LinkedHashMap<A, B> {
private int maxSize;


public LRUMap(int maxSize) {
super(maxSize + 1, 1.0f, true);
this.maxSize = maxSize;
}


@Override
protected boolean removeEldestEntry(final Map.Entry<A, B> eldest) {
return size() > maxSize;
}
}

这个方法很巧妙的基于LinkedHashMap访问有序,实现了一个LRUMap。

首先通过maxSize限定缓存Map的大小,调用父类构造函数,扩容因子为1.0f,第三个入参表示accessOrder=true。重写了removeEldestEntry。

其次通过LinkedHashMap,在get方法时候,如果accessOrder是true,会将get到的元素,放到链表的尾部。保证Map缓存中最新访问的元素的不会被LRU掉。get方法源码如下:

1
2
3
4
5
6
7
8
kotlin复制代码public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}

可以看到有一个方法afterNodeAccess,通过这个来控制LinkedList访问时,单向链表的顺序,从而达到访问有序。

在put方法的时候有一个扩展的入口afterNodeInsertion中,HashMap默认这个方法是空的,什么都不做。但是LinkedHashMap实现了这个方法,并且通过removeEldestEntry+accessOrder控制,如果访问有序参数为true,并且头节点有元素,且 removeEldestEntry 满足,即size() > maxSize,缓存大小达到上限maxSize,会进行一次removeNode操作,移除链表第一个元素。第一个元素就是最近最少访问的元素。如下图所示:

file

TreeSet和TreeMap应用:HDFS中文件租约时长维护

TreeSet和TreeMap应用:HDFS中文件租约时长维护

在HDFS中LeaseManager,有一个非常经典契约检查的机制,对所有的契约,按照续约的时间在TreeSet里排序,后面检查的时候,每次就挑最老的那个契约来检查,是否超过60分钟。如果超过,就释放契约再检查第二老的那个契约,如果最老的契约都没过期,那就说明其他的契约肯定都没过期。

用这个方法,可以巧妙的避免说,后台线程每隔一定时间,就要把所有的契约都遍历一遍来检查里面的最近一次续约的时间

如果一个客户端申请契约过后,超过1小时,都还没有续约,那么这个契约会被自动释放掉,这是他的一个很重要的机制。

如下图所示:

file

好了,到这里,《JDK源码成长记》集合篇的知识基本就带大家学习完了。当然,你一定要结合学到的,不断自己看源码分析思路,之后讲给同事,和他们讨论一下,才能更多的融会贯通。而不是看过文章之后,80%还给我了。

你可以在评论区回复你遇见的使用这些集合的场景,欢迎你评论。

你也可以搜索下你们的业务代码,看看使用了哪些集合类,怎么用的,有没什么隐患?

下一篇我会进行章节总结,也会给大家提供一些常见面试题,让大家检测下学习成果。相信大家掌握了源码原理后,无论是看源码,还是面试,或者应用都可以游刃有余。

金句甜点

金句甜点

除了今天知识,技能的成长,给大家带来一个金句甜点,结束我今天的分享:坚持的三个秘诀之一个性化。

坚持的秘诀除了之前提到的视觉化、目标化,最后一个就是个性化。每个人都自己喜欢的事情,你不能坚持你不喜欢的事情,还是那个例子,假如你要减肥,比如你就不喜欢吃西蓝花,他虽然是热量低的食物,的确很适合减脂塑形的时候吃,但是非要你吃,头两天还好,你能忍受,但是你肯定是坚持不下来的,你要找到适合你自己的低热量食物,定制化的调整,不喜欢的事情怎么能坚持下来呢?运动也是一样,你就是不喜欢做俯卧撑,就喜欢平板支撑,那就换成你喜欢的,你做不了Burbee跳,你就可以做半个等等….而且个性化很重要的一点是比如不喜欢看成长记的时候,觉得费脑子,就看看今日头条,微博,朋友圈奖励自己一下,再看一篇成长记,之后再奖励自己做些自己喜欢的事情。可以做一些重要的事情,在做一些自己喜欢的事情。适当的个性化调整,也是让你坚持前进,渐渐形成习惯的个性化。时间久了相信你不这么做都会觉得难受的。

所以,当你选择你能坚持而且喜欢的,变成一个好的习惯,还一直提醒自己觉得值,就一定能坚持下来。记住坚持的秘诀视觉化、目标化、个性化,你可以试试。

最后,大家可以在阅读完源码后,在茶余饭后的时候问问同事或同学,你也可以分享下,讲给他听听。

欢迎大家在评论区留言和我交流。

(声明:JDK源码成长记基于JDK 1.8版本,部分章节会提到旧版本特点)

本文由博客一文多发平台 OpenWrite 发布!

本文转载自: 掘金

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

Java实现批量下载多文件(夹)压缩包(zip)续

发表于 2021-10-18

问题现状

在Java实现批量下载多文件(夹)压缩包(zip)篇幅中通过在服务器上创建临时文件,借助hutool的ZipUtil将文件(夹)压缩写入至response的OutputStream,实现了多文件(夹)的压缩包下载。其大致流程图可大致描述为:

POST请求下载文件

经过分析和验证上述方式实现的批量下载存在着下列问题

  • 1.文件非常大的情形下,步骤1.2. 4将文件先下载到服务器带来了额外的耗时操作,对于用户来说下载文件只需要将文件从文件系统直接写入响应即可。
  • 2.由于请求类型为POST,所以浏览器不能自动下载文件,步骤5即使将流已写入响应,但是浏览器并不能打开下载页面,需要前端接收到所有Blob才能打开下载,用户体验极差,易给用户造成批量下载没反应的错觉。

是否存在一种方案,可以将批量下载接口转为GET请求,且可以将文件(夹)直接写入到response的OutputStream?

解决思路

1.首先由于批量下载接口batchDownloadFile的参数类型为List<DownloadFileParam>为复杂参数,故无法直接将POST请求修改为GET;这时候该怎么办呢?

架构思维中,比较常用的一种思路便是分层架构!我门可以将批量下载接口拆为两个接口

通过POST方式保存下载参数List<DownloadFileParam>到Redis,并返回Redis中该下载参数对应唯一标示key的接口getBatchDownloadKey如下

1
2
java复制代码@PostMapping(value = "/getBatchDownloadKey")
public String getBatchDownloadKey(@RequestBody List<DownloadFileParam> params)...

根据返回下载参数唯一标示Key进行批量下载的GET接口batchDownloadFile接口,定义如下

1
2
3
java复制代码
@GetMapping(value = "/batchDownloadFile", produces = "application/octet-stream)
public void batchDownloadFile(@RequestParam("downLoadKey") String downLoadKey)
2.Java提供了类ZipArchiveOutputStream允许我们可以直接将带有目录结构的文件压缩到OutputStream,其使用的伪代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码ZipArchiveOutputStream zous = new ZipArchiveOutputStream(response.getOutputStream());
//file为带有目录结构的文件比如:/文件夹/子文件夹/文件.txt
ArchiveEntry entry = new ZipArchiveEntry(file);
InputStream inputStream = file.getInputStream();
zous.putArchiveEntry(entry);
try {
int len;
byte[] bytes = new byte[1024];
//inputStream为文件流
while ((len = inputStream.read(bytes)) != -1) {
zous.write(bytes, 0, len);
}
zous.closeArchiveEntry();
zous.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
IoUtil.close(inputStream);
}

这样我们就可以避免将文件下载到服务器带来的性能消耗。

3.整个过程的流程图如下

GET请求批量下载 (1)

代码实现

保存下载参数请求getBatchDownloadKey

1
2
3
4
5
6
7
8
9
10
11
java复制代码@PostMapping(value = "/getBatchDownloadKey")
public String getBatchDownloadKey(@RequestBody List<DownloadFileParam> params) throws Exception {
try {
String key = IdGenerator.newShortId();
redisTemplate.opsForValue().set(key, JSONObject.toJSONString(params), 60, TimeUnit.SECONDS);
return key;
} catch (Exception e) {
logger.error("getBatchDownloadKey error params={}", params, e);
throw e;
}
}

根据Key下载文件的接口定义batchDownloadFile

1
2
3
4
5
6
7
8
9
java复制代码@GetMapping(value = "/pass/batchDownloadFile", produces = "application/octet-stream;charset=UTF-8")
public void batchDownloadFile(@RequestParam("downLoadKey") String downLoadKey,@RequestParam("token") String token) throws Exception {
try {
fileService.batchDownloadFile(downLoadKey, getRequest(), getResponse(),token);
} catch (Exception e) {
logger.error("batchDownloadFile error params={}", downLoadKey, e);
throw e;
}
}

fileService.batchDownloadFile

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
java复制代码@Override
public void batchDownloadFile(String key, HttpServletRequest request, HttpServletResponse response,String token) throws Exception {
if (redisUtil.get(token) != null) {
UserSession userSession = JSONObject.parseObject(redisUtil.get(token).toString(), UserSession.class);
//如果存在session或者token是存在于project_token配置的值,通过认证
if (userSession != null) {
Object result = redisTemplate.opsForValue().get(key);
if (result == null) {
throw new ParamInvalidException("无效的批量下载参数key");
}
List<DownloadFileParam> params = JSONArray.parseArray(result.toString(), DownloadFileParam.class);
//创建虚拟文件夹
String mockFileName = IdGenerator.newShortId();
String tmpDir = "";
FileUtil.mkdir(tmpDir);
ZipArchiveOutputStream zous = null;
try {
//设置响应
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Accept-Ranges", "bytes");

String fileName = URLEncoder.encode(DateFormatUtil.formatDate(DateFormatUtil.yyyyMMdd, new Date()) + ".zip", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
//参数组装
zous = new ZipArchiveOutputStream(response.getOutputStream());
zous.setUseZip64(Zip64Mode.AsNeeded);

DownloadFileParam downloadFileParam = new DownloadFileParam();
downloadFileParam.setFileName(mockFileName);
downloadFileParam.setIsFolder(1);
downloadFileParam.setChilds(params);

//递归文件流添加zip
downloadFileToServer(tmpDir, downloadFileParam, zous);
zous.closeArchiveEntry();
} finally {
zous.close();
}
} else {
throw new ResultException("服务内部错误");
}
} else {
throw new ResultException("用户已下线,请重新登录");
}
}

downloadFileToServer

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
java复制代码private void downloadFileToServer(String tmpDir, DownloadFileParam downloadFileParam, ZipArchiveOutputStream zous) throws Exception {
List<DownloadFileParam> childs = downloadFileParam.getChilds();
if (EmptyUtils.isNotEmpty(childs)) {
final String finalPath = tmpDir;
childs.stream().forEach(dwp -> dwp.setFile(EmptyUtils.isNotEmpty(finalPath) ? finalPath + File.separator + dwp.getFileName() : dwp.getFileName()));
for (int i = 0; i < childs.size(); i++) {
DownloadFileParam param = childs.get(i);
if (param.getIsFolder() == 0) {
FileInfo fileInfo = fileInfoDao.findById(param.getFileId()).orElseThrow(() -> new DataNotFoundException("文件不存在或已被删除!"));
List<GridFsResource> gridFSFileList = fileChunkDao.findAll(fileInfo.getFileMd5());
ArchiveEntry entry = new ZipArchiveEntry(param.getFile());
zous.putArchiveEntry(entry);
if (gridFSFileList != null && gridFSFileList.size() > 0) {
try {
for (GridFsResource gridFSFile : gridFSFileList) {
InputStream inputStream = gridFSFile.getInputStream();
try {
int len;
byte[] bytes = new byte[1024];
while ((len = inputStream.read(bytes)) != -1) {
zous.write(bytes, 0, len);
}
} finally {
IoUtil.close(inputStream);
}
}
zous.closeArchiveEntry();
zous.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
}
//递归下载文件到压缩流
downloadFileToServer(tmpDir, param, zous);
}
}
}

方案总结

一般情况下下载接口最好用GET方式,浏览器会自动开始下载,除此之外,接口参数与下载接口参数间通过添加中间层解藕帮我们解决了POST下载转化为GET下载方式的问题,分层的架构思想是软件架构最常用的一种方式,再解决工作实际问题的过程中,我们要善于变通采用该方式。

本文转载自: 掘金

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

【来我的楼踩踩|开奖贴】公平给掘金活动抽奖的小代码

发表于 2021-10-18

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

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

声明:才疏学浅,如有错误,恳请指正。

活动来我的楼踩踩开奖了,开奖地址。我们中了摩天大楼奖,
为了活动公平抽奖,特发此贴,如果你认为抽奖过程存在不公平行为,请评论此文,提出公平方案。

分析

我们想获取盖楼用户的数据,就要从掘金的接口获取,首先我们找到了这个接口
https://api.juejin.cn/interact_api/v1/comment/list?aid=2608&uuid=6950557314831795745
我们发现需要传递的参数如下

1
2
3
4
5
6
7
8
json复制代码{
"item_id": "7017671938038333471",
"item_type": 4,
"cursor": "0",
"limit": 20,
"sort": 0,
"client_type": 2608
}

通过分析发现cursor就是页数,而limit就是当前页查询的条数,经过测试limit最大支持的条数是50。通过响应的数据

image.png
可以看到共有274条,也就是我们得多次请求才能获取全部的数据。通过has_more可以判断是否获取完全部的数据。

获取全部数据

通过上述分析代码如下:

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复制代码String url = "https://api.juejin.cn/interact_api/v1/comment/list?aid=2608&uuid=6950557314831795745";
HashMap<String, Object> map = new HashMap<String, Object>(6);
map.put("item_id", "7017671938038333471");
map.put("item_type", 4);
map.put("cursor", "0");
map.put("limit", 50);
map.put("sort", 0);
map.put("client_type", 2608);
Boolean condition = Boolean.TRUE;
do {
System.out.println("当前cursor:" + map.get("cursor"));
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, map, String.class);
if (responseEntity.getStatusCode().value() == 200) {
LuckDto data = JSONObject.parseObject(responseEntity.getBody(), LuckDto.class);
condition = data.getHas_more();
map.put("cursor", data.getCursor());
List<JSONObject> ReplyList = data.getData();
for (int i = 0; i < ReplyList.size(); i++) {
if (ReplyList.get(i).get("is_author").equals(Boolean.FALSE)) {
JSONObject userInfo = (JSONObject) ReplyList.get(i).get("user_info");
redisTemplate.boundSetOps("luckdraw").add(userInfo.getString("user_id"));
}
}

}
} while (condition);

因为redis的set集合的成员是唯一的,所以可以排除同一个用户的多条评论。通过打印System.out.println("当前cursor:" + map.get("cursor"));可以判断请求的次数,和每次请求的参数,是否符合我们的预期要求。

用户数据

image.png
最后redis获取到的用户数据共262条。我们将这些用户id参与抽奖。

抽奖

采取的现成的在线抽奖软件,这个抽奖地址,大家常用,能保证抽奖结果的可靠性。

image.png
原有抽奖人数262,抽奖三次,最后剩余抽奖人数262-3=259。

获取中奖人的昵称

1
2
3
java复制代码if (userInfo.getString("user_id").equals("4441682709316647") || userInfo.getString("user_id").equals("2277843826120318") || userInfo.getString("user_id").equals("3403743732709197")) {
System.out.println(userInfo.getString("user_name"));
}

image.png

image.png

image.png

image.png

最后

恭喜上述三位好友。

  • 1、smars1990
  • 2、zangeci
  • 3、神奇de柠檬

因本周三晚上需要提交问卷,此文公布至周三中午十二点之前,没有疑问就是上述三位好友分别获得官方的徽章。如有疑问,可重新抽奖。

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

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

本文转载自: 掘金

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

万字博文教你爬虫必备->Selenium【详解篇】(初篇)

发表于 2021-10-18

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

  1. 👻👻相信不少小伙伴们在经历过我的上几篇关于爬虫技术的万字博文的轮番轰炸后,已经可以独立开发出属于自己的爬虫项目!!!——爬虫之路,已然开启!👻👻
  2. 😬😬但是前几天有粉丝VX问了我这样一个问题:“我在浏览器中通过开发者工具看到的网页源码与我通过requests库爬取下来的网页源码完全不一样!这是怎么一回事啊?通过博主你教的方法都解决不了哎!”😬😬

其实这就涉及到了前端方面的知识,但是本人精力时间有限,所以目前暂只更新了一篇HTML的必备知识文,关注本博主——后面会加把劲继续更新CSS及JavaScript相关知识的文哦!  

💦身为爬虫人也必须要会的前端知识前两篇之HTML讲解。 不久就会出了哦!

  1. ⏰⏰关于这个问题,我们先要知道为啥子会出现这种情况然后才能对症下药。首先要知道的是requests获取的都是原始的HTML文档,而浏览器中的页面都是经过JavaScript处理数据后生成的结果,这些数据的来源多种多样,可能是通过Ajax加载的,可能是包含在HTML文档里的,也可能是经过JavaScript和特定算法计算后生成的。 ⏰⏰

对于第一种情况:数据加载是一种异步加载方式,原始的页面最初不会包含某些数据,原始页面加载完后,会再向服务器请求某个接口获取数据,然后数据才被处理从而呈现在网页上,这其实就是发送了一个Ajax请求(这就是JavaScript动态渲染页面的一种情形!);
 
 对于第三种情况:数据加载是通过JavaScript和特定算法计算后生成的,并非原始HTML代码,这其中也并不包含Ajax请求。

  1. 📻📻原理知道了,下面的问题就是我们到底该如何解决呢?📻📻
首先第一种方法(适用于解决上面第一种情况):分析网页后台向接口发送的Ajax请求,使用requests库来模拟Ajax请求,这样就可以成功抓取了! —

本文转载自: 掘金

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

HashMap源码解读

发表于 2021-10-18

前言

HashMap作为面试中经常被问到的数据结构,而且我们平时用到的也非常多,那么今天就来盘一盘HashMap的源码。结合平时背的关于HashMap的八股文来看一看他底层到底的怎么实现的,这样以后再面对面试官的夺命连环问也能应付自如了不是。

本文是基于java8的HashMap分析。

构造函数

图片.png
有4个构造函数,其中无参构造是给loadFactor赋一个默认值。这个loadFactor就是负载因子。默认0.75;

图片.png
我们也可以自己指定负载因子数,但是我们指定的负载因子必须大于0。

图片.png

注释上写的明明白白,这个initialCapacity是初始化的大小。

我们知道HashMap底层是数组+链表+红黑树(jdk8),以K,V的形式存在,那他刚创建的数组的大小就是可以通过initialCapacity这个参数来指定,默认是16。

那么问题来了。

面试官:他是在new HashMap的时候就给初始化数组大小的么?

我:额。。。是啊。

面试官:指定初始容量创建的时候是指定多少就是多少么?比如指定容量大小为17,那他创建的数组大小是17么?

我:额。。。不是么?

面试官:。。。。。

图片.png

话不多说,上代码:

图片.png
这里好像并没有数组什么事,只是把初始容量经过tableSizeFor后赋给了threshold,这个tableSizeFor方法等会再看。

那我们再看数组上面的注释。

图片.png

在第一次put的时候才去初始化大小!!

而在指定初始容量大小的创建的时候只是把阈值大小给指定了。看代码:

图片.png

图片.png

我们再回头看这个给threshold赋值的方法:tableSizeFor这个方法:

图片.png

图片.png

根据注释,我们看出:他是获取目标值的最小二次幂。

那么这两个问题的答案就显而易见了,
首先,他是在第一次put的时候才去进行数组的初始化。

然后他初始化的大小是传入值的最小二次幂,比如指定是8 那么初始化大小为8。指定是15 初始化大小为16。

数组+链表+红黑树

HashMap经过1.8版本优化之后他是把链表的头插法改成的尾插法,链表到达一定条件后转为红黑数的数据结构。那我们来看下这两个步骤是在什么地方进行的吧。

图片.png

图片.png
我们再看这个转红黑树的方法:他是直接就把链表转成红黑树吗?

图片.png
他是进行了一次判断:如果数组的长度大于64的时候才去转红黑树的,否则把原来的数据进行一次resize();

那么我们就得出结论:链表在长度大于8的时候,且数组的长度大于64的时候才会转为红黑树的数据结构。

经过我们一顿分析,可以画出HashMap的数据结构:

图片.png

扩容

我们知道HashMap扩容是每次扩容为原来的两倍,那他是怎么进行扩容的呢?答案就再resize()方法里面

图片.png

这时候面试官又问了:

面试官:扩容的时候旧数据进行rehash之后不是和原来的hash值一样吗,还是会造成hash冲突,HashMap是怎么保证resize之后数据均匀的分布呢?

你:。。。。

看代码:

图片.png

圈起来的地方就是答案所在。把hash值和长度进行逻辑与处理后再放到新数组,这样就可以保证旧数据再新的数组里面均匀分布啦。

经过文本的分析,想必各位靓仔对HashMap的理解已经臻至化境。下次再遇到关于HashMap的面试题那不是随便拿捏。

图片.png

最后说一句

才疏学浅,难免会有纰漏,如果文章有不对的地方欢迎大家指正,一起进步。

本文转载自: 掘金

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

IOS应用内购买(IAP)调研

发表于 2021-10-18

概述

In-App Purchase 允许用户购买应用内的内容和功能。用户可以在应用程序中进行购买,或者直接从App Store中购买。

  • 如果您想要在 app 内解锁特性或功能 (解锁方式有:订阅、游戏内货币、游戏关卡、优质内容的访问权限或解锁完整版等),则必须使用 App 内购买项目。App 不得使用自身机制来解锁内容或功能,如许可证密钥、增强现实标记、二维码等。App 及其元数据不得包含按钮、外部链接或其他行动号召用语,以指引用户使用非 App 内购买项目机制进行购买。
  • 如果 app 允许用户购买将在 app 之外使用的实体商品或服务,则必须使用 App 内购买项目以外的购买方式来收取相应款项,如 Apple Pay 或传统的信用卡入口。

来源:App Store 审核指南

StoreKit 框架可代表您的 app 连接到 App Store,以提示并安全地处理付款。然后,该框架会通知您的 app,后者便会交付购买的产品。要验证购买,您可以在服务器上通过 App Store 验证收据或在设备端验证收据。对于自动续期订阅,App Store 也可以向您的服务器发出重要订阅事件通知。

要使用 In-App Purchase,必须首先在 App Store Connect 中配置产品。App 内购买项目配置流程

Apple 共提供了四种类型的IAP项目:

  1. 消耗品(Consumables):是一种在使用一次后就用完的类型。可以多次购买。例如:生命、宝石、金币、体力等
  2. 非消耗品(Non-consumables):是一种客户只需购买一次的类型,它们不会过期。例如:游戏场景、美颜APP中的滤镜等
  3. 非自动续期订阅(Non‑Renewing Subscriptions):用户可以购买一定时间内的服务或内容访问权限。这种类型的订阅不会自动续订,如果结束之后想继续访问,用户需要购买一个新的订阅。例如,视频或音乐软件中的 1月VIP 或 1年VIP
  4. 自动续期订阅(Auto-renewable subscriptions):用户可以在你的应用程序中购买内容、服务或高级功能的持续访问。用户在决定取消之前,将按循环次数收取费用。比如,视频软件中连续包月VIP。

苹果还要求为所有可恢复的 App 内购买项目设计一套恢复机制:

如果用户重新下载应用程序或者切换到一个新的设备,一定要立即提供对他们过去在应用程序内购买的内容或功能的访问。根据 App Store Review Guidelines 3.1.1,应用程序必须包含一个恢复机制,用于任何可恢复的应用程序内购买。

来源:App 内购买项目

IAP基本流程

截屏2021-09-12 21.05.53.png

图片来源:What’s New in StoreKit - WWDC 2017

  1. 加载购买项目的Product Identifiers,这部分可以硬编码在客户端本地或者从服务端获取
  2. 从AppStore获取产品信息,由StoreKit框架从AppStore获取
  3. 展示商品UI,这一步也可能是和第一步同时进行,从服务端拉取商品列表包括 Product Identifier
  4. 请求用户付款,使用StoreKit完成
  5. 应用程序处理交易,StoreKit回调应用程序。应用程序应该校验交易的正确性,交易生成之后,设备端会有交易的Receipt,可以用于应用程序校验交易的正确性和合法性。
  6. 解锁内容或权益,应用程序自己的逻辑
  7. 完成交易,调用StoreKit的方法结束交易。

校验收据有两种类型:
截屏2021-09-12 21.27.35.png

图片来源:What’s New in StoreKit - WWDC 2017

  1. 在客户端本地对收据进行解析并校验
  2. 客户端将收据信息进行Base64编码后发往服务端,服务端发到AppStore,由AppStore进行校验,AppStore会返回解码后的收据信息,服务端也可以进行一些校验。

不要在您的 app 内调用 App Store 服务器 verifyReceipt(英文) 端点。您无法直接在用户设备和 App Store 之间建立受信任的连接,因为您无法控制该连接的任何端点,从而容易遭受中间人攻击。

来源:通过 App Store 验证收据

恢复购买

截屏2021-09-13 上午10.08.12.png

图片来源:What’s New in StoreKit - WWDC 2017

您可以使用 StoreKit 跨设备同步和恢复非消耗型项目和自动续期订阅。当用户购买自动续期订阅或非续期订阅时,您的 app 应当让用户能够在所有设备上访问这一订阅,并让用户能够恢复以前购买的项目。
来源:App 内购买项目

StoreKit

StoreKit有两个版本API:

  • In-App Purchase: 一个基于 Swift 的 API,以 JSON Web Signature (JWS) 格式提供 Apple 签名交易验证,从 iOS 15、macOS 12、tvOS 15 和 watchOS 8 开始提供。
  • Original API for In-App Purchase : 一个使用 App Store 收据提供交易信息的API,从 iOS 3、macOS 10.7、tvOS 9 和 watchOS 6.2 开始提供。

苹果现在把原来的 StoreKit v1 定义为 Original API for In-App Purchase,StoreKit v2 定义为 In-App Purchase, StoreKit V2是在WWDC2021中发布的。这两个 API 都提供了对AppStore中数据的访问。用户使用其中任何一个API进行的应用内购买对这两个API都是完全可用的。

对于开发者如何选择:Choosing a StoreKit API for In-App Purchase

验证收据

收据提供了一个销售记录或者任何购买记录,可以在客户端或者服务端验证收据来验证购买的内容是否真实有效。

截屏2021-09-12 21.24.49.png

如何选择收据验证方法:Choosing a Receipt Validation Technique

消耗型产品的收据在完成交易之后不会再保留在收据中,而非消耗型产品、自动续期订阅、非续期订阅或免费订阅的 App 内购买项目收据无限期保留在收据中。

服务端验证收据

App Store Receipts

收据的结构:developer.apple.com/documentati…

AppStore 服务端通知

App Store 服务端通知 能够提供用户状态的实时更新,以及与应用内购买相关的关键事件,比如退款或订阅状态的更改。开发者收到这些通知时候可以及时地作出处理。通知的类型有如下几种(notification_type):

  • INITIAL_BUY:用户首次购买订阅时发生。
  • DID_RENEW:用户的订阅已成功自动更新到新的周期,应用程序应该提供对订阅内容的访问。
  • DID_CHANGE_RENEWAL_STATUS:订阅状态发生变更,表示用户手动在设置中关闭或者打开了自动订阅的开关。
  • DID_FAIL_TO_RENEW:由于计费问题无法续订。
  • DID_RECOVER:在自动续订扣费失败之后,Apple自动重试成功。
  • INTERACTIVE_RENEWAL:升级或者过期之后重新订阅
  • DID_CHANGE_RENEWAL_PREF:降级
  • REFUND:App Store为 “非消耗品”,”消耗品”, “非续期订阅”类型的应用内购买项完成了退款。
  • CANCEL:用户主动取消了 自动续期订阅,并且用户收到了退款。
  • CONSUMPTION_REQUEST:用户为应用内购买发起了退款请求,App Store请求 开发者服务器提供用户的消费数据。
  • PRICE_INCREASE_CONSENT:表示AppStore已经开始要求用户同意价格上涨请求。
  • REVOKE:家庭共享相关,暂不介绍。

除了REFUND是针对”非消耗品”,”消耗品”, “非续期订阅”类型,其他类型的通知均是针对”自动续期订阅”类型。

通知的结构:developer.apple.com/documentati…

苹果在WWDC2021又发布了App Store Server Notifications V2(Manage in-app purchases on your server - WWDC 2021),细分了更多的订阅通知,但目前没有看到官方文档。

自动续期订阅

自动续期订阅

自动续期订阅能够让用户持续访问APP中的服务或内容,除非用户选择取消订阅,否则AppStore会在到期时自动续期。

自动续期订阅的净收入结构和 App Store 上的其他商业模式不同。在订阅用户第一年服务的每个结算周期,您会收到订阅价格的 70% (减去适用税款)。订阅用户累积满一年付费服务后,您的收入将增加到订阅价格的 85% (减去适用税款)。

订阅群组以及订阅级别

自动续期订阅群组设置概述这里介绍了订阅群组的一些概述。

一个订阅群组可以包含多个具有不同访问级别、价格和持续时间的订阅,便于用户选择最符合自己需求的选项。但是一个用户只能从一个群组中购买一个订阅,因此对于大多数 app 而言,最佳做法是只创建一个群组,这样可以防止用户意外购买多个订阅。

同时对于一个订阅群组中的订阅产品,可以设置不同的订阅级别。

如果您在一个群组中提供多个订阅价格等级,可以将每个订阅分配到不同级别。这个排序将决定订阅用户可以使用的升级、降级和跨级路径。请按照降序方式对您的订阅进行排序,将提供最新内容、功能或服务访问权限的订阅排在最前面,而不考虑持续时间。如果提供的内容相当,您可以向同一个级别添加多个订阅。

用户可以在 App Store 上的帐户设置中管理他们的订阅。在这里,他们可以看到所有续订选项和订阅群组,并可以随意选择订阅的升级、跨级或降级。当用户更改订阅级别时,根据更改的具体内容不同,更改生效的时间也有所不同:

升级。 用户购买服务级别高于当前订阅的订阅。他们的订阅服务会立即升级,并会获得原始订阅的按比例退款。如果您希望用户能够立即访问更多内容或功能,请为该订阅指定较高排名,将其作为升级选项。

降级。 用户选择服务级别低于当前订阅的订阅。订阅会继续保持不变,直到下一个续订日期,然后以较低级别和价格续订。

跨级。 用户切换到相同级别的新订阅。如果两个订阅的持续时间相同,新订阅会立即生效。如果持续时间不同,新订阅会在下一个续订日期生效。

对于VIP会员的连续包月/包季/包年,最佳做法是在同一个订阅群组中添加三个订阅产品,并设置相同的订阅级别,因为其提供的服务是相同的,只有时限不同。

订阅失效

在订阅到期之前的24小时内,App Store 开始尝试自动续订。在一段时间内,应用商店会多次尝试自动续订,但如果尝试失败次数太多,最终会停止。当第一次扣费失败后,则进入重试状态,在这种状态下,App Store 将最多尝试60天直到过期。开发者也可以为扣费失败的情况提供一个宽限期,如果在宽限期内扣费成功,则用户的订阅周期不会发生变化。

Handling Subscriptions Billing

账单宽限期

账单宽限期是指如果订阅因付款问题而无法自动续期,订阅者在一段时间内仍可访问 App 中的付费内容,在此期间 Apple 将继续尝试收取费用。如果 Apple 能够在您订阅产品的帐单宽限期内恢复订阅,则订阅者的付费服务天数累积将不会中断,您的收益也不会受影响。如果未启用帐单宽限期,该订阅者的付费服务天数将暂停累积,直到 Apple 成功收取费用。但需要注意,启用了账单宽限期之后需要保证在账单宽限期内也能提供服务。

如何知道用户订阅是否可用

  1. 每个周期扣款之后,客户端会生成receipt,客户端将这个receipt发送到服务端进行校验,服务端校验成功为其开通权限。这种方式严重依赖于客户端,如果用户没有打开客户端,服务端可能收不到新的收据,也就无法开通权益,当用户在其他平台比如Web网页上则无法使用。
  2. 服务端进行状态轮询。AppStore 的 verifyReceipt 接口除了会返回当前收据的交易信息,还会返回最新的交易信息,所以服务端可以保存上次最新的收据,并进行状态轮询,以便能够及时得到最新的交易。
  3. AppStore 服务端通知。在每个订阅的生命周期中,AppStore会将关键的事件以通知的形式发送给开发者服务器,服务器收到之后可以进行相关业务逻辑的处理。

退款

对于退款操作,当用户请求退款,并且退款成功之后,苹果会发送通知,通知类型如下:

  • CANCEL:对于自动续期类型的订阅。当用户升级到同一个订阅群组中的另一个产品时,也会收到这个通知。
  • REFUND:对于其他类型的订阅
    开发者服务器收到这两类通知之后可以进行业务上的处理。

截屏2021-09-14 下午1.12.27.png

在WWDC 2021,苹果在决定是否能给用户退款的决策中加入了一个新的决策影响因素:开发者信号。当用户请求退款之后,AppStore会给开发者服务器发送一个通知,类型为 CONSUMPTION_REQUEST,请求开发者提供一些消费信息,用于协助决策是否给用户退款,开发者可在12小时内调用 AppStore Server API 中的Send Consumption Information接口提供相关信息。如果之后AppStore为用户完成了退款,那么开发者会收到REFUND或者CANCEL通知,开发者可进行相应的业务逻辑处理。

App Store 服务端 API

苹果在WWDC2021上发布了 App Store Server API,使开发者可以在服务端查询用户的交易信息,目前提供了三个接口:

  1. 获取交易历史记录。获取用户在应用中的所有交易记录。Get Transaction History
  2. 获取所有订阅的状态。获取用户在应用中的所有订阅状态。Get All Subscription Statuses
  3. 发送消费信息。当用户申请退款时,苹果会通知开发者服务器(CONSUMPTION_REQUEST 类型),在服务器接收到CONSUMPTION_REQUEST 通知后,可以在12小时内向 App Store 发送关于该用户的消费信息,协助苹果决定是否同意用户退款。Send Consumption Information

JWT验证

新的App Store API 都需要使用 JWT验证。

相关链接

  1. 苹果iOS内购三步曲:App内退款、历史订单查询、绑定用户防掉单!— WWDC21
  2. iOS 内购(In-App Purchase)总结
  3. 苹果应用内购买(IAP)—从入门到放弃
  4. www.dengshunlai.com/blog/iap.ht…
  5. iOS内购IAP - 19篇

Apple 中文文档

  1. APP审核指南
  2. Apple 简体中文文档
  3. App 内购买项目
  4. StoreKit - App 内购买项目
  5. StoreKit - 通过 App Store 验证收据
  6. 自动续期订阅
  7. App 内购买项目配置流程

Apple 英文文档

  1. App Store Review Guidelines
  2. In‑App Purchase
  3. Auto-renewable Subscriptions
  4. StoreKit
  5. StoreKit - Choosing a StoreKit API for In-App Purchase
  6. StoreKit - In-App Purchase
  7. StoreKit - Original API for In-App Purchase
  8. App Store Receipts
  9. Choosing a Receipt Validation Technique
  10. App Store Server Notifications
  11. App Store Server API

WWDC 视频

  1. All Videos
  2. Meet StoreKit 2 - WWDC 2021
  3. Manage in-app purchases on your server - WWDC 2021
  4. Support customers and handle refunds - WWDC 2021
  5. What’s new with in-app purchase - WWDC 2020
  6. Architecting for subscriptions - WWDC 2020
  7. In-App Purchases and Using Server-to-Server Notifications - WWDC 2019
  8. Subscription Offers Best Practices - WWDC 2019
  9. Best Practices and What’s New with In-App Purchases - WWDC 2018
  10. Engineering Subscriptions - WDC 2018
  11. What’s New in StoreKit - WWDC 2017
  12. Advanced StoreKit - WWDC 2017

本文转载自: 掘金

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

screw 再升级,备份你的数据库,直接帮你写好的代码还不拿

发表于 2021-10-18

小知识,大挑战!本文正在参与“ 程序员必备小知识

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

今天有空整了下之前写的数据库备份的代码。

1、工作中的问题
数据库开发流程一般是先在power design 中新建表结构(因为pd其他部门要看的),然后拷贝生成的DDL建表语句,在数据库中执行,然后才算创建了一张表。这样的工作流程中间有一些问题。

1、不方便修改,打断了代码开发的专注。
如果在开发的过程中想要修改表,我会直接在数据库中通过Navicat修改表结构,进行增删改,正常的情况下然后还要同步到pd中。这样的流程打断了我开发代码的专注度,因此需要将我们从这样的繁琐事中解脱出来。

2、容易遗忘,会有心理负担
修改数据是基本操作,如果在开发功能的过程中频繁修改,去同步pd,会给开发产生负担,这样的情况下就会产生遗忘,在功能开发完成的情况下,基本上很少去再次补全pd,一份不完整的pd意义是不大的。毕竟不想每次都有人问表的结构怎么样,也不想费口舌,也会有自己没有维护好,没有做好的感觉。

3、游戏版本更新频繁,无法回滚数据库
在最忙的时候,游戏基本上是两周一个新版本,每个版本都会伴随一些表的变更,虽然我们的游戏代码都会有版本记录,但是数据的表结构一直没有好的备份,这样的情况下造成数据库表结构很难回滚,所以需要想办法对数据库进行备份。

2、解决方案
有这样的问题,必然想要解决。解决问题了才能提升工作效率(有时间划水),减少犯错的可能性(不想背锅)。下面是我写的整理数据的工具,使用了screw,我新增了对数据库表结构的备份,下载代码,简单改下配置就可以直接运行,拿去不谢。

优化点:

1、对生成的文件加了日期作为版本号

2、在表结构中新加了删除表的语句

3、新增了建表sql备份

下面的代码直接拷贝放在编辑器修改数据库连接信息后,可直接运行

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
java复制代码package screw;

import cn.smallbun.screw.core.Configuration;
import cn.smallbun.screw.core.engine.EngineConfig;
import cn.smallbun.screw.core.engine.EngineFileType;
import cn.smallbun.screw.core.engine.EngineTemplateType;
import cn.smallbun.screw.core.execute.DocumentationExecute;
import cn.smallbun.screw.core.process.ProcessConfig;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class App {


public static void main(String[] ags) throws IOException, SQLException {

String dbName = "fate";
HikariConfig config = new HikariConfig();
config.setDriverClassName("com.mysql.cj.jdbc.Driver");
config.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/" + dbName +" ?serverTimezone=UTC");
config.setUsername("root");
config.setPassword("root");
config.addDataSourceProperty("useInformationSchema", "true");
config.setMinimumIdle(2);
config.setMaximumPoolSize(5);
DataSource ds = new HikariDataSource(config);
String userDir = System.getProperty("user.dir") + "\\src\\test\\java\\com\\pdool\\";
System.out.println(userDir);
SimpleDateFormat dataFormat = new SimpleDateFormat("yyyyMMdd");
String versionStr = dataFormat.format(new Date());
List<String> ignoreTable = new ArrayList<>();
List<String> ignorePrefix = new ArrayList<>();
List<String> ignoreSuffix = new ArrayList<>();
ignoreSuffix.add("_test");
ignoreSuffix.add("test");

for (int i = 0; i < 10; i++) {
ignoreSuffix.add(String.valueOf(i));
}
createHtml(ds, userDir, versionStr, ignoreTable, ignorePrefix, ignoreSuffix);
createSql(dbName, ds, userDir, versionStr, ignoreTable, ignorePrefix, ignoreSuffix);
}

public static void createHtml(DataSource dataSource, String userDir, String versionStr, List<String> ignoreTable, List<String> ignorePrefix, List<String> ignoreSuffix) {

EngineConfig engineConfig = EngineConfig.builder()
.fileOutputDir(userDir)
.openOutputDir(false)
.fileType(EngineFileType.HTML)
.produceType(EngineTemplateType.freemarker)
.build();

ProcessConfig processConfig = ProcessConfig.builder()
.ignoreTableName(ignoreTable)
.ignoreTablePrefix(ignorePrefix)
.ignoreTableSuffix(ignoreSuffix)
.build();

Configuration config = Configuration.builder()
.version(versionStr)
.description("数据库文档")
.dataSource(dataSource)
.engineConfig(engineConfig)
.produceConfig(processConfig).build();

new DocumentationExecute(config).execute();
}

public static void createSql(String dbName, DataSource dataSource, String userDir, String versionStr, List<String> ignoreTable, List<String> ignorePrefix, List<String> ignoreSuffix) throws IOException, SQLException {
Statement tmt = null;
PreparedStatement pstmt = null;
List<String> createSqlList = new ArrayList<>();
String sql = "select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA = '"+dbName+"' and TABLE_TYPE = 'BASE TABLE'";
tmt = dataSource.getConnection().createStatement();
pstmt = dataSource.getConnection().prepareStatement(sql);
ResultSet res = tmt.executeQuery(sql);
while (res.next()) {
String tableName = res.getString(1);
if (tableName.contains("`")) {
continue;
}
if (ignoreTable.contains(tableName)) {
continue;
}
boolean isContinue = false;
for (String prefix : ignorePrefix) {

if (tableName.startsWith(prefix)) {
isContinue = true;
break;
}
}
if (isContinue) {
continue;
}
for (String suffix : ignoreSuffix) {
if (tableName.startsWith(suffix)) {
isContinue = true;
break;
}
}
if (isContinue) {
continue;
}
ResultSet rs = pstmt.executeQuery("show create Table `" + tableName + "`");

while (rs.next()) {
createSqlList.add("DROP TABLE IF EXISTS '" + tableName + "'");
createSqlList.add(rs.getString(2));
}
}

String head = "-- 数据库建表语句 \r\n";
head += "-- db:" + dbName + " version: " + versionStr + "\r\n";
String collect = String.join(";\r\n", createSqlList);
collect = head + collect + ";";
string2file(collect, userDir + dbName + "_" + versionStr + ".sql");
}

public static void string2file(String collect, String dirStr) throws IOException {
System.out.println("文件地址 "+ dirStr);
OutputStreamWriter osw = null;
try {
osw = new OutputStreamWriter(new FileOutputStream(new File(dirStr)), StandardCharsets.UTF_8);
osw.write(collect);
osw.flush();
} finally {
if (osw != null) {
osw.close();
}
}
}


}

3、运行的结果
生成的文件如下图

文件地址:控制台也有打印文件地址

文件说明:fate_20210304.sql :数据库名 fate ,生成日期是20210304,内容是建表语句。主要用来和代码对应,恢复数据库。

fate_数据库文档__20210304.html::数据库名 fate ,生成日期是20210304,内容是html,主要用来给其他部门交流。

图片

图片

总结

本来在公司已经写好的工具,可惜公司内网没办法拿出来,自己又重写了一遍,也花费了不少时间,挺晚了,准备睡觉。

PS:原创不易,点个赞

本文转载自: 掘金

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

java接口文档 springboot整合knife-4j,

发表于 2021-10-18
简介

knife简单来说为Swagger的UI增强版。和Swagger在代码上面的注解并没有太多不同,只需要做几个引入和配置即可。

写一个knife demo
  1. 引入jar包
  • 因为是Swagger的增强版,所以还是需要引入Swagger的jar包
1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-ui</artifactId>
<version>3.0.2</version>
</dependency>
  1. 添加配置类
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
java复制代码import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {

ApiInfo apiInfo = new ApiInfoBuilder()
.title("这里是swagger的标题")
.description("这里是描述内容")
.contact(new Contact("Z", "", ""))
.termsOfServiceUrl("http://localhost:18099/") //* 地址 用于显示 不会影响
.version("1.0")
.build();

return new Docket(DocumentationType.SWAGGER_2)
.host("http://localhost:28099/")//* 地址 用于显示 不会影响
.groupName("后台接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.demo.controller"))
.paths(PathSelectors.any())
.build();
}
}
  1. 开放静态资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class IntercpetorConfig implements WebMvcConfigurer {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 设置swagger静态资源访问
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
  1. 启动项目,访问http://{ip}:{host}/doc.html 即可。
Swagger常用注解
  1. @Api 描述接口类
  • 使用位置: 接口类上面
  • 参数:
    • tags:类名。可以传多个值。
    • description: 描述
      1
      2
      3
      java复制代码    @Api(tags = {"文件接口"},description = "文件接口类")
      public class FileController {
      }
  1. @ApiOperation 描述接口方法
  • 使用位置: 写在方法上
  • 参数:
    • value: 方法描述
    • notes: 提示信息
      1
      2
      3
      4
      java复制代码    @PostMapping("/resume")
      @ApiOperation(value = "续传接口",notes = "file对象从request中获取")
      public Map resume(HttpServletRequest req) throws Exception {
      }
  1. @ApiParam 描述接口方法参数
  • 使用位置: 写在方法参数上
  • 参数:
    • name: 参数名称
    • value: 参数描述
    • required: 必填标志
      1
      2
      3
      4
      java复制代码    @PostMapping("/resume")
      @ApiOperation(value = "续传接口",notes = "file对象从request中获取")
      public Map resume(@ApiParam(name = "req",value = "request对象",required = true) HttpServletRequest req) throws Exception {
      }
  1. @ApiModel 实体类描述
  • 使用位置: 写在实体类上
  • 参数:
    • value: 实体类名称
    • description: 实体类描述
      1
      2
      3
      4
      5
      6
      java复制代码    @ApiModel(value = "用户类",description = "用户实体类")
      public class User {
      @ApiModelProperty(value = "姓名",name = "name",required = true,example = "张三")
      String name;
      String password;
      }
  1. @ApiModelProperty 实体类属性描述
  • 使用位置: 写在实体类参数上
  • 参数:
    • value: 描述
    • name: 属性名
    • required: 是否必填
    • hidden: 隐藏
    • example: 示例
      1
      2
      3
      4
      5
      6
      java复制代码    @ApiModel(value = "用户类",description = "用户实体类")
      public class User {
      @ApiModelProperty(value = "姓名",name = "name",required = true,example = "张三")
      String name;
      String password;
      }

本文转载自: 掘金

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

1…484485486…956

开发者博客

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