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

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


  • 首页

  • 归档

  • 搜索

jdk动态代理异常处理分析,UndeclaredThrowa

发表于 2017-12-03

HTTP 404 - SegmentFault

杯具啊
杯具啊!
====

HTTP 404……
可能这个页面已经飞走了 回首页 1024 © SegmentFault

本文转载自: 掘金

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

一点一滴探究 JVM 之内存结构

发表于 2017-12-03

前言

我一直尝试着用不一样的文字来写博客!原因很简单,你讲的知识书上都有,那么每个人为什么不选择看书而选择看你的博文来学习呢?因为书上的内容都是大片大片描述性的文字,对于jvm这块的知识,又是异常枯燥,但又不能不学习的硬骨头!这恰好也就能说明Head First系列的书籍为什么比较火的原因,每个人接收图形知识的速度往往比文字性的东西要快很多。今后我也会尝试用自己的特色来写博客,尽量能引起读者的兴趣,能从中学到东西,我就知足了!

今天的一点一滴探究JVM系列,打算复习一下jvm内存结构!至于学习这块知识的好处?一,从面试的角度来看,你了解jvm,并且java基础扎实,你才更有竞争力(因为我本人本科还没毕业,所以考虑问题经常从面试者的角度来考虑)。其二,提高你对java的理解,知道你创建的每一个对象,每一个变量,都在什么地方,如果不知道这些稀里糊涂得写代码,总会有一天会”翻车”的!好了,废话不多说了,我们开始正题吧!

开始之前

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的”墙”, 墙外的人想进去,墙内的人想出来。
或许你经常看到StackOverFlowError, OutOfMemoryError无从下手,因为你压根不知道,究竟是什么东西造成内存爆了,当然,你也无法解决!

举个简单的例子

1
2
3
4
5
6
7
8
复制代码public class test {
private int f() {
f();
}
public static void main(String[] args) {
f();
}
}

这个简单的递归,不对,它不算是递归,因为没有终止条件,但是你知道它最终会报什么错误,知道为什么会报这个错误吗?究竟是那块内存发生了错误?

这个问题,我们留在后面回答,是留在后面你自己解答,看完这篇博文,不用我说,这些问题你都会很清楚!相信我!

目标

你可能会好奇,你看完这篇文章你能学到什么?

清楚你的对象会被分配在哪里(不绝对)
理解哪些区域对线程来说是私有区,哪些区域是线程共享区域
知道方法调用发生了什么?
…
等等等,你可能还会解释你以前遇到一些匪夷所思的问题!总之,你如果之前没了解过这些知识,那么这些东西对你来说,就是成长!

墙内的世界

你可能很好奇,墙内究竟是什么样?接下来跟着我一探究竟

上图就是jvm比较详细的内存划分,下面我们来按线程私有共享来划分jvm内存区

下面我们来着重介绍一下这几块内存区域

程序计数器(Program Counter Register)

什么是程序计数器呢,学过汇编的都知道,cs:ip组成的物理地址是下一条要执行的指令的地址,来吧!看图

我们可以很清楚的看到,当前cs:ip指向的内存地址恰好就是我们要执行的下一条指令的位置,前面我们图中(按线程私有共享划分jvm内存的图)又说了,程序计数器是线程私有的,再联想一下我举cs:ip的例子,我们可以很自然的想到,程序计数器其实就是记录线程当前执行到了哪一条指令,因为什么要记录这个值呢?因为,如果我们有很多个线程,线程执行顺序又是不可预料的,假如某一时刻我们在执行线程A里面的指令,然后线程B又获得了cpu的资源,去执行去线程B的指令,假如再过了一段时间之后,A又获得了cpu的资源,想吃回头草,此时回到线程A执行,它不知道要执行线程A的哪条指令!这是没有程序计数器所形成的尴尬局面,但是有了线程私有的程序计数器,这个问题就不存在了,这就是程序计数器出现的原因,以及它的用处,我想你看完这段文字,应该已经对程序计数器这个概念完全理解了!

另外,我需要说明的一点是,程序计数器是Java虚拟机规范中唯一一个没有规定任何内存错误的区域!

虚拟机栈(Vm Stack)

这块区域是干啥的?为啥也是线程私有的?

虚拟机栈描述的是Java方法执行的内存模型
我们来解读这句话,为什么说Vm Stack是描述Java方法执行的内存模型呢?其实:

每个方法执行的时候都会创建一个栈帧(Stack Frame)的东西,学过c/c++的应该都对这个概念熟悉。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口信息等。每个方法从调用开始到结束的过程,都对应这Vm Stack中的入栈出栈的过程!这也就能回答开头我们看到的那个问题了,很简单错误在单线程情况下肯定是StackOverFlowError,多线程下OutOfMemoryError(上图已经写得很清楚了)

比如

1
2
3
4
5
复制代码public void test() {
String name = "stormma";
int age = 21;
}
JAVA架构群:678779467

上面的例子的age变量和name引用都是存储在虚拟机栈的栈帧里面的(因为我们前面说过了,一个方法从开始调用到结束调用的过程都对应着一个Vm Stack出栈入栈的过程)。

我们前面说了,这块区域存储了局部变量表,操作数栈,动态链接,还有方法出口信息等,我想你应该比较好奇这几个概念。

