HTTP 404 - SegmentFault
杯具啊!
====
HTTP 404……
可能这个页面已经飞走了 回首页 1024 © SegmentFault
本文转载自: 掘金
开发者博客 – 科技是第一生产力
我一直尝试着用不一样的文字来写博客!原因很简单,你讲的知识书上都有,那么每个人为什么不选择看书而选择看你的博文来学习呢?因为书上的内容都是大片大片描述性的文字,对于jvm这块的知识,又是异常枯燥,但又不能不学习的硬骨头!这恰好也就能说明Head First系列的书籍为什么比较火的原因,每个人接收图形知识的速度往往比文字性的东西要快很多。今后我也会尝试用自己的特色来写博客,尽量能引起读者的兴趣,能从中学到东西,我就知足了!
今天的一点一滴探究JVM系列,打算复习一下jvm内存结构!至于学习这块知识的好处?一,从面试的角度来看,你了解jvm,并且java基础扎实,你才更有竞争力(因为我本人本科还没毕业,所以考虑问题经常从面试者的角度来考虑)。其二,提高你对java的理解,知道你创建的每一个对象,每一个变量,都在什么地方,如果不知道这些稀里糊涂得写代码,总会有一天会”翻车”的!好了,废话不多说了,我们开始正题吧!
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的”墙”, 墙外的人想进去,墙内的人想出来。
或许你经常看到StackOverFlowError, OutOfMemoryError无从下手,因为你压根不知道,究竟是什么东西造成内存爆了,当然,你也无法解决!
举个简单的例子
1 | 复制代码public class test { |
这个简单的递归,不对,它不算是递归,因为没有终止条件,但是你知道它最终会报什么错误,知道为什么会报这个错误吗?究竟是那块内存发生了错误?
这个问题,我们留在后面回答,是留在后面你自己解答,看完这篇博文,不用我说,这些问题你都会很清楚!相信我!
你可能会好奇,你看完这篇文章你能学到什么?
清楚你的对象会被分配在哪里(不绝对)
理解哪些区域对线程来说是私有区,哪些区域是线程共享区域
知道方法调用发生了什么?
…
等等等,你可能还会解释你以前遇到一些匪夷所思的问题!总之,你如果之前没了解过这些知识,那么这些东西对你来说,就是成长!
你可能很好奇,墙内究竟是什么样?接下来跟着我一探究竟
上图就是jvm比较详细的内存划分,下面我们来按线程私有共享来划分jvm内存区
下面我们来着重介绍一下这几块内存区域
程序计数器(Program Counter Register)
什么是程序计数器呢,学过汇编的都知道,cs:ip组成的物理地址是下一条要执行的指令的地址,来吧!看图
我们可以很清楚的看到,当前cs:ip指向的内存地址恰好就是我们要执行的下一条指令的位置,前面我们图中(按线程私有共享划分jvm内存的图)又说了,程序计数器是线程私有的,再联想一下我举cs:ip的例子,我们可以很自然的想到,程序计数器其实就是记录线程当前执行到了哪一条指令,因为什么要记录这个值呢?因为,如果我们有很多个线程,线程执行顺序又是不可预料的,假如某一时刻我们在执行线程A里面的指令,然后线程B又获得了cpu的资源,去执行去线程B的指令,假如再过了一段时间之后,A又获得了cpu的资源,想吃回头草,此时回到线程A执行,它不知道要执行线程A的哪条指令!这是没有程序计数器所形成的尴尬局面,但是有了线程私有的程序计数器,这个问题就不存在了,这就是程序计数器出现的原因,以及它的用处,我想你看完这段文字,应该已经对程序计数器这个概念完全理解了!
另外,我需要说明的一点是,程序计数器是Java虚拟机规范中唯一一个没有规定任何内存错误的区域!
这块区域是干啥的?为啥也是线程私有的?
虚拟机栈描述的是Java方法执行的内存模型
我们来解读这句话,为什么说Vm Stack是描述Java方法执行的内存模型呢?其实:
每个方法执行的时候都会创建一个栈帧(Stack Frame)的东西,学过c/c++的应该都对这个概念熟悉。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息等。每个方法从调用开始到结束的过程,都对应这Vm Stack中的入栈出栈的过程!这也就能回答开头我们看到的那个问题了,很简单错误在单线程情况下肯定是StackOverFlowError,多线程下OutOfMemoryError(上图已经写得很清楚了)
比如
1 | 复制代码public void test() { |
上面的例子的age变量和name引用都是存储在虚拟机栈的栈帧里面的(因为我们前面说过了,一个方法从开始调用到结束调用的过程都对应着一个Vm Stack出栈入栈的过程)。
我们前面说了,这块区域存储了局部变量表,操作数栈,动态链接,还有方法出口信息等,我想你应该比较好奇这几个概念。
局部变量表: 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和(returnAddress)类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成计算的,即在Java程序被编译成Class文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
操作数栈: 操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32位数据类型所占的栈容量为1, 64位数据类型所占的栈容量为2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差
动态链接: 每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。
方法返回地址: 当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
我想关于这个区域的东西我已经介绍完了,我想你也应该懂了。
下面我们来下一个区域: 堆(heap)
堆区,是一块很有意思的区域,为啥有意思,因为这块区域是所有线程共享的,也是我们大部分的对象的聚居地(为啥说是大部分呢?这个概念我们之后的文章会进行详细的讲解,如果你特别好奇,可以看一下我之前的文章, Java逃逸分析)!也是jvm管理的最大一块内存(对了,上面的图的大小不代表内存占比,只是为了看着舒服而已)!也是gc开展工作的主要区域。
堆内存中分为一块区域,用于存储类信息,静态变量等等数据,这一块区域之前叫做方法区后面又叫永久带,之后改名叫做Meta-Area/Meta Space Area,元数据空间,名字不重要,我们要清楚这块区域是什么作用就行了!
Meta-Area
这块区域也是线程共享的区域,它主要存储jvm加载类的类信息,类变量,常量(这个在meta-area的常量区),即时编译器编译后的代码等数据。
运行时常量区
这个区域是Meta-Area的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。这在我们的上一篇博客有所涉及。
枯燥概念性的东西看完之后,我们来看一个例子,来加深一下这块的印象:
1 | 复制代码public void test() { |
对于这段代码会涉及Vm Stack、Java Heap、Meta-Area三个最重要的内存区域。
结合我们前面的例子,因为test()方法涉及到Vm Stack区,我想你应该明白,obj会存放在局部变量表中,new Object(),我们前面说过我们大部分的对象都会存储在Java Heap这个区域,所以,Java Heap存储了这个实例对象!那么你会很好奇,Meta-Area为啥会涉及到呢?
我们知道Meta-Area存储了类的信息,类变量常量等等东西!因为我们实例化Object对应的时候,要用到Object这个类的信息,所以它会访问Meta-Area的Object.class这个Class对象来获得一些实例化对象需要的东西。
对了,作为补充,我想你还需要知道, obj引用怎么你能访问到Java Heap区的那个实例化对象
有两种方式,一种使用过句柄指针(学过c/c++对这些概念应该会很熟悉)
还有一种就是通过指针直接访问
上图来自深入理解JVM一书
这块区域相对来说,没有前面几个概念重要。
该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。
比如Java调用c/c++/汇编就用到这块区域
我想你看完这篇博文,应该达到了我们文章开始之前的目标!这篇文章介绍的比较浅显,本着用例子来解释说明内存区域的作用,这样我想你会更容易接收,总比大片的文字描述让你更有兴趣!如果你有什么建议或者疑惑,可以通过GitHub联系我!
本文转载自: 掘金
目前很多时候需要绑定hosts,来进行相关的测试,hosts的路径一般是固定的,那么如何快速修改hosts呢?这里利用JavaFX做了一个hosts管理工具,目前可以实现IP高亮,IP自动检测,监听Ctrl+S进行保存。
这里利用正则去识别IP与注释,并且利用RichTextEditor来进行显示,JavaFX可以加载css文件
1 | 复制代码codeArea = new CodeArea(); |
利用正则去计算哪些应该高亮,使用哪个样式
1 | 复制代码private static final String KEYWORD_PATTERN = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}\\b"; |
动态实时高亮显示
1 | 复制代码private Task<StyleSpans<Collection<String>>> computeHighlightingAsync() { |
加载css文件
1 | 复制代码Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(codeArea)), 600, 400); |
大家理解的快捷键,可以在这里使用方便大家进行快速的更改
1 | 复制代码codeArea.setOnKeyPressed(new EventHandler<KeyEvent>() |
增加图标是JavaFX提供的功能,目前mac上面需要特殊处理一下,代码如下
1 | 复制代码if(SystemUtils.IS_OS_MAC){ |
以上为JHosts Manager的实现,大家如果想运行可以在上面的代码仓库中下载,使用maven构建可以直接运行
本文转载自: 掘金
上一期我们介绍了HashMap的基本原理,没看过的小伙伴们可以点击下面的链接:
这一期我们来讲解高并发环境下,HashMap可能出现的致命问题。
HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。
这时候,HashMap需要扩展它的长度,也就是进行Resize。
影响发生Resize的因素有两个:
1.Capacity
HashMap的当前长度。上一期曾经说过,HashMap的长度是2的幂。
2.LoadFactor
HashMap负载因子,默认值为0.75f。
衡量HashMap是否进行Resize的条件如下:
HashMap.Size >= Capacity * L**oadFactor**
1.扩容
创建一个新的Entry空数组,长度是原数组的2倍。
2.ReHash
遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
让我们回顾一下Hash公式:
index = HashCode(Key) & (Length - 1)
当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。
Resize前的HashMap:
Resize后的HashMap:
ReHash的Java代码如下:
1 | 复制代码/** |
注意:下面的内容十分烧脑,请小伙伴们坐稳扶好。
假设一个HashMap已经到了Resize的临界点。此时有两个线程A和B,在同一时刻对HashMap进行Put操作:
此时达到Resize条件,两个线程各自进行Rezie的第一步,也就是扩容:
这时候,两个线程都走到了ReHash的步骤。让我们回顾一下ReHash的代码:
假如此时线程B遍历到Entry3对象,刚执行完红框里的这行代码,线程就被挂起。对于线程B来说:
e = Entry3
next = Entry2
这时候线程A畅通无阻地进行着Rehash,当ReHash完成后,结果如下(图中的e和next,代表线程B的两个引用):
直到这一步,看起来没什么毛病。接下来线程B恢复,继续执行属于它自己的ReHash。线程B刚才的状态是:
e = Entry3
next = Entry2
当执行到上面这一行时,显然 i = 3,因为刚才线程A对于Entry3的hash结果也是3。
我们继续执行到这两行,Entry3放入了线程B的数组下标为3的位置,并且e指向了Entry2。此时e和next的指向如下:
e = Entry2
next = Entry2
整体情况如图所示:
接着是新一轮循环,又执行到红框内的代码行:
e = Entry2
next = Entry3
整体情况如图所示:
接下来执行下面的三行,用头插法把Entry2插入到了线程B的数组的头结点:
整体情况如图所示:
第三次循环开始,又执行到红框的代码:
e = Entry3
next = Entry3.next = null
最后一步,当我们执行下面这一行的时候,见证奇迹的时刻来临了:
newTable[i] = Entry2
e = Entry3
**Entry2.next = Entry3**
Entry3.next = Entry2
链表出现了环形!
整体情况如图所示:
此时,问题还没有直接产生。当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!
这种情况,不禁让人联想到一道经典的面试题:
1.Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是
HashMap.Size >= Capacity * LoadFactor。****
**2.Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。**
—————END—————
喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容
本文转载自: 掘金
爬虫大家都很熟悉,像 scrapy 这种 Python 爬虫框架也很成熟,不过每写一个爬虫都得重新复制一份代码,这部分如果做成可配置的话,能相应减少一些工作量,对新手也会友好些,所以我花了点时间,开发了一个动态可配置的爬虫网站 www.anycrawl.info ,基于 scrapy ,提供一些配置项,5分钟就可生成一个通用爬虫,并可直接下载代码使用。
我举 www.anycrawl.info/project/15/ 豆瓣小组爬虫的例子来介绍下网站的使用方法。
0x00 需求
我们希望能够爬取害羞组下的所有话题的标题,作者,以及对应的内容和图片
0x01 基础配置
基础配置主要是 scrapy 的 settings.py 的一些选项
我们配置如下
0x02 规则列表
如上面 /group/\w+/discussion?start=[0-9]{0,4}$ 这个对应的是小组分页的链接正则,遇到这些链接,只要丢到队列中,由 scrapy 下次处理,/group/topic/\d+/ 这个对应的是话题的详情链接,遇到这些,则执行 parse_topic 函数,那么这个函数具体执行什么内容,这就看下面配置的 xpath 规则,xpath 教程 ,如我们需要 title, author, description, create_time, image_urls 这几个字段,直接配置即可,只要能通过 xpath 语法找到。
配置完成后,会对应生成如下的源码:
0x03 爬取状态
点击提交后,就跳转到下载页面
这里面有个数据指标的功能,记录你爬取的数目,目前看起来有点鸡肋,如果不需要记录,将 settings 里面的 COUNT_DATA 改为 False 就行。
0x04 运行爬虫
运行爬虫必须有 python 和 scrapy 环境,安装 python 和 pip 这里不介绍了,安装 scrapy 命令如下
1 | 复制代码pip install scrapy |
然后下载刚才的项目代码,解压后,进入 output/xxxxxx 目录,执行
1 | 复制代码python scripts.py |
或者直接用 scrapy
1 | 复制代码scrapy crawl <项目名> |
0x05 Todo
本文转载自: 掘金
HashMap为什么会是面试中的常客呢?我觉得有以下几点原因:
* 考察你阅读源码的能力
* 是否了解内部数据结构
* 是否了解其存储和查询逻辑
* 对非线程安全情况下的使用考虑
前段时间一同事面试蚂蚁金服,就被问到了这个问题;其实很多情况下都是从hashMap,hashTable,ConcurrentHahMap三者之间的关系衍生而出,当然也有直接就针对hashMap原理直接进行考察的。实际上本质都一样,就是为了考察你是否对集合中这些常用集合的原理、实现和使用场景是否清楚。一方面是我们开发中用的多,当然用的人也就多,但是用的好的人却不多(我也用的多,用的也不好)。所以就借此机会(强行蹭一波)再来捋一捋这个HashMap。
本文基于jdk1.7.0_80;jdk 1.8之后略有改动,这个后面细说。
1 | 复制代码public class HashMap<K,V> |
hashMap实现了Map、Cloneable、Serializable三个接口,并且继承了AbstractMap这个抽象类。hashTable继承的是Dictionary这个类,同时也实现了Map、Cloneable、Serializable三个接口。
1 | 复制代码 /** |
1 | 复制代码/** |
1 | 复制代码/** |
1 | 复制代码/** |
1 | 复制代码/** |
1 | 复制代码/** |
1 | 复制代码/** |
1 | 复制代码 /** |
1 | 复制代码/** |
1 | 复制代码/** |
1 | 复制代码static class Entry<K,V> implements Map.Entry<K,V> |
hashmap中是通过使用一个继承自Map中内部类Entry的Entry静态内部类来存储每一个K-V值的。看下具体代码:
1 | 复制代码static class Entry<K,V> implements Map.Entry<K,V> { |
HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干(也就是上面的table–桶)。
看一张图:
hashmap初始化时各个空间的默认值为null,当插入元素时(具体插入下面分析),根据key值来计算出具体的索引位置,如果重复,则使用尾插入法进行插入后面链表中。
之前我是通过插入17条数据来试验的(具体数据数目随意,越大重复的几率越高)
1 | 复制代码public static void main(String[] args) throws Exception { |
通过断点查看next,可以得出我们上面的结论:
1.索引冲突时会使用链表来存储;
2.插入链表的方式是从尾部开始插入的(官方的解释是一般情况下,后来插入的数据被使用的频次较高),这样的话有利于查找。
主要方法
我们平时在开发是最常用的hashMap中的方法无非就是先创建一个HashMap对象,然后存,接着取;对应的方法就是:
构造函数
1 | 复制代码 /** |
另外两个构造方法实际上都是对上面这个构造方法的调用:
1 | 复制代码//只制定默认容量 |
还有一个是:
1 | 复制代码public HashMap(Map<? extends K, ? extends V> m) { |
构造一个映射关系与指定 Map 相同的新 HashMap。所创建的 HashMap 具有默认加载因子 (0.75) 和足以容纳指定 Map 中映射关系的初始容量。
put方法
首先,我们都知道hashmap中的key是允许为null的,这一点也是面试中最常问到的点。那我先看下为什么可以存null作为key值。
1 | 复制代码public V put(K key, V value) { |
hash方法:
检索对象哈希代码,并将附加哈希函数应用于结果哈希,该哈希函数防止质量差的哈希函数。 这是至关重要的,因为HashMap使用两个长度的哈希表,否则会碰到hashCode的冲突,这些hashCodes在低位上没有区别。 注意:空键总是映射到散列0,因此索引为0。
1 | 复制代码/** |
冲突具体过程描述:
get方法
返回指定键映射到的值;如果此映射不包含键映射,则返回null。
1 | 复制代码 public V get(Object key) { |
getEntry方法
1 | 复制代码 final Entry<K,V> getEntry(Object key) { |
在前面提到过threshold,扩容变量,表示当HashMap的size大于threshold时会执行resize操作。其计算方式是:threshold=capacity*loadFactor。
从上面的式子中我们可以得知hashmap的扩容时机是当前当前size的值超过容量乘以负载因子时就会触发扩容。来看下源码:
1 | 复制代码void addEntry(int hash, K key, V value, int bucketIndex) { |
需要注意的是,扩容并不是在hashmap满了之后才进行的,看下面断点:
通过默认构造函数new了一个map对象出来,通过for循环插入12条数据,断点到执行结束,我们看到当前table的容量是16,扩容变量threshold为12(16x0.75),现在我们将12改为13.
此时13还是小于16的,但是还是触发了hashmap 的扩容。当前table容量为32(扩容为了之前的两倍),threshold为24(32x0.75),通过这两种图我们得知:
本文就单纯的扒了一波源码,并对源码中的注释并结合自己的理解进行了翻译,通过断点调试简单的介绍了尾插法在hashmap的应用。最后通过几张图描述了下hashmap发生索引冲突时的解决方案。hashmap在面试时真的是可深可浅,但是源码的阅读还是很有必要的,下面推荐两篇博客给大家。
本文转载自: 掘金
作者:Eugen Paraschiv
转载自公众号:stackgc
在本文中,我们将使用 Spring Security 实现一个基本的注册流程。该示例是建立在上一篇文章的基础上。
本文目标是添加一个完整的注册流程,可以注册用户、验证和持久化用户数据。
首先,让我们实现一个简单的注册页面,有以下字段:
下例展示了一个简单的 registration.html 页面:
示例 2.1
1 | 复制代码<html> |
我们需要一个数据传输对象(Data Transfer Object,DTO)来将所有注册信息封装起来发送到 Spring 后端。当创建和填充 User 对象时,DTO 对象应该要有之后需要用到的所有信息:
1 | 复制代码public class UserDto { |
注意,我们在 DTO 对象的字段上使用了标准的 javax.validation 注解。稍后,我们还将实现自定义验证注解来验证电子邮件地址格式和确认密码。(见第 5 节)
登录页面上的注册链接跳转到 registration 页面。该页面的后端位于注册控制器中,其映射到 /user/registration:
示例 4.1 — showRegistration 方法
1 | 复制代码@RequestMapping(value = "/user/registration", method = RequestMethod.GET) |
当控制器收到 /user/registration 请求时,它会创建一个新的 UserDto 对象,绑定它并返回注册表单,很简单。
让我们看看控制器在注册新账户时所执行的验证:
对于简单的检查,我们在 DTO 对象上使用开箱即用的 bean 验证注解 — @NotNull、@NotEmpty 等。
为了触发验证流程,我们只需使用 @Valid 注解对控制器层中的对象进行标注:
1 | 复制代码public ModelAndView registerUserAccount( |
让我们来验证电子邮件地址并确保其格式正确。我们要创建一个自定义的验证器,以及一个自定义验证注解,将它命名为 @ValidEmail。
要注意的是,我们使用的是自定义注解,而不是 Hibernate 的 @Email,因为 Hibernate 会将内网地址(如 myaddress@myserver)视为有效的电子邮箱地址格式(见 Stackoverflow 文章),这并不好。
以下是电子邮件验证注解和自定义验证器:
例 5.2.1 — 用于电子邮件验证的自定义注解
1 | 复制代码@Target({TYPE, FIELD, ANNOTATION_TYPE}) |
请注意,我们定义了 FIELD 级别注解。
例 5.2.2 — 自定义 EmailValidator:
1 | 复制代码public class EmailValidator |
之后在 UserDto 实现上使用新的注解:
1 | 复制代码@ValidEmail |
我们还需要一个自定义注解和验证器来确保 password 和 matchingPassword 字段匹配:
例 5.3.1 — 验证密码确认的自定义注解
1 | 复制代码@Target({TYPE,ANNOTATION_TYPE}) |
请注意,@Target 注解指定了这是一个 TYPE 级别注解。因为我们需要整个 UserDto 对象来执行验证。
下面为由此注解调用的自定义验证器:
例 5.3.2 — PasswordMatchesValidator 自定义验证器
1 | 复制代码public class PasswordMatchesValidator |
将 @PasswordMatches 注解应用到 UserDto 对象上:
1 | 复制代码@PasswordMatches |
我们要执行的第四项检查:验证电子邮件帐户是否存在于数据库中。
这是在表单验证之后执行的,并且是在 UserService 实现的帮助下完成的。
例 5.4.1 — 控制器的 createUserAccount 方法调用 UserService 对象
1 | 复制代码@RequestMapping(value = "/user/registration", method = RequestMethod.POST) |
例 5.4.2 — UserService 检查重复的电子邮件
1 | 复制代码@Service |
UserService 使用 UserRepository 类来检查指定的电子邮件地址的用户是否已经存在于数据库中。
持久层中 UserRepository 的实际实现与当前文章无关。你可以使用 Spring Data 来快速生成资源库(repository)层。
最后,在控制器层实现注册逻辑:
例 6.1.1 — 控制器中的 RegisterAccount 方法
1 | 复制代码@RequestMapping(value = "/user/registration", method = RequestMethod.POST) |
上面的代码中需要注意以下事项:
让我们来完成 UserService 中注册操作实现:
例 7.1 — IUserService 接口
1 | 复制代码public interface IUserService { |
例 7.2 — UserService 类
1 | 复制代码@Service |
在之前的文章中,登录使用了硬编码的凭据。现在让我们修改一下,使用新注册的用户信息和凭证。我们将实现一个自定义的 UserDetailsService 来检查持久层的登录凭据。
从自定义的 user detail 服务实现开始:
1 | 复制代码@Service |
为了在 Spring Security 配置中启用新的用户服务,我们只需要在 authentication-manager 元素内添加对 UserDetailsService 的引用,并添加 UserDetailsService bean:
例子 8.2 — 验证管理器和 UserDetailsService
1 | 复制代码<authentication-manager> |
或者,通过 Java 配置:
1 | 复制代码@Autowired |
终于完成了 —— 一个由 Spring Security 和 Spring MVC 实现的几乎可用于准生产环境的注册流程。在后续文章中,我们将通过验证新用户的电子邮件来探讨新注册帐户的激活流程。
该 Spring Security REST 教程的实现源码可在 GitHub 项目上获取 —— 这是一个 Eclipse 项目。
本文转载自: 掘金
大四毕业前夕,计算机学院的小灰又一次顶着炎炎烈日,
去某IT公司面试研发工程师岗位……
半小时后,公司会议室,面试开始……
小灰奋笔疾书,五分钟后……
小灰的思路十分简单。他使用暴力枚举的方法,试图寻找到一个合适的整数 i,看看这个整数能否被两个整型参数numberA和numberB同时整除。
这个整数 i 从2开始循环累加,一直累加到numberA和numberB中较小参数的一半为止。循环结束后,上一次寻找到的能够被两数整除的最大 i 值,就是两数的最大公约数。
事后,垂头丧气的小灰去请教同系的学霸大黄……
辗转相除法, 又名欧几里得算法(Euclidean algorithm),目的是求出两个正整数的最大公约数。它是已知最古老的算法, 其可追溯至公元前300年前。
这条算法基于一个定理:两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。比如10和25,25除以10商2余5,那么10和25的最大公约数,等同于10和5的最大公约数。
有了这条定理,求出最大公约数就简单了。我们可以使用递归的方法来把问题逐步简化。
首先,我们先计算出a除以b的余数c,把问题转化成求出b和c的最大公约数;然后计算出b除以c的余数d,把问题转化成求出c和d的最大公约数;再然后计算出c除以d的余数e,把问题转化成求出d和e的最大公约数……
以此类推,逐渐把两个较大整数之间的运算简化成两个较小整数之间的运算,直到两个数可以整除,或者其中一个数减小到1为止。
五分钟后,小灰改好了代码……
更相减损术, 出自于中国古代的《九章算术》,也是一种求最大公约数的算法。
他的原理更加简单:两个正整数a和b(a>b),它们的最大公约数等于a-b的差值c和较小数b的最大公约数**。**比如10和25,25减去10的差是15,那么10和25的最大公约数,等同于10和15的最大公约数。
由此,我们同样可以通过递归来简化问题。首先,我们先计算出a和b的差值c(假设a>b),把问题转化成求出b和c的最大公约数;然后计算出c和b的差值d(假设c>b),把问题转化成求出b和d的最大公约数;再然后计算出b和d的差值e(假设b>d),把问题转化成求出d和e的最大公约数……
以此类推,逐渐把两个较大整数之间的运算简化成两个较小整数之间的运算,直到两个数可以相等为止,最大公约数就是最终相等的两个数。
五分钟后,小灰重写了代码……
众所周知,移位运算的性能非常快。对于给定的正整数a和b,不难得到如下的结论。其中gcb(a,b)的意思是a,b的最大公约数函数:
当a和b均为偶数,gcb(a,b) = 2*gcb(a/2, b/2) = 2*gcb(a>>1, b>>1)
当a为偶数,b为奇数,gcb(a,b) = gcb(a/2, b) = gcb(a>>1, b)
当a为奇数,b为偶数,gcb(a,b) = gcb(a, b/2) = gcb(a, b>>1)
当a和b均为奇数,利用更相减损术运算一次,gcb(a,b) = gcb(b, a-b), 此时a-b必然是偶数,又可以继续进行移位运算。
比如计算10和25的最大公约数的步骤如下:
在两数比较小的时候,暂时看不出计算次数的优势,当两数越大,计算次数的节省就越明显。
最后总结一下上述所有解法的时间复杂度:
1.暴力枚举法:时间复杂度是O(min(a, b)))
2.辗转相除法:时间复杂度不太好计算,可以近似为O(log(max(a, b))),但是取模运算性能较差。
3.更相减损术:避免了取模运算,但是算法性能不稳定,最坏时间复杂度为O(max(a, b)))
4.更相减损术与移位结合:不但避免了取模运算,而且算法性能稳定,时间复杂度为O(log(max(a, b)))
本文原本只写到辗转相除法就终告结束,后来网友们指出还有更优化的解法,看来自己还是才疏学浅,很感谢大家指出问题。另外,方法的参数默认必定是正整数,所以在代码中省去了合法性检查。
文中描述的更相减损术是简化了的方式。在九章算术原文中多了一步验证:如果两数都是偶数,计算差值之前会首先让两个数都折半,使得计算次数更少。这种方法做到了部分优化,但古人似乎没想到一奇一偶的情况也是可以优化的。
由于篇幅所限,本文省略了关于辗转相除法原和更相减损术的原理及证明。其实证明过程并不复杂,细心的同学们也可以自己尝试研究一下。谢谢大家的捧场!
—————END—————
喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容
本文转载自: 掘金
Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。泛型的引入可以解决JDK5之前的集合类框架在使用过程中较为容出现的运行时类型转换异常,因为编译器可以在编译时通过类型检查,规避掉一些潜在的风险。
在JDK5之前,使用集合框架时,是没有类型信息的,统一使用Object,我找了一段JDK4 List接口的方法签名
如下是JDK5开始引入泛型,List接口的改动,新的方法签名,引入了类型参数。
1 | 复制代码boolean add(E e); |
在JDK5之前,使用集合类时,可以往其中添加任意元素,因为其中的类型是Object,在取出的阶段做强制转换,由此可能引发很多意向不到的运行时强制转换错误,比如以下代码。
1 | 复制代码public class Test1 { |
如上代码就会在运行时阶段带来强转异常,在编译时间不能够排查出潜在风险。
如果使用泛型机制,可以在编译期间就检查出List的类型插入的有问题,进行规避,如下代码。
1 | 复制代码public class Test1 { |
引入泛型后,编译器会在编译时先根据类型参数进行类型检查,杜绝掉一些潜在风险。
为何说是在编译时检查,因为在运行时仍然是可以通过反射,将不符合类型参数的数据插入至list中,如下代码所示。
1 | 复制代码public class Test1 { |
引入泛型的同时,也为了兼容JDK5之前的类库,JDK5开始引入的其实是伪泛型,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List等类型,在编译后都会变成List,也就自然兼容了JDK5之前的代码。
Java的泛型机制和C++等的泛型机制实现不同,Java的泛型靠的还是类型擦除,目标代码只会生成一份,牺牲的是运行速度。C++的模板会对针对不同的模板参数静态实例化,目标代码体积会稍大一些,运行速度会快很多。
进行类型擦除后,类型参数原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object)替换。
1 | 复制代码class Pair<T> { |
在Pair中,类型擦除,使用Object,其结果就是一个普通的类,如同泛型加入java编程语言之前已经实现的那样。在程序中可以包含不同类型的Pair,如Pair或Pair,但是,擦除类型后它们就成为原始的Pair类型了,原始类型都是Object。ArrayList被擦除类型后,原始类型也变成了Object,通过反射我们就可以存储字符串了。
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
1 | 复制代码public class Test1 { |
因为类型擦除的问题,所有的泛型类型变量最后都会被替换为原始类型,但在泛型的使用中,我们不需要对取出的数据做强制转换。
1 | 复制代码public class Test1 { |
我们从字节码的角度来探索一下。
1 | 复制代码public static void main(java.lang.String[]); |
在偏移量38的位置可以看到,JVM使用了checkcast指令,说明虽然在编译时进行了类型擦除,但是JVM中仍然保留了类型参数的元信息,在取出时自动进行了强转,这也算是使用泛型的方便之处吧。
在别人的例子有看到说类型擦除和多态的冲突,举了一个例子。
1 | 复制代码public class Test1 { |
因为在类型擦除后,父类也就变成了一个普通的类,如下所示
1 | 复制代码class Pair { |
但这样setValue就从重写变成了重载,显然打破了想达到的目的,那么JVM是如何帮助解决这个冲突的呢?答案是 JVM帮我们搭了一个桥,具体我们从字节码的角度再来看看。
1 | 复制代码class DateInter extends Pair<java.util.Date> { |
从编译的结果来看,我们本意重写setValue和getValue方法的子类,有4个方法,最后的两个方法,就是编译器自己生成的桥接方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法,打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。
最后附上最近在浏览一些别人经验时得到一些tips。
本文转载自: 掘金
本文首发于 https://jaychen.cc
作者 JayChen
Laravel 中提供了数据库迁移的方式来管理数据库,想象一个场景:在一个多人开发的项目中,你的同事修改了某个数据库结构并修改了代码,通过 git 你可以即时的同步同事修改的代码,但是数据库结构,你只能通过手工的方式来复制同事修改的 SQL 语句,执行以保证数据库的结构一致。那么,Laravel 中的数据库迁移概念,就是用于解决团队中保证数据库结构一致的方案。
migration 使用非常简单,编写一定的 php 代码并执行,那么 Laravel 就会自动的更新数据库。假设你的同事要修改数据库某个字段,那么只要编写 php 代码,接着你通过 git 更新了代码,执行 migrate 操作之后,你的数据库结构就和他的同步了。下面我们就来看具体的使用方法。
Laravel 把编写数据库改动的 php 代码称为迁移,可以通过 php artisan make:migration filename 的方式来创建迁移文件。假设你需要创建一张新的 user 表,那么你可以通过执行 php artisan make:migration create_user_table --create=user 来创建一个迁移文件,执行命令会在 database/migrations/ 目录下建立一个 文件创建时间_filename 的 php 文件,那么这个文件就是我们接下来用来编写数据库结构变化的文件了。这里要提一点,虽然说创建迁移文件的名称可以随意,但是为了管理方便,最好文件名可以体现要执行的数据库操作,比如这里我们要创建一张 user 表,所以文件名称为 create_user_table。
php artisan make:migration filename 有两个可选参数
--create=tablename 表明该迁移是用来创建表。--table=tablename 表明该迁移是用来对 tablename 这张表进行操作。我们创建出来的迁移文件 create_user_table 会包含两个方法。
1 | 复制代码 |
这两个方法是互逆的操作,比如我们可以再 up 方法中编写我们要创建的 user 表的相关信息,而 down 方法中则是删除 user 表的操作。这样,我们就可以做到回滚操作,当我们创建 user 表之后发现某个字段名写错了,就可以通过 down 来删除 user 表,进而重新建立 user 表。
假设 user 表有 id,username,email 三个字段,那么可以再 up 方法中写
1 | 复制代码public function up() |
一般,我们的逻辑会在闭包函数中写。上面的代码,即时不能完全明白,也可以大概猜出以下几点:
increments 方法,所以 id 为 auto_increment,调用了 index 方法说明给 id 添加了索引,最后 comment 等同于注释。string 方法说明 name 是 varchar/char 类型的,default 定义了 name 字段的默认值。nullable 方法说明运行 email 字段为空。Laravel 中的方法是满足你对 sql 语句的所有操作,如果你需要定义一个类型为 text 的字段,那么可以调用 text() 方法,更多的方法说明可以参见文档 Laravel 数据库结构构造器。
我们已经编写好了 user 表的结构,接下来执行 php artisan migrate,Laravel 会根据 create 方法自动为我们创建 user 表。至此,我们已经成功的通过 Larvel 的迁移功能来实现创建表。
使用 Laravel 的迁移功能可以有后悔药吃。
执行 php artisan migrate 创建 user 表之后,觉得不行,不要 user 这张表,于是你打算删除这张表。那么这时候我们就要使用刚刚说到的 down 方法了。
1 | 复制代码public function down() |
这里 Laarvel 已经为我们写好逻辑了,dropIfExists 函数就是用来删除表,我们只需要执行 php artisan migrate :rollback 就可以回滚到执行 php artisan migrate 之前的状态。
除了创建表,也可以用迁移记录表的其他任何操作,包括修改表属性,修改字段等等操作。这里再举个例子说明如何用迁移来对表进行重命名。
php artisan make:migration rename_user_to_users --table user 来创建迁移文件。up 方法中写我们要重命名表的逻辑。1 | 复制代码 |
down 方法中编写撤销重命名操作的逻辑。1 | 复制代码public function up() |
php artisan migrate 就就可以完成对 user 表的重命名操作。如果需要回滚,只要执行 php artisan migrate:rollback。你会发现,如果执行一次迁移之后,如果执行第二次迁移是不会重复执行的,这是因为 Laravel 会在数据库中建立一张 migrations 的表来记录哪些已经进行过迁移。
基本的 migration 介绍就到这里,以上的内容可以应对大部分的需求,如果需要更详细的介绍,可能需要阅读 Laravel 那不知所云的文档了。:)
Laravel 中除了 migration 之外,还有一个 seeder 的东西,这个东西用于做数据填充。假设项目开发中需要有一些测试数据,那么同样可以通过编写 php 代码来填充测试数据,那么通过 git 同步代码,所有的人都可以拥有一份同样的测试数据。
同样,数据填充在 Laravel 中被称为 Seeder,如果需要对某张表填充数据,需要先建立一个 seeder。通过执行 php artisan make:seeder UserTableSeeder 来生成一个 seeder 类。这里我们希望填充数据的表示 test 表,所以名字为 UserTableSeeder。当然这个名字不是强制性的,只是为了做到见名知意。
创建 UserTableSeeder 之后会在 database/seeders 目录下生成一个 UserTableSeeder 类,这个类只有一个 run 方法。你可以在 run 方法中写插入数据库的代码。假设我们使用 DB facade 来向 test 表插入数据。
1 | 复制代码class UserTableSeeder extends Seeder |
编写完代码之后,执行 php artsian db:seeder --class= UserTableSeeder 来进行数据填充。执行完毕之后查看数据库已经有数据了。
如果我们有多个表要进行数据填充,那么不可能在编写完 php 代码之后,逐个的执行 php artisan db:seeder --class=xxxx 来进行填充。有一个简便的方法。在 DatabaseSeeder 的 run 方法中添加一行 $this->call(UserTableSeeder::class);,然后执行 php artisan db:seeder,那么 Laravel 就会执行 DatabaseSeeder 中的 run 方法,然后逐个执行迁移。
和 migration 不同,如果多次执行 php artisan db:seeder 就会进行多次数据填充。
加入你想一次性插入大量的测试数据 ,那么在 run 方法中使用 DB facade 来逐个插入显然不是一个好的方法。Laravel 中提供了一种模型工厂的方式来创建创建大量的数据。
模型工厂,意味着本质其实是一个工厂模式。那么,在使用模型工厂创建数据需要做两件事情
Laravel 中通过执行 php artisan make:factory UserFactory --model=User 来为 User Model 创建一个工厂类,该文件会放在 database/factory 目录下。打开该文件可以看到如下代码:
1 | 复制代码$factory->define(App\User::class, function (Faker $faker) { |
这里, return 的值就是我们第 2 步调用工厂获取到的数据。生成数据的逻辑也只需要写在闭包函数中就可以。这里需要提一下 Faker 这个类。这是一个第三方库,Laravel 集成了这个第三方库。这个库的作用很好玩:**用于生成假数据。**假设 User 表需要插入 100 个用户,那么就需要 100 个 username,那么你就不必自己写逻辑生成大量的 test01,test02 这样子幼稚的假数据,直接使用 Faker 类,会替你生成大量逼真的 username。(我也不知道这个算不算无聊了 :)。。。)。
现在假设 User 表有 id, email, username 三个字段,那么我要生成 100 个用户,首先在工厂类中实现逻辑。
1 | 复制代码$factory->define(App\Models\User::class, function (Faker $faker) { |
现在,我们已经定义好了工厂,现在我们就要在 UserSeeder@run 函数中使用模型工厂来生成测试数据。
1 | 复制代码 |
run 函数中这一波行云流水的链式调用在我刚刚开始接触 Laravel 的时候也是一脸黑线,不过习惯之后感觉这代码可读性确实很强
factory(App\User::class) 指明返回哪个工厂,参数 App\User::class 就是工厂的唯一标识。这里我们在定义工厂的时候 define 的第一个参数已经指明了。->times(10) 指明需要工厂模式生成 10 个 User 数据。即会调用 10 次 define 函数的第二个参数。->make() 把生成的 10 个 User 数据封装成 Laravel 中的集合对象。->each() 是 Laravel 集合中的函数,each 函数会针对集合中的每个元素进行操作。这里直接把数据保存到数据库。好了,数据迁移和数据填充的基本操作也就这些了。更多复杂的用法。。。。也不一定能用上。
本文转载自: 掘金