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

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


  • 首页

  • 归档

  • 搜索

django开发时遇到问题的正确求助姿势

发表于 2017-11-30

自 django博客教程发布以来,已有超过上万名读者学习了该教程。一些学习者跟随教程顺利地完成了个人博客的搭建,但一直以来也不断地收到读者的评论留言、QQ 留言、邮件等求助信息,他们被开发中的一些问题卡主了,并且不知道该如何解决。随着教程阅读者越来越多,我收到的求助信息也越来越多。一个人的力量始终是有限的,我个人也难以回答所有求助者的问题。为此,我想向大家介绍一些当初我学习 django 时遇到问题如何有效求助的一些经验,一些更容易得到解决方案的求助渠道,以及一些可供查阅的 django 资料等。

求助首选项:django 官方文档

我所遇到过的,以及我收到的很多新人的问题 70% 都能够通过 django 的官方文档找到答案。但是为什么还是有很多人会问这些在官方文档中可以找到答案的问题呢?原因他们对官方文档不熟悉。学习 django 开发,官方文档是最为全面、权威的学习资料。我的建议是在简单地入门了 django 之后,一定要花费一定量的时间开始通读官方文档的内容。也许你害怕内容太多,但我们要做的是通读文档,知道文档的哪一部分讲了一个什么问题,对 django 相关组件的文档说明有一个鸟瞰式的掌握,这样当遇到某个问题时你就能想起这个问题曾在文档的某个部分有过讨论,因此可以快速定位到文档的相关部分,找到问题的解决方案。同理,对于你正在使用的第三方库,文档依然是首选求助对象。

当然,我了解绝大部分人不想阅读官方文档的原因不是被庞大的内容量吓退的,而是被英语吓退的。但是,在目前这样的技术环境下,熟练阅读英文技术文档和书籍是一个合格的开发者必备的技能之一。如果英语对你来说是一个大的挑战,你应该为此制定一个长期的英语学习计划,而不该急功近利地想快速掌握一门开发的技术。如果项目紧急,你可以尝试先求助一些中文翻译文档,例如 django 有 1.8 的中文文档(我不贴地址,希望你阅读本文后已经学到如何寻找资料的技巧)。但是注意,大部分英文文档翻译都是热心的网友贡献的,一是文档更新缓慢,翻译不全,二是翻译人员众多,错误在所难免,因此一定不能长期依赖,提高自身英文水平才是硬道理。

求助搜索引擎

开发过程中不可避免的会遇到很多问题,这时候要善于利用社区和搜索引擎来帮助自己解决问题。千万不要一个人关起门来和问题死磕,有时候卡了你几天不得解的问题,可能经他人一句话提醒就会是使你茅塞顿开。在这里分享一下我遇到问题通常是如何求助的。

首先最重要的一点就是要抛弃百度。从我个人经验来看,django 开发的大部分问题很难在百度搜到答案。与之相比的是 Google,我通常遇到问题会使用 Google 搜索,使用关键字 django + 问题简短的英文描述,90% 以上的问题都可以在 Google 的搜索结果里找到解决方案,几乎不用求助于他人。如果你没有适当的科学上网的方法,也可以使用雅虎搜索或者必应搜索代替。

当然,我知道很多人不是不想使用 Google,而是不知道问题对应的关键字该如何用英语表达。还是那句话,在目前这样的技术环境下,熟练阅读英文技术文档和书籍是一个合格的开发者必备的技能之一。如果英语对你来说是一个大的挑战,你应该为此制定一个长期的英语学习计划。如果情况紧急,你也可以尝试使用一些翻译软件,Google 的搜索一大好处是能够帮你自动纠正语法错误。例如曾今一个朋友问我 django 该如何显示图片,显然这种问题问 Google 比问我更容易得到更快更好的答案,只需要在搜索框输入:how to display pictures in django,你会看到排名靠前的基本都是你所需要的答案。

求助开发者问答社区

不知道 stackoverflow 的开发者不是一个好的开发者,有一句话叫面向 stackoverflow 编程,甚至还有人出版从如何 stackoverflow copy 代码到项目上线的过程,stackoverflow 对开发者的重要性由此可见一般。通常,我们使用第二步提及的搜索引擎搜索到的答案基本来自 stackoverflow。但如果遇到搜索引擎都无法解决的问题,你就可以尝试在 stackoverflow 上提问,只要问题描述的很好(见下文关于如何正确提问),基本上很快就能得到热心的来自世界各地的开发者的解答。

我任然知道很多人不想使用 stackoverflow 的原因,但还是那句话,重要的事情说三遍:在目前这样的技术环境下,熟练阅读英文技术文档和书籍是一个合格的开发者必备的技能之一。如果英语对你来说是一个大的挑战,你应该为此制定一个长期的英语学习计划。

短期来说,可以寻找一些国内的问答社区。例如可以把 segmentfault 看作是国内版的 stackoverflow,尽管其获取解答的时效性可能不及 stackoverflow。还有国内的技术社区 v2ex 的 Python 板块,也是国内较为活跃,大牛云集的地方。但在这些地方,请讨论一些有价值的问题,而不是 stupid question。还有为了解决 django 开发者的问题,我搭建一个论坛 pythonzhcn,我推荐在以上一些地方发表了问题后,可以转载到论坛上来,一些朋友(包括我)看到了的话有时间就可以为你提供解答。

求助有经验的开发者

如果你使用了以上方法任然无法获得问题的解决方案,那说明你这个问题有一定挑战性了。你可以请教一些有经验的人,或者也欢迎把问题发给我,如果我有能力和时间的话,会和你一起探讨这个问题。但请确保正确的提问方式,只有问题越清晰明确,信息越完整,回答者才能尽快地使用他们的经验为你寻找解决方案,否则在来回的沟通过程中会浪费大量的时间(如何正确提问请看下方)。

如何正确提问

正确的提问就是要保证问题的目的性、完整性、清晰性、明确性、和信息量。当然如何区分一个问题是否是好问题难以找到一个合适的标准,我们不妨从反面来定义这个问题,以下一些问题我相信你一看就知道是有问题的,但我任然经常收到此类让我不知所措的问题:

我运行开发服务器,但总是报错,运行不起来,我该怎么办?

我在运行程序后提示 XX 异常,这是怎么回事呀?

我写好了代码,但是看不到你所说的效果是怎么回事?

我该怎么在服务器上创建一个文件并写入内容?

诸如此类,总之以上问题的通病就是信息不完整,或者只有一个问题的笼统描述,或者就只有一个程序的异常报错信息,我相信这种问题即使是相关技术的创始人恐怕也很难回答,更别说我这种只比你多学了一点点东西的老菜鸟了。

推荐阅读这一篇文章:能有效解决问题的提问方法


最后,如果有 Python 和 Django 相关的问题,欢迎和我讨论交流,当然前提是你已经按照这篇文章的指导对问题进行了正确的处理。

我的个人博客:追梦人物的博客

本文转载自: 掘金

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

尝试Java加锁新思路:原子变量和非阻塞同步算法

发表于 2017-11-30

进年以来,并发算法领域的重点都围绕在非拥塞算法,该种算法依赖底层硬件对于原子性指令的支持,避免使用锁来维护数据一致性和多线程安全。非拥塞算法虽然在设计上更为复杂,但是拥有更好的可伸缩性和性能,被广泛应用于实现计数器、序列发生器和统计数据收集器等

  1. 锁的劣势

前文中曾经对比同步方法的内置锁相比和显式锁,来说明它们各自的优势,但是无论是内置说还是显式锁,其本质都是通过加锁来维护多线程安全。

由于加锁机制,线程在申请锁和等待锁的过程中,必然会造成线程的挂起和恢复,这样的线程上线文间切换会带来很大的资源开销,尤其是在锁资源竞争激烈的情况下。

同时,线程在等待锁的过程中,因为阻塞而什么也做,无限条件的等待不仅性能效率不佳,同时也容易造成死锁。

  1. 悲观锁和乐观锁

无论是内置锁还是显式锁,都是一种独占锁,也是悲观锁。所谓悲观锁,就是以悲观的角度出发,认为如果不上锁,一定会有其他线程修改数据,破坏一致性,影响多线程安全,所以必须通过加锁让线程独占资源。

与悲观锁相对,还有更高效的方法——乐观锁,这种锁需要借助冲突检查机制来判断在更新的过程中是否存在来气其他线程的干扰,如果没有干扰,则操作成功,如果存在干扰则操作失败,并且可以重试或采取其他策略。换而言之,乐观锁需要原子性“读-改-写”指令的支持,来读取数据是否被其他线程修改,改写数据内容并将最新的数据写回到原有地址。现在大部分处理器以及可以支持这样的操作。

  1. 比较并交换操作CAS

大部分处理器框架是通过实现比较并交换(Compare and Swap,CAS)指令来实现乐观锁。CAS指令包含三个操作数:需要读写的内存位置V,进行比较的值A和拟写入新值B。当且仅当V处的值等于A时,才说明V处的值没有被修改过,指令才会使用原子方式更新其为B值,否者将不会执行任何操作。无论操作是否执行, CAS都会返回V处原有的值。下面的代码模仿了CAS的语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码public class SimulatedCAS {
@GuardedBy("this") private int value;

public synchronized int get() {
return value;
}

// CAS = compare and swap
public synchronized int compareAndSwap(int expectedValue,
int newValue) {
int oldValue = value;
if (oldValue == expectedValue)
value = newValue;
return oldValue;
}

public synchronized boolean compareAndSet(int expectedValue,
int newValue) {
return (expectedValue
== compareAndSwap(expectedValue, newValue));
}
}

当多个线程尝试更新同一个值时,只会有一个线程成功,其他线程都会失败,但是在CAS中,失败的线程不会被拥塞,可以自主定义失败后该如何处理,是重试还是取消操作,更具有灵活性。

通常CAS的使用方法为:先从V中读取A值,并根据A值计算B值,然后再通过CAS以原子的方法各部分更新V中的值。以计数器为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class CasCounter {
private SimulatedCAS value;

public int getValue() {
return value.get();
}

public int increment() {
int v;
do {
// 获得当前的值
v = value.get();
} while (v != value.compareAndSwap(v, v + 1));
// 如果返回值不同,则说明更新成功了
return v + 1;
}
}

以不加锁的方式实现了原子的“读-改-写”操作。

CAS的方法在性能上有很大优势:在竞争程度不是很大的情况下,基于CAS的操作,在性能上远远超过基于锁的计数器;在没有竞争的情况下,CAS的性能更高。

但是CAS的缺点是:将竞争的问题交给调用者来处理,但是悲观锁自身就能处理竞争。

  1. 原子变量

随着硬件上对于原子操作指令的支持,Java中也引入CAS。对于int、long和对象的引用,Java都支持CAS操作,也就是原子变量类,JVM会把对于原子变量类的操作编译为底层硬件提供的最有效的方法:如果硬件支持CAS,则编译为CAS指令,如果不支持,则编译为上锁的操作。

原子变量比锁的粒度更细, 更为轻量级,将竞争控制在单个变量之上。因为其不需要上锁,所以不会引发线程的挂起和恢复,因此避免了线程间上下文的切换,性能更好,不易出现延迟和死锁的现象。

常见的原子变量有AtomicInteger、AtomicLong、AtomicBoolean和AtomicReference,这些类都支持原子操作,使用get和set方法来获取和更新对象。原子变量数组只支持AtomicInteger、AtomicLong和AtomicReference类型,保证数组中每个元素都是可以以volatile语义被访问。

需要注意的是原子变量没有定义hashCode和equals方法,所以每个实例都是不同的,不适合作为散列容器的key。