局部变量表: 局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和(returnAddress)类型(它指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成计算的,即在Java程序被编译成Class文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

操作数栈: 操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32位数据类型所占的栈容量为1, 64位数据类型所占的栈容量为2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差

动态链接: 每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

方法返回地址: 当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。
我想关于这个区域的东西我已经介绍完了,我想你也应该懂了。

下面我们来下一个区域: 堆(heap)

堆(Heap)

堆区,是一块很有意思的区域,为啥有意思,因为这块区域是所有线程共享的,也是我们大部分的对象的聚居地(为啥说是大部分呢?这个概念我们之后的文章会进行详细的讲解,如果你特别好奇,可以看一下我之前的文章, Java逃逸分析)!也是jvm管理的最大一块内存(对了,上面的图的大小不代表内存占比,只是为了看着舒服而已)!也是gc开展工作的主要区域。

堆内存中分为一块区域,用于存储类信息,静态变量等等数据,这一块区域之前叫做方法区后面又叫永久带,之后改名叫做Meta-Area/Meta Space Area,元数据空间,名字不重要,我们要清楚这块区域是什么作用就行了!

Meta-Area

这块区域也是线程共享的区域,它主要存储jvm加载类的类信息,类变量,常量(这个在meta-area的常量区),即时编译器编译后的代码等数据。

运行时常量区

这个区域是Meta-Area的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。这在我们的上一篇博客有所涉及。

枯燥概念性的东西看完之后,我们来看一个例子,来加深一下这块的印象:

1
2
3
4
复制代码public void test() {
Object obj = new Object();
}
JAVA架构群:678779467

对于这段代码会涉及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一书

本地方法栈(Native Method Stack)

这块区域相对来说,没有前面几个概念重要。

该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。

比如Java调用c/c++/汇编就用到这块区域

结尾

我想你看完这篇博文,应该达到了我们文章开始之前的目标!这篇文章介绍的比较浅显,本着用例子来解释说明内存区域的作用,这样我想你会更容易接收,总比大片的文字描述让你更有兴趣!如果你有什么建议或者疑惑,可以通过GitHub联系我!

本文转载自: 掘金

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

JAVAFX实现HOSTS管理工具

发表于 2017-12-03

目前很多时候需要绑定hosts,来进行相关的测试,hosts的路径一般是固定的,那么如何快速修改hosts呢?这里利用JavaFX做了一个hosts管理工具,目前可以实现IP高亮,IP自动检测,监听Ctrl+S进行保存。

下载体验:

gitee.com/accacc/JHos…

技术栈

  • JavaFX
  • RichTextEditor
  • Java 1.8+

支持

  • 高亮显示IP、注释等内容
  • 支持Ctrl+S进行保存

即将支持

  • 多个hosts配置支持
  • 远程hosts文件更新
  • windows下exe文件安装

部分实现

IP注释高亮

这里利用正则去识别IP与注释,并且利用RichTextEditor来进行显示,JavaFX可以加载css文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码codeArea = new CodeArea();
        codeArea.setParagraphGraphicFactory(LineNumberFactory.get(codeArea));
        codeArea.richChanges()
                .filter(ch -> !ch.getInserted().equals(ch.getRemoved())) // XXX
                .successionEnds(Duration.ofMillis(500))
                .supplyTask(this::computeHighlightingAsync)
                .awaitLatest(codeArea.richChanges())
                .filterMap(t -> {
                    if(t.isSuccess()) {
                        return Optional.of(t.get());
                    } else {
                        t.getFailure().printStackTrace();
                        return Optional.empty();
                    }
                })
                .subscribe(this::applyHighlighting);
        codeArea.replaceText(0, 0, HostsUtil.getHostsContent());

利用正则去计算哪些应该高亮,使用哪个样式

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
复制代码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";
    private static final String COMMENT_PATTERN = "#[^\n]*";
 
    private static final Pattern PATTERN = Pattern.compile(
        "(?<KEYWORD>" + KEYWORD_PATTERN + ")"
        + "|(?<COMMENT>" + COMMENT_PATTERN + ")"
    );
 
private static StyleSpans<Collection<String>> computeHighlighting(String text) {
        Matcher matcher = PATTERN.matcher(text);
        int lastKwEnd = 0;
        StyleSpansBuilder<Collection<String>> spansBuilder
                = new StyleSpansBuilder<>();
        while(matcher.find()) {
            String styleClass =
                    matcher.group("KEYWORD") != null ? "keyword" :
                    matcher.group("COMMENT") != null ? "comment" :
                    null; /* never happens */ assert styleClass != null;
            spansBuilder.add(Collections.emptyList(), matcher.start() - lastKwEnd);
            spansBuilder.add(Collections.singleton(styleClass), matcher.end() - matcher.start());
            lastKwEnd = matcher.end();
        }
        spansBuilder.add(Collections.emptyList(), text.length() - lastKwEnd);
        return spansBuilder.create();
    }

动态实时高亮显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码private Task<StyleSpans<Collection<String>>> computeHighlightingAsync() {
        String text = codeArea.getText();
        Task<StyleSpans<Collection<String>>> task = new Task<StyleSpans<Collection<String>>>() {
            @Override
            protected StyleSpans<Collection<String>> call() throws Exception {
                return computeHighlighting(text);
            }
        };
        executor.execute(task);
        return task;
    }
 
    private void applyHighlighting(StyleSpans<Collection<String>> highlighting) {
        codeArea.setStyleSpans(0, highlighting);
    }

加载css文件

1
2
3
4
5
复制代码Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(codeArea)), 600, 400);
        scene.getStylesheets().add(HostsEditor.class.getResource("hosts-keywords.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.setTitle("JHosts Manager");
        primaryStage.show();

增加快捷键监听

大家理解的快捷键,可以在这里使用方便大家进行快速的更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码codeArea.setOnKeyPressed(new EventHandler<KeyEvent>()
        {
            @Override
            public void handle(KeyEvent event)
            {
                if (keyCombinationCtrlS.match(event))
                {
                    Boolean ret = HostsUtil.saveHostsContent(codeArea.getText());
                    if(ret == true){
                        System.out.println("save success!");
                        codeArea.setStyle("-fx-border-color: green;");
                    } else {
                        System.out.println("save error!");
                        codeArea.setStyle("-fx-border-color: red;");
                    }
                }
            }
        });

根据不同的电脑增加图标

增加图标是JavaFX提供的功能,目前mac上面需要特殊处理一下,代码如下

1
2
3
4
5
复制代码if(SystemUtils.IS_OS_MAC){
            primaryStage.getIcons().addAll(new Image("file:icon.icns"));
        } else {
            primaryStage.getIcons().addAll(new Image("file:icon.png"));
        }

结语

以上为JHosts Manager的实现,大家如果想运行可以在上面的代码仓库中下载,使用maven构建可以直接运行

相关文章

  • 逆向分析某app并使用Java与PHP语言实现RC4加解密
  • 解决Windows磁盘爆满却不知如何清理问题
  • 利用ping、nslookup、route、traceroute、mtr等工具排查网络异常
  • 针对包含签名加密随机生成特殊请求参数等情况使用JMeter进行压力测试
  • CentOS_MINI配置方案3_更换更新源_安装常用软件_创建管理用户_服务启动配置
  • CentOS_MINI配置方案2_配置网卡
  • CentOS_MINI配置方案1_关闭SELinux
  • PHP框架之Yaf框架入门

本文转载自: 掘金

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

漫画:高并发下的HashMap

发表于 2017-12-03

上一期我们介绍了HashMap的基本原理,没看过的小伙伴们可以点击下面的链接:

漫画:什么是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

注意:下面的内容十分烧脑,请小伙伴们坐稳扶好。

假设一个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—————

喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容

本文转载自: 掘金

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

动态可配置化 Python 爬虫

发表于 2017-12-03

爬虫大家都很熟悉,像 scrapy 这种 Python 爬虫框架也很成熟,不过每写一个爬虫都得重新复制一份代码,这部分如果做成可配置的话,能相应减少一些工作量,对新手也会友好些,所以我花了点时间,开发了一个动态可配置的爬虫网站 www.anycrawl.info ,基于 scrapy ,提供一些配置项,5分钟就可生成一个通用爬虫,并可直接下载代码使用。

我举 www.anycrawl.info/project/15/ 豆瓣小组爬虫的例子来介绍下网站的使用方法。

0x00 需求



我们希望能够爬取害羞组下的所有话题的标题,作者,以及对应的内容和图片

0x01 基础配置

基础配置主要是 scrapy 的 settings.py 的一些选项

  1. 项目名,别名不说了,域名指的是 allowed_domains,也就是允许爬虫在哪些域名下爬取
  1. 爬虫开始的链接指的是 start_urls,指的是爬虫从哪个链接开始爬取,比如,www.douban.com/group/haixi… , 从豆瓣害羞组的第一页爬取。
  1. 保存数据的方式,目前支持 json, csv, image, mongodb, elasticsearch。(如果选择 image 的方式,则需要在下面配置规则的时候,选择保存图片链接 到 image_urls 这个字段,scrapy 会自动下载图片。mongodb, elasticsearch 则需要自己在 settings.py 配置你的
    host 和 port)

我们配置如下


0x02 规则列表

  1. 规则列表分成两部分,链接正则 和 xpath 规则,必须先配置链接正则,链接正则对应的概念是 scrapy 的 Rule 的概念,意思是根据你给定的链接正则去匹配,如果匹配的到,则执行回调函数 callback, callback 可以为空,如果为空,则放入队列中。


如上面 /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

  1. 支持自动登录
  1. 支持动态爬取
  1. api 接口爬虫
  1. 能适配更多爬取场景
  1. 支持 css 解析网页
  1. 其他

本文转载自: 掘金

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

从源码来聊一聊hashmap

发表于 2017-12-03

HashMap为什么会是面试中的常客呢?我觉得有以下几点原因:

* 考察你阅读源码的能力

* 是否了解内部数据结构

* 是否了解其存储和查询逻辑

* 对非线程安全情况下的使用考虑

前段时间一同事面试蚂蚁金服,就被问到了这个问题;其实很多情况下都是从hashMap,hashTable,ConcurrentHahMap三者之间的关系衍生而出,当然也有直接就针对hashMap原理直接进行考察的。实际上本质都一样,就是为了考察你是否对集合中这些常用集合的原理、实现和使用场景是否清楚。一方面是我们开发中用的多,当然用的人也就多,但是用的好的人却不多(我也用的多,用的也不好)。所以就借此机会(强行蹭一波)再来捋一捋这个HashMap。
本文基于jdk1.7.0_80;jdk 1.8之后略有改动,这个后面细说。

继承关系

1
2
3
复制代码public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

hashMap实现了Map、Cloneable、Serializable三个接口,并且继承了AbstractMap这个抽象类。hashTable继承的是Dictionary这个类,同时也实现了Map、Cloneable、Serializable三个接口。

主要属性

  • DEFAULT_INITIAL_CAPACITY 默认初始容量 16 (hashtable 是11) 常量
1
2
3
4
5
复制代码 /**
* The default initial capacity - MUST be a power of two.
* 默认初始容量-必须是2的幂。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  • MAXIMUM_CAPACITY 默认最大容量 常量
1
2
3
4
5
6
7
复制代码/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*如果有一个更大的值被用于构造HashMap,则使用最大值
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
  • DEFAULT_LOAD_FACTOR 负载因子(默认0.75) 常量
1
2
3
4
5
复制代码/**
* The load factor used when none specified in constructor.
* 加载因子,如果构造函数中没有指定,则使用默认的
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
  • EMPTY_TABLE 默认的空表
1
2
3
4
5
复制代码/**
* An empty table instance to share when the table is not inflated.
* 当表不膨胀时共享的空表实例。
*/
static final Entry<?,?>[] EMPTY_TABLE = {};
  • table 表,必要时调整大小。长度必须是两个幂。
    这个也是hashmap中的核心的存储结构
1
2
3
4
复制代码/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
  • size 表示HashMap中存放KV的数量(为链表/树中的KV的总和)
1
2
3
4
复制代码/**
* The number of key-value mappings contained in this map.
*/
transient int size;
  • threshold 扩容变量,表示当HashMap的size大于threshold时会执行resize操作。
    threshold=capacity*loadFactor
1
2
3
4
5
6
7
复制代码/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
  • loadFactor 负载因子 负载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时装载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。(桶的概念后续介绍)
1
2
3
4
5
6
复制代码    /**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
  • modCount
    这个HashMap的结构修改的次数是那些改变HashMap中的映射数量或修改其内部结构(例如rehash)的那些。这个字段用于使迭代器对HashMap失败快速的集合视图。(见ConcurrentModificationException)。
1
2
3
4
5
6
7
8
复制代码/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
  • hashSeed 与此实例相关联的随机值,用于哈希键的散列代码,使散列冲突更难找到。如果0,那么替代哈希是禁用的。
1
2
3
4
5
6
复制代码/**
* A randomizing value associated with this instance that is applied to
* hash code of keys to make hash collisions harder to find. If 0 then
* alternative hashing is disabled.
*/
transient int hashSeed = 0;

结构分析

1
复制代码static class Entry<K,V> implements Map.Entry<K,V>

hashmap中是通过使用一个继承自Map中内部类Entry的Entry静态内部类来存储每一个K-V值的。看下具体代码:

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
复制代码static class Entry<K,V> implements Map.Entry<K,V> {
final K key; //键对象
V value; //值对象
Entry<K,V> next; //指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部
int hash; //键对象的hash值

/**
* 构造对象
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
/**
* 获取key
*/
public final K getKey() {
return key;
}
/**
* 获取value
*/
public final V getValue() {
return value;
}
/**
* 设置value,这里返回的是oldValue(这个不太明白,哪位大佬清楚的可以留言解释下,非常感谢)
*/
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/**
* 重写equals方法
*/
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
/**
* 重写hashCode方法
*/
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}

public final String toString() {
return getKey() + "=" + getValue();
}

/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}

/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干(也就是上面的table–桶)。
看一张图:

hashmap初始化时各个空间的默认值为null,当插入元素时(具体插入下面分析),根据key值来计算出具体的索引位置,如果重复,则使用尾插入法进行插入后面链表中。

  • 尾插法

之前我是通过插入17条数据来试验的(具体数据数目随意,越大重复的几率越高)

1
2
3
4
5
6
7
复制代码public static void main(String[] args) throws Exception {
HashMap<String, Object> map=new HashMap<>();
for (int i = 0; i < 170; i++) {
map.put("key"+i, i);
}
System.out.println(map);
}

通过断点查看next,可以得出我们上面的结论:

1.索引冲突时会使用链表来存储;
2.插入链表的方式是从尾部开始插入的(官方的解释是一般情况下,后来插入的数据被使用的频次较高),这样的话有利于查找。
主要方法


我们平时在开发是最常用的hashMap中的方法无非就是先创建一个HashMap对象,然后存,接着取;对应的方法就是:

  • 构造函数
  • put函数
  • get函数

构造函数

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
复制代码 /**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity 指定的初始化容量大小
* @param loadFactor the load factor 指定的负载因子
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
//如果初始化容量小于0,则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始化容量大于最大容量,则使用默认最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//如果负载因子小于0或者非数值类型,则抛出异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//初始化负载因子
this.loadFactor = loadFactor;
//初始化threshold
threshold = initialCapacity;
//这个初始化方法是个空方法,应该是意在HashMap的子类中由使用者自行重写该方法的具体实现
init();
}

另外两个构造方法实际上都是对上面这个构造方法的调用:

1
2
3
4
5
6
7
8
复制代码//只制定默认容量
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//使用HashMap默认的容量大小和负载因子
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

还有一个是:

1
2
3
4
5
6
7
复制代码public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);

putAllForCreate(m);
}

构造一个映射关系与指定 Map 相同的新 HashMap。所创建的 HashMap 具有默认加载因子 (0.75) 和足以容纳指定 Map 中映射关系的初始容量。

put方法

首先,我们都知道hashmap中的key是允许为null的,这一点也是面试中最常问到的点。那我先看下为什么可以存null作为key值。

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
复制代码public V put(K key, V value) {
//如果table是空的
if (table == EMPTY_TABLE) {
//inflate:扩容/膨胀的意思
inflateTable(threshold);
}
//如果key为null 此处敲下桌子,为什么可以存null?
if (key == null)
//执行putForNullKey方法,这个方法的作用是如果key为null,就将当前的k-v存放到table[0],即第一个桶。
return putForNullKey(value);
//对key进行一次hash运算,获取hash值
int hash = hash(key);
//根据key值得hash值和表的长度来计算索引位置
int i = indexFor(hash, table.length);
//移动数据,插入数据
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
//上面Entry中的setValue中也有提到,返回的都是旧的数据
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

hash方法:
检索对象哈希代码,并将附加哈希函数应用于结果哈希,该哈希函数防止质量差的哈希函数。 这是至关重要的,因为HashMap使用两个长度的哈希表,否则会碰到hashCode的冲突,这些hashCodes在低位上没有区别。 注意:空键总是映射到散列0,因此索引为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码/**
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}

h ^= k.hashCode();

//这个函数确保在每个比特位置上仅以恒定倍数不同
//的散列码具有有限数量的冲突(在默认加载因子下大约为8)。
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

冲突具体过程描述:

  • 一个空的hashmap表
  • 插入元素,通过hash计算得出索引为3,因为当前3的位置没有元素,因此直接插入进去即可
  • 再次插入元素,通过hash计算得出索引还是3,发生冲突,则将当前新插入的元素放在原来的已有的元素位置,并将其next指向原来已经存在的元素。

get方法

返回指定键映射到的值;如果此映射不包含键映射,则返回null。

1
2
3
4
5
6
7
8
9
复制代码 public V get(Object key) {
//和存null key一样,取的时候也是从table[0]取
if (key == null)
return getForNullKey();
//获取entry
Entry<K,V> entry = getEntry(key);

return null == entry ? null : entry.getValue();
}

getEntry方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码 final Entry<K,V> getEntry(Object key) {
//size等于0,说明当前hashMap中没有元素,直接返回null(每个entry默认值为null)
if (size == 0) {
return null;
}
//根据key值计算hash值
int hash = (key == null) ? 0 : hash(key);
//通过hash值获取到索引位置,找到对应的桶链进行遍历查找
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//如果找到则返回,如果没有链表指针移动到下一个节点继续查找。
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}

扩容机制

在前面提到过threshold,扩容变量,表示当HashMap的size大于threshold时会执行resize操作。其计算方式是:threshold=capacity*loadFactor。
从上面的式子中我们可以得知hashmap的扩容时机是当前当前size的值超过容量乘以负载因子时就会触发扩容。来看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码void addEntry(int hash, K key, V value, int bucketIndex) {
//如果当前size超过threshold 并且满足桶索引位置不为null的情况下,扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容之后为原来的两倍
resize(2 * table.length);
//重新计算hash值
hash = (null != key) ? hash(key) : 0;
//重写计算索引
bucketIndex = indexFor(hash, table.length);
}
//执行具体的插入操作
createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
//先取到当前桶的entry
Entry<K,V> e = table[bucketIndex];
//将新的数据插入到table[bucketIndex],再将之前的entry通过链表简介到table[bucketIndex]的next指向;前面的图已经进行了描述。
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}

需要注意的是,扩容并不是在hashmap满了之后才进行的,看下面断点:

通过默认构造函数new了一个map对象出来,通过for循环插入12条数据,断点到执行结束,我们看到当前table的容量是16,扩容变量threshold为12(16x0.75),现在我们将12改为13.

此时13还是小于16的,但是还是触发了hashmap 的扩容。当前table容量为32(扩容为了之前的两倍),threshold为24(32x0.75),通过这两种图我们得知:

  • 每次扩容之后的容量为原有容量的两倍(2n)
  • 触发扩容并不是因为当前hashmap对象已经满了,而是通过threhold扩容变量来控制触发时机的。

小结

本文就单纯的扒了一波源码,并对源码中的注释并结合自己的理解进行了翻译,通过断点调试简单的介绍了尾插法在hashmap的应用。最后通过几张图描述了下hashmap发生索引冲突时的解决方案。hashmap在面试时真的是可深可浅,但是源码的阅读还是很有必要的,下面推荐两篇博客给大家。

  • 1.关于hashmap与hashtable的具体对比可以参考这个博客:

HashMap和HashTable到底哪不同?

  • 2.关于为什么hashmap中的容量必须是2的幂,这篇博客大家可以看下:

什么是hashmap?

  • 3.关于hashmap非线程安全的解释

并发安全问题之HashMap

本文转载自: 掘金

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

【译】 学习 Spring Security(三):注册流程

发表于 2017-12-02

www.baeldung.com/registratio…

作者:Eugen Paraschiv

转载自公众号:stackgc

1、概述

在本文中,我们将使用 Spring Security 实现一个基本的注册流程。该示例是建立在上一篇文章的基础上。

本文目标是添加一个完整的注册流程,可以注册用户、验证和持久化用户数据。

2、注册页面

首先,让我们实现一个简单的注册页面,有以下字段:

  • name
  • emal
  • password

下例展示了一个简单的 registration.html 页面:

示例 2.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码<html>
<body>
<h1 th:text="#{label.form.title}">form</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
<div>
<label th:text="#{label.user.firstName}">first</label>
<input th:field="*{firstName}"/>
<p th:each="error: ${fields.errors('firstName')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.lastName}">last</label>
<input th:field="*{lastName}"/>
<p th:each="error : ${fields.errors('lastName')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.email}">email</label>
<input type="email" th:field="*{email}"/>
<p th:each="error : ${fields.errors('email')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.password}">password</label>
<input type="password" th:field="*{password}"/>
<p th:each="error : ${fields.errors('password')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.confirmPass}">confirm</label>
<input type="password" th:field="*{matchingPassword}"/>
</div>
<button type="submit" th:text="#{label.form.submit}">submit</button>
</form>

<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">login</a>
</body>
</html>

3、User DTO 对象

我们需要一个数据传输对象(Data Transfer Object,DTO)来将所有注册信息封装起来发送到 Spring 后端。当创建和填充 User 对象时,DTO 对象应该要有之后需要用到的所有信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class UserDto {
@NotNull
@NotEmpty
private String firstName;

@NotNull
@NotEmpty
private String lastName;

@NotNull
@NotEmpty
private String password;
private String matchingPassword;

@NotNull
@NotEmpty
private String email;

// standard getters and setters
}

注意,我们在 DTO 对象的字段上使用了标准的 javax.validation 注解。稍后,我们还将实现自定义验证注解来验证电子邮件地址格式和确认密码。(见第 5 节)

4、注册控制器

登录页面上的注册链接跳转到 registration 页面。该页面的后端位于注册控制器中,其映射到 /user/registration:

示例 4.1 — showRegistration 方法

1
2
3
4
5
6
复制代码@RequestMapping(value = "/user/registration", method = RequestMethod.GET)
public String showRegistrationForm(WebRequest request, Model model) {
UserDto userDto = new UserDto();
model.addAttribute("user", userDto);
return "registration";
}

当控制器收到 /user/registration 请求时,它会创建一个新的 UserDto 对象,绑定它并返回注册表单,很简单。

5、验证注册数据

让我们看看控制器在注册新账户时所执行的验证:

  1. 所有必填字段都已填写(无空白字段或 null 字段)
  2. 电子邮件地址有效(格式正确)
  3. 密码确认字段与密码字段匹配
  4. 帐户不存在

5.1、内置验证

对于简单的检查,我们在 DTO 对象上使用开箱即用的 bean 验证注解 — @NotNull、@NotEmpty 等。

为了触发验证流程,我们只需使用 @Valid 注解对控制器层中的对象进行标注:

1
2
3
4
5
复制代码public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto accountDto,
BindingResult result, WebRequest request, Errors errors) {
...
}

5.2、使用自定义验证检查电子邮件有效性

让我们来验证电子邮件地址并确保其格式正确。我们要创建一个自定义的验证器,以及一个自定义验证注解,将它命名为 @ValidEmail。

要注意的是,我们使用的是自定义注解,而不是 Hibernate 的 @Email,因为 Hibernate 会将内网地址(如 myaddress@myserver)视为有效的电子邮箱地址格式(见 Stackoverflow 文章),这并不好。

以下是电子邮件验证注解和自定义验证器:

例 5.2.1 — 用于电子邮件验证的自定义注解

1
2
3
4
5
6
7
8
9
复制代码@Target({TYPE, FIELD, ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
String message() default "Invalid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

请注意,我们定义了 FIELD 级别注解。

例 5.2.2 — 自定义 EmailValidator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public class EmailValidator 
implements ConstraintValidator<ValidEmail, String> {

private Pattern pattern;
private Matcher matcher;
private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+
(.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(.[A-Za-z0-9]+)*
(.[A-Za-z]{2,})$";
@Override
public void initialize(ValidEmail constraintAnnotation) {
}
@Override
public boolean isValid(String email, ConstraintValidatorContext context){
return (validateEmail(email));
}
private boolean validateEmail(String email) {
pattern = Pattern.compile(EMAIL_PATTERN);
matcher = pattern.matcher(email);
return matcher.matches();
}
}

之后在 UserDto 实现上使用新的注解:

1
2
3
4
复制代码@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3、密码确认使用自定义验证

我们还需要一个自定义注解和验证器来确保 password 和 matchingPassword 字段匹配:

例 5.3.1 — 验证密码确认的自定义注解

1
2
3
4
5
6
7
8
9
复制代码@Target({TYPE,ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

请注意,@Target 注解指定了这是一个 TYPE 级别注解。因为我们需要整个 UserDto 对象来执行验证。

下面为由此注解调用的自定义验证器:

例 5.3.2 — PasswordMatchesValidator 自定义验证器

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class PasswordMatchesValidator 
implements ConstraintValidator<PasswordMatches, Object> {

@Override
public void initialize(PasswordMatches constraintAnnotation) {
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
return user.getPassword().equals(user.getMatchingPassword());
}
}

将 @PasswordMatches 注解应用到 UserDto 对象上:

1
2
3
4
复制代码@PasswordMatches
public class UserDto {
...
}

5.4、检查帐户是否存在

我们要执行的第四项检查:验证电子邮件帐户是否存在于数据库中。

这是在表单验证之后执行的,并且是在 UserService 实现的帮助下完成的。

例 5.4.1 — 控制器的 createUserAccount 方法调用 UserService 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount
(@ModelAttribute("user") @Valid UserDto accountDto,
BindingResult result, WebRequest request, Errors errors) {
User registered = new User();
if (!result.hasErrors()) {
registered = createUserAccount(accountDto, result);
}
if (registered == null) {
result.rejectValue("email", "message.regError");
}
// rest of the implementation
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
User registered = null;
try {
registered = service.registerNewUserAccount(accountDto);
} catch (EmailExistsException e) {
return null;
}
return registered;
}

例 5.4.2 — UserService 检查重复的电子邮件

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
复制代码@Service
public class UserService implements IUserService {
@Autowired
private UserRepository repository;

@Transactional
@Override
public User registerNewUserAccount(UserDto accountDto)
throws EmailExistsException {

if (emailExist(accountDto.getEmail())) {
throw new EmailExistsException(
"There is an account with that email adress: "
+ accountDto.getEmail());
}
...
// 其余的注册操作逻辑
}
private boolean emailExist(String email) {
User user = repository.findByEmail(email);
if (user != null) {
return true;
}
return false;
}
}

UserService 使用 UserRepository 类来检查指定的电子邮件地址的用户是否已经存在于数据库中。

持久层中 UserRepository 的实际实现与当前文章无关。你可以使用 Spring Data 来快速生成资源库(repository)层。

6、持久化数据和完成表单处理

最后,在控制器层实现注册逻辑:

例 6.1.1 — 控制器中的 RegisterAccount 方法

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
复制代码@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto accountDto,
BindingResult result,
WebRequest request,
Errors errors) {

User registered = new User();
if (!result.hasErrors()) {
registered = createUserAccount(accountDto, result);
}
if (registered == null) {
result.rejectValue("email", "message.regError");
}
if (result.hasErrors()) {
return new ModelAndView("registration", "user", accountDto);
}
else {
return new ModelAndView("successRegister", "user", accountDto);
}
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
User registered = null;
try {
registered = service.registerNewUserAccount(accountDto);
} catch (EmailExistsException e) {
return null;
}
return registered;
}

上面的代码中需要注意以下事项:

  1. 控制器返回一个 ModelAndView 对象,它可将模型数据(user)传入到要绑定的视图中。
  2. 如果在验证时发生错误,控制器将重定向到注册表单。
  3. createUserAccount 方法调用 UserService 持久化数据 。我们将在下一节讨论 UserService 实现

7、UserService - 注册操作

让我们来完成 UserService 中注册操作实现:

例 7.1 — IUserService 接口

1
2
3
4
复制代码public interface IUserService {
User registerNewUserAccount(UserDto accountDto)
throws EmailExistsException;
}

例 7.2 — UserService 类

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
复制代码@Service
public class UserService implements IUserService {
@Autowired
private UserRepository repository;

@Transactional
@Override
public User registerNewUserAccount(UserDto accountDto)
throws EmailExistsException {

if (emailExist(accountDto.getEmail())) {
throw new EmailExistsException(
"There is an account with that email address: + accountDto.getEmail());
}
User user = new User();
user.setFirstName(accountDto.getFirstName());
user.setLastName(accountDto.getLastName());
user.setPassword(accountDto.getPassword());
user.setEmail(accountDto.getEmail());
user.setRoles(Arrays.asList("ROLE_USER"));
return repository.save(user);
}
private boolean emailExist(String email) {
User user = repository.findByEmail(email);
if (user != null) {
return true;
}
return false;
}
}

8、加载 User Detail 用于安全登录

在之前的文章中,登录使用了硬编码的凭据。现在让我们修改一下,使用新注册的用户信息和凭证。我们将实现一个自定义的 UserDetailsService 来检查持久层的登录凭据。

8.1、自定义 UserDetailsService

从自定义的 user detail 服务实现开始:

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
复制代码@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {

@Autowired
private UserRepository userRepository;
//
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {

User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(
"No user found with username: "+ email);
}
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new org.springframework.security.core.userdetails.User
(user.getEmail(),
user.getPassword().toLowerCase(), enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked,
getAuthorities(user.getRoles()));
}

private static List<GrantedAuthority> getAuthorities (List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
}

8.2、启用新的验证提供器

为了在 Spring Security 配置中启用新的用户服务,我们只需要在 authentication-manager 元素内添加对 UserDetailsService 的引用,并添加 UserDetailsService bean:

例子 8.2 — 验证管理器和 UserDetailsService

1
2
3
4
5
6
复制代码<authentication-manager>
<authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>

<beans:bean id="userDetailsService"
class="org.baeldung.security.MyUserDetailsService"/>

或者,通过 Java 配置:

1
2
3
4
5
6
7
8
复制代码@Autowired
private MyUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}

9、结论

终于完成了 —— 一个由 Spring Security 和 Spring MVC 实现的几乎可用于准生产环境的注册流程。在后续文章中,我们将通过验证新用户的电子邮件来探讨新注册帐户的激活流程。

该 Spring Security REST 教程的实现源码可在 GitHub 项目上获取 —— 这是一个 Eclipse 项目。

原文示例代码

  • github.com/eugenp/spri…

本文转载自: 掘金

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

漫画算法:辗转相除法是什么鬼?

发表于 2017-12-02

​大四毕业前夕,计算机学院的小灰又一次顶着炎炎烈日,

去某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. 整数10通过移位,可以转换成求5和25的最大公约数
  2. 利用更相减损法,计算出25-5=20,转换成求5和20的最大公约数
  3. 整数20通过移位,可以转换成求5和10的最大公约数
  4. 整数10通过移位,可以转换成求5和5的最大公约数
  5. 利用更相减损法,因为两数相等,所以最大公约数是5

在两数比较小的时候,暂时看不出计算次数的优势,当两数越大,计算次数的节省就越明显。

最后总结一下上述所有解法的时间复杂度:

1.暴力枚举法:时间复杂度是O(min(a, b)))

2.辗转相除法:时间复杂度不太好计算,可以近似为O(log(max(a, b))),但是取模运算性能较差。

3.更相减损术:避免了取模运算,但是算法性能不稳定,最坏时间复杂度为O(max(a, b)))

4.更相减损术与移位结合:不但避免了取模运算,而且算法性能稳定,时间复杂度为O(log(max(a, b)))

本文原本只写到辗转相除法就终告结束,后来网友们指出还有更优化的解法,看来自己还是才疏学浅,很感谢大家指出问题。另外,方法的参数默认必定是正整数,所以在代码中省去了合法性检查。

文中描述的更相减损术是简化了的方式。在九章算术原文中多了一步验证:如果两数都是偶数,计算差值之前会首先让两个数都折半,使得计算次数更少。这种方法做到了部分优化,但古人似乎没想到一奇一偶的情况也是可以优化的。

由于篇幅所限,本文省略了关于辗转相除法原和更相减损术的原理及证明。其实证明过程并不复杂,细心的同学们也可以自己尝试研究一下。谢谢大家的捧场!

—————END—————

喜欢本文的朋友们,欢迎长按下图关注订阅号程序员小灰,收看更多精彩内容

本文转载自: 掘金

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

Java随笔-Java泛型的一点学习

发表于 2017-12-02

Java泛型

Java泛型(generics)是JDK 5中引入的一个新特性,允许在定义类和接口的时候使用类型参数(type parameter)。声明的类型参数在使用时用具体的类型来替换。泛型最主要的应用是在JDK 5中的新集合类框架中。泛型的引入可以解决JDK5之前的集合类框架在使用过程中较为容出现的运行时类型转换异常,因为编译器可以在编译时通过类型检查,规避掉一些潜在的风险。

在JDK5之前,使用集合框架时,是没有类型信息的,统一使用Object,我找了一段JDK4 List接口的方法签名

clipboard.png

如下是JDK5开始引入泛型,List接口的改动,新的方法签名,引入了类型参数。

1
复制代码boolean add(E e);

在JDK5之前,使用集合类时,可以往其中添加任意元素,因为其中的类型是Object,在取出的阶段做强制转换,由此可能引发很多意向不到的运行时强制转换错误,比如以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class Test1 {
public static void main(String[] args) {
List a = new ArrayList();
a.add("123");
a.add(1);
// 以上代码可以正常通过编译,其中同时含有了Integer类型和String类型
for (int i = 0 ; i < a.size(); i++) {
int result = (Integer)a.get(i); // 在取出时需要对Object进行强制转型
System.out.println(result);
}
}
}

如上代码就会在运行时阶段带来强转异常,在编译时间不能够排查出潜在风险。

clipboard.png

如果使用泛型机制,可以在编译期间就检查出List的类型插入的有问题,进行规避,如下代码。

1
2
3
4
5
6
7
复制代码public class Test1 {
public static void main(String[] args) {
List<Integer> a = new ArrayList();
a.add("123"); // 编译不通过
a.add(1);
}
}

引入泛型后,编译器会在编译时先根据类型参数进行类型检查,杜绝掉一些潜在风险。
为何说是在编译时检查,因为在运行时仍然是可以通过反射,将不符合类型参数的数据插入至list中,如下代码所示。

1
2
3
4
5
6
7
8
复制代码public class Test1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<Integer> a = new ArrayList();
List b = new ArrayList();
a.getClass().getMethod("add",Object.class).invoke(a,"abc");
// 以上代码编译通过,运行通过
}
}

引入泛型的同时,也为了兼容JDK5之前的类库,JDK5开始引入的其实是伪泛型,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List等类型,在编译后都会变成List,也就自然兼容了JDK5之前的代码。
Java的泛型机制和C++等的泛型机制实现不同,Java的泛型靠的还是类型擦除,目标代码只会生成一份,牺牲的是运行速度。C++的模板会对针对不同的模板参数静态实例化,目标代码体积会稍大一些,运行速度会快很多。

进行类型擦除后,类型参数原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object)替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}

Pair<T>的原始类型为:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}

在Pair中,类型擦除,使用Object,其结果就是一个普通的类,如同泛型加入java编程语言之前已经实现的那样。在程序中可以包含不同类型的Pair,如Pair或Pair,但是,擦除类型后它们就成为原始的Pair类型了,原始类型都是Object。ArrayList被擦除类型后,原始类型也变成了Object,通过反射我们就可以存储字符串了。

在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class Test1 {

public static void main(String[] args) {

/** 不指定泛型的时候 */
int i = Test1.add(1, 2); // 这两个参数都是Integer,所以T为Integer类型
Number f = Test1.add(1, 1.2);// 这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
Object o = Test1.add(1, "asd");// 这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object

/** 指定泛型的时候 */
int a = Test1.<Integer> add(1, 2);// 指定了Integer,所以只能为Integer类型或者其子类
int b = Test1.<Integer> add(1, 2.2);// 编译错误,指定了Integer,不能为Float
Number c = Test1.<Number> add(1, 2.2); // 指定为Number,所以可以为Integer和Float
}

