WeYunx

WeYunx 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 weyunx.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

WeYunx 发布了文章 · 2019-03-20

登录页面——请不要这么设计

本文翻译自:http://bradfrost.com/blog/pos...

原文作者:Brad Frost

译文原地址:https://weyunx.com/2019/03/17...

随着 1Password 以及 Chrome 的密码管理越来越普及,有些网站的登录页面的弊端就显现出来了,本文就来总结一下有哪些不好的设计,同时来看一下如何改进。

首先网站开发人员需要明白的是用户是怎样去登录他们的网站,然后基本的一个原则就是简单、直接、不突兀以及适配密码管理器。

让我们先看几个例子吧:

不要这么做

下面是几个反例,我认为需要避免的:

不要弹出登录页面

img

Hertz 以及它一系列的网站使用的是弹出窗口或弹出层来跳转登录页面,这样会有如下两个问题:

  • 额外的登录步骤 - 「1.点击菜单按钮,2.选择登录,3.填写信息」。以往的用户可以直接通过搜索、历史记录、书签、密码管理器等来直接跳转,进而填写登录信息,只需要 2 步。
  • 登录页面没有链接 - 这对客服来说是致命的,因为他们需要提供一系列的操作介绍来指引用户如何登录,这比直接提供一个登录链接麻烦的多。同时这也让密码管理器无法自动填充,毕竟登录页面是隐藏的。

不要隐藏输入项

img

Delta 官网的登录页面将 “Last Name” 输入项给隐藏了,原因应该是为了让页面看起来更简洁。但问题是这个输入项是必输的,而因它的隐藏又导致了密码管理器不能自动填充。

MacOS 的登录也隐藏了密码输入项来简化 UI,这可能是为了鼓励使用 TouchID,但是这同样可能会给用户带来一些使用上的困惑。

不要异想天开

img

这种使用邮箱验证码登录的方式先后在 Slack 、Notion 上出现。这种方式完美的避免了忘记密码的烦恼,但是:

  • 这种方式及其愚蠢 - 「1、输入邮箱。2、打开邮箱应用。3、打开收件箱。4、寻找邮件。5、打开邮件。6、拷贝密码。7、回到登录页面。8、粘贴密码。9、登录。」可见步骤之繁琐。
  • 不适配密码管理器
  • 强制用户去适应新的习惯 - 用户长久以来通过潜移默化的形式来学习例如登录、导航、退出等操作。不是说不应该去创新,但是我们需要明白用户进入我们的网站或产品的时候,他们对网络操作已经有了自己的一个认知和习惯,我们不能自作聪明,让他们去熟悉新的流程,况且这操作流程更繁琐了。

不要将登录拆分为多个页面

img

img

img

Shopify 有个烦人的操作就是会把登录页面分成两页。当然,本意是好的:他们想让页面更简洁,但是这种方式其实更适合特定的场景,比如电商的某些页面,像账单信息、物流信息、配送地址和信用卡信息这些,显然如果用在登录页面上就有些用力过猛了:

  • 需要额外的步骤来登录 - 同样的三个输入项的登录页面,结果用户需要三个页面才能完成,显然多此一举。
  • 不适配密码管理器

这么做

所以,好的登录页面应该是什么样的呢?我认为经典的登录页面就是最好的,如:

img

以及:

img

你看,简单明了不花哨,完美适配密码管理器。

OK,让我们看一下结论:

  • 一个可以直接跳转的登录页面
  • 展开所有的输入项
  • 一个页面搞定
  • 不要异想天开,经典就好
查看原文

赞 0 收藏 0 评论 0

WeYunx 发布了文章 · 2019-03-19

登录页面——请不要这么设计

本文翻译自:http://bradfrost.com/blog/pos...

原文作者:Brad Frost

译文原地址:https://weyunx.com/2019/03/17...

随着 1Password 以及 Chrome 的密码管理越来越普及,有些网站的登录页面的弊端就显现出来了,本文就来总结一下有哪些不好的设计,同时来看一下如何改进。

首先网站开发人员需要明白的是用户是怎样去登录他们的网站,然后基本的一个原则就是简单、直接、不突兀以及适配密码管理器。

让我们先看几个例子吧:

不要这么做

下面是几个反例,我认为需要避免的:

不要弹出登录页面

img