原子变量可以被视为一种更好volatile变量,通过compareAndSet方法尝试以CAS方式更新数据,下面以实现数字区间为示例代码展示如何使用AtomicReference。

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
复制代码public class CasNumberRange {
@Immutable
private static class IntPair {
// INVARIANT: lower <= upper
final int lower;
final int upper;

public IntPair(int lower, int upper) {
this.lower = lower;
this.upper = upper;
}
}


//源自引用 IntPair 初始化为[0,0]
private final AtomicReference<IntPair> values =
new AtomicReference<IntPair>(new IntPair(0, 0));

public int getLower() {
return values.get().lower;
}

public int getUpper() {
return values.get().upper;
}

//设置下限
public void setLower(int i) {
//开始循环尝试
while (true) {
// 获得变量值
IntPair oldv = values.get();
// 如果下限设置比当前上限还要大
if (i > oldv.upper)
//抛出异常
throw new IllegalArgumentException("Can't set lower to " + i + " > upper");
IntPair newv = new IntPair(i, oldv.upper);
//原子性更新
if (values.compareAndSet(oldv, newv))
//如果更新成功则直接返回,否者重新尝试
return;
}
}

//设置上限 过程和setLower类似
public void setUpper(int i) {
while (true) {
IntPair oldv = values.get();
if (i < oldv.lower)
throw new IllegalArgumentException("Can't set upper to " + i + " < lower");
IntPair newv = new IntPair(oldv.lower, i);
if (values.compareAndSet(oldv, newv))
return;
}
}
}

性能对比:

前文已经提过,原子变量因其使用CAS的方法,在性能上有很大优势:在竞争程度不是很大的情况下,基于CAS的操作,在性能上远远超过基于锁的计数器;在没有竞争的情况下,CAS的性能更高;但是在高竞争的情况下,加锁的性能将会超过原子变量性能(类似于,交通略拥堵时,环岛疏通效果好,但是当交通十分拥堵时,信号灯能够实现更高的吞吐量)。

不过需要说明的是,在真实的使用环境下,资源竞争的强度绝大多数情况下不会大到可以让锁的性能超过原子变量。所以还是应该优先考虑使用原子变量。

锁和原子变量在不同竞争程度上性能差异很好地说明了各自的优势:在中低程度的竞争之下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能够有效地避免竞争。

当然,如果能避免在多线程间使用共享状态,转而使用线程封闭(如ThreadLocal),代码的性能将会更进一步地提高。

  1. 非阻塞算法

如果某种算法中,一个线程的失败或者挂起不会导致其他线程也失败和挂起,这该种算法是非阻塞的算法。如果在算法的每一步中都存在某个线程能够执行下去,那么该算法是无锁(Lock-free)的算法。

如果在算法中仅仅使用CAS用于协调线程间的操作,并且能够正确的实现,那么该算法既是一种无阻塞算法,也是一种无锁算法。在非拥塞算法中,不会出现死锁的优先级反转的问题(但是不排除活锁和资源饥饿的问题,因为算法中会反复尝试)。

上文中的CasNumberRange 就是一种非阻塞算法,其很好的说明了非拥塞算法设计的基本模式:在更新某个值时存在不确定性,如果失败就重新尝试。其中关键点在于将执行CAS的范围缩小在单一变量上。

5.1 非阻塞的栈

我们以非阻塞的栈为例说明非拥塞算法的设计思路。创建非阻塞算法的关键在于将原子修改的范围缩小到单个变量上,同时保证数据一致性。

栈是最简单的链式数据结构:每个元素仅仅指向一个元素,每个元素也仅被一个元素引用,关键的操作入栈(push)和出栈(pop)都是针对于栈顶元素(top)的。因此每次操作只需要保证栈顶元素的一致性,将原子操作的范围控制在指向栈顶元素的引用即可。实例代码如下:

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
复制代码//非阻塞的并发栈
public class ConcurrentStack <E> {
//原子对象 栈顶元素
AtomicReference<Node<E>> top = new AtomicReference<Node<E>>();

public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do { //循环尝试
oldHead = top.get();//获得旧值
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead)); //比较旧值是否被修改,如果没有则操作成功,否者继续尝试;
}

public E pop() {
Node<E> oldHead;
Node<E> newHead;
do {
oldHead = top.get();
if (oldHead == null)
return null;
newHead = oldHead.next;
} while (!top.compareAndSet(oldHead, newHead));
return oldHead.item;
}

private static class Node <E> {
public final E item;
public Node<E> next;

public Node(E item) {
this.item = item;
}
}
}

以上代码充分体现了非阻塞算法的特点:某项操作的完成具有不确定性,如不成功必须重新执行。这个栈通过compareAndSet来修改栈顶元素,该方法为原子操作,如果发现被其他线程干扰,则修改操作失败,方法将重新尝试。

算法中的多线程安全性依赖于compareAndSet,其提供和加锁机制一样的安全性。既保证原子性,有保证了可见性。除此之外,AtomicReference对象上使用get方法,也保证了内存可见性, 和使用volatile变量一样。

5.2 非阻塞的链表

链表的结构比栈更为复杂,其必须支持头指针和尾指针,且同时有两个指针指向尾部,分别是尾指针和最后一个元素next指针。如何保证两个指针的数据一致性是一个难题,这不能通过一个CAS操作来完成。

这个难题可以应用这样一个技巧来解决:当线程B发现线程A正在修改数据结构时,数据结构中应该有足够多的信息使得线程B能帮助线程A完成操作,保证数据结构维持一致性。

我们以插入操作为例分析。在插入过程中有两个步骤:

  1. 插入新节点,将原有尾节点的next域指向该节点;
  2. 将尾指针移动到新的尾节点处。

所以我们可以根据尾节点的next域判断链表是否在稳定状态:如尾节点的next域为null,则说明该链表是稳定状态,没有其他线程在执行插入操作;反之,节点的next域不为null,则说明有其他线程在插入数据。

如果链表不处于稳定状态该怎么办呢?可以让后到的线程帮助正在插入的线程将尾部指针向后推移到新插入的节点处。示例代码如下:

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
复制代码public class LinkedQueue <E> {

private static class Node <E> {
final E item;
//下一个节点
final AtomicReference<Node<E>> next;

public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<Node<E>>(next);
}
}

//哑结点 也是头结点
private final Node<E> dummy = new Node<E>(null, null);
private final AtomicReference<Node<E>> head
= new AtomicReference<Node<E>>(dummy);
//尾部节点
private final AtomicReference<Node<E>> tail
= new AtomicReference<Node<E>>(dummy);

public boolean put(E item) {
Node<E> newNode = new Node<E>(item, null);
while (true) {
Node<E> curTail = tail.get();
Node<E> tailNext = curTail.next.get();
//得到尾部节点
if (curTail == tail.get()) {
// 1. 尾部节点的后续节点不为空,则队列处于不一致的状态
if (tailNext != null) {
// 2. 将为尾部节点向后退进;
tail.compareAndSet(curTail, tailNext);
} else {
// 3. 尾部节点的后续节点为空,则队列处于一致的状态,尝试更新
if (curTail.next.compareAndSet(null, newNode)) {
// 4. 更新成功,将为尾部节点向后退进;
tail.compareAndSet(curTail, newNode);
return true;
}
}
}
}
}
}

假如步骤一处发现链表处在非稳定状态,则会以原子的方法尝试将尾指针移动到新插入的节点,无论是否成功这时链表都会回到稳定状态,tail.next=null,此时再去重新新尝试。如果步骤二出已经将链表的尾指针移动,则步骤四处的原子操作就会失败,不过这没有关系,因为别的线程已经帮助其完成了该操作,链表保持稳定状态。

5.3 原子域更新器

上面提到的非拥塞链表,在ConcurrentLinkedQueue就有所应用,但是ConcurrentLinkedQueue并不是使用原子变量,而是使用普通的volatile变量,通过基于反射的原子域更新器(AtomicReferenceFieldUpdater)来进行更新。

原子域更新器是现有volatile域的一种基于反射的“视图”,能够在volatile域上使用CAS指令。原子域更新器没有构造器,要构建对象需要使用工厂方法newUpdater,函数然注释如下

1
2
3
4
5
6
7
8
复制代码    /**
* @param tclass 持有待更新域的类
* @param vclass 待更新域的类型
* @param fieldName 待更新域的名字
*/
public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
Class<W> vclass,
String fieldName);

使用更新器的好处在于避免构建原子变量的开销,但是这只适用于那些频繁分配且生命周期很短对象,比如列表的节点,其他情况下使用原子变量即可。

5.4 带有版本号原子变量

CAS操作是通过比较值来判断原值是否被修改,但是还有可能出现这样的情况:原值为A被修改为B,然后又被修改为A,也就是A-B-A的修改情况。这时再通过比较原值就不能判断是否被修改了。这个问题也被称为ABA问题。

ABA问题的解决方案是为变量的值加上版本号,只要版本号变化,就说明原值被修改了,这就是带有时间戳的原子变量AtomicStampedReference

1
2
复制代码//原值和时间戳
public AtomicStampedReference(V initialRef, int initialStamp);

总结

非拥塞算法通过底层CAS指令来维护多线程的安全性,CAS指令被封装成原子变量的形式对外公开,是一种更好的volatile变量,可以提供更好伸缩性,防止死锁,但是设计和实现较为复杂,对开发人员要求很高。

扩展阅读:

  1. 多线程安全性:每个人都在谈,但是不是每个人都谈地清
  2. 对象共享:Java并发环境中的烦心事
  3. 从Java内存模型角度理解安全初始化
  4. 从任务到线程:Java结构化并发应用程序
  5. 关闭线程的正确方法:“优雅”的中断
  6. 驾驭Java线程池:定制与扩展
  7. 探秘Java并发模块:容器与工具类
  8. Java高级上锁机制:显式锁 ReentrantLock

本文转载自: 掘金

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

JDK不同操作系统的FileSystem(Windows)下

发表于 2017-11-30

前言

我们知道不同的操作系统有各自的文件系统,这些文件系统又存在很多差异,而Java 因为是跨平台的,所以它必须要统一处理这些不同平台文件系统之间的差异,才能往上提供统一的入口。

关于FileSystem类

JDK 里面抽象出了一个 FileSystem 来表示文件系统,不同的操作系统通过继承该类实现各自的文件系统,比如 Windows NT/2000 操作系统则为 WinNTFileSystem,而 unix-like 操作系统为 UnixFileSystem。

需要注意的一点是,WinNTFileSystem类 和 UnixFileSystem类并不是在同一个 JDK 里面,也就是说它们是分开的,你只能在 Windows 版本的 JDK 中找到 WinNTFileSystem,而在 Linux 版本的 JDK 中找到 UnixFileSystem,同样地,其他操作系统也有自己的文件系统实现类。

这里分成两个系列分析 JDK 对两种(Windows 和Linux)操作系统的文件系统的实现类,先讲 Windows操作系统,对应为 WinNTFileSystem 类。 由于篇幅较长,《JDK不同操作系统的FileSystem(Windows)》分为上中下篇,此为下篇。

继承结构

1
2
3
复制代码--java.lang.Object
--java.io.FileSystem
--java.io.WinNTFileSystem

createFileExclusively方法

该方法用于创建文件,本地方法逻辑是,

  1. 将路径转成宽字符形式。
  2. 判断路径是否为系统保留设备名。
  3. 调用 CreateFileW 函数创建文件,使用了 CREATE_NEW 模式,仅仅在不存在该文件时才创建。
  4. 如果已经存在该文件,尽量不抛出异常,而是返回 false,此过程还会尝试读取该文件的属性,失败则抛IO异常。
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
复制代码public native boolean createFileExclusively(String path)
throws IOException;