// 这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return y;
}
}

因为类型擦除的问题,所有的泛型类型变量最后都会被替换为原始类型,但在泛型的使用中,我们不需要对取出的数据做强制转换。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码public class Test1 {

public static void main(String[] args) {
List<Integer> a = new ArrayList();
a.add(1);

for (int i = 0 ; i < a.size(); i++) {
int result = a.get(i);
System.out.println(result);
}
}
}

我们从字节码的角度来探索一下。

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
复制代码public static void main(java.lang.String[]);
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
18: pop
19: iconst_0
20: istore_2
21: iload_2
22: aload_1
23: invokeinterface #6, 1 // InterfaceMethod java/util/List.size:()I
28: if_icmpge 58
31: aload_1
32: iload_2
33: invokeinterface #7, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
38: checkcast #8 // class java/lang/Integer 这里JVM做了强转
41: invokevirtual #9 // Method java/lang/Integer.intValue:()I
44: istore_3
45: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
48: iload_3
49: invokevirtual #11 // Method java/io/PrintStream.println:(I)V
52: iinc 2, 1
55: goto 21
58: return

在偏移量38的位置可以看到,JVM使用了checkcast指令,说明虽然在编译时进行了类型擦除,但是JVM中仍然保留了类型参数的元信息,在取出时自动进行了强转,这也算是使用泛型的方便之处吧。