Hertz 以及它一系列的网站使用的是弹出窗口或弹出层来跳转登录页面,这样会有如下两个问题:

  • 额外的登录步骤 - 「1.点击菜单按钮,2.选择登录,3.填写信息」。以往的用户可以直接通过搜索、历史记录、书签、密码管理器等来直接跳转,进而填写登录信息,只需要 2 步。
  • 登录页面没有链接 - 这对客服来说是致命的,因为他们需要提供一系列的操作介绍来指引用户如何登录,这比直接提供一个登录链接麻烦的多。同时这也让密码管理器无法自动填充,毕竟登录页面是隐藏的。

不要隐藏输入项

img

Delta 官网的登录页面将 “Last Name” 输入项给隐藏了,原因应该是为了让页面看起来更简洁。但问题是这个输入项是必输的,而因它的隐藏又导致了密码管理器不能自动填充。

MacOS 的登录也隐藏了密码输入项来简化 UI,这可能是为了鼓励使用 TouchID,但是这同样可能会给用户带来一些使用上的困惑。

不要异想天开

img

这种使用邮箱验证码登录的方式先后在 Slack 、Notion 上出现。这种方式完美的避免了忘记密码的烦恼,但是:

  • 这种方式及其愚蠢 - 「1、输入邮箱。2、打开邮箱应用。3、打开收件箱。4、寻找邮件。5、打开邮件。6、拷贝密码。7、回到登录页面。8、粘贴密码。9、登录。」可见步骤之繁琐。
  • 不适配密码管理器
  • 强制用户去适应新的习惯 - 用户长久以来通过潜移默化的形式来学习例如登录、导航、退出等操作。不是说不应该去创新,但是我们需要明白用户进入我们的网站或产品的时候,他们对网络操作已经有了自己的一个认知和习惯,我们不能自作聪明,让他们去熟悉新的流程,况且这操作流程更繁琐了。

不要将登录拆分为多个页面

img

img

img

Shopify 有个烦人的操作就是会把登录页面分成两页。当然,本意是好的:他们想让页面更简洁,但是这种方式其实更适合特定的场景,比如电商的某些页面,像账单信息、物流信息、配送地址和信用卡信息这些,显然如果用在登录页面上就有些用力过猛了:

  • 需要额外的步骤来登录 - 同样的三个输入项的登录页面,结果用户需要三个页面才能完成,显然多此一举。
  • 不适配密码管理器

这么做

所以,好的登录页面应该是什么样的呢?我认为经典的登录页面就是最好的,如:

img

以及:

img

你看,简单明了不花哨,完美适配密码管理器。

OK,让我们看一下结论:

  • 一个可以直接跳转的登录页面
  • 展开所有的输入项
  • 一个页面搞定
  • 不要异想天开,经典就好
查看原文

赞 0 收藏 0 评论 0

WeYunx 赞了回答 · 2019-03-07

解决java 函数式接口例子 求解运行过程

题主好,以下是我个人对于函数式接口的理解

以往我们在看一个方法时,看它的关键点其实就是这么几个

  1. 方法名
  2. 方法参数
  3. 方法体
  4. 方法返回类型

我们随便拿一个方法来举例:

clipboard.png

所以可以看到方法其实是以固定参数返回固定值的一个抽象过程,而这里的固定参数就是我们平常用的数据类型
因此至此可以说,方法和数据类型是分开的,两个没啥实际关联,是不同种类的东西,方法只是需要数据进行执行而已

举个实际例子,例如如何钓鱼这个方法,需要用鱼竿和诱饵这两个实际的东西配合钓鱼的方法才可以执行
不过钓鱼也可以是一个实际东西,比如我把如何用鱼竿和诱饵钓鱼的方法写在纸上,交给你
这个时候这个纸也成了一个实际东西,也就是说钓鱼这个方法也就成了一个实际东西了

函数式接口就是这么一个纸,把方法承载下来,当然它也是一个数据类型,也就是说你可以在方法里传递方法

函数式接口也是一个方法,那它也符合方法刚才那4个关键点,以Builder.java为例,我们写一个Builder的函数式接口的实例

Builder builder = (name, supplier) -> {
            name.toString();
            return;
        };

clipboard.png

所以可以看到只是写法不一样,由于这个Builder的函数式接口定义为void返回,所以可以写出return;也可以不用写,这么就可以不用大括号了

Builder builder = (name, supplier) -> name.toString();

这是那张纸。。。所以你得需要实际数据去执行,执行时,只用看函数式接口定义的方法名就可以了,为了方便举例,我们换个函数式接口来看,比如java.util.function.Function

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

可以看到这个Function的定义中,用了泛型,那就是说,只要是满足一个参数T,返回一个R的方法都可以叫Function,哈哈,所以这个很抽象的东西,只要符合都是Function的实例

