当前位置: 首页 > 编程日记 > 正文

介绍并扩展Fitnesse的测试模块化机制:Scenario

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

首先普及一下概念,什么是Fitnesse,听一听.NET版Cucumber的创始人Aslak Hellesøy谈Fitnesse与Cucumber对比:

FIT/Fitnesse和Cucumber都执行高级语言编写的验收测试。FIT仅识别HTML,Fitnesse则通过提供Wiki语法来简化编写测试的过程。在FIT/Fitnesse当中,所有的测试都以表格的形式呈现。
FitNesse比Cucumber的优势在于Wiki支持。

原文链接:http://www.infoq.com/cn/news/2009/11/interview-cucumber-for-dotnet

1.Scenario是什么

Fitneese的SliM UserGuide中介绍了 Scenario

原文是这么介绍Scenario的:

A Scenario table is a table that can be called from other tables; namely Script Table and Decision Table.

The format of a Scenario table is the same as the format of a Script Table, but with a few differences. You can see a Scenario table in action here.

Scenario是一种Table,可以被Script Table 和 Decision Table调用。

由此很多人都对Scenario报了很大的期望,希望能用Scenario模块化封装测试步骤。

2.Scenario能力展示

下面是我结合Script示例和Scenario示例写的一个Scenario演示用例:

wiki文本:

!define TEST_SYSTEM {slim}
!path classes|import|
|fitnesse.slim.test|!4 定义scenario checkLogin: 登录并检查结果
| scenario | checkLogin | u || p || ensure || logged |
| @{ensure} | login with username | @{u} | and password | @{p} |
| check @{logged} | login message | @{u} logged in. |
| show | number of login attempts |!4 创建script实例,后面调用scenario都是针对这个实例
| script | login dialog driver | Bob | xyzzy |!4 Invoking a scenario from a !-DecisionTable-!
| checkLogin |
| u | p | ensure | logged |
| Bob | xyzzy | ensure |  |
| Bob | zzyxx | reject | not |
| Cat | xyzzy | reject | not |!4 Invoking a scenario from a !-ScriptTable-!
| script |
| checkLogin | Bob || zzyxx || reject || not |
| checkLogin | Bob || xyzzy || ensure ||  |!4 script原示例
| script | login dialog driver | Bob | xyzzy |
| login with username | Bob | and password | xyzzy |
| check | login message | Bob logged in. |
| reject | login with username | Bob | and password | bad password |
| check | login message | Bob not logged in. |
| check not | login message | Bob logged in. |
| ensure | login with username | Bob | and password | xyzzy |
| note | this is a comment |
| show | number of login attempts |
| $symbol= | login message |The fixture for this table is:{{{public class LoginDialogDriver {private String userName;private String password;private String message;private int loginAttempts;public LoginDialogDriver(String userName, String password) {this.userName = userName;this.password = password;}public boolean loginWithUsernameAndPassword(String userName, String password) {loginAttempts++;boolean result = this.userName.equals(userName) && this.password.equals(password);if (result)message = String.format("%s logged in.", this.userName);elsemessage = String.format("%s not logged in.", this.userName);return result;}public String loginMessage() {return message;}public int numberOfLoginAttempts() {return loginAttempts;}
} }}}

测试用例页面:

测试用例页面

点击Test执行后:

执行结果

展开DecisionTable调用Scenario的测试结果:

DecisionTable调用Scenario

展开ScriptTable调用Scenario的测试结果:

ScriptTable调用Scenario

至此,我们看到Scenario可以把Script步骤封装起来,取个模块名,然后使用DecisionTable或ScriptTable调用。

3.Scenario的局限

请注意调用Scenario前的这一行:

创建Script实例

目的是在调用Scenario前先创建好Script实例。

如果去掉这一句,再执行,是这样的结果:

没有Script实例测试结果

再尝试一下,把创建Script实例的语句塞到Scenario中:

!4 定义scenario checkLogin: 登录并检查结果
| scenario | checkLogin | u || p || ensure || logged |
| script | login dialog driver | Bob | xyzzy |   <--这是新加的创建Script实例的语句
| @{ensure} | login with username | @{u} | and password | @{p} |
| check @{logged} | login message | @{u} logged in. |
| show | number of login attempts |

保存后执行测试:

无法测试

4.不满意怎么办?

我还想使用Scenario封装TableTable,比如RestFixture定义的TableTable, 国外最著名的软件开发问答网站stackoverflow.com也在问: Can I make a scenario of RestFixture table in fitnesse?, or is there another way to make reusable components?

stackoverflow

我准备修改Fitneese代码,使得Scenario能直接封装ScriptTable和TableTable,请往下看……

5.修改ScenarioTable.java,使Scenario能直接封装ScriptTable

Scenario的源代码在目录D:\git\FitnesseKit\fitnesse\src\fitnesse\testsystems\slim\tables下:

scenario-src

打开ScenarioTable.java后,关键代码是Scenario的参数@xxx是怎么替换的:

      @Overridepublic String substitute(String content) throws SyntaxError {for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {String arg = scenarioArgument.getKey();if (getInputs().contains(arg)) {String argument = scenarioArguments.get(arg);content = StringUtil.replaceAll(content, "@" + arg, argument);content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);} else {throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));}}return content;}});

增加两行打印System.out.println:

      @Overridepublic String substitute(String content) throws SyntaxError {
+      System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:" + content);for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {String arg = scenarioArgument.getKey();if (getInputs().contains(arg)) {String argument = scenarioArguments.get(arg);content = StringUtil.replaceAll(content, "@" + arg, argument);content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);} else {throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));}}
+      System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:" + content);return content;}

在D:\git\FitnesseKit\fitnesse\src\fitnesse\testsystems\slim\tables\SlimTable.java的构造函数SlimTable中增加一行打印:

      public SlimTable(Table table, String id, SlimTestContext testContext) {
+      System.out.println("SlimTable.SlimTable table:"+table);this.id = id;this.table = table;this.testContext = testContext;tableName = getTableType() + "_" + id;}

目的是查看每次启动的测试Table,比如一次import,一次ScriptTable,一次DecisionTable,一次TableTable,等等。

使用命令ant compile重新编译Fitnesse,并输入ant run重新启动Fitneese: D:\git\FitnesseKit\fitnesse>ant compile ... D:\git\FitnesseKit\fitnesse>ant run

再次运行刚刚失败的测试,现在看命令行打印:

 [java] ScenarioTable.call.substitute <<<<<<<<<< content:<table>[java]     <tr>[java]             <td>scenario</td>[java]             <td>checkLogin</td>[java]             <td>u</td>[java]             <td></td>[java]             <td>p</td>[java]             <td></td>[java]             <td>ensure</td>[java]             <td></td>[java]             <td>logged</td>[java]     </tr>[java]     <tr>[java]             <td>Script</td>[java]             <td>login dialog driver</td>[java]             <td>Bob</td>[java]             <td colspan="6">xyzzy</td>[java]     </tr>[java]     <tr>[java]             <td>@{ensure}</td>[java]             <td>login with username</td>[java]             <td>@{u}</td>[java]             <td>and password</td>[java]             <td colspan="5">@{p}</td>[java]     </tr>[java]     <tr>[java]             <td>check @{logged}</td>[java]             <td>login message</td>[java]             <td colspan="7">@{u} logged in.</td>[java]     </tr>[java]     <tr>[java]             <td>show</td>[java]             <td colspan="8">number of login attempts</td>[java]     </tr>[java] </table>[java] ScenarioTable.call.substitute >>>>>>>>>> content:<table>[java]     <tr>[java]             <td>scenario</td>[java]             <td>checkLogin</td>[java]             <td>u</td>[java]             <td></td>[java]             <td>p</td>[java]             <td></td>[java]             <td>ensure</td>[java]             <td></td>[java]             <td>logged</td>[java]     </tr>[java]     <tr>[java]             <td>Script</td>[java]             <td>login dialog driver</td>[java]             <td>Bob</td>[java]             <td colspan="6">xyzzy</td>[java]     </tr>[java]     <tr>[java]             <td>ensure</td>[java]             <td>login with username</td>[java]             <td>Bob</td>[java]             <td>and password</td>[java]             <td colspan="5">xyzzy</td>[java]     </tr>[java]     <tr>[java]             <td>check </td>[java]             <td>login message</td>[java]             <td colspan="7">Bob logged in.</td>[java]     </tr>[java]     <tr>[java]             <td>show</td>[java]             <td colspan="8">number of login attempts</td>[java]     </tr>[java] </table>[java] SlimTable.SlimTable table:[[scenario,checkLogin,u,,p,,ensure,,logged],[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]

再去运行一个没有被Scenario的封装的Script:

| Script | login dialog driver | Bob | xyzzy |
| ensure | login with username | Bob | and password | xyzzy |
| check | login message | Bob logged in. |
| show | number of login attempts |

命令行打印如下内容:

 [java] SlimTable.SlimTable table:[[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]

对比一下两种运行的打印:

[java] SlimTable.SlimTable table:[[scenario,checkLogin,u,,p,,ensure,,logged],[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]


[java] SlimTable.SlimTable table:[[Script, login dialog driver, Bob, xyzzy], [ensure, login with username, Bob, and password, xyzzy], [check , login message, Bob logged in.], [show, number of login attempts]]

只要想办法在运行封装时,去掉**[scenario,checkLogin,u,,p,,ensure,,logged],**,说不定就可以了。

接下去,修改substitute函数:

      public String substitute(String content) throws SyntaxError {System.out.println("ScenarioTable.call.substitute <<<<<<<<<< content:" + content);
+       int trLeftFirstIndex = content.indexOf("<tr>");
+       int trRightFirstIndex = content.indexOf("</tr>");
+       int trLeftSecondIndex = content.indexOf("<tr>", trLeftFirstIndex + 1);
+       int trRightSecondIndex = content.indexOf("</tr>", trRightFirstIndex + 1);
+       int scriptIndex = content.toLowerCase().indexOf("<td>script</td>");
+       if(scriptIndex > trLeftSecondIndex && scriptIndex < trRightSecondIndex) {
+         StringBuffer removeFirstTr = new StringBuffer();
+         removeFirstTr.append(content.substring(0, trLeftFirstIndex));
+         removeFirstTr.append(content.substring(trRightFirstIndex + "</tr>".length()));
+         content = removeFirstTr.toString();
+       }for (Map.Entry<String, String> scenarioArgument : scenarioArguments.entrySet()) {String arg = scenarioArgument.getKey();if (getInputs().contains(arg)) {String argument = scenarioArguments.get(arg);content = StringUtil.replaceAll(content, "@" + arg, argument);content = StringUtil.replaceAll(content, "@{" + arg + "}", argument);} else {throw new SyntaxError(String.format("The argument %s is not an input to the scenario.", arg));}}System.out.println("ScenarioTable.call.substitute >>>>>>>>>> content:" + content);return content;}        

再次编译,运行Fitneese:

运行成功

耶,一击中的!

具体的代码在 git.oschina.net

6.尝试用Scenario封装TableTable

因为RestFixture是用TableTable实现的,所以我还想用Scenario封装TableTable,以便在使用RestFixture时,可以模块化组织测试步骤。

首先看一个TableTable例子:

!define TEST_SYSTEM {slim}!path D:\git\FitnesseKit\RestFixture\target\dependencies\*
!path D:\git\FitnesseKit\RestFixture\target\classes
!path D:\git\FitnesseKit\RestFixture\extra\slf4j-simple-1.6.6.jar| import |
| smartrics.rest.fitnesse.fixture |获取开始时间
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | begin | js | (new Date()).getTime() | |调用某个服务,这里用 sleep 5秒 模拟
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | sleepMiliSeconds | js | {{{
var start = (new Date()).getTime();
var now;
do {now = (new Date()).getTime();
} while(now - start < 5000);
now - start }}} | |获取结束时间
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | end | js | (new Date()).getTime() | |打印调用服务所花时间
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | spendSeconds | js | (%end% - %begin%) / 1000 | |

测试结果是这样的:

RestFixture测试结果

本测试用例的主要目的是检查调用某个服务所花的时间,本例子是5秒。

接下去我想把上面的获取当前时间,调用服务,计算所花时间都写成Scenario,然后用Script调用Scenario,使测试步骤具有良好的可读性:

!define TEST_SYSTEM {slim}!path D:\git\FitnesseKit\RestFixture\target\dependencies\*
!path D:\git\FitnesseKit\RestFixture\target\classes
!path D:\git\FitnesseKit\RestFixture\extra\slf4j-simple-1.6.6.jar| import |
| smartrics.rest.fitnesse.fixture |获取当前时间
| scenario |  getTime | _t |
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | @{_t} | js | (new Date()).getTime() | |计算所花时间
| scenario |  spendSeconds | _s || beginTime || endTime |
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | @{_s} | js | (@{endTime} - @{beginTime}) / 1000 | |调用某个服务,用sleep模拟
| scenario |  sleep | s |
| Table:Rest Fixture | http://localhost:${FITNESSE_PORT} |
| let | sleepMiliSeconds | js | {{{
var start = (new Date()).getTime();
do {var now = (new Date()).getTime();
} while(now - start < @{s} * 1000);
now - start }}} | |打印调用某个服务所花时间
| script |
| getTime | begin |
| sleep | 5 |
| getTime | end |
| spendSeconds | spend || %begin% || %end% |

测试结果是这样的:

scriptTableActor. does not exist

保存内容是The instance scriptTableActor. does not exist,意思为从已定义的script中找不到。

修改ScenarioTable.java后,测试结果:

Scenario封装Table

ScenarioTable.java的主要修改内容:

ScenarioTable.java的主要修改内容

请到git.oschina.net具体查看。

转载于:https://my.oschina.net/fitnessefan/blog/299488

相关文章:

cmake, This may result in binaries being created in the wrong place

现象&#xff1a; CMake Error: The current CMakeCache.txt directory /home/etouch/minigui-64/minigui/libmgeff-1.0.0/CMakeCache.txt is different than the directory /home/weiym/devel/minigui/rel-3-0/libmgeff-1.0 where CMakeCache.txt was created. This may resul…

GDB调试技巧

1. GDB 调试程序 1.Run a program without any argument. gdb program 2. Run a program with arguments gdb --args program arg1 arg2 ... argN or gdb program (gdb) r arg1 arg2 ... argN 3. start with both an executable program and a core file specified gdb…

php 进程管理,php如何管理进程

进程管理-防止进程成为僵尸进程创建好了进程&#xff0c;那么怎么对子进程进行管理呢&#xff1f;使用信号&#xff0c;对子进程的管理&#xff0c;一般有两种情况&#xff1a;(推荐学习&#xff1a;PHP编程从入门到精通)posix_kill()&#xff1a;此函数并不能顾名思义&#xf…

Linux分区的认识

有时候为了便于管理硬盘或允许在一块硬盘上使用多个文件系统或操作系统&#xff0c;需要对硬盘进行分区操作。硬盘的分区分为3种&#xff1a;主分区、扩展分区、逻辑分区。通常因为计算机BIOS和MBR的限制&#xff0c;一块硬盘最多只能有4个分区&#xff0c;其中一个主分区可用扩…

求小数的小数点的第n位是什么

链接&#xff1a;https://ac.nowcoder.com/acm/contest/548/B来源&#xff1a;牛客网 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K64bit IO Format: %lld题目描述 立华奏在学习初中数学的时候遇到了这样一…

Git 简介1-常用术语

常用术语 1. origin origin是对项目最初克隆(clone)的远程仓库的缩写。 更准确地说&#xff0c;origin 是用来代替原始(original)远程仓库的URL, 从而使在git 命令中使用原始仓库更加容易。 2. master master 是分支的命名约定。 从远程服务器克隆后&#xff0c;生成的本…

要过一遍的博客列表

面向GC的Java编程 (finish) java程序员也应该知道的系统知识系列 (finish) 一致性哈希算法及其在分布式系统中的应用(finish) NoSQL数据库的分布式算法 深入理解java内存模型系列文章 转载于:https://www.cnblogs.com/dongxiao-yang/p/4767179.html

b站弹幕 xml php 乱码,B站弹幕Python爬行XML响应中的代码转换问题,python,之,取,b,xml,时,转码...

在学习过程中&#xff0c;可以发现&#xff0c;对于xml类型的响应&#xff0c;了解到的方式lxml和bs解析器。frombs4importBeautifulSoup #主要使用BeautifulSoup类事实上可以认为&#xff1a;HTML文档和标签树&#xff0c;BeautifulSoup类是等价的Beautiful Soup库解析器&…

java.io包和杯子测楼

1 java.io 字符流&#xff1a;Reader 字节流&#xff1a;InputStream 2 杯子测楼 一种杯子&#xff0c;若在第N层被摔破&#xff0c;则在任何比N高的楼层均会破&#xff0c;若在第M层不破&#xff0c;则在任何比M低的楼层均不会破&#xff0c;给你两个这样的杯子&#xff0c;让…

JUnit测试类完成后事务是默认 回滚的。只能查询数据,不能增删改。

JUnit测试类完成后事务是默认 回滚的。只能查询数据&#xff0c;不能增删改。 在测试类或者测试方法上面加上注解 Rollback(false) 表示事物不回滚&#xff0c;这样数据就可以提交到数据库中了。 转载于:https://www.cnblogs.com/zhangcheng1/p/11156389.html

gcc 编译选项

下载gcc文档&#xff0c;第三章有详细的build options的介绍。 最近我用到2个关键的option 来定位问题&#xff0c;简单介绍一下&#xff1a; 1. -E: 只是进行预编译&#xff0c;不会编译和link。用于检查宏在代码中的展开是否符合预期&#xff1b; 2.--verbose: 开启verbos…

ionic中的后退方法

1&#xff09;$ionicHistory.goBack();2&#xff09;$ionicNavBarDelegate.back(); 个人感觉&#xff1a; 1&#xff09;$ionicHistory.goBack()会按照html历史来后退 2&#xff09;$ionicNavBarDelegate.back()会按照ion-nav-view的层级来跳转&#xff1b;但是&#xff0c;调用…

php 生成非对称密钥,php实现非对称加密

使用非对称加密主要是借助openssl的公钥和私钥&#xff0c;用公钥加密私钥解密&#xff0c;或者私钥加密公钥解密。1.安装openssl和PHP的openssl扩展2.生成私钥&#xff1a;openssl genrsa用于生成rsa私钥文件&#xff0c;生成是可以指定私钥长度和密码保护1. openssl genrsa -…

HDU 4951 Multiplication table(2014 Multi-University Training Contest 8)

思路 如果进制为p 那么当x<p时 &#xff08;p-1&#xff09;*&#xff08;p-x&#xff09;(p-&#xff08;x1&#xff09;) *p x 因为x<p 所以没有进位 所以高位上的数字为 p-&#xff08;x1&#xff09;。 根据上面所述。 只要我们能找出 p-1 那么我们…

H3C 静态默认路由配置

转载于:https://www.cnblogs.com/fanweisheng/p/11156783.html

malloc为什么会报错:memory corruption

最近遇到一个问题&#xff0c;很有意思&#xff0c;在此记录下&#xff0c;以备后续参考。 程序运行异常&#xff0c;报错&#xff1a;malloc: memory corruption. 用gdb 调试程序&#xff0c;bt 如下&#xff0c;程序在申请344 bytes内存时失败。 疑问&#xff1a;344bytes内…

php 静态类内存,php面向对象中static静态属性与方法的内存位置分析

本文实例分析了php面向对象中static静态属性与方法的内存位置。分享给大家供大家参考。具体如下&#xff1a;static静态属性的内存位置——>类&#xff0c;而不是对象。下面做测试来证明一下header("content-type:text/html;charsetutf-8");class Human{static pu…

Android中实现为TextView添加多个可点击的文本

这篇文章主要介绍了Android中实现为TextView添加多个可点击的文本,可实现类似Android社交软件显示点赞用户并通过用户名称进入该用户主页的功能,是非常实用的技巧,需要的朋友可以参考下。具体如下&#xff1a; 很多时候我们在使用社交软件的过程中多多少少会为别人的帖子点赞&a…

Set和存储顺序深入探讨、SortedSet排序的示例

2019独角兽企业重金招聘Python工程师标准>>> Set和存储顺序深入探讨、SortedSet排序的示例 package org.rui.collection2.set;import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; import java.util.TreeSet;//TypesForSets.java /**…

设计模式七大原则(C++描述)

前言 最近在学习一些基本的设计模式,发现很多博客都是写了六个原则,但我认为有7个原则,并且我认为在编码中思想还是挺重要,所以写下一篇博客来总结下 之后有机会会写下一些设计模式的博客(咕咕咕........ 设计模式的七大原则 1.单一职责原则 2.开放-封闭原则 3.依赖倒置原则 4.…

gdb高级调试技巧

1. 反向调试 gdb支持程序反向执行。 record 让程序开始记录反向调试所必要的信息 rn : reverse next rc: reverse continue &#xff0c;Continue program being debugged but run it in reverse record stop: 停止记录 2. 格式化&#xff08;pretty print&#xff09;打…

php代码实现关键词搜索,PHP代码实现百度统计关键词及来路推送

搜索热词勾起我搞这个的兴趣是因为有个卖软件的&#xff0c;老是向我的百度统计后台推送引流软件广告。搜索后发现早就有人做过这方面的研究&#xff0c;然而随着统计代码版本升级&#xff0c;部分功能暂时还未解决。今天这篇 PHP 代码实现提交虚假数据给百度统计就教大家&…

linux跨主机复制文件

scp -r billing10.200.171.111:/billdata2/user/yanhm/redis/* /newboss/billing/user/aabb 其中&#xff1a; 10.200.171.111&#xff1a;远程主机 billing&#xff1a;远程主机的用户名 /billdata2/user/yanhm/redis/&#xff1a;要复制远程主机的文件路径 /newboss/billing/…

delphi使用outputdebugstring调试程序和写系统日志

delphi使用outputdebugstring调试程序和写系统日志 procedure TForm1.btn1Click(Sender: TObject); beginOutputDebugString(dddddd);OutputDebugString(11); end;procedure TForm1.btn2Click(Sender: TObject); varEvtSrcHand: THandle;EvtMsg: String; p:Pointer; i:integer;…

一个下载Windows镜像的地址

https://www.52pojie.cn/thread-633128-1-1.html转载于:https://www.cnblogs.com/blogs-jch/p/11163849.html

perf + 火焰图分析程序性能

From: https://www.cnblogs.com/happyliu/p/6142929.html 1、perf命令简要介绍 性能调优时&#xff0c;我们通常需要分析查找到程序百分比高的热点代码片段&#xff0c;这便需要使用 perf record 记录单个函数级别的统计信息&#xff0c;并使用 perf report 来显示统计结果&a…

jquery 设置css样式

$("#61dh a").css(color, 多个样式属性 var divcss {background: #EEE,width: 478px,margin: 10px 0 0,padding: 5px 10px,border: 1px solid #CCC};$("#result").css(divcss);查看某个元素的css属性值。 $("#61dh a").css("color"…

php改7z,PHP的7z扩展名? - php

我找不到一个&#xff0c;也不知道PHP Compression and Archive Extensions中的任何一个是否可以工作。您认为我可以使用compression stream从7z文件读取数据吗&#xff1f;更新7z forums对php扩展有很多要求参考方案7z文件格式可以使用各种compression algorithms&#xff0c;…

Classloader内存泄露

2019独角兽企业重金招聘Python工程师标准>>> 最近遇到了这个问题&#xff0c;在修改了-Xmx后有时仍然会出现&#xff0c;下文分析的很有启发&#xff0c;看了下文重新分析我的应用&#xff0c;在项目中我使用了spring mvc作为控制层&#xff0c;由于使用到了微信公众…

Springboot + oauth2 单点登录 - 原理篇

OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。授权码模式(authorization code)密码模式(resource owner password credentials)客户端模式(client credentials) 不常用。