在别人的例子有看到说类型擦除和多态的冲突,举了一个例子。

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

public static void main(String[] args) {
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object());// 编译错误
}
}

class Pair<T> {
private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}

class DateInter extends Pair<Date> {
@Override
public Date getValue() {
return super.getValue();
}

@Override
public void setValue(Date value) {
super.setValue(value);
}
}

因为在类型擦除后,父类也就变成了一个普通的类,如下所示

1
2
3
4
5
6
7
8
9
10
11
复制代码class Pair {
private Object value;

public Object getValue() {
return value;
}

public void setValue(Object value) {
this.value = value;
}
}

但这样setValue就从重写变成了重载,显然打破了想达到的目的,那么JVM是如何帮助解决这个冲突的呢?答案是 JVM帮我们搭了一个桥,具体我们从字节码的角度再来看看。

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
复制代码class DateInter extends Pair<java.util.Date> {
DateInter();
Code:
0: aload_0
1: invokespecial #1 // Method Pair."<init>":()V
4: return

public java.util.Date getValue();
Code:
0: aload_0
1: invokespecial #2 // Method Pair.getValue:()Ljava/lang/Object;
4: checkcast #3 // class java/util/Date
7: areturn

public void setValue(java.util.Date);
Code:
0: aload_0
1: aload_1
2: invokespecial #4 // Method Pair.setValue:(Ljava/lang/Object;)V
5: return

public void setValue(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: checkcast #3 // class java/util/Date
5: invokevirtual #5 // Method setValue:(Ljava/util/Date;)V
8: return

public java.lang.Object getValue();
Code:
0: aload_0
1: invokevirtual #6 // Method getValue:()Ljava/util/Date;
4: areturn
}

从编译的结果来看,我们本意重写setValue和getValue方法的子类,有4个方法,最后的两个方法,就是编译器自己生成的桥接方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法,打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

最后附上最近在浏览一些别人经验时得到一些tips。

  1. 使用JSON串反序列化对象集合时,记得标注对象的class类型,不然会得到一个只有原始类型也就是Object的集合,可能引起类型转换错误,尤其是在服务调用的这种场景下。
  2. 重视编译器提出的警告信息。

本文转载自: 掘金

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

Laravel 中数据迁移和数据填充 migration S

发表于 2017-12-02

本文首发于 https://jaychen.cc

作者 JayChen

这是一篇基础教程,对标 Laravel 文档中的数据迁移和数据填充。

migration

Laravel 中提供了数据库迁移的方式来管理数据库,想象一个场景:在一个多人开发的项目中,你的同事修改了某个数据库结构并修改了代码,通过 git 你可以即时的同步同事修改的代码,但是数据库结构,你只能通过手工的方式来复制同事修改的 SQL 语句,执行以保证数据库的结构一致。那么,Laravel 中的数据库迁移概念,就是用于解决团队中保证数据库结构一致的方案。

migration 使用非常简单,编写一定的 php 代码并执行,那么 Laravel 就会自动的更新数据库。假设你的同事要修改数据库某个字段,那么只要编写 php 代码,接着你通过 git 更新了代码,执行 migrate 操作之后,你的数据库结构就和他的同步了。下面我们就来看具体的使用方法。

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
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码
public function up()
{
Schema::create('user', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
});
}