比如:Object的方法toString

Function function = o -> o.toString();

假如你有个A类的实例a,想执行atoString方法

A a = new A();
a.toString();

哈哈哈,这只是弟弟的执行方式

你可以这么骚起来

A a = new A();
Function function = o -> o.toString();
function.apply(a);

所以要是你不知道函数式接口怎么执行的,比如上面那个例子,不知道function怎么执行,你直接找到这个function的接口方法apply在哪里调用的,function.apply(a),这里apply的传入的参数是a,这个时候,你用a当作参数,去执行方法体o.toString()就可以了

这里要提到一点,有个方法引用的语法糖吧,比如o -> o.toString(),这里其实用的是ObjecttoString()方法,所以你可以直接写成Object::toString

A a = new A();
Function function = Object::toString;
function.apply(a);

说了这么多,再回到题主问题上

“那builder的add方法是如何实现的呢?并没有看到对add方法进行lambda表达式的实现?”

我们知道builder是函数式接口Builder的实例,要知道它是如何实现的,我们就先找到Builder的接口方法add
然后我们再找add方法什么时候执行的

clipboard.png

找到了,builder.add(WeaponType.SWORD, Sword::new);,这里就已经执行了builderadd方法,往回看builder的定义,但是这时候builder并不是像我们之前的function一样在上一行有定义,而是builder本身就在一个lamdba表达式中,所以这就是方法中嵌套方法了,类似之前钓鱼的例子,如何钓鱼的纸上写的是一张地图,需要先要找到另一张纸才可以找到如何钓鱼的方法

所以还是按照之前我说的方法,既然builder这个时候是lamdba表达式中的一个参数,那还是看这个函数式接口是在哪里调用的即可

clipboard.png

经过查找,我们知道这个时候是调用的WeaponFactory.factory方法,而这个方法参数是个Consumer<Builder>Consumer是个自带的函数式接口,代表任何一个参数T,执行后,不返回

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

它的接口方法是accept,所以找到其执行的方法位置consumer.accept(map::put);
也就是说这个参数是map::put,这是一个方法引用,其实就是Map.put方法,那这个明明是Builder泛型的Consumer,它传入Map.put是个Builder么?

我们看看Map.put的定义

V put(K key, V value);

Builder是传入WeaponTypeSupplier<Weapon>,不返回
Map.put是传入泛型K和泛型V,返回V

感觉不一样。。。其实是一样的。。。因为不返回其实也是返回,它返回的是大写开头的Void,其实Builder应该是Builder<WeaponType, Supplier<Weapon>, Void>,此时由于Void,必须返回,且必须返回null,因此Map.put是一个Builder的实例

所以最终串起来,WeaponType.SWORD, Sword::new两个参数实际是执行了一个Map.put方法...

emmm,所以第一次看完你这个工厂,我咋感觉写了一个像这样的东西而已,不用BuilderWeaponFactory,因为底层用的map,所以map就完事了

public class App {
    private static Map<WeaponType, Supplier<Weapon>> map = new HashMap<>();

    static {
        map.put(WeaponType.SWORD, Sword::new);
        map.put(WeaponType.AXE, Axe::new);
        map.put(WeaponType.SPEAR, Spear::new);
        map.put(WeaponType.BOW, Bow::new);
    }

    /**
     * Program entry point.
     *
     * @param args command line args
     */
    public static void main(String[] args) {
        Weapon axe = map.get(WeaponType.AXE).get();
    }
}

当然这么写感觉不是很高逼格,或者说还不是很好,本来是想要表示一个一对一的对应关系的,所以花费一个额外的map对象来存储对应的关系,其实在Java中表示一一对应关系的可以才用枚举嘛,枚举就可以解决,我平常也比较喜欢用枚举来表示固定的一一对应关系,也好写注释,用枚举WeaponTypeMapper 来代替map

@Getter
@AllArgsConstructor
public enum WeaponTypeMapper {

    SWORD(WeaponType.SWORD, Sword::new),
    AXE(WeaponType.AXE, Axe::new),
    SPEAR(WeaponType.SPEAR, Spear::new),
    BOW(WeaponType.BOW, Bow::new),

    ;

    private WeaponType weaponType;
    private Supplier<Weapon> supplier;