JNIEXPORT jboolean JNICALL
Java_java_io_WinNTFileSystem_createFileExclusively(JNIEnv *env, jclass cls,
jstring path)
{
HANDLE h = NULL;
WCHAR *pathbuf = pathToNTPath(env, path, JNI_FALSE);
if (pathbuf == NULL)
return JNI_FALSE;
if (isReservedDeviceNameW(pathbuf)) {
free(pathbuf);
return JNI_FALSE;
}
h = CreateFileW(
pathbuf,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
CREATE_NEW,
FILE_ATTRIBUTE_NORMAL |
FILE_FLAG_OPEN_REPARSE_POINT,
NULL);

if (h == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
if ((error != ERROR_FILE_EXISTS) && (error != ERROR_ALREADY_EXISTS)) {
DWORD a = GetFileAttributesW(pathbuf);
if (a == INVALID_FILE_ATTRIBUTES) {
SetLastError(error);
JNU_ThrowIOExceptionWithLastError(env, "Could not open file");
}
}
free(pathbuf);
return JNI_FALSE;
}
free(pathbuf);
CloseHandle(h);
return JNI_TRUE;
}

list方法

该方法用于列出指定目录下的所有文件和目录,本地方法处理逻辑如下,

  1. 获取 java/lang/String类对象,并检查不能为NULL。
  2. 获取 File 对象对应的路径。
  3. 按照路径长度重新分配空间并将路径拷贝到 search_path。
  4. 通过 GetFileAttributesW 函数获取指定路径的文件属性,如果得到 INVALID_FILE_ATTRIBUTES 或如果为目录则直接返回NULL。
  5. 去除尾部多余的空格。
  6. 在路径的尾部添加*或\*。
  7. 通过 FindFirstFileW 函数获取到第一个文件。
  8. 接着通过 while 循环和 FindNextFileW 函数不断获取下一个文件,并将得到的文件名放到字符串数组中,而且文件名不能为.和..。
  9. 返回文件名数组,其中可以看到文件名数组的初始长度为16,如果超过该长度后则按照原来长度的两倍重新创建字符串数组对象,再将原数组复制到新数组中。
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
复制代码public native String[] list(File f);

JNIEXPORT jobjectArray JNICALL
Java_java_io_WinNTFileSystem_list(JNIEnv *env, jobject this, jobject file)
{
WCHAR *search_path;
HANDLE handle;
WIN32_FIND_DATAW find_data;
int len, maxlen;
jobjectArray rv, old;
DWORD fattr;
jstring name;
jclass str_class;
WCHAR *pathbuf;

str_class = JNU_ClassString(env);
CHECK_NULL_RETURN(str_class, NULL);

pathbuf = fileToNTPath(env, file, ids.path);
if (pathbuf == NULL)
return NULL;
search_path = (WCHAR*)malloc(2*wcslen(pathbuf) + 6);
if (search_path == 0) {
free (pathbuf);
errno = ENOMEM;
JNU_ThrowOutOfMemoryError(env, "native memory allocation failed");
return NULL;
}
wcscpy(search_path, pathbuf);
free(pathbuf);
fattr = GetFileAttributesW(search_path);
if (fattr == INVALID_FILE_ATTRIBUTES) {
free(search_path);
return NULL;
} else if ((fattr & FILE_ATTRIBUTE_DIRECTORY) == 0) {
free(search_path);
return NULL;
}

len = (int)wcslen(search_path);
while (search_path[len-1] == L' ') {
len--;
}
search_path[len] = 0;

if ((search_path[0] == L'\\' && search_path[1] == L'\0') ||
(search_path[1] == L':'
&& (search_path[2] == L'\0'
|| (search_path[2] == L'\\' && search_path[3] == L'\0')))) {
wcscat(search_path, L"*");
} else {
wcscat(search_path, L"\\*");
}

handle = FindFirstFileW(search_path, &find_data);
free(search_path);
if (handle == INVALID_HANDLE_VALUE) {
if (GetLastError() != ERROR_FILE_NOT_FOUND) {
return NULL;
} else {
rv = (*env)->NewObjectArray(env, 0, str_class, NULL);
return rv;
}
}

len = 0;
maxlen = 16;
rv = (*env)->NewObjectArray(env, maxlen, str_class, NULL);
if (rv == NULL)
return NULL;
do {
if (!wcscmp(find_data.cFileName, L".")
|| !wcscmp(find_data.cFileName, L".."))
continue;
name = (*env)->NewString(env, find_data.cFileName,
(jsize)wcslen(find_data.cFileName));
if (name == NULL)
return NULL;
if (len == maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, maxlen <<= 1, str_class, NULL);
if (rv == NULL || JNU_CopyObjectArray(env, rv, old, len) < 0)
return NULL;
(*env)->DeleteLocalRef(env, old);
}
(*env)->SetObjectArrayElement(env, rv, len++, name);
(*env)->DeleteLocalRef(env, name);

} while (FindNextFileW(handle, &find_data));

if (GetLastError() != ERROR_NO_MORE_FILES)
return NULL;
FindClose(handle);

if (len < maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, len, str_class, NULL);
if (rv == NULL)
return NULL;
if (JNU_CopyObjectArray(env, rv, old, len) < 0)
return NULL;
}
return rv;
}

createDirectory方法

该方法用来创建目录,本地方法很简单,就是获取 File 对象对应的路径,再调用 CreateDirectoryW 函数创建目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public native boolean createDirectory(File f);

JNIEXPORT jboolean JNICALL
Java_java_io_WinNTFileSystem_createDirectory(JNIEnv *env, jobject this,
jobject file)
{
BOOL h = FALSE;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
if (pathbuf == NULL) {
return JNI_FALSE;
}
h = CreateDirectoryW(pathbuf, NULL);
free(pathbuf);

if (h == 0) {
return JNI_FALSE;
}

return JNI_TRUE;
}

setLastModifiedTime方法

该方法用来设置文件或目录的最后修改时间,本地方法是先获取 File 对象对应的路径,再用 CreateFileW 函数打开指定文件或目录,最后用 SetFileTime 函数设置最后修改时间。

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
复制代码public native boolean setLastModifiedTime(File f, long time);

JNIEXPORT jboolean JNICALL
Java_java_io_WinNTFileSystem_setLastModifiedTime(JNIEnv *env, jobject this,
jobject file, jlong time)
{
jboolean rv = JNI_FALSE;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
HANDLE h;
if (pathbuf == NULL)
return JNI_FALSE;
h = CreateFileW(pathbuf,
FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
0);
if (h != INVALID_HANDLE_VALUE) {
LARGE_INTEGER modTime;
FILETIME t;
modTime.QuadPart = (time + 11644473600000L) * 10000L;
t.dwLowDateTime = (DWORD)modTime.LowPart;
t.dwHighDateTime = (DWORD)modTime.HighPart;
if (SetFileTime(h, NULL, NULL, &t)) {
rv = JNI_TRUE;
}
CloseHandle(h);
}
free(pathbuf);

return rv;
}

setReadOnly方法

该方法用于将指定文件设置成只读。本地方法逻辑为,

  1. 获取 File 对象对应的路径。
  2. 通过 GetFileAttributesW 函数获取文件属性。
  3. 如果文件属于超链接或者快捷方式,则先获取对应的最终路径,然后再用 GetFileAttributesW 函数获取文件属性。
  4. 判断不为目录的话则通过 SetFileAttributesW 函数设置文件为只读,对应标识为 FILE_ATTRIBUTE_READONLY。
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
复制代码public native boolean setReadOnly(File f);

JNIEXPORT jboolean JNICALL
Java_java_io_WinNTFileSystem_setReadOnly(JNIEnv *env, jobject this,
jobject file)
{
jboolean rv = JNI_FALSE;
DWORD a;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
if (pathbuf == NULL)
return JNI_FALSE;
a = GetFileAttributesW(pathbuf);

if ((a != INVALID_FILE_ATTRIBUTES) &&
((a & FILE_ATTRIBUTE_REPARSE_POINT) != 0))
{
WCHAR *fp = getFinalPath(env, pathbuf);
if (fp == NULL) {
a = INVALID_FILE_ATTRIBUTES;
} else {
free(pathbuf);
pathbuf = fp;
a = GetFileAttributesW(pathbuf);
}
}

if ((a != INVALID_FILE_ATTRIBUTES) &&
((a & FILE_ATTRIBUTE_DIRECTORY) == 0)) {
if (SetFileAttributesW(pathbuf, a | FILE_ATTRIBUTE_READONLY))
rv = JNI_TRUE;
}
free(pathbuf);
return rv;
}

delete方法

该方法用于删除 File 对象指定路径,需要将标准路径缓存和标准路径前缀缓存都清掉,然后调用本地方法 delete0 执行删除操作。

1
2
3
4
5
6
7
复制代码public boolean delete(File f) {
cache.clear();
prefixCache.clear();
return delete0(f);
}

private native boolean delete0(File f);

本地方法先获取 File 对象对应的路径,然后再调用 removeFileOrDirectory 函数删除目录或文件。而 removeFileOrDirectory 函数的逻辑是先将指定路径文件或目录设置成 FILE_ATTRIBUTE_NORMAL,然后再用 GetFileAttributesW 函数获取文件属性,最后如果指定路径为目录则调用 RemoveDirectoryW 函数删除目录,如果是文件则调用 DeleteFileW 函数删除文件。

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
复制代码JNIEXPORT jboolean JNICALL
Java_java_io_WinNTFileSystem_delete0(JNIEnv *env, jobject this, jobject file)
{
jboolean rv = JNI_FALSE;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);
if (pathbuf == NULL) {
return JNI_FALSE;
}
if (removeFileOrDirectory(pathbuf) == 0) {
rv = JNI_TRUE;
}
free(pathbuf);
return rv;
}

static int
removeFileOrDirectory(const jchar *path)
{
DWORD a;

SetFileAttributesW(path, FILE_ATTRIBUTE_NORMAL);
a = GetFileAttributesW(path);
if (a == INVALID_FILE_ATTRIBUTES) {
return 1;
} else if (a & FILE_ATTRIBUTE_DIRECTORY) {
return !RemoveDirectoryW(path);
} else {
return !DeleteFileW(path);
}
}

rename方法

该方法用于重命名文件,需要将标准路径缓存和标准路径前缀缓存都清掉,然后调用本地方法 rename0 执行重命名操作。

1
2
3
4
5
6
7
复制代码public boolean rename(File f1, File f2) {
cache.clear();
prefixCache.clear();
return rename0(f1, f2);
}

private native boolean rename0(File f1, File f2);

本地方法分别先获取原来的文件路径和重命名的文件路径,再通过 _wrename 函数进行重命名操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码JNIEXPORT jboolean JNICALL
Java_java_io_WinNTFileSystem_rename0(JNIEnv *env, jobject this, jobject from,
jobject to)
{

jboolean rv = JNI_FALSE;
WCHAR *frompath = fileToNTPath(env, from, ids.path);
WCHAR *topath = fileToNTPath(env, to, ids.path);
if (frompath != NULL && topath != NULL && _wrename(frompath, topath) == 0) {
rv = JNI_TRUE;
}
free(frompath);
free(topath);
return rv;
}

access方法

该方法用于检查指定路径文件或目录是否可读,这里主要是JVM层的权限检查,所以用的是 SecurityManager 安全管理器来检测。

1
2
3
4
5
6
7
8
9
复制代码private boolean access(String path) {
try {
SecurityManager security = System.getSecurityManager();
if (security != null) security.checkRead(path);
return true;
} catch (SecurityException x) {
return false;
}
}

listRoots方法