public function down()
{
Schema::dropIfExists('user');
}

这两个方法是互逆的操作,比如我们可以再 up 方法中编写我们要创建的 user 表的相关信息,而 down 方法中则是删除 user 表的操作。这样,我们就可以做到回滚操作,当我们创建 user 表之后发现某个字段名写错了,就可以通过 down 来删除 user 表,进而重新建立 user 表。

假设 user 表有 id,username,email 三个字段,那么可以再 up 方法中写

1
2
3
4
5
6
7
8
9
复制代码public function up()
{
Schema::create('user', function (Blueprint $table) {
$table->increments('id')->index()->comment('用户id');
$table->string('name')->default('')->comment('用户名');
$table->string('email')->nullable()->comment('用户邮箱');
$table->timestamps();
});
}

一般,我们的逻辑会在闭包函数中写。上面的代码,即时不能完全明白,也可以大概猜出以下几点:

  • 我们操作的表是 user 表。
  • user 表中定义了 id 字段,因为调用了 increments 方法,所以 id 为 auto_increment,调用了 index 方法说明给 id 添加了索引,最后 comment 等同于注释。
  • 有了 id 的经验,那么 name 字段也很好理解了,string 方法说明 name 是 varchar/char 类型的,default 定义了 name 字段的默认值。
  • email 字段 调用了 nullable 方法说明运行 email 字段为空。
  • 定义字段结构的时候可以使用链式调用的方式。