    public static Weapon getWeapon(WeaponType weaponType){
        Weapon weapon = Arrays.stream(WeaponTypeMapper.values())
                .filter(weaponTypeMapper -> weaponTypeMapper.getWeaponType().equals(weaponType))
                .findFirst()
                .map(WeaponTypeMapper::getSupplier)
                .map(Supplier::get).get();
        return weapon;
    }
}

然后获取Weapon的时候,直接

public class App {
    public static void main(String[] args) {
        Weapon axe = WeaponTypeMapper.getWeapon(WeaponType.AXE);
    }
}

囧。。。以上。。。仅供参考

关注 3 回答 2

WeYunx 关注了标签 · 2019-03-06

java

Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

Java编程语言的风格十分接近 C++ 语言。继承了 C++ 语言面向对象技术的核心,Java舍弃了 C++ 语言中容易引起错误的指針,改以引用取代,同时卸载原 C++ 与原来运算符重载,也卸载多重继承特性,改用接口取代,增加垃圾回收器功能。在 Java SE 1.5 版本中引入了泛型编程、类型安全的枚举、不定长参数和自动装/拆箱特性。太阳微系统对 Java 语言的解释是:“Java编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言”。

版本历史

重要版本号版本代号发布日期
JDK 1.01996 年 1 月 23 日
JDK 1.11997 年 2 月 19 日
J2SE 1.2Playground1998 年 12 月 8 日
J2SE 1.3Kestrel2000 年 5 月 8 日
J2SE 1.4Merlin2002 年 2 月 6 日
J2SE 5.0 (1.5.0)Tiger2004 年 9 月 30 日
Java SE 6Mustang2006 年 11 月 11 日
Java SE 7Dolphin2011 年 7 月 28 日
Java SE 8JSR 3372014 年 3 月 18 日
最新发布的稳定版本:
Java Standard Edition 8 Update 11 (1.8.0_11) - (July 15, 2014)
Java Standard Edition 7 Update 65 (1.7.0_65) - (July 15, 2014)

更详细的版本更新查看 J2SE Code NamesJava version history 维基页面

新手帮助

不知道如何开始写你的第一个 Java 程序?查看 Oracle 的 Java 上手文档

在你遇到问题提问之前,可以先在站内搜索一下关键词,看是否已经存在你想提问的内容。

命名规范

Java 程序应遵循以下的 命名规则,以增加可读性,同时降低偶然误差的概率。遵循这些命名规范,可以让别人更容易理解你的代码。

  • 类型名(类,接口,枚举等)应以大写字母开始,同时大写化后续每个单词的首字母。例如:StringThreadLocaland NullPointerException。这就是著名的帕斯卡命名法。
  • 方法名 应该是驼峰式,即以小写字母开头,同时大写化后续每个单词的首字母。例如:indexOfprintStackTraceinterrupt
  • 字段名 同样是驼峰式,和方法名一样。
  • 常量表达式的名称static final 不可变对象)应该全大写,同时用下划线分隔每个单词。例如:YELLOWDO_NOTHING_ON_CLOSE。这个规范也适用于一个枚举类的值。然而,static final 引用的非不可变对象应该是驼峰式。

Hello World

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

编译并调用:

javac -d . HelloWorld.java
java -cp . HelloWorld

Java 的源代码会被编译成可被 Java 命令执行的中间形式(用于 Java 虚拟机的字节代码指令)。

可用的 IDE

学习资源

常见的问题

下面是一些 SegmentFault 上在 Java 方面经常被人问到的问题:

(待补充)

关注 133796

WeYunx 回答了问题 · 2019-03-06

接口在请求几十次后突然变慢,该从什么方向发现问题

要么把时间断点多放几个,看看到底哪块代码执行时间变久了

关注 3 回答 2

WeYunx 发布了文章 · 2019-03-01

浅谈 Gitflow

本文翻译自:https://www.infoq.com/article...
原文作者:Victor Grazi , Bryan Gardner
译文原地址:https://weyunx.com/2019/02/28...

前言

过去开发者花上几周或几个月开发完一个应用功能之后,他们需要进行合并代码的工作。这时候需要有专人,也许是版本管理员,把所有的新功能集成起来,解决代码冲突、然后准备发布新的版本。代码的合并总是让人担惊受怕,毕竟会伴随着不可预见的错误,这可能让我们一个挺好的应用变成了「集成地狱」。在 2000 年的时候,Kent Beck 发布了具有开创性的著作《Extreme Programming Explained》,其中提出了「持续集成」的概念,即开发人员需要每几个小时或最多一天内进行编译然后合并代码到主分支最后再运行自动化测试。