该方法用于获取可用的文件系统的根文件对象的数组。逻辑如下,

  1. 先通过 listRoots0 本地方法获取所有根文件。
  2. listRoots0 方法很简单,就是直接用 GetLogicalDrives 函数获取到操作系统的逻辑驱动器字符。需要注意的是 GetLogicalDrives 返回的是一个 int 类型,那么它是怎么表示驱动器字符的呢?其实也是通过位来标识,每一位对应表示一个逻辑驱动器是否存在,比如第一位如果是”1”则表示驱动器”A:”存在, 第二位如果是“1”则表示驱动器“B:”存在,以此类推。
  3. 得到所有驱动器字符后通过一个for循环遍历检测驱动器的访问权限,去掉无权限的驱动器,并统计一个有n个驱动器,这里只需要循环26次,因为最多就是26个大写字母。
  4. 实例化一个 File 数组,大小为n。
  5. 再次通过一个26次的循环遍历得到有权限的驱动器,根据此驱动器符号实例化一个 File 对象,添加到 File 数组中。
  6. 返回 File 数组。
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
复制代码public File[] listRoots() {
int ds = listRoots0();
int n = 0;
for (int i = 0; i < 26; i++) {
if (((ds >> i) & 1) != 0) {
if (!access((char)('A' + i) + ":" + slash))
ds &= ~(1 << i);
else
n++;
}
}
File[] fs = new File[n];
int j = 0;
char slash = this.slash;
for (int i = 0; i < 26; i++) {
if (((ds >> i) & 1) != 0)
fs[j++] = new File((char)('A' + i) + ":" + slash);
}
return fs;
}

private static native int listRoots0();

JNIEXPORT jint JNICALL
Java_java_io_WinNTFileSystem_listRoots0(JNIEnv *env, jclass ignored)
{
return GetLogicalDrives();
}

getSpace方法

该方法用于获取文件空间大小,包括总空间大小、剩余空间大小和可用空间大小,Java 层分别用 SPACE_TOTAL = 0 SPACE_FREE = 1 SPACE_USABLE = 2标识。要查询某个文件的根目录的某某空间大小则将对应的标识传入,通过 getSpace0 本地方法获得。

1
2
3
4
5
6
7
8
复制代码public long getSpace(File f, int t) {
if (f.exists()) {
return getSpace0(f, t);
}
return 0;
}

private native long getSpace0(File f, int t);

本地方法的逻辑是,

  1. 获取 File 对象对应的文件或目录路径。
  2. 通过 GetVolumePathNameW 函数获取指定路径对应的根路径。
  3. 通过 GetDiskFreeSpaceExW 函数将总空间大小、空闲空间大小和可用空间大小获取到。
  4. 根据传入的标识返回不同的指标,比如 SPACE_TOTAL则返回总空间大小,其他类似。
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
复制代码JNIEXPORT jlong JNICALL
Java_java_io_WinNTFileSystem_getSpace0(JNIEnv *env, jobject this,
jobject file, jint t)
{
WCHAR volname[MAX_PATH_LENGTH + 1];
jlong rv = 0L;
WCHAR *pathbuf = fileToNTPath(env, file, ids.path);

if (GetVolumePathNameW(pathbuf, volname, MAX_PATH_LENGTH)) {
ULARGE_INTEGER totalSpace, freeSpace, usableSpace;
if (GetDiskFreeSpaceExW(volname, &usableSpace, &totalSpace, &freeSpace)) {
switch(t) {
case java_io_FileSystem_SPACE_TOTAL:
rv = long_to_jlong(totalSpace.QuadPart);
break;
case java_io_FileSystem_SPACE_FREE:
rv = long_to_jlong(freeSpace.QuadPart);
break;
case java_io_FileSystem_SPACE_USABLE:
rv = long_to_jlong(usableSpace.QuadPart);
break;
default:
assert(0);
}
}
}

free(pathbuf);
return rv;
}

getNameMax方法

该方法用于获取系统允许的最大文件名长度。在调用 getNameMax0 本地方法前会先做一些处理,如果路径时绝对路径,则获取根路径并加上 \\。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public int getNameMax(String path) {
String s = null;
if (path != null) {
File f = new File(path);
if (f.isAbsolute()) {
Path root = f.toPath().getRoot();
if (root != null) {
s = root.toString();
if (!s.endsWith("\\")) {
s = s + "\\";
}
}
}
}
return getNameMax0(s);
}

private native int getNameMax0(String path);

本地方法中通过 GetVolumeInformationW 函数得到系统允许的最大文件名长度 maxComponentLength

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
复制代码JNIEXPORT jint JNICALL
Java_java_io_WinNTFileSystem_getNameMax0(JNIEnv *env, jobject this,
jstring pathname)
{
BOOL res = 0;
DWORD maxComponentLength;

if (pathname == NULL) {
res = GetVolumeInformationW(NULL,
NULL,
0,
NULL,
&maxComponentLength,
NULL,
NULL,
0);
} else {
WITH_UNICODE_STRING(env, pathname, path) {
res = GetVolumeInformationW(path,
NULL,
0,
NULL,
&maxComponentLength,
NULL,
NULL,
0);
} END_UNICODE_STRING(env, path);
}

if (res == 0) {
JNU_ThrowIOExceptionWithLastError(env,
"Could not get maximum component length");
}

return (jint)maxComponentLength;
}

compare方法

该方法用于比较两个 File 对象,其实就是直接比较路径字符串。

1
2
3
复制代码public int compare(File f1, File f2) {
return f1.getPath().compareToIgnoreCase(f2.getPath());
}

hashCode方法

该方法用于获取 File 对象的哈希值,获取 File对象路径,再将字符串变成小写,再调用字符串的 hashCode 方法,最后与 1234321 进行异或运算,得到的值即为该文件的哈希值。

1
2
3
复制代码public int hashCode(File f) {
return f.getPath().toLowerCase(Locale.ENGLISH).hashCode() ^ 1234321;
}

以下是广告和相关阅读

=============广告时间===============

公众号的菜单已分为“分布式”、“机器学习”、“深度学习”、“NLP”、“Java深度”、“Java并发核心”、“JDK源码”、“Tomcat内核”等,可能有一款适合你的胃口。

鄙人的新书《Tomcat内核设计剖析》已经在京东销售了,有需要的朋友可以购买。感谢各位朋友。

为什么写《Tomcat内核设计剖析》

=========================

相关阅读:

JDK不同操作系统的FileSystem(Windows)上篇
JDK不同操作系统的FileSystem(Windows)中篇

欢迎关注:

这里写图片描述

这里写图片描述

本文转载自: 掘金

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

在Nginx上部署Python Flask应用 在Nginx

发表于 2017-11-30

在Nginx上部署Python Flask应用

由 Find · 2017年11月30日 11 看过

  1. Introduction

本文主要系翻译自digitalocean的教程How To Serve Flask Applications with uWSGI and Nginx on Ubuntu 16.04 ,部分进行了修改。

主要介绍了在nginx服务器上利用uWSGI部署Flask应用的步骤。

之前写过的相关内容:

uwsgi配置https以及python2无法使用supervisor

nginx配置https

  1. 准备工作

在开始之前,先确保有一个非root的用户部署在你的服务器上,这个用户必须使用sudo才能执行管理员命令。

FindHao注:非root用户是为了安全性考虑,但是由于购买的vps系统一般默认root用户,通过lnmp.org一键安装lnmp之后,会创建www用户和www用户组,因此下文的配置是以root用户配置为例,但是期间会插入修改目录权限的步骤。

  1. 安装必要的软件

我们将尽量通过piporpip3来安装包,而非使用发行版源里的工具,避免源里工具版本过低造成的问题。

更新源,并安装python。

python2:

1
2
复制代码apt update
apt install python-pip python-dev

python3:

1
2
复制代码apt update
apt install python3-pip python3-dev
  1. 创建python虚拟环境

4.1 安装virtualenv

为了将我们的Flask应用与系统里的其他python文件项目隔离开,接下来配置python虚拟环境。

使用pip安装virtualenv:

1
2
3
4
复制代码# python2
pip install virtualenv
# python3
pip3 install virtualenv

现在,为我们的Flask应用创建目录:

1
2
复制代码mkdir ~/myproject
cd ~/myproject

或者直接在lnmp的目录里,clone你的项目:

1
2
3
复制代码cd /home/wwwroot/
git clone XXXX/myproject.git
# 或者直接拷贝你的项目到对应的域名目录下

FindHao注:

由于我们是在root下操作的,因此应该将项目的文件所属权都移交给www