Laravel 中的方法是满足你对 sql 语句的所有操作,如果你需要定义一个类型为 text 的字段,那么可以调用 text() 方法,更多的方法说明可以参见文档 Laravel 数据库结构构造器。

我们已经编写好了 user 表的结构,接下来执行 php artisan migrate,Laravel 会根据 create 方法自动为我们创建 user 表。至此,我们已经成功的通过 Larvel 的迁移功能来实现创建表。

Rollback

使用 Laravel 的迁移功能可以有后悔药吃。

执行 php artisan migrate 创建 user 表之后,觉得不行,不要 user 这张表,于是你打算删除这张表。那么这时候我们就要使用刚刚说到的 down 方法了。

1
2
3
4
复制代码public function down()
{
Schema::dropIfExists('user');
}

这里 Laarvel 已经为我们写好逻辑了,dropIfExists 函数就是用来删除表,我们只需要执行 php artisan migrate :rollback 就可以回滚到执行 php artisan migrate 之前的状态。

重命名表

除了创建表,也可以用迁移记录表的其他任何操作,包括修改表属性,修改字段等等操作。这里再举个例子说明如何用迁移来对表进行重命名。

  • 假设有表 user,我们需要对它重命名为 users。首先要执行 php artisan make:migration rename_user_to_users --table user 来创建迁移文件。
  • 在 up 方法中写我们要重命名表的逻辑。