说明:本文的项目是使用 Java 和 Maven,使用 GitLab CI 来执行脚本(Jenkins 和 GitHub CI plugin 也同样支持)。使用 Jira 来跟踪问题单,IntelliJ IDEA 作为 IDE,Nexus 作为仓库管理,Ansible 作为自动化部署工具。

谈谈 Gitflow

Gitflow 提倡使用 feature branches 模式来开发各个相互独立的功能,同时分成不同的分支以便进行集成和发布,如下图:

img

作为 Git 使用者,我们应该对 master 分支已经不陌生了,它是 Git 初始化项目的时候默认创建的分支,是项目的主干。在使用 Gitflow 模式之前,你很可能会直接提交代码到 master 分支上。

开始 Gitflow

开始使用 Gitflow 之前,需要做一步一次性的初始化动作,就是从 master 分支上创建一个 develop 分支。自此,develop 分支将变成一个类似全能的分支,用来存放、测试所有的代码,同时也是主要是用来合并代码、集成功能的分支。

img

作为一个开发人员,在这是不允许直接提交代码到 develop 分支上的,更更更不允许直接提交到 master 分支。master 分支代表的是一个「stable」的分支,包含的是已投产或即将投产的代码。如果一段代码在 master 分支上,即代表它已经投产或即将投产发布。

develop 分支代表着「unstable」,它包含了需要编译并且需要测试通过的代码,甚至是没有完成的代码,所以称之为「unstable」。

接下来将介绍我们是如何开展工作的:

比如,当你被派到了一个 Jira 问题单,你需要立即从 delevop 分支上创建一个 feature 分支。

img

feature 分支的命名规则上,我们约定以 「feat-」开头,后面跟上问题单编号。如「feat-SDLC-123-add-name-field」。以「feat-」开头,可以让 CI 服务器识别出这是一个 feature 分支,「SDLC-123」是我们 Jira 问题单的编号,可以链接到问题单,剩下的部分则是对该功能的简短的说明。

这样,我们的开发工作就可以并行地开展,每个人都可以同时在各自的 feature 分支上开发,当我们持续的将功能 mergedevelop 分支上后,我们可以大大减少变成「集成地狱」的可能。

GitLab CI

现在我们让团队更频繁的提交代码,那么,我们是怎么避免冲突呢?答案是使用 GitLab CI 来进行 build,我们将 GitLab CI 绑定到以「feat-」开头命名的分支即 feature 分支,在 feature 分支上执行 Maven verify (本地编译以及运行 tests),但不发布到 Nexus 仓库。

GitLab CI 是通过项目根目录里的 .gitlab-ci.yml 文件进行配置,包含了 CI/CD 的各个步骤。这个功能的绝妙之处是它可以将运行脚本和代码提交进行绑定。

如下是 GitLab CI 的配置例子,其中我们通过正则表达式来绑定了 feature 分支:

feature-build:
  stage: 
    build
  script:
    - mvn clean verify sonar:sonar
  only:
    - /^feat-\w+$/

团队提倡频繁地提交代码,每次提交代码都会独立的运行 tests,以保证当前提交的代码不会影响到项目原有的任何其它功能。

测试覆盖率

接下来我们该讨论测试覆盖率,IntelliJ idea 自带 coverage 运行模式,允许运行测试代码检查测试覆盖率,会通过侧边栏粉色或绿色来标记是否被测试代码覆盖到。同时建议在 Maven 里添加测试覆盖率插件,如 jacoco,它可以在 GitLab CI 运行集成编译的时候生成报告。

Maven 的 test 阶段执行单元测试,verify 阶段执行集成测试。建议安装 SonarQube 和 Maven SonarQube 插件来进行静态代码的分析和测试。这样,每一个 feature 分支的每一次提交都会执行上述所有的 test

集成工作

回到 Gitflow,现在我们已经开发完了新功能,同时将代码提交到了 feature 分支,根据「持续集成」到思想,我们要求开发团队需要频繁地把代码从 feature 分支 mergedevelop 分支上,要求频率最晚不超过一天。

同时 GitLab 里内置了一个代码复查的机制,即发起一个 merge 请求后,我们必须复查完代码才可以将代码 mergedevelop 分支上。

img

img

根据不同的 SDLC(软件开发生命周期)要求,比如我们强制不同的且具有相关职责的开发人员进行代码复查,或者可以更简单点,开发人员自己复查自己的代码,这样起码鼓励了开发人员可以至少复查一下自己的代码,当然也很明显的增加了很多不靠谱的风险。