1
2
3
> 复制代码chown -R www.www ./*
>
>

由于lnmp会在域名目录下创建一个.user.ini的文件,导致你无法删除lnmp创建的目录,运行下面代码后删除即可:

chattr -i ``/home/wwwroot/yoursite/``.user.ini

如果是需要修改文件,记得修改完以后运行:

chattr +i ``/home/wwwroot/yoursite/``.user.ini

4.2 创建并激活虚拟环境

创建虚拟环境的目录:

1
复制代码virtualenv myenv

myenv的目录是用来存放本地python的镜像,以及后面通过pip安装的包将安装到myenv目录里,而不是系统的目录里。

在安装之前,需要先激活刚刚建立的虚拟环境:

1
复制代码source myenv/bin/activate

你的命令行前面会多个提示,表明你是在虚拟环境操作:

1
复制代码(myenv)user@host:~/myproject#.
  1. 设置Flask应用

5.1 安装Flaks和uWSGI

注意,无论你使用的哪个python版本,进入虚拟环境以后,应该使用pip,而不是pip3

1
复制代码(myenv) # pip install uwsgi flask

5.2 建立sample

现在我们创建一个简单的样例程序来测试flask和uwsgi是否正常运行。

1
复制代码(myenv) # vim index.py

FindHao注:

修改文件权限:

1
2
3
> 复制代码chown www.www index.py
>
>

内容如下:

1
2
3
4
5
6
7
8
9
复制代码from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return "<h1 style='color:blue'>Hello There!</h1>"

if __name__ == "__main__":
app.run(host='0.0.0.0')

如果你开启了防火墙,要在你防火墙规则里关闭对5000端口的禁用。

运行我们的flask sample:

1
复制代码(myenv) # python index.py

在浏览器里访问你的vps ip+:5000端口:

1
复制代码http://server_domain_or_IP:5000

如果看到如下内容表示运行正常,可以CTRL C终止sample的运行继续下一步了。

Flask sample app

  1. 配置uWSGI

6.1 测试uWSGI

首先我们需要测试uWSGI运行正常:

1
复制代码(myenv) # uwsgi --socket 0.0.0.0:5000 --protocol=http -w wsgi:app

你应该再次看到上面的hell there的提示。

CTRL C终止,并deactivate退出虚拟环境

6.2 创建uWSGI配置文件

为了让我们的项目启动和配置更灵活,我们创建uWSGI配置文件:

1
复制代码# vim myproject.ini

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码[uwsgi]
module = wsgi:app

master = true
processes = 5

socket = myproject.sock
chmod-socket = 660
vacuum = true

die-on-term = true

logto = /var/log/myproject.log

[uwsgi]头表示我们这是个uwsgi的配置。module指向wsgi.py文件,文件里的回调是app。

接下来的部分表明项目运行在master mode,并且有五个worker进程处理请求。

在上面测试的时候,我们的uWSGI是通过网络端口暴露出来的,但是由于后面我们将使用nginx来处理实际的client连接,然后传递给uwsgi,而由于这些操作都是在一个机器上运行的,因此这里我们改成socket的模式更安全快速。我们指定socket文件是当前项目目录下的myproject.sock。

我们还必须改变socket的权限。待会我们会迁移uWSGI进程的所属组到nginx的组,因此我们必须确保socket的所属组用户能从它那里读写信息。同时在进程结束的时候,也需要清理socket(vacuum)。

die-on-term,可以确保init system和uWSGI有相同的环境。

logto是保存日志文件。

你可能会注意到,我们没有像前面命令行一样指定一个协议,这是因为uWSGI默认使用uwsgi协议,一个快速的二进制协议,nginx内置了这个协议,用这个协议比http协议更快。

6.3 日志

前面我们指定了logto来保存日志,但是www用户对日志的目录没有写权限,因此需要手动建立这个log文件,并chown给www。

同时由于默认日志是一直在写入,文件会不停的增长,因此还需要使用系统的logrotate日志工具对其进行处理。

创建一个日志的配置文件

1
复制代码# vim /etc/logrotate.d/myproject-log-file

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码/var/log/myproject.log {
monthly
rotate 5
compress
delaycompress
missingok
notifempty
create 644 root root
postrotate
/usr/bin/killall -HUP rsyslogd
endscript
}
  • monthly: 日志文件将按月轮循。其它可用值为‘daily’,‘weekly’或者‘yearly’。
  • rotate 5: 一次将存储5个归档日志。对于第六个归档,时间最久的归档将被删除。
  • compress: 在轮循任务完成后,已轮循的归档将使用gzip进行压缩。
  • delaycompress: 总是与compress选项一起用,delaycompress选项指示logrotate不要将最近的归档压缩,压缩将在下一次轮循周期进行。这在你或任何软件仍然需要读取最新归档时很有用。
  • missingok: 在日志轮循期间,任何错误将被忽略,例如“文件无法找到”之类的错误。
  • notifempty: 如果日志文件为空,轮循不会进行。
  • create 644 root root: 以指定的权限创建全新的日志文件,同时logrotate也会重命名原始日志文件。
  • postrotate/endscript: 在所有其它指令完成后,postrotate和endscript里面指定的命令将被执行。在这种情况下,rsyslogd 进程将立即再次读取其配置并继续运行。
  1. 创建systemd文件

Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数发行版的标准配置。

接下来我们将创建一个service文件来保证系统自动启动uWSGI和我们的Flask应用。

以Debian为例,创建一个myproject.service文件:

1
复制代码# vim /lib/systemd/system/myproject.service

内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码[Unit]
Description=uWSGI instance to serve myproject
After=network.target

[Service]
User=www
Group=www
WorkingDirectory=/home/sammy/myproject
Environment="PATH=/home/sammy/myproject/myprojectenv/bin"
ExecStart=/home/sammy/myproject/myprojectenv/bin/uwsgi --ini myproject.ini

PIDFile=/run/findyoutube.net.pid
ExecReload=/home/wwwroot/www.findyoutube.net/ftbenv/bin/uwsgi --reload /run/findyoutube.net.pid
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/findyoutube.net.pid
TimeoutStopSec=3
KillMode=mixed

[Install]
WantedBy=multi-user.target

7.1 Unit字段

[Unit]区块通常是配置文件的第一个区块,用来定义 Unit 的元数据,以及配置与其他 Unit 的关系。

  • Description:简短描述
  • After:如果该字段指定的 Unit 也要启动,那么必须在当前 Unit 之前启动

7.2 Service字段

[Service]区块用来 Service 的配置,只有 Service 类型的 Unit 才有这个区块。

  • ExecStart:启动当前服务的命令
  • ExecReload:重启当前服务时执行的命令
  • ExecStop:停止当前服务时执行的命令
  • Environment:指定环境变量
  • KillMode: mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号

user和group用来指定进程所属用户和组。lnmp默认是www.www。设置working directory和path来告诉init system我们的项目在哪里,虚拟环境是怎样的。然后执行uwsgi开启Flask应用。

FindHao注:

在这个service文件里,我添加了PIDFile、ExecReload、ExecStop等几个字段,因为如果没有reload和stop字段,要重启我们的Flask应用,只能通过重启vps或者杀掉uwsgi进程的方式。

PIDFile指定了我们Flask应用的进程文件,可以传给后面stop和reload用。

reload使用uwsgi自带的reload。

stop则通过进程文件的防止找到指定进程并结束掉它。

7.3 Install 字段

[Install]通常是配置文件的最后一个区块,用来定义如何启动,以及是否开机启动。

  • WantedBy:它的值是一个或多个 Target,当前 Unit 激活时(enable)符号链接会放入/etc/systemd/system目录下面以 Target 名 + .wants后缀构成的子目录中

7.4 运行

保存上面的配置文件后,常用的systemd命令有:

1
2
3
4
5
6
7
复制代码systemctl start myproject # 开启应用
systemctl enable myproject # 添加开机自启动
systemctl disable myproject # 取消开机启动
systemctl restart myproject # 重启应用
systemctl reload myproject # 重新加载配置
systemctl stop myproject # 停止应用
systemctl status myproject # 查看应用状态

开启应用后,查看应用状态,如果是active,表明成功启动运行,如果failed,可以通过journalctl查看运行日志,并G(vim的查看形式,即Shift + g到最后一行)看看启动的错误是什么。

  1. 配置Nginx来处理请求

如果是lnmp安装的nginx,则nginx的配置目录为/usr/local/nginx/conf/vhost。

如果之前没有安装,则可以通过apt直接安装nginx,但是源里的nginx可能版本比较老,对于一些新特性不支持,建议添加nginx官方源里安装,参考 《nginx配置https》 3.6 nginx 支持HTTP2。单独安装的nginx配置文件目录一般为/etc/nginx/sites-available

在nginx的配置目录新建一个nginx的配置文件myproject,内容如下:

1
2
3
4
5
6
7
8
9
复制代码server {
listen 80;
server_name server_domain_or_IP;

location / {
include uwsgi_params;
uwsgi_pass unix:/home/sammy/myproject/myproject.sock;
}
}

listen表示监听80端口,server_name则是你的域名或者ip。

localtion /则表示对于域名或者ip/的请求处理方式。首先通过包含uwsgi_params来加载一些默认的uWSGI参数。然后uwsgi_pass转发请求给我们定义的socket。

这只是创建了可用的配置文件,如果要启用,还需要将其软连接到enable目录:

1
复制代码# ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled

检查我们的配置文件是否编写正确:

1
复制代码# nginx -t

如果提示ok,则重启nginx:

1
复制代码systemctl restart nginx

访问我们的域名或者ip,可以看成已经正常运行。

Reference

lnmp无法删除目录,目录包含.user.ini

How To Serve Flask Applications with uWSGI and Nginx on Ubuntu 16.04

uwsgi配置https以及python2无法使用supervisor

Systemd 入门教程:命令篇

Linux日志文件总管——logrotate

本文转载自: 掘金

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

基于TP5原有的bootstrap分页做的新样式 - Cod

发表于 2017-11-30

Typechopage.php

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
复制代码<?php
/**
* Created by PhpStorm.
* User: hainuo
* Date: 2017/11/30
* Time: 上午9:36
*/

namespace osc\common\paginator;

use think\Paginator;

class Typechopage extends Paginator
{

/**
* 上一页按钮
* @param string $text
* @return string
*/
protected function getPreviousButton($text = "«")
{

if ($this->currentPage() <= 1) {
return $this->getDisabledTextWrapper($text);
}

$url = $this->url(
$this->currentPage() - 1
);

return $this->getPageLinkWrapper($url, $text);
}

/**
* 下一页按钮
* @param string $text
* @return string
*/
protected function getNextButton($text = '»')
{
if (!$this->hasMore) {
return $this->getDisabledTextWrapper($text);
}

$url = $this->url($this->currentPage() + 1);

return $this->getPageLinkWrapper($url, $text);
}

/**
* 页码跳转按钮
* @return string
*/
protected function getGoToPage()
{

return '<li class=""><span style="padding: 0"><input style="width: 50px;padding: 5px;text-align: center" type="number" id="vThinkPHPGoPage" value="' . $this->currentPage() . '" /></span><button type="button" class="btn btn-sm btn-success" onclick="goPage(document.getElementById(\'vThinkPHPGoPage\').value)">go</button><script>function goPage(p){p=parseInt(p);if(p >0 ){var url = "' . $this->getGoToUrl() . $this->buildFragment() . '"+p;window.location.href=url;}else{alert("请输入正确的页码!");}}</script></li>';
}

/**
* 页码按钮
* @return string
*/
protected function getLinks()
{
if ($this->simple)
return '';

$block = [
'first' => null,
'slider' => null,
'last' => null
];

$side = 3;
$window = $side * 2;

if ($this->lastPage < $window + 6) {
$block['first'] = $this->getUrlRange(1, $this->lastPage);
} elseif ($this->currentPage <= $window) {
$block['first'] = $this->getUrlRange(1, $window + 2);
$block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
} elseif ($this->currentPage > ($this->lastPage - $window)) {
$block['first'] = $this->getUrlRange(1, 2);
$block['last'] = $this->getUrlRange($this->lastPage - ($window + 2), $this->lastPage);
} else {
$block['first'] = $this->getUrlRange(1, 2);
$block['slider'] = $this->getUrlRange($this->currentPage - $side, $this->currentPage + $side);
$block['last'] = $this->getUrlRange($this->lastPage - 1, $this->lastPage);
}

$html = '';

if (is_array($block['first'])) {
$html .= $this->getUrlLinks($block['first']);
}

if (is_array($block['slider'])) {
$html .= $this->getDots();
$html .= $this->getUrlLinks($block['slider']);
}

if (is_array($block['last'])) {
$html .= $this->getDots();
$html .= $this->getUrlLinks($block['last']);
}

return $html;
}

/**
* 渲染分页html
* @return mixed
*/
public function render()
{
if ($this->hasPages()) {
if ($this->simple) {
return sprintf(
'<ul class="pager">%s %s %s</ul>',
$this->getPreviousButton(),
$this->getNextButton(),
$this->getGoToPage()
);
} else {
return sprintf(
'<ul class="pagination">%s %s %s %s</ul>',
$this->getPreviousButton(),
$this->getLinks(),
$this->getNextButton(),
$this->getGoToPage()
);
}
}
}

/**
* 获取跳转的链接 不带页码
*
* @return string
*/
protected function getGoToUrl()
{

$parameters = [];
$path = $this->options['path'];
if (count($this->options['query']) > 0) {
$parameters = array_merge($this->options['query'], $parameters);
}
$url = $path;
if (!empty($parameters)) {
$url .= '?' . urldecode(http_build_query($parameters, null, '&'));
$url .= '&page=';
} else {
$url .= '?page=';
}
return $url;
}

/**
* 生成一个可点击的按钮
*
* @param string $url
* @param int $page
* @return string
*/
protected function getAvailablePageWrapper($url, $page)
{
return '<li><a href="' . htmlentities($url) . '">' . $page . '</a></li>';
}

/**
* 生成一个禁用的按钮
*
* @param string $text
* @return string
*/
protected function getDisabledTextWrapper($text)
{
return '<li class="disabled"><span>' . $text . '</span></li>';
}

/**
* 生成一个激活的按钮
*
* @param string $text
* @return string
*/
protected function getActivePageWrapper($text)
{
return '<li class="active"><span>' . $text . '</span></li>';
}

/**
* 生成省略号按钮
*
* @return string
*/
protected function getDots()
{
return $this->getDisabledTextWrapper('...');
}

/**
* 批量生成页码按钮.
*
* @param array $urls
* @return string
*/
protected function getUrlLinks(array $urls)
{
$html = '';

foreach ($urls as $page => $url) {
$html .= $this->getPageLinkWrapper($url, $page);
}

return $html;
}

/**
* 生成普通页码按钮
*
* @param string $url
* @param int $page
* @return string
*/
protected function getPageLinkWrapper($url, $page)
{
if ($page == $this->currentPage()) {
return $this->getActivePageWrapper($page);
}

return $this->getAvailablePageWrapper($url, $page);
}
}

本文转载自: 掘金

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