1
2
3
4
5
6
7
复制代码
public function up()
{
Schema::table('user', function (Blueprint $table) {
Schema::rename('user','users');
});
}
  • 为了可以 rollback 可以顺利执行,我们还需要在 down 方法中编写撤销重命名操作的逻辑。
1
2
3
4
5
6
7
复制代码public function up()
{
Schema::table('user', function (Blueprint $table) {
//
Schema::rename('users','user');
});
}
  • 最后执行 php artisan migrate 就就可以完成对 user 表的重命名操作。如果需要回滚,只要执行 php artisan migrate:rollback。

你会发现,如果执行一次迁移之后,如果执行第二次迁移是不会重复执行的,这是因为 Laravel 会在数据库中建立一张 migrations 的表来记录哪些已经进行过迁移。

基本的 migration 介绍就到这里,以上的内容可以应对大部分的需求,如果需要更详细的介绍,可能需要阅读 Laravel 那不知所云的文档了。:)

Seeder

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
2
3
4
5
6
7
8
复制代码class UserTableSeeder extends Seeder
{

public function run()
{
DB::table('users')->insert($insertData);
}
}

编写完代码之后,执行 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 中提供了一种模型工厂的方式来创建创建大量的数据。

模型工厂

模型工厂,意味着本质其实是一个工厂模式。那么,在使用模型工厂创建数据需要做两件事情

  1. 创建工厂,定义好工厂要返回的数据。
  2. 调用工厂获取数据。

Laravel 中通过执行 php artisan make:factory UserFactory --model=User 来为 User Model 创建一个工厂类,该文件会放在 database/factory 目录下。打开该文件可以看到如下代码:

1
2
3
4
5
复制代码$factory->define(App\User::class, function (Faker $faker) {
return [
//
];
});

这里, return 的值就是我们第 2 步调用工厂获取到的数据。生成数据的逻辑也只需要写在闭包函数中就可以。这里需要提一下 Faker 这个类。这是一个第三方库,Laravel 集成了这个第三方库。这个库的作用很好玩:**用于生成假数据。**假设 User 表需要插入 100 个用户,那么就需要 100 个 username,那么你就不必自己写逻辑生成大量的 test01,test02 这样子幼稚的假数据,直接使用 Faker 类,会替你生成大量逼真的 username。(我也不知道这个算不算无聊了 :)。。。)。

现在假设 User 表有 id, email, username 三个字段,那么我要生成 100 个用户,首先在工厂类中实现逻辑。

1
2
3
4
5
6
7
复制代码$factory->define(App\Models\User::class, function (Faker $faker) {
return [
// 直接调用 faker API 生成假数据,更多 faker 相关查看 文档。
'username' => $faker->name,
'email' => $faker->unique()->safeEmail,
];
});

现在,我们已经定义好了工厂,现在我们就要在 UserSeeder@run 函数中使用模型工厂来生成测试数据。

1
2
3
4
5
6
7
8
9
10
11
复制代码
class UserTableSeeder extends Seeder
{

public function run()
{
factory(App\User::class)->times(10)->make()->each(function($user,$index){
$user->save();
});
}
}

run 函数中这一波行云流水的链式调用在我刚刚开始接触 Laravel 的时候也是一脸黑线,不过习惯之后感觉这代码可读性确实很强

  • factory(App\User::class) 指明返回哪个工厂,参数 App\User::class 就是工厂的唯一标识。这里我们在定义工厂的时候 define 的第一个参数已经指明了。
  • ->times(10) 指明需要工厂模式生成 10 个 User 数据。即会调用 10 次 define 函数的第二个参数。
  • ->make() 把生成的 10 个 User 数据封装成 Laravel 中的集合对象。
  • ->each() 是 Laravel 集合中的函数,each 函数会针对集合中的每个元素进行操作。这里直接把数据保存到数据库。

好了,数据迁移和数据填充的基本操作也就这些了。更多复杂的用法。。。。也不一定能用上。

本文转载自: 掘金

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

1…921922923…956

开发者博客

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