最终,经过几天的努力,项目功能已经开发完毕,而且已经全部 mergedevelop 分支上,并验证完毕,同时,其它几个功能也开发完毕也准备发布。记住此时我们只是在每一次的提交时进行了验证,并没有进行部署,如发布 SNAPSHOT 版本到 Nexus 仓库上,这是我们下一步将要做的。

此时,我们在 develop 分支上新建一个 release 分支,然而与传统的 Gitflow 不同,新建的 release 分支是以版本号来命名,版本号命名规则可参考这里。如果 SNAPSHOT 版本的版本号是 1.2.1-SNAPSHOT,那么此次的 release 分支应该命名为 1.2.1。

配置 GitLab CI

配置 GitLab CI 使用正则表达式来识别一个 release 分支,同时执行相关的脚本。

release-build:
  stage:
    build
  script: 
    - mvn versions:set -DnewVersion=${CI_COMMIT_REF_NAME}-SNAPSHOT
    # now commit the version to the release branch
    - git add .
    - git commit -m "create snapshot [ci skip]"
    - git push
    # Deploy the binary to Nexus:
    - mvn deploy
  only:
    - /^\d+\.\d+\.\d+$/
  except:
    - tags

主要特别注意的是提交的时候需要加上 [ciskip] 防止新的提交再次触发 GitLab CI,从而进入死循环。

Bugs

在测试中,难免发现 bug,我们可以直接在 release 分支上修改,修改完后再 mergedevelop 分支上(develop 分支包含的是已发布或者即将发布的代码)。

img

发布

最后, release 分支被验证通过,我们将会把它 mergemaster 分支中。合并时,release 分支中的版本号还是 SNAPSHOT 版本,GitLab runner 会通过 Maven 版本插件将版本号后缀 SNAPSHOT 去掉,同时生成下一个 SNAPSHOT 版本号并发布到 Nexus。此时还会将其部署到 UAT 环境中进行测试,测试无问题后再部署到生产环境。

img

相关的 CI 配置如下:

master-branch-build:
  stage:
    build
  script:
    # Remove the -SNAPSHOT from the POM version
    - mvn versions:set -DremoveSnapshot
    # use the Maven help plugin to determine the version. Note the grep -v at the end, to prune out unwanted log lines.
    - export FINAL_VERSION=$(mvn --non-recursive help:evaluate -Dexpression=project.version | grep -v '\[.*')
    # Stage and commit the binaries (again using [ci skip] in the comment to avoid cycles)
    - git add .
    - git commit -m "Create release version [ci skip]"
    # Tag the release
    - git tag -a ${FINAL_VERSION} -m "Create release version"
    - git push 
    - mvn sonar:sonar deploy
  artifacts:
    paths:
    # list our binaries here for Ansible deployment in the master-branch-deploy stage
      - target/my-binaries-*.jar
  only:
    - master
 
master-branch-deploy:
  stage:
    deploy
  dependencies:
    - master-branch-build
  script:
   # "We would deploy artifacts (target/my-binaries-*.jar) here, using ansible
  only:
    - master

Hotfixes

还有一个必须说明的分支是 hotfixes 分支。这个分支是负责在生产环境上发现的问题,如 bug 或者性能问题等。 hotfixes 分支和 release 分支类似,都以 release 版本号命名,唯一的区别就是 hotfixes 是新建于 master 分支,release 分支则是从 develop 分支而来。

img

hotfix 就是这样,和 release 一样,都会触发 Nexus SNAPSHOT 发布,然后部署到 UAT 环境。当一切都没问题验证通过后,需要再将它 mergedevelop 分支,然后再 mergemaster 以进行投产发布。

Summary

总结图表如下:

img

以上就是 Gitflow 的特点,我们建议大家积极尝试文中所说的各种方法,可以带来如下一些优势:

  • 功能相互隔离。开发人员可以独立的变更功能,使得团队集成工作更加轻松,或者代码的合并加频繁。
  • 功能相互独立,在每个发布的新版本中可以挑选想要发布的功能,同时可以支持我们持续发布新的功能。
  • 更多、更合规的代码复查工作。
  • 自动化测试、部署和交付到各个环境。

后记

现在互联网公司都在讨论「持续交付」,如果你的团队每天都会发布很多版本,本文的方法估计不太适合你,如果你所在的是一个传统企业,如一些金融机构,在版本发布方面更加谨慎,那本文所介绍的分支管理、持续集成、自动化测试以及自动化部署等方面内容也许对你有所帮助。原文篇幅较长,只把主要部分翻译了过来,就当作抛砖引玉,要是感兴趣可以深入研究,如果发现译文存在错误或其他需要改进的地方,欢迎斧正。