Python3 CookBook 数据结构和算法(二)

发表于 2017-11-30

欢迎关注我的微信公众号 AlwaysBeta,更多精彩内容等你来。

以下测试代码全部基于 Python3

1、查找最大或最小的 N 个元素

工作中有时会遇到这样的需求,取出数据中前面 10% 的值,或者最后 10% 的值。

我们可以先对这个列表进行排序,然后再进行切片操作,很轻松的解决这个问题。但是,有没有更好的方法呢?

heapq 模块有两个函数 nlargest() 和 nsmallest() 可以完美解决这个问题。

1
2
3
4
5
6
7
8
9
复制代码In [50]: import heapq

In [51]: n = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2, 23, 45, 76]

In [52]: heapq.nlargest(3, n)
Out[52]: [76, 45, 42]

In [53]: heapq.nsmallest(3, n)
Out[53]: [-4, 1, 2]

如果是取排在前面的 10% 应该怎么做?

1
复制代码heapq.nlargest(round(len(n)/10), n)

而且,使用这两个函数还会有更好的性能,因为在底层实现里面,会先把数据进行堆排序后放入一个列表中,然后再进行后续操作。大家如果对堆数据结构感兴趣的话,可以继续进行深入研究,由于我了解的并不深,也没办法再展开了。

但是也并不是什么时候都是这两个函数效果更好,比如只取一个最大值或者最小值,那还是 min() 或 max() 效果更好;如果要查找的元素个数已经跟集合元素个数接近时,那还是用 sorted(items)[:N] 更好,具体情况具体分析吧。

2、序列中出现次数最多的元素

以前碰到这类问题时,我都会手动创建一个字典,然后以列表中元素作为 key,进而统计出 key 出现的次数,再进行比较得到出现次数最多的元素。

殊不知 collections 中就有专门为这类问题设计的类 Counter,瞬间感觉自己蠢爆了,话不多说,直接上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码In [54]: from collections import Counter

In [55]: w = ['a', 'b', 'c', 'd', 'a', 'a', 'b']

In [56]: w_count = Counter(w)

In [57]: w_count
Out[57]: Counter({'a': 3, 'b': 2, 'c': 1, 'd': 1})

In [58]: w_count['a']
Out[58]: 3

In [59]: top = w_count.most_common(2)

In [60]: top
Out[60]: [('a', 3), ('b', 2)]

可以看到,Counter 返回的就是一个字典,想知道哪个元素出现几次,直接取,是不是很方便?

而且还有 most_common 函数,简直不要太棒。

3、过滤序列元素

有一个列表,如下:

1
复制代码In [61]: a = [1, 2, 3, 4, 5, -3]

要求过滤所有负数。需要新建一个列表?直接一行代码搞定。

1
2
复制代码In [64]: [n for n in a if n > 0]
Out[64]: [1, 2, 3, 4, 5]

如果要把负数替换成 0 呢?

1
2
复制代码In [67]: [n if n > 0 else 0 for n in a]
Out[67]: [1, 2, 3, 4, 5, 0]

但是有时候过滤条件可能比较复杂,这时就需要借助于 filter() 函数了。

1
2
3
4
5
6
7
8
9
10
11
复制代码values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
try:
x = int(val)
return True
except ValueError:
return False

ivals = list(filter(is_int, values))
print(ivals)
# Outputs ['1', '2', '-3', '4', '5']

4、通过某个关键字将记录分组

有下面这个字典:

1
2
3
4
5
6
7
8
9
10
复制代码rows = [
{'address': '5412 N CLARK', 'date': '07/01/2012'},
{'address': '5148 N CLARK', 'date': '07/04/2012'},
{'address': '5800 E 58TH', 'date': '07/02/2012'},
{'address': '2122 N CLARK', 'date': '07/03/2012'},
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'},
{'address': '1060 W ADDISON', 'date': '07/02/2012'},
{'address': '4801 N BROADWAY', 'date': '07/01/2012'},
{'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]

那么怎么对这个字典按照 date 进行分组呢?借助于 itertools.groupby() 函数可以解决这个问题,代码如下:

1
2
3
4
5
6
7
复制代码# Sort by the desired field first
rows.sort(key=itemgetter('date'))
# Iterate in groups
for date, items in groupby(rows, key=itemgetter('date')):
print(date)
for i in items:
print(' ', i)

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码07/01/2012
{'address': '5412 N CLARK', 'date': '07/01/2012'}
{'address': '4801 N BROADWAY', 'date': '07/01/2012'}
07/02/2012
{'address': '5800 E 58TH', 'date': '07/02/2012'}
{'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}
{'address': '1060 W ADDISON', 'date': '07/02/2012'}
07/03/2012
{'address': '2122 N CLARK', 'date': '07/03/2012'}
07/04/2012
{'address': '5148 N CLARK', 'date': '07/04/2012'}
{'address': '1039 W GRANVILLE', 'date': '07/04/2012'}

需要注意的是,groupby() 函数仅仅检查连续相同的元素,所以在分组之前,一定要先对数据,按照分组字段进行排序。如果没有排序,便得不到想要的结果。

5、映射名称到序列元素

我常常有这样的苦恼,就是有一个列表,然后通过下标来取值,取值时很认真的数所需要元素在第几个,很怕取错值。取到值后开始下面的运算。

一段时间之后,再看这段代码,感觉很陌生,已经忘了带下标的值是什么了,还需要重新看一下这个列表的由来,才找到回忆。

如果能有一个名称映射到元素上就好了,直接通过名称就可以知道元素的含义。collections.namedtuple() 函数就可以解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码In [76]: from collections import namedtuple

In [77]: subscriber = namedtuple('Subscriber', ['addr', 'joined'])

In [78]: sub = subscriber('jonesy@example.com', '2012-10-19')

In [79]: sub
Out[79]: Subscriber(addr='jonesy@example.com', joined='2012-10-19')

In [80]: sub.addr
Out[80]: 'jonesy@example.com'

In [81]: sub.joined
Out[81]: '2012-10-19'

这样就可以通过名称来取值了,代码可读性也更高。

需要注意的是,这种命名元祖的方式不能直接修改其中的值,直接修改会报错

1
2
3
4
5
6
7
8
9
10
11
复制代码In [82]: a = namedtuple('SSS', ['name', 'shares', 'price'])

In [83]: _a = a('yongxinz', 1, 2)

In [84]: _a.shares = 4
-----------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-84-f62a5288a29a> in <module>()
> 1 _a.shares = 4

AttributeError: can't set attribute

想要修改的话可以使用 _replace() 函数。

1
2
复制代码In [85]: _a._replace(shares=4)
Out[85]: SSS(name='yongxinz', shares=4, price=2)

但是还有一个疑问,如果这个列表元素比较多的话,那就需要定义很多的名称,也比较麻烦,还有更好的方式吗?

未完待续。。。

本文转载自: 掘金

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

算法入门:堆排序

发表于 2017-11-30

基础概念

堆排序是比较基础的排序算法,也是我认为比较难的一种算法,因为它的流程比较多,理解起来不会像冒泡排序和选择排序那样直观。
要理解堆排序,需要先理解二叉树:
二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree),而二叉树还有个名字叫做二叉堆(看起来一堆。。。)。
二叉堆是一棵被完全填满的二叉树,有例外的可能是底层元素,底层元素从左到右填入,这样的树被称为完全二叉树。
仔细观察可以发现,其实一个数组可以被表示成一个二叉树:


Paste_Image.png
左边为数组,而右边为数组的二叉树的表达方式。

观察上图可以发现,任意一位置i上元素,其左儿子为2i+1上,右儿子在2i+2上。

我们现在的需求是对数组元素进行从小到大排序,那么我们需要根据既定的数据构建堆,这也是堆排序必要的一步。在构建堆的时候,我们需要满足堆序性质。
堆序性质:任意一个节点小于(大于)它的后裔,这取决于你测排序方式。
这里以从小到大排序为例。那么,此时我们需要节点要小于它的后裔,那么这样我们就可以保证根节点是最小的元素。

堆排序主要分三步:
(1).构建堆
(2).调整堆
(3).堆排序

首先需要明确一点,构建堆是在数组基础上构建的,换句话说就是将数组抽象成一个二叉堆,而不需要另构建。
在构建堆之前需要保证一点,构建之后的结构需要堆序性质,什么是堆序性质?
堆序性质所描述的是:在一个二叉堆中任意父节点大于其两个子节点。

堆排序的流程和实现

下面通过一个例子来看一下堆排序是一个怎样的流程。
首先要构建堆,构建堆其实是先将数组抽象成二叉堆之后调整堆的过程。
首先给定数组S、O、R、T、E、X、A、M、P、L、E,排序规则为字典序。
根据已知数组构建堆如下:


Paste_Image.png

上图可以看出来,初始的堆顺序和数组顺序是一致的。显然上图是不符合堆序性质的,那么接下来需要进行堆的调整。
调整堆时我们需要制定一个起始点进行调整,我们将这个起始点定为N/2,N为目标数组的长度。由于二叉堆是一个完全二叉树,那么N/2对应着倒数第二层,且有子节点的最后一个节点(没有子节点不需要进行调整)。那么这个节点是E节点index为5。由于E字典序小于L,同时需要构建大根堆,所以需要交换E和L。


Paste_Image.png
接下来调整到T节点,T节点大于下面两个节点,那么不需要进行调整。


Paste_Image.png
接下来到R节点,由于R小于X,所以进行交换:


Paste_Image.png
接下来调整到O节点,O节点比较特殊,它小于T节点同时小于P节点,所以O节点会下沉到最后一层。


Paste_Image.png
最后调整到S节点,由于S小于X,那么进行调整:


Paste_Image.png

调整之后的结构如上所示,上述堆结构保证了堆的有序,继而能确定全局的最大节点X。
构建堆的代码如下:

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
复制代码   /**
* 此调整为从上到下调整,直到节点超出范围
* @param data
* @param heapSize
* @param index
*/
private static void maxHeap(String[] data,int heapSize,int index){
//取得当前节点的左右节点,当前节点为index
int left=getChildLeftIndex(index);
int right=getChildRightIndex(index);
//对左右节点和当前节点进行比较
int largest=index;
if(left<heapSize&&data[index].compareTo(data[left])<0){
largest=left;
}
if(right<heapSize&&data[largest].compareTo(data[right])<0){
largest=right;
}
//交换位置
if(largest!=index){
String temp=data[index];
data[index]=data[largest];
data[largest]=temp;
maxHeap(data,heapSize,largest);
}
}
/**
* 初始化构建堆
* @param data
*/
private static void buildMaxHeap(String[] data){
//根据最后一个元素获取,开始调整的位置
int startIndex=getParentIndex(data.length-1);
//反复进行调整
for(int i=startIndex;i>=0;i--){
maxHeap(data,data.length,i);
}
}

其实搞定了调整堆,堆排序就成功了一半了,那么接下来需要做的是,循环N次 ,进行N次调整堆操作,每一次调整 堆得到的最大值,将此值和数组的最后一个元素进行交换,交换后“减小”数组的长度(最后n个值不参与堆的调整),直到最后一个元素,就完成了堆的排序.
此过程是从上到下的调整过程,因为构建好之后的堆具有堆序性质,从根节点调整时只选择一个子节点一直进行调整即可。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码   /**
* 排序操作
* @param data
*/
private static void heapSort(String[] data){
//每次循环都能取到一个最大值,该值为根节点
for(int i=data.length-1;i>0;i--){
String temp=data[0];
data[0]=data[i];
data[i]=temp;
//每次调整都是从根节点开始i不断减小,保证前一次最大节点不会参与到调整堆
maxHeap(data,i,0);
}
}

从代码可以看出来,每次调整都是从根节点开始,不断的缩小排序范围。继而达到把所有的节点全部排序。

完整代码如下:

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

public class HeapSort{
private static String[] sort=new String[]{"S","O","R","T","E","X","A","M","P","L",
"E"};

public static void main(String[] args){
buildMaxHeap(sort);
heapSort(sort);
print(sort);
}

/**
* 初始化构建堆
* @param data
*/
private static void buildMaxHeap(String[] data){
//根据最后一个元素获取,开始调整的位置
int startIndex=getParentIndex(data.length-1);
//反复进行调整
for(int i=startIndex;i>=0;i--){
maxHeap(data,data.length,i);
}
}

/**
* 此调整为从上到下调整,直到节点超出范围
* @param data
* @param heapSize
* @param index
*/
private static void maxHeap(String[] data,int heapSize,int index){
//取得当前节点的左右节点,当前节点为index
int left=getChildLeftIndex(index);
int right=getChildRightIndex(index);
//对左右节点和当前节点进行比较
int largest=index;
if(left<heapSize&&data[index].compareTo(data[left])<0){
largest=left;
}
if(right<heapSize&&data[largest].compareTo(data[right])<0){
largest=right;
}
//交换位置
if(largest!=index){
String temp=data[index];
data[index]=data[largest];
data[largest]=temp;
maxHeap(data,heapSize,largest);
}
}

/**
* 排序操作
* @param data
*/
private static void heapSort(String[] data){
//每次循环都能取到一个最大值,该值为根节点
for(int i=data.length-1;i>0;i--){
String temp=data[0];
data[0]=data[i];
data[i]=temp;
//每次调整都是从根节点开始i不断减小,保证前一次最大节点不会参与到调整堆
maxHeap(data,i,0);
}
}

/**
* 获取父节点的位置
* @param current
* @return
*/
private static int getParentIndex(int current){
return(current-1)>>1;
}

/**
* 获得左子节点的位置
* @param current
* @return
*/
private static int getChildLeftIndex(int current){
return(current<<1)+1;
}

/**
* 获得右子节点的位置
* @param current
* @return
*/
private static int getChildRightIndex(int current){
return(current<<1)+2;
}

private static void print(String[] data){
for(int i=0;i<data.length;i++){
System.out.print(data[i]+",");
}
}

}

时间复杂度

堆排序是一种十分高效的排序算法,因为它的排序流程分三步,那么可以分别计算时间复杂度进行相加:
1.构建堆:
构建堆是从N/2处开始进行调整,每一次调整的时间复杂度为节点的深度H,那么N/2次调整则为O(H1)+O(H2)…..O(HnN/2)。由于H为常数,那么时间复杂度为O(N)
2.调整堆:
调整堆比较简单,由二叉堆具有堆序性质,那么调整堆的过程其实,就是堆的深度即lgN
3.堆排序
此过程是进行N-1次调整堆的操作,那么此过程的时间复杂为(N-1)
lgN。
汇总后整体的时间复杂度为O(N+(N-1)*lgN)
~ O(NlgN)
可见堆排序的时间复杂度是比较低的,但是这种排序一般比较适合大数据集合的排序,因为大量使用了递归操作,那么在小数据集的情况下是十分消耗性能的,在小数据集的情况下最好使用插入、选择这种简单的排序算法,往往能起到更好的效果。
对TopK问题的优势:
堆排序另外一个比较好的特性就是TopK,因为堆排序是渐进排序,也就是说不是将所有的数据排序好后输出。这种特性也就决定了,在TopK场景往往能够有更好的表现。

空间复杂度

从上面程序可以看出来,我们并没有引入第二个存储元素,而每一次元素的交换仅仅依靠一个元素的存储空间,所以堆排序的空间复杂度为O(1)

参考:

  1. book.douban.com/subject/199…
  2. blog.csdn.net/morewindows…
  3. blog.csdn.net/michealtx/a…

本文转载自: 掘金

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

深入理解JVM类加载器

发表于 2017-11-30

在我的深入理解JVM类加载机制中,类加载器的部分我只谈了一点点内容,这篇文章将深入了解Java中的类加载器是如何工作的。

类加载器

类加载的第一个阶段就需要通过一个类的全限定名来获取描述此类的二进制字节流,实现这个动作的模块就是类加载器。

类加载器虽然只是实现类的加载动作,但是在Java程序中的作用远不止于此。在Java中一个类的唯一性不仅仅是看类本身,还要看它的加载器。通俗地说:比较两个类是否相等,只有在两个类时由同一个类加载器加载的前提下才有意义,否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,两个类也不相等。

下面看周志明老师那本书上例举的一段代码:

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
复制代码import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {

public static void main(String[] args) throws Exception{
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is==null) {
return super.loadClass(name);
}
byte[] b =new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();
System.out.println(obj.getClass()); //class ClassLoaderTest
System.out.println(obj instanceof ClassLoaderTest); //false
}
}

注释后打印了输出内容,我们使用自己定义的类加载器去加载本身产生的class文件,产生Class类,再实例化产生了obj对象。从打印的第一句看出,obj确实是ClassLoaderTest实例化的对象,但第二句返回了false,因为系统内存在两个ClassLoaderTest类,一个是系统应用程序类加载器加载的,一个是我们自定义的类加载器加载的,虽然来自于同一个class文件,但是却是两个类。

上面我们提到了应用程序加载器,自定义类加载器什么的,别急,继续往下看。

双亲委派模型

什么是双亲委派模型想必也是面试中的常问考点。之前我们也提到过在JVM中,只存在两种类加载器:

  • 启动类加载器(Bootstrap ClassLoader): C++实现,虚拟机的一部分
  • 用户自定义类加载器(User-Defined Class Loader): Java语言实现,独立于虚拟机外部,并且都是继承与java.lang.ClassLoader

一般来说,在讨论类加载器时,我们会划分的更细,我们可以看下图:

类加载器

类加载器

下面的讨论都将基于这幅图。

绝大部分的Java程序都会使用到以下3种系统提供的类加载器。

  • 启动类加载器(Bootstrap ClassLoader): 这个类将负责把<JAVA_HOME>\lib\目录中的,或者-Xbootclasspath参数指定的目录所指定的路径中的,并且是虚拟机识别的的类库加载到虚拟机内存中,如rt.jar,识别仅按照文件名识别,如果名字不符合,即使在这个目录下,也不会被加载。启动类加载器无法被java程序直接引,用户如果在编写自定义的类加载器时,如果需要把加载请求委托给引导类加载器,那么直接用null代替即可。
  • 扩展类加载器(Extension ClassLoader): 这个类加载器由sun.misc.Launcher $ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序加载类(Application ClassLoader): 这个类加载器是由sun.misc.Launcher $App-ClassLoader实现。该加载器是由ClassLoader的getSystemClassLoader()方法返回,所以一般称它为系统类加载器。一般它加载用户类路径(ClassPath)所指定的类库,开发者一般直接使用这个类加载器,如果没有定义自己的类加载器,那么这个应用程序加载类就是程序中默认的类加载器。

我们来解释下什么是双亲委派模型?

双亲委派模型要求除了顶层的Bootstrap ClassLoader外,其它所有类加载器都要有自己的父类加载器。这里的父子关系一般不会议继承实现,而是通过组合实现。它的基本工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每个层次的类加载器都是如此,因此最后所有的请求都会传递到顶层的启动类加载器中,只有当父加载器返回自己无法完成这个加载请求(即它的搜索范围内没有找到所需要的类),子加载器才会尝试去自己加载。

使用双亲委派模型的好处呢?

使用双亲委派模型最直接的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如jaba.lang.Object类,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都要委派给最上层的boostrap ClassLoader,所以Object类在程序的各种类加载器环境中都是同一个类。假如没有使用双亲委派模型,由各个类各自加载Object,那么系统里将会出现各种版本的Object类,导致整个系统的混乱。

下面我们从ClassLoader的源码来看看双亲委派模式:

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
复制代码protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name); //检查该类是否加载过了
if (c == null) {//没加载过的情况
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果自定义的类加载器的parent不为null,就调用parent的loadClass进行加载类
c = parent.loadClass(name, false);
} else {
//否则就去找bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

从上面的代码来看,使用指定名称加载类分为以下3步:

  1. 调用findLoadedClass(String)来检查类是否已经被加载;
  2. 调用父类的loadClass方法,如果父类为空,就调用虚拟机内置的引导类加载器加载;
  3. 调用findClass(String)来查找该类。

因为loadClass封装了双亲委派模型,所以在开发自己的类加载器时,Java标准提覆写findClass()方法。

通常情况下,Java虚拟机都是从文件系统里load一个class,但是有一些类不一定来自一个文件,它们也可能来自别的源,比如网络,加密文件等等,假设我们写一个自己的类加载器,加载服务器下载的class文件。

示例使用代码:

1
2
复制代码ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();

在定义时,我们覆写findClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码class NetworkClassLoader extends ClassLoader {
String host;
int port;

public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}

private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}

这里重要的还有个defineClass函数,用来把一组二进制字节转换为Class的实例,转换为Class后再交给后续的类加载过程解析。后续步骤就又回到深入理解JVM类加载机制中所描述的了。

本文转载自: 掘金

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

聊聊单元测试 单元测试 Spring项目中的单元测试实践 总

发表于 2017-11-30

遇到问题多思考、多查阅、多验证,方能有所得,再勤快点乐于分享,才能写出好文章。

单元测试

定义与特点

单元测试(unit testing):是指对软件中的最小可测试单元进行检查和验证。

这个定义有点抽象,这里举几个单元测试的特性,大家感受一下:一般是一个函数配几个单元测试、单元测试不应该依赖外部系统、单元测试运行速度很快、单元测试不应该造成测试环境的脏数据、单元测试可以重复运行。

优点

单元测试使得我们可以放心修改、重构业务代码,而不用担心修改某处代码后带来的副作用。

单元测试可以帮助我们反思模块划分的合理性,如果一个单元测试写得逻辑非常复杂、或者说一个函数复杂到无法写单测,那就说明模块的抽象有问题。

单元测试使得系统具备更好的可维护性、具备更好的可读性;对于团队的新人来说,阅读系统代码可以从单元测试入手,一点点开始后熟悉系统的逻辑。

本文要解决的痛点

  1. 单测何时写?
    如果你的团队在坚持TDD的风格,那就是在编码之前写;如果没有,也不建议在全部业务代码编写完成之后再开始补单元测试。单元测试比较(最)合适的时机是:一块业务逻辑写完后,跟着写几个单元测试验证下。
  2. 单测怎么写?
    分层单测:数据库操作层、中间件依赖层、业务逻辑层,各自的单元测试各自写,互相不要有依赖。
  3. 单测运行太慢?
  • dao层测试,使用H2进行测试,做独立的BaseH2Test、独立的test-h2-applicationContext.xml,只对dao的测试
  • service层测试,依赖mockito框架,使用@RunWith(MockitoJUnitRunner.class)注解,就无需加载其他spring bean,具体用法
  • 对于依赖外部的中间件(例如redis、diamond、mq),在处理单测的时候要注意分开加载和测试,尤其是与dao的测试分开

Spring项目中的单元测试实践

我们基于unit-test-demo这个项目进行单元测试的实践。

dao层单元测试

最开始写单测的时候,要连着DEV的数据库,这时候会有两个烦恼:网络有问题的时候单测运行不通过、数据库里造成脏数据的时候会导致应用程序异常。这里我们选择H2进行DAO层的单元测试。有如下几个步骤:

  • 在resources下新建目录h2,存放schema.sql和data-prepare-user.sql文件,前者用于保存建表语句,后者用于准备初始数据
  • test-data-source.xml
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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">


<!-- 初始化数据表结构 -->
<jdbc:initialize-database data-source="dataSource">
<jdbc:script location="classpath:h2/schema.sql" encoding="UTF-8"/>
<jdbc:script location="classpath:h2/data-prepare-*.sql" encoding="UTF-8"/>
</jdbc:initialize-database>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="url" value="${user.jdbc.url}"/>
<property name="username" value="${user.jdbc.username}"/>
<property name="password" value="${user.jdbc.password}"/>
<!-- 连接池初始连接数 -->
<property name="initialSize" value="3"/>
<!-- 允许的最大同时使用中(在被业务线程持有,还没有归还给druid) 的连接数 -->
<property name="maxActive" value="30"/>
<!-- 允许的最小空闲连接数,空闲连接超时踢除过程会最少保留的连接数 -->
<property name="minIdle" value="3"/>
<!-- 从连接池获取连接的最大等待时间 5 秒-->
<property name="maxWait" value="5000"/>

<!-- 强行关闭从连接池获取而长时间未归还给druid的连接(认为异常连接)-->
<property name="removeAbandoned" value="true"/>
<!-- 异常连接判断条件,超过180 秒 则认为是异常的,需要强行关闭 -->
<property name="removeAbandonedTimeout" value="180"/>

<!-- 从连接池获取到连接后,如果超过被空闲剔除周期,是否做一次连接有效性检查 -->
<property name="testWhileIdle" value="true"/>
<!-- 从连接池获取连接后,是否马上执行一次检查 -->
<property name="testOnBorrow" value="false"/>
<!-- 归还连接到连接池时是否马上做一次检查 -->
<property name="testOnReturn" value="false"/>
<!-- 连接有效性检查的SQL -->
<property name="validationQuery" value="SELECT 1"/>
<!-- 连接有效性检查的超时时间 1 秒 -->
<property name="validationQueryTimeout" value="1"/>

<!-- 周期性剔除长时间呆在池子里未被使用的空闲连接, 10秒一次-->
<property name="timeBetweenEvictionRunsMillis" value="10000"/>
<!-- 空闲多久可以认为是空闲太长而需要剔除 30 秒-->
<property name="minEvictableIdleTimeMillis" value="30000"/>

<!-- 是否缓存prepareStatement,也就是PSCache,MySQL建议关闭 -->
<property name="poolPreparedStatements" value="false"/>
<property name="maxOpenPreparedStatements" value="-1"/>

<!-- 是否设置自动提交,相当于每个语句一个事务 -->
<property name="defaultAutoCommit" value="true"/>
<!-- 记录被判定为异常的连接 -->
<property name="logAbandoned" value="true"/>
<!-- 网络读取超时,网络连接超时 -->
<property name="connectionProperties" value="connectTimeout=1000;socketTimeout=3000"/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:mybatis/mapper/*Mapper.xml"/>
<property name="typeAliasesPackage" value="org.learnjava.dq.core.dal.bean"/>
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.learnjava.dq.core.dal.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
</beans>
  • test-h2-applicationContext.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 激活自动代理功能 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- spring容器启动时,静态配置替换 -->
<context:property-placeholder location="classpath*:*.properties" ignore-unresolvable="true"/>

<context:component-scan base-package="org.learnjava.dq.core.dal.dao"/>

<import resource="test-data-sources.xml"/>
</beans>
  • UserInfoDAOTest
    这个文件是DAO层单元测试的主要内容,我只写了一个,读者朋友可以下载代码自己练习,把剩余的几个写了。PS:这里我们只有一个DAO,所以spring容器加载就放在这个文件里了,如果DAO多的话,建议抽出一个BaseH2Test文件,这样所有的DAO单元测试只需要加载一次spring容器。
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
复制代码package org.learnjava.dq.core.dal.dao;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.learnjava.dq.core.dal.bean.UserInfoBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Date;
import javax.annotation.Resource;
import static org.junit.Assert.*;

/**
* 作用:
* User: duqi
* Date: 2017/6/24
* Time: 09:33
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:test-h2-applicationContext.xml")
public class UserInfoDAOTest {

@Resource
private UserInfoDAO userInfoDAO;

@Test
public void saveUserInfoBean() throws Exception {
UserInfoBean userInfoBean = new UserInfoBean();
userInfoBean.setUserId(1003L);
userInfoBean.setNickname("wangwu");
userInfoBean.setMobile("18890987675");
userInfoBean.setSex(1);
userInfoBean.setUpdateTime(new Date());
userInfoBean.setCreateTime(new Date());

int rows = userInfoDAO.saveUserInfoBean(userInfoBean);

assertEquals(1, rows);
}

@Test
public void updateUserInfoBean() throws Exception {
}

@Test
public void getUserInfoBeanByUserId() throws Exception {
}

@Test
public void getUserInfoBeanByMobile() throws Exception {
}

@Test
public void listUserInfoBeanByUserIds() throws Exception {
}

@Test
public void removeUserInfoBeanByUserId() throws Exception {
}

}

service层单元测试

  • Mockito
    Mocktio是一个非常易用的mock框架。开发者可以依靠Mockito提供的简洁的API写出漂亮的单元测试。

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.

  • UserInfoManagerImplTest
    单元测试,不应该依赖于DAO层的执行逻辑是否正确【否则就是集成测试】,需要假设DAO的行为是什么样子,然后再看本层的逻辑是否正确。
    这里使用@RunWith(MockitoJUnitRunner.class)修饰当前的单元测试类,如果有多个单元测试类的话,可以考虑抽出一个基础的BaseBizTest类。
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
复制代码package org.learnjava.dq.biz.manager.impl;


import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.learnjava.dq.biz.domain.UserInfo;
import org.learnjava.dq.biz.manager.UserInfoManager;
import org.learnjava.dq.core.dal.bean.UserInfoBean;
import org.learnjava.dq.core.dal.dao.UserInfoDAO;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.runners.MockitoJUnitRunner;

import static org.junit.Assert.*;

import static org.mockito.Mockito.*;

/**
* 作用:
* User: duqi
* Date: 2017/6/24
* Time: 09:55
*/
@RunWith(MockitoJUnitRunner.class)
public class UserInfoManagerImplTest {

@Mock //用于定义被Mock的组件
private UserInfoDAO userInfoDAO;

@InjectMocks //用于定义待测试的组件
private UserInfoManager userInfoManager = new UserInfoManagerImpl();

private UserInfo userInfoToSave;

@Before
public void setUp() throws Exception {
//用于初始化@Mock注解修饰的组件
MockitoAnnotations.initMocks(this);

userInfoToSave = new UserInfo();
userInfoToSave.setMobile("18978760099");
userInfoToSave.setUserId(7777L);
userInfoToSave.setSex(1);
}

@Test
public void saveUserInfo_case1() throws Exception {
//step1 准备数据和动作
doReturn(1).when(userInfoDAO).saveUserInfoBean(any(UserInfoBean.class));

//step2 运行待测试模块
Boolean res = userInfoManager.saveUserInfo(userInfoToSave);

//step3 验证测试结果
assertTrue(res);
}

@Test
public void saveUserInfo_case2() throws Exception {
//step1 准备数据和动作
doReturn(0).when(userInfoDAO).saveUserInfoBean(any(UserInfoBean.class));

//step2 运行待测试模块
Boolean res = userInfoManager.saveUserInfo(userInfoToSave);

//step3 验证测试结果
assertFalse(res);
}

@Test
public void updateUserInfo() throws Exception {
}

@Test
public void getUserInfoByUserId() throws Exception {
}

@Test
public void getUserInfoByMobile() throws Exception {
}

@Test
public void listUserInfoByUserIds() throws Exception {
}

@Test
public void removeUserInfoByUserId() throws Exception {
}

}
  • Mockito要点
    • MockitoJUnitRunner:用于提供单元测试运行的容器环境
    • Mock:用于模拟待测试模块中依赖的外部组件
    • InjectMock:用于标识待测试组件
    • org.mockito.Mockito.*:这个类里的方法可以用于指定Mock组件的预期行为,包括异常处理。

总结

  1. 单元测试的三个步骤
  • 准备数据、行为
  • 测试目标模块
  • 验证测试结果
  1. 除了本文中提到的Junit、Mockito、H2,还有很多其他的单元测试框架,例如TestNG、spock等。
  2. 在Java Web项目中,controller层一般不写业务逻辑,也就没有必要写单元测试,但是如果要写,也有办法,可以参考我之前的文章:在Spring Boot项目中使用Spock框架。
  3. 单元测试代码也是线上代码,要和业务代码一样认真对待,也需要注意代码和测试数据的复用。

参考资料

  1. 使用Mockito的Annotation简化测试 – 使用Mockito和JUnit【二】
  2. 单元测试的艺术
  3. 阿里巴巴 Java编码规范

本文转载自: 掘金

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

python数字图像处理:图像自动阈值分割

发表于 2017-11-30

图像阈值分割是一种广泛应用的分割技术,利用图像中要提取的目标区域与其背景在灰度特性上的差异,把图像看作具有不同灰度级的两类区域(目标区域和背景区域)的组合,选取一个比较合理的阈值,以确定图像中每个像素点应该属于目标区域还是背景区域,从而产生相应的二值图像。

在skimage库中,阈值分割的功能是放在filters模块中。

我们可以手动指定一个阈值,从而来实现分割。也可以让系统自动生成一个阈值,下面几种方法就是用来自动生成阈值。

1、threshold_otsu

基于Otsu的阈值分割方法,函数调用格式:

skimage.filters.threshold_otsu(image, nbins=256)

参数image是指灰度图像,返回一个阈值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码from skimage import data,filters
import matplotlib.pyplot as plt
image = data.camera()
thresh = filters.threshold_otsu(image) #返回一个阈值
dst =(image <= thresh)*1.0 #根据阈值进行分割

plt.figure('thresh',figsize=(8,8))

plt.subplot(121)
plt.title('original image')
plt.imshow(image,plt.cm.gray)

plt.subplot(122)
plt.title('binary image')
plt.imshow(dst,plt.cm.gray)

plt.show()

返回阈值为87,根据87进行分割得下图:

2、threshold_yen

使用方法同上:

1
复制代码thresh = filters.threshold_yen(image)

返回阈值为198,分割如下图:

3、threshold_li

使用方法同上:

1
复制代码thresh = filters.threshold_li(image)

返回阈值64.5,分割如下图:

4、threshold_isodata

阈值计算方法:

threshold = (image[image <= threshold].mean() +image[image > threshold].mean()) / 2.0

使用方法同上:

1
复制代码thresh = filters.threshold_isodata(image)

返回阈值为87,因此分割效果和threshold_otsu一样。

5、threshold_adaptive

调用函数为:

skimage.filters.threshold_adaptive(image, block_size, method=’gaussian’)

block_size: 块大小,指当前像素的相邻区域大小,一般是奇数(如3,5,7。。。)

method: 用来确定自适应阈值的方法,有’mean’, ‘generic’, ‘gaussian’ 和 ‘median’。省略时默认为gaussian

该函数直接访问一个阈值后的图像,而不是阈值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码from skimage import data,filters
import matplotlib.pyplot as plt
image = data.camera()
dst =filters.threshold_adaptive(image, 15) #返回一个阈值图像

plt.figure('thresh',figsize=(8,8))

plt.subplot(121)
plt.title('original image')
plt.imshow(image,plt.cm.gray)

plt.subplot(122)
plt.title('binary image')
plt.imshow(dst,plt.cm.gray)

plt.show()

大家可以修改block_size的大小和method值来查看更多的效果。如:

1
2
复制代码dst1 =filters.threshold_adaptive(image,31,'mean') 
dst2 =filters.threshold_adaptive(image,5,'median')

两种效果如下:

本文转载自: 掘金

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

1…924925926…956

开发者博客

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