查看原文

赞 1 收藏 1 评论 0

WeYunx 赞了回答 · 2019-02-28

java多个线程写同一个数组的不同部分是线程安全的吗?

线程安全对应是的数据争用,按照官方说法是,没有经过happens-Before关系排序的数据争用就会出现 不同步现象。
鉴于你操作的是一个数组的不同部分,多个线程没有同时对同一个数组元素进行读写操作(也就是一个线程读,另一个线程同时写),哪怕没有经过正确的同步,也不会出现数据争用(data race),所以是线程安全的。

关注 3 回答 3

WeYunx 回答了问题 · 2019-02-28

java map传值问题

改成 mm.put("1",arrStr.clone()); 试试。

关注 4 回答 3

WeYunx 评论了文章 · 2019-02-27

Spring Boot 之 LogBack 配置

本文为[原创]文章,转载请标明出处。
原文链接:https://weyunx.com/2019/02/01...
原文出自微云的技术博客

LogBack 默认集成在 Spring Boot 中,是基于 Slf4j 的日志框架。默认情况下 Spring Boot 是以 INFO 级别输出到控制台。

它的日志级别是:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < OFF

配置

LogBack 可以直接在 application.propertiesapplication.yml 中配置,但仅支持一些简单的配置,复杂的文件输出还是需要配置在 xml 配置文件中。配置文件可命名为 logback.xml , LogBack 自动会在 classpath 的根目录下搜索配置文件,不过 Spring Boot 建议命名为 logback-spring.xml,这样会自动引入 Spring Boot 一些扩展功能。

如果需要引入自定义名称的配置文件,需要在 Spring Boot 的配置文件中指定,如:

logging:
  config: classpath:logback-spring.xml

同时 Spring Boot 提供了一个默认的 base.xml 配置,可以按照如下方式引入:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
</configuration>

base.xml 提供了一些基本的默认配置以及在控制台输出时的关键字配色,具体文件内容可以看这里,可以查看到一些常用的配置写法。

详细配置

变量

可以使用 <property> 来定义变量:

<property name="log.path" value="/var/logs/application" />

同时可以引入 Spring 的环境变量:

<property resource="application.yml" />
<property resource="application.properties" />

推荐使用 <springProperty>,相比 <property> 提供了 scopedefaultValue

<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host"
        defaultValue="localhost"/>

所有的变量都可以通过 ${} 来调用。

输出到控制台

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%.-1level|%-40.40logger{0}|%msg%n</pattern>
    </encoder>
  </appender>
 
  <logger name="com.mycompany.myapp" level="debug" />
  <logger name="org.springframework" level="info" />
  <logger name="org.springframework.beans" level="debug" />
 
  <root level="warn">
    <appender-ref ref="console" />
  </root>
</configuration>

输出到文件

<property name="LOG_FILE" value="LogFile" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_FILE}.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- 每日归档日志文件 -->
        <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.gz</fileNamePattern>
        <!-- 保留 30 天的归档日志文件 -->
        <maxHistory>30</maxHistory>
        <!-- 日志文件上限 3G,超过后会删除旧的归档日志文件 -->
        <totalSizeCap>3GB</totalSizeCap>
    </rollingPolicy>
    <encoder>
        <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
</appender> 

多环境配置

LogBack 同样支持多环境配置,如 devtestprod

<springProfile name="dev">
    <logger name="com.mycompany.myapp" level="debug"/>
</springProfile>

启动的时候 java -jar xxx.jar --spring.profiles.active=dev 即可使配置生效。

如果要使用 Spring 扩展的 profile 支持,配置文件名必须命名为 LogBack_Spring.xml,此时当 application.properties 中指定为 spring.profiles.active=dev 时,上述配置才会生效。

参考

查看原文

WeYunx 发布了文章 · 2019-02-27

Spring Security 单点登录简单示例

本文为[原创]文章,转载请标明出处。
本文链接:https://weyunx.com/2019/02/12...
本文出自微云的技术博客

Overview

最近在弄单点登录,踩了不少坑,所以记录一下,做了个简单的例子。

目标:认证服务器认证后获取 token,客户端访问资源时带上 token 进行安全验证。

可以直接看源码

关键依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/>
</parent>

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
</dependencies>

认证服务器

认证服务器的关键代码有如下几个文件:

image

AuthServerApplication:

@SpringBootApplication
@EnableResourceServer
public class AuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }

}

AuthorizationServerConfiguration 认证配置:


@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    TokenStore tokenStore;

    @Autowired
    BCryptPasswordEncoder encoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置客户端
        clients
                .inMemory()
                .withClient("client")
                .secret(encoder.encode("123456")).resourceIds("hi")
                .authorizedGrantTypes("password","refresh_token")
                .scopes("read");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .tokenStore(tokenStore)
                .authenticationManager(authenticationManager);
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        //允许表单认证
        oauthServer
                .allowFormAuthenticationForClients()
                .checkTokenAccess("permitAll()")
                .tokenKeyAccess("permitAll()");
    }
}

代码中配置了一个 client,id 是 client,密码 123456authorizedGrantTypespasswordrefresh_token 两种方式。

SecurityConfiguration 安全配置:

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
               .passwordEncoder(encoder())
               .withUser("user_1").password(encoder().encode("123456")).roles("USER")
               .and()
               .withUser("user_2").password(encoder().encode("123456")).roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.csrf().disable()
                .requestMatchers()
                .antMatchers("/oauth/authorize")
                .and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll();
        // @formatter:on
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


}

上面在内存中创建了两个用户,角色分别是 USERADMIN。后续可考虑在数据库或者 Redis 中存储相关信息。

AuthUser 配置获取用户信息的 Controller:

@RestController
public class AuthUser {
        @GetMapping("/oauth/user")
        public Principal user(Principal principal) {
            return principal;
        }

}

application.yml 配置,主要就是配置个端口号:

---
spring:
  profiles:
    active: dev
  application:
    name: auth-server
server:
  port: 8101

客户端配置

客户端的配置比较简单,主要代码结构如下:

image

application.yml 配置:

---
spring:
  profiles:
    active: dev
  application:
    name: client

server:
  port: 8102
security:
  oauth2:
    client:
      client-id: client
      client-secret: 123456
      access-token-uri: http://localhost:8101/oauth/token
      user-authorization-uri: http://localhost:8101/oauth/authorize
      scope: read
      use-current-uri: false
    resource:
      user-info-uri: http://localhost:8101/oauth/user

这里主要是配置了认证服务器的相关地址以及客户端的 id 和 密码。user-info-uri 配置的就是服务器端获取用户信息的接口。

HelloController 访问的资源,配置了 ADMIN 的角色才可以访问:

@RestController
public class HelloController {
    @RequestMapping("/hi")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<String> hi() {
        return ResponseEntity.ok().body("auth success!");
    }
}

WebSecurityConfiguration 相关安全配置:

@Configuration
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true) 
class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .csrf().disable()
                // 基于token,所以不需要session
              .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .anyRequest().authenticated();
    }


}

其中 @EnableGlobalMethodSecurity(prePostEnabled = true) 开启后,Spring Security 的 @PreAuthorize,@PostAuthorize 注解才可以使用。

@EnableOAuth2Sso 配置了单点登录。

ClientApplication

@SpringBootApplication
@EnableResourceServer
public class ClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }

}

验证

启动项目后,我们使用 postman 来进行验证。

首先是获取 token:

image

选择 POST 提交,地址为验证服务器的地址,参数中输入 username,password,grant_typescope ,其中 grant_type 需要输入 password

然后在下面等 Authorization 标签页中,选择 Basic Auth,然后输入 client 的 id 和 password。

{
    "access_token": "02f501a9-c482-46d4-a455-bf79a0e0e728",
    "token_type": "bearer",
    "refresh_token": "0e62dddc-4f51-4cb5-81c3-5383fddbb81b",
    "expires_in": 41741,
    "scope": "read"
}

此时就可以获得 access_token 为: 02f501a9-c482-46d4-a455-bf79a0e0e728。需要注意的是这里是用 user_2 获取的 token,即角色是 ADMIN

然后我们再进行获取资源的验证:

image

使用 GET 方法,参数中输入 access_token,值输入 02f501a9-c482-46d4-a455-bf79a0e0e728

点击提交后即可获取到结果。

如果我们不加上 token ,则会提示无权限。同样如果我们换上 user_1 获取的 token,因 user_1 的角色是 USER,此资源需要 ADMIN 权限,则此处还是会获取失败。

简单的例子就到这,后续有时间再加上其它功能吧,谢谢~

未完待续...

查看原文

赞 3 收藏 3 评论 1

认证与成就

  • 获得 9 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-13
个人主页被 332 人浏览