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

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


  • 首页

  • 归档

  • 搜索

设计模式学习笔记-装饰者模式-Decorator Patte

发表于 2017-12-11

定义


装饰者模式:在不改变原类文件以及不使用继承的情况下,动态地将责任附加到对象上,从而实现动态拓展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

设计原则


要使用装饰者模式,需要满足以下设计原则:

  1. 多用组合,少用继承
  2. 开放-关闭原则:类应该对拓展开放,对修改关闭

UML类图


我们先来看看装饰者模式的类图,再来详细讲述:

由上自下:
1、Girl是基类。通常是一个抽象类或者一个接口,定义了属性或者方法,方法的实现可以由子类实现或者自己实现。通常不会直接使用该类,而是通过继承该类来实现特定的功能,它约束了整个继承树的行为。比如说,如果Girl代表人,即使通过装饰也不会使人变成别的动物。
2、AmericanGirl是Girl的子类,实现了相应的方法,它充当了“被装饰者”的角色。
3、GirlDecorator也是Girl的子类,它是装饰者共同实现的抽象类(也可以是接口)。比如说,GirlDecorator代表学习课程或兴趣这一类装饰者,那么它的子类应该是科学、艺术课这样的具体的装饰者。
4、Science是GirlDecorator的子类,是具体的装饰者,由于它同时也是Girl的子类,因此它能方便地拓展Girl的状态(比如添加新的方法)。每个装饰者都应该有一个实例变量用以保存某个Girl(Component也可以理解为抽象组件)的引用,这也是利用了组合的特性。在持有Girl的引用后,由于其自身也是Girl的子类,那么,相当于Science(ConcreteDecorator)具体的装饰者包裹了Girl(Component)抽象对象,不但有Girl的特性,同时自身也可以有别的特性,也就是所谓的装饰。

Java 实例


为了更加深刻地理解装饰者模式,我们来看一个简单的栗子。首先,我们假设现在有这样一个需求:你是一个教育培训机构,这边女孩有来自各个国家的,然后针对某个国家的女孩学习不同课程需要继续和描述。此时,某个国家的女孩就是被装饰着,某个课程就是装饰者。

Step 1、创建Component基类

因为总体对象是女孩,所以我们可以把人抽象为基类,新建Girl.java:

1
2
3
4
5
6
7
复制代码public abstract class Girl {
String description = "no particular";
public String getDescription(){
return description;
}
public abstract int cost();
}

Step 2、创建被装饰者——ConcreteComponent

具体的客户有来自各个国家的,美国的,加拿大等。

1
2
3
4
5
6
7
8
9
10
复制代码public class AmericanGirl extends Girl {
//一个实例,相当于被装饰者。
public AmericanGirl(){
description = description + "+American";
}
@Override
public int cost() {
return 0;
}
}

Step 3、创建Decorator

创建装饰者类GirlDecorator.java

1
2
3
复制代码public abstract class GirlDecorator extends Girl{
public abstract String getDescription();
}

Step 4、创建ConcreteDecorator

创建Science.java和Art.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public class Science extends GirlDecorator {
private Girl girl;
public Science(Girl girl){
this.girl = girl;
}
@Override
public String getDescription() {
//其中this.girl.getDescription()可能是原始被装饰者
//或者已经装饰过的girl了
return this.girl.getDescription() + "+Like Science";
}
//装饰者附加上的功能
public void caltulateStuff() {
System.out.println("scientific calculation!");
}
@Override
public int cost() {
//装饰者,去装饰girl对象,然后加上装饰的新属性和任务
return girl.cost() + 100;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class Art extends GirlDecorator {
private Girl girl;
public Art(Girl girl) {
this.girl = girl;
}
@Override
public String getDescription() {
return this.girl.getDescription() + "+Like Art";
}
@Override
public int cost() {
return girl.cost() + 200;
}
}

最后我们在测试类内测试我们的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class Main {
public static void main(String[] args) {
//普通美国女孩
Girl g1 = new AmericanGirl();
System.out.println(g1.getDescription());
//喜欢科学的
Science g2 = new Science(g1);
System.out.println(g2.getDescription());
//喜欢艺术的
Art g3 = new Art(g2);
System.out.println(g3.getDescription());
//喜欢体育
Sports g4 = new Sports(g3);
System.out.println(g4.getDescription());
System.out.println("g1,g2,g3,g4包装后的费用:"+g4.cost());
}
}

最终的结果

1
2
3
4
5
复制代码no particular+American
no particular+American+Like Science
no particular+American+Like Science+Like Art
no particular+American+Like Science+Like Art; also learn Sports
g1,g2,g3,g4包装后的费用:600

特点


以上就是装饰者模式的一个小栗子,讲述了装饰者的基本用法。通过上述的例子,我们可以总结一下装饰者模式的特点。
(1)装饰者和被装饰者有相同的接口(或有相同的父类)。
(2)装饰者保存了一个被装饰者的引用。
(3)装饰者接受所有客户端的请求,并且这些请求最终都会返回给被装饰者。
(4)在运行时动态地为对象添加属性,不必改变对象的结构。

使用装饰者模式的最大好处就是其拓展性十分良好,通过使用不同的装饰类来使得对象具有多种多样的属性,灵活性比直接继承好。然而它也有缺点,那就是会出现很多小类,即装饰类,使程序变得复杂。

应用


学习了装饰者模式用法、特点以及优缺点后,我们再来看看装饰者模式在实际开发过程的应用。装饰者模式在Java中经常出现的地方就是JavaIO。提到JavaIO,脑海中就冒出了大量的类:InputStream、FileInputStream、BufferedInputStream……等,真是头都大了,其实,这里面大部分都是装饰类,只要弄清楚这一点就容易理解了。我们来看看JavaIO是怎样使用装饰者模式的。
从字符流来分析,我们知道,有两个基类,分别是InputStream和OutputStream,它们也就是我们上面所述的Component基类。接着,它有如下子类:FileInputStream、StringBufferInputStream等,它们就代表了上面所述的ConcreteComponent,即装饰对象。此外,InputStream还有FilterInputStream这个子类,它就是一个抽象装饰者,即Decorator,那么它的子类:BufferedInputStream、DataInputStream等就是具体的装饰者了。那么,从装饰者模式的角度来看JavaIO,是不是更加容易理解了呢?

下面,我们来自己实现自己的JavaIO的装饰者。要实现的功能是:把一段话里面的每个单词的首字母大写。我们先新建一个类:UpperFirstWordInputStream.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public class UpperFirstWordInputStream extends FilterInputStream {
private int cBefore = 32;
public UpperFirstWordInputStream(InputStream in){
//由于FilterInputStream已经保存了装饰对象的引用,这里直接调用super即可
super(in);
}
public int read() throws IOException {
//根据前一个字符是否是空格来判断是否要大写
int c = super.read();
if(cBefore == 32)
{
cBefore = c;
return (c == -1 ? c: Character.toUpperCase((char) c));
}else{
cBefore = c;
return c;
}
}
}

接着编写一个测试类:InputTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public class InputTest {
public static void main(String[] args) throws IOException {
int c;
StringBuffer sb = new StringBuffer();
try {
//这里用了两个装饰者,分别是BufferedInputStream和我们的UpperFirstWordInputStream
InputStream in = new UpperFirstWordInputStream(new BufferedInputStream(new FileInputStream("/Users/nezha/Desktop/test.txt")));
while((c = in.read()) >= 0)
{
sb.append((char) c);
}
System.out.println(sb);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}

参考文献:


学习、探究Java设计模式——装饰者模式


  • 交流或更多内容请关注我的公众号:nezha_blog
  • 我的技术博客:nezha.github.io

微信公众号

本文转载自: 掘金

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

[2]elasticsearch源码深入分析——启动过程

发表于 2017-12-11

本篇为elasticsearch源码分析系列文章的第二篇,由于技术不精,而叙述的不好或不对的地方还请大家指出 ^ _ ^!!!

源码主要模块

distribution:elasticsearch的打包发行相关,将elasticsearch打成各种发行包(zip,deb,rpm,tar)的模块。具体用法如是,在相应的发行版本模块下执行publishToMavenLocal这个Task,如果执行成功的话就会在路径build/distributions下生成对应的发行包,这种打好的包就能在生产服务器上运行。如下图所示:

通过distribution模块打包发行版本.png

core:核心包,elasticsearch的源码主要在这个里面

buildSrc:elasticsearch的构建相关的代码

client:作为连接elasticsearch的客户端相关代码,接口如下图:

client

modules:作为elasticsearch除核心外的必备模块相关代码,结构如下图:

modules

plugins:作为elasticsearch必备的插件的相关代码,结构如下图:

plugins

启动入口

在上面提到的distribution模块中的src/main/resources/bin路径下能看到elasticsearch的启动脚本。如下图所示:

elasticsearch启动脚本

脚本先载入了jvm配置文件jvm.options(在我们下载解压的发行包的config文件夹中)

ES_JVM_OPTIONS=”$ES_PATH_CONF”/jvm.options

然后载入我们在Run/Debug Configurations中配置的VM参数。

1
2
3
复制代码-Des.path.conf=D:\seymour\elasticsearch\elasticsearch-6.0.0-rc2
-Des.path.home=D:\seymour\elasticsearch\elasticsearch-6.0.0-rc2
-Dlog4j2.disable.jmx=true

最后启动org.elasticsearch.bootstrap.Elasticsearch这个主类中的main方法。

main方法首先添加了关闭钩子,

关闭钩子

然后配置日志输出器,

日志输出器

然后检查了elasticsearch的三个环境参数:

1
2
3
复制代码    putSystemPropertyIfSettingIsMissing(settings, "path.data", "es.path.data");
putSystemPropertyIfSettingIsMissing(settings, "path.home", "es.path.home");
putSystemPropertyIfSettingIsMissing(settings, "path.logs", "es.path.logs");

所有的检查做完后,代码流转到了*org.elasticsearch.bootstrap.Bootstrap
*类的init()方法,而正是Bootstrap类完成了elasticsearch的启动

1
复制代码Bootstrap.init(!daemonize, pidFile, quiet, initialEnv);

Bootstrap类

下面我们来看一下Bootstrap中的几个重要方法:

init
1
复制代码init(final boolean foreground,final Path pidFile,final boolean quiet,final Environment initialEnv) throws BootstrapException, NodeValidationException, UserException

这个方法不用说也知道是做了一些启动前的初始化工作

参数详解

  • foreground:标识elasticsearch是否是作为后台守护进程启动的,
  • pidFile:通过parser解析args后得到,实际是解析了默认命令行参数(verbose,E,silent,version,help,quiet,daemonize,pidfile)
  • quiet:同上
  • initialEnv:Environment实例化的环境参数对象,保存了一些类似于repoFile,configFile,pluginsFile,binFile,libFile等参数。

主要工作

  • 首先会实例化一个Bootstrap对象
  • 配置log输出器
  • 创建pid文件,会在磁盘上持久化一个记录应用pid的文件
  • 通过参数foreground和quiet来控制日志输出
  • 调用Bootstrap的setup方法和start方法
setup
1
复制代码setup(boolean addShutdownHook, Environment environment)throws BootstrapException

主要工作

  • 通过environment生成本地插件控制器

  • 初始化本地资源

  • 在安全管理器安装之前初始化探针

  • 添加关闭钩子

  • 检查jar重复

  • 在安全管理器安装之前配置日志输出器

  • 安装安全管理器

  • 通过参数environment实例化Node

start
1
复制代码start() throws NodeValidationException

主要工作

  • 启动已经实例化的Node
  • 启动keepAliveThread 线程,这个线程在Bootstrap初始化的时候就已经实例化了,该线程创建了一个计数为1的CountDownLatch,目的是在启动完成后能顺利添加关闭钩子,而这句:

意思就是在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作。

可以看到启动的重点在setup方法中,启动过后就是Node的事了。

Node类

Node是通过NodeBuilder来实例化的,使用google的注入框架Guice的Injector进行注入与获取实例。elasticsearch里面的组件都是用上面的方法进行模块化管理,elasticsearch对guice进行了封装,通过ModulesBuilder类构建elasticsearch的模块:

Node的启动就是Node里每个组件的启动,同样的,分别调用不同的的start方法来启动这个组件,如下

至此elasticsearch就启动完成了,后面我会继续讲解elasticsearch细节内容,请大家多多支持 ^ _ ^ !!!

本文转载自: 掘金

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

源码 newTaskFor()和适配器模式 newTaskF

发表于 2017-12-11

AbstractExecutorService提供了一个创建任务的工厂方法——newTaskFor()。工厂方法大家很熟悉了,但newTaskFor()中用到的适配器模式却少有人提到。

JDK版本:oracle java 1.8.0_102

高能预警!这篇,又!短!又!没!用!

newTaskFor()和工厂方法模式

在AbstractExecutorService中,可以提交三种形式的task:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}

三种形式类似,主要区别在于工厂方法AbstractExecutorService#newTaskFor():

1
2
3
4
5
6
复制代码protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}

直接调用了FutureTask的有参构造函数。

FutureTask实现了RunnableFuture接口:

1
复制代码public class FutureTask<V> implements RunnableFuture<V>

该接口继承了Runnable、Future接口,并只有一个run方法。看下FutureTask的构造方法:

1
2
3
4
5
6
7
8
9
10
复制代码public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

8行是一个适配器模式:

1
复制代码this.callable = Executors.callable(runnable, result);

通过静态工厂方法兼适配器Executors.callable()将Runnbale实例和result适配为Callable实例。

callable()和适配器模式

1
2
3
4
5
复制代码public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}

RunnableAdapter完成实际的适配工作:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码static final class RunnableAdapter<T> implements Callable<T> {
final Runnable task;
final T result;
RunnableAdapter(Runnable task, T result) {
this.task = task;
this.result = result;
}
public T call() {
task.run();
return result;
}
}

通过RunnableAdapter实例调用Callable#call()方法时,RunnableAdapter仅仅先调用Runnable#run(),再返回result。框架并没有什么地方会操作result——换句话说,这个适配器仅仅是让Runable实例能够被提交到ExecutorService中,与返回值并没有半分钱的关系。

对,就是这么没用,但是用来讲适配器还是阔以的。

总结

通过AbstractExecutorService#newTaskFor()学习工厂方法模式,通过Executors.callable()学习适配器模式——嗯,,,虽然这个适配器没什么用。

另外,注意到AbstractExecutorService#newTaskFor()的访问权限为protected,我们可以在扩展类中覆写或直接使用该方法。如ForkJoinPool:

1
2
3
4
5
6
7
8
> 复制代码protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
> return new ForkJoinTask.AdaptedRunnable<T>(runnable, value);
> }
> protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
> return new ForkJoinTask.AdaptedCallable<T>(callable);
> }
>
>

适配器走起。


本文链接:源码|newTaskFor()和适配器模式

作者:猴子007

出处:monkeysayhi.github.io

本文基于知识共享署名-相同方式共享 4.0国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

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

条件队列大法好:wait和notify的基本语义 基本概念

发表于 2017-12-11

条件队列是我们常用的轻量级同步机制,也被称为“wait+notify”机制。但很多刚刚接触并发的朋友可能会对wait和notify的语义和配合过程感到迷惑。

今天从join()方法的实现切入,重点讲解wait()方法的语义,简略提及notify()与notifyAll()的语义,最后总结二者的配合过程。

本篇的知识点很浅,但牢固掌握很重要。后面会再写一篇文章,介绍wait+nofity的用法,和使用时的一些问题。

基本概念

线程、Thread与Object

在理解“wait+notify”机制时,注意区分线程、Thread与Object的概念,明确三者在wait、 notify、锁竞争等事件中充当的角色:

  • 线程指操作系统中的线程
  • Thread指Java中的线程类
  • Object指Java中的对象

Thread继承自Object,也是一个对象(多态),并从Object类中继承得到了wait()、notify()(还有notifyAll())方法;同时,Thread也被JVM用于映射操作系统中的线程。

wait()

迷惑的join()方法

通过join()方法确认你是否理解了wait+notify机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码Thread f = new Thread(new Runnable() {
@Overide
public run() {
Thread s = new Thread(new Runnable() {
@Overide
public run() {
for (int i : 1000000) {
sout(i);
}
}
});
s.start();
sout("************* son thread started *************");
s.join();
sout("************* son thread died *************");
}
});
f.start();

join()方法的语义很简单,可以不严谨的表述为“让父线程等待子线程退出”。现在我们来观察Thread#join()的实现,让你对这个语义产生迷惑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

重点看15-22行。逻辑很简单,一个限时阻塞的经典写法。不过,你可能会产生和我一样的迷惑:

为什么调用子线程的wait()方法,进入等待状态的却是父线程呢?

分析

让我们用前面提到的线程、Thread和Object三个概念来解释这段代码。事件序列如下:

  1. 主线程t0执行1-17行,在Java中创建了Thread实例f,处于NEW状态;同时,f也是一个Object实例
  • 主线程t0执行18行后,操作系统中创建了线程t1,Thread实例f转入RUNNABLE状态(Java中,Thread没有RUN状态,因为线程是否正在执行由JVM之外的调度策略决定)
  • 假设线程t1正在执行,则线程t1执行4-11行,在Java中创建了Thread实例s,处于NEW状态;同时,s也是一个Object实例
  • 线程t1执行12行后,操作系统中创建了线程t2,Thread实例s转入RUNNABLE状态
  • 假设线程t1、t2均正在执行,则线程t1执行12行之后、14行之前,可能线程t1与线程t2同时在向标准输出打印内容(t1执行13行,t2执行7-9行)
  • 线程t1执行14行的过程中,操作系统中的线程t1转入阻塞或等待状态(取决于操作系统的实现),Thread实例f转入TIMED_WAITING状态,Thread实例s不受影响,仍处于RUNNABLE状态
  • 线程t2死亡后,被操作系统标记为死亡,Thread实例s转入为TERMINATED状态
  • 线程t1中,Thread实例f发现Thread实例s不再存活,随即转入RUNNABLE状态,操作系统中的线程t1转入运行状态
  • 线程t1从14行s.join()返回,执行15行,打印
  • 最后,线程t1死亡,Thread实例也转入了TERMINATED状态

当然,在事件6(线程t1执行14行的过程中),Thread实例f在TIMED_WAITING状态与RUNNABLE状态之间来回转换,也因此,才能发现Thread实例s不再存活。但可忽略RUNNABLE状态,不影响理解。

上一节提出的问题忽略了线程、Thread与Object的区别。现在,耐心分析过事件序列之后,让我们使用这三个概念,重新表述该问题:

为什么在父线程t1中调用s.join(),进而调用s.wait(),进入等待状态的却是Thread实例f对应的父线程t1,而不是子线程t2呢?

该表述同时也是回答。因为wait()影响的是调用wait()的线程,而不是wait()所属的Object实例。具体说,wait()的语义是“将调用s.wait()的线程t1放入Object实例s的等待集合”。这与s是否同时是Thread实例并无关系——如果s恰好是一个Thread实例,那么其所对应的线程t2可以照常运行,毫无影响。

虽然线程的状态与Thread实例的状态不能一一对应,但用Thread实例的状态代替线程的状态,可以简化条件队列的模型,又不影响核心的正确性。在事件6(线程t1执行14行的过程中)中,各角色的关系如图:

image.png

更容易理解的用法

我们之所以会在join()方法的实现上产生困惑,是因为它以一种难以理解的姿势使用wait+notify机制。

wait+notify机制本质上是一种基于条件队列的同步。JVM为每个对象都内置了监视器,与java.util.concurrent包中的条件队列Condition对应。

条件队列本身很容易理解,但join()方法使用wait()的姿势让人迷惑。它将Thread实例s作为条件队列,共享于父线程t1、子线程t2中——Thread实例s既能够被创建它的Thread实例f访问,也能够被它自己(this)访问。可读性很差,不建议学习。

那么,如何使用wait()才更容易理解呢?可参考Java实现生产者-消费者模型中的“实现二:wait && notify”,使用明确可读的条件队列。简化如下:

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 class WaitNotifyModel implements Model {
private final Object BUFFER_LOCK = new Object();
...
private class ConsumerImpl extends AbstractConsumer implements Consumer, Runnable {
@Override
public void consume() throws InterruptedException {
synchronized (BUFFER_LOCK) {
while (buffer.size() == 0) {
BUFFER_LOCK.wait();
}
Task task = buffer.poll();
assert task != null;
System.out.println("consume: " + task.no);
BUFFER_LOCK.notifyAll();
}
}
}

private class ProducerImpl extends AbstractProducer implements Producer, Runnable {
@Override
public void produce() throws InterruptedException {
synchronized (BUFFER_LOCK) {
while (buffer.size() == cap) {
BUFFER_LOCK.wait();
}
Task task = new Task(increTaskNo.getAndIncrement());
buffer.offer(task);
System.out.println("produce: " + task.no);
BUFFER_LOCK.notifyAll();
}
}
}
...
}

BUFFER_LOCK即是内置的条件队列。所有生产者线程和消费者线程都共享BUFFER_LOCK,通过BUFFER_LOCK的wait+notify机制实现同步。

  • notify()和notifyAll()接下来讲。
  • 之所以命名为BUFFER_LOCK,是因为同时还要在将BUFFER_LOCK作为内置锁来使用。命名为BUFFER_LOCK或BUFFER_COND都是可接受的。

notify()与notifyAll()

可以认为notify与wait是对偶的。s.wait()将当前线程c放入Object实例s的等待集合中,s.notify()随机将一个线程t从s的等待集合中取出来(也可能不是随机的,这取决于操作系统的实现。但很明显JVM的使用者不应该依赖其是否随机)。如果s的等待集合中有多个线程,那么t可能是刚才放入的线程c,也可能是其他线程。

虽然我们通常说“wait+notify”机制,但是使用更多的是notifyAll()而不是notify()。因为notify()只能唤醒一个线程,并且通常是随机的——而被唤醒线程所等待的条件不一定已经被满足(因为多个条件可以使用同一个条件队列),从而会再次进入等待状态;真正满足了条件的线程却因为没被选中而继续等待。这类似于“信号丢失”,可以称为信号劫持。

notifyAll()则一次唤醒全部等待在该条件队列上的线程。虽然notifyAll()解决了“信号劫持”的问题,但一次性唤醒全部线程去竞争锁,也大大加剧了无效竞争。

关于notify()与notifyAll()的自问自答

如何同时解决信号劫持与无效竞争?

不过,只要保证notify()每次都能叫醒正确的人,就能在解决信号劫持的前提下,避免无效竞争。方法很简单,禁止不同类型的线程共用条件队列。具体来说:

  • 一个条件队列只用来维护一个条件
  • 每个线程被唤醒后执行的操作相同

使用join()方法的过程中,没有任何线程调用notify(),如何唤醒线程t1?

为了方便理解,前面事件8(线程t1中,Thread实例f发现Thread实例s不再存活)采用了不正确的描述。在事件8之前,线程t1已经处于阻塞状态,从而Thread实例f无法发现s是否不再存活。那么,使用join()方法的过程中,没有任何线程调用notify(),如何唤醒线程t1?

在线程t1死亡的时候,JVM会帮忙调用s.notify()(或非正常死亡时抛出InterruptedException),以唤醒线程t1;t1中做判断,发现s不再存活,便能够正常只是后面的逻辑。

这是必要的。假设JVM不会帮忙(调用s.notify()或抛出InterruptedException),在最坏的情况下,如果线程t1被用户从操作系统中强制杀死,那么在条件队列s上等待的主线程t0将永远阻塞,而不知道此时发生的异常情况。

同时,这种帮助在JVM规范下没有副作用。因为JVM要求用户从wait()方法返回后检查条件是否得到满足。如果用户编写了错误的同步逻辑,使得线程t2正常执行结束后,条件仍不能得到满足,那么虽然JVM的“帮助”使得线程t1提前唤醒,但wait()返回后的检查使线程t1再次进入阻塞状态,符合用户编写的同步逻辑(尽管是错误的)。另一方面,如果没有线程等待条件队列,那么notify也不会做任何事。

wait+notify的配合过程

仍然用Thread实例的状态代替线程的状态。

  1. 调用wait()前

调用wait()前,线程t1对应的Thread实例f、t2对应的s都处于RUNNABLE状态:

image.png

  1. 调用wait()后,调用notify()前

在线程t1中调用s.wait()后,其他线程调用s.notify()前,t1对应的f转入WAITING状态,进入对象s的等待队列(即,条件队列);s不受影响,仍处于RUNNABLE状态:

image.png

  1. 调用notify()后

假设在主线程t0中主动调用s.notify(),那么在此之后,线程t1对应的Thread实例f转入RUNNABLE状态;s仍然不受影响:

image.png


本文链接:条件队列大法好:wait和notify的基本语义

作者:猴子007

出处:monkeysayhi.github.io

本文基于知识共享署名-相同方式共享 4.0国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

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

微服务架构中整合网关、权限服务

发表于 2017-12-10

前言:之前的文章有讲过微服务的权限系列和网关实现,都是孤立存在,本文将整合后端服务与网关、权限系统。安全权限部分的实现还讲解了基于前置验证的方式实现,但是由于与业务联系比较紧密,没有具体的示例。业务权限与业务联系非常密切,本次的整合项目将会把这部分的操作权限校验实现基于具体的业务服务。

  1. 前文回顾与整合设计

在认证鉴权与API权限控制在微服务架构中的设计与实现系列文章中,讲解了在微服务架构中Auth系统的授权认证和鉴权。在微服务网关中,讲解了基于netflix-zuul组件实现的微服务网关。下面我们看一下这次整合的架构图。

ms

微服务架构权限

整个流程分为两类:

  • 用户尚未登录。客户端(web和移动端)发起登录请求,网关对于登录请求直接转发到auth服务,auth服务对用户身份信息进行校验(整合项目省略用户系统,读者可自行实现,直接硬编码返回用户信息),最终将身份合法的token返回给客户端。
  • 用户已登录,请求其他服务。这种情况,客户端的请求到达网关,网关会调用auth系统进行请求身份合法性的验证,验证不通则直接拒绝,并返回401;如果通过验证,则转发到具体服务,服务经过过滤器,根据请求头部中的userId,获取该user的安全权限信息。利用切面,对该接口需要的权限进行校验,通过则proceed,否则返回403。

第一类其实比较简单,在讲解认证鉴权与API权限控制在微服务架构中的设计与实现就基本实现,现在要做的是与网关进行结合;第二类中,我们新建了一个后端服务,与网关、auth系统整合。

下面对整合项目涉及到的三个服务分别介绍。网关和auth服务的实现已经讲过,本文主要讲下这两个服务进行整合需要的改动,还有就是对于后端服务的主要实现进行讲解。

  1. gateway实现

微服务网关已经基本介绍完了网关的实现,包括服务路由、几种过滤方式等。这一节将重点介绍实际应用时的整合。对于需要修改增强的地方如下:

  • 区分暴露接口(即对外直接访问)和需要合法身份登录之后才能访问的接口
  • 暴露接口直接放行,转发到具体服务,如登录、刷新token等
  • 需要合法身份登录之后才能访问的接口,根据传入的Access token进行构造头部,头部主要包括userId等信息,可根据自己的实际业务在auth服务中进行设置。
  • 最后,比较重要的一点,引入Spring Security的资源服务器配置,对于暴露接口设置permitAll(),其余接口进入身份合法性校验的流程,调用auth服务,如果通过则正常继续转发,否则抛出异常,返回401。

绘制的流程图如下:

gwflow

网关路由流程图

2.1 permitAll实现

对外暴露的接口可以直接访问,这可以依赖配置文件,而配置文件又可以通过配置中心进行动态更新,所以不用担心有hard-code的问题。
在配置文件中定义需要permitall的路径。

1
2
3
4
5
6
复制代码auth:
permitall:
-
pattern: /login/**
-
pattern: /web/public/**

服务启动时,读入相应的Configuration,下面的配置属性读取以auth开头的配置。

1
2
3
4
5
复制代码    @Bean
@ConfigurationProperties(prefix = "auth")
public PermitAllUrlProperties getPermitAllUrlProperties() {
return new PermitAllUrlProperties();
}

当然还需要有PermitAllUrlProperties对应的实体类,比较简单,不列出来了。

2.2 加强头部

Filter过滤器,它是Servlet技术中最实用的技术,Web开发人员通过Filter技术,对web服务器管理的所有web资源进行拦截。这边使用Filter进行头部增强,解析请求中的token,构造统一的头部信息,到了具体服务,可以利用头部中的userId进行操作权限获取与判断。

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
复制代码public class HeaderEnhanceFilter implements Filter {

//...

@Autowired
private PermitAllUrlProperties permitAllUrlProperties;

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

//主要的过滤方法
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String authorization = ((HttpServletRequest) servletRequest).getHeader("Authorization");
String requestURI = ((HttpServletRequest) servletRequest).getRequestURI();
// test if request url is permit all , then remove authorization from header
LOGGER.info(String.format("Enhance request URI : %s.", requestURI));
//将isPermitAllUrl的请求进行传递
if(isPermitAllUrl(requestURI) && isNotOAuthEndpoint(requestURI)) {
//移除头部,但不包括登录端点的头部
HttpServletRequest resetRequest = removeValueFromRequestHeader((HttpServletRequest) servletRequest);
filterChain.doFilter(resetRequest, servletResponse);
return;
}
//判断是不是符合规范的头部
if (StringUtils.isNotEmpty(authorization)) {
if (isJwtBearerToken(authorization)) {
try {
authorization = StringUtils.substringBetween(authorization, ".");
String decoded = new String(Base64.decodeBase64(authorization));

Map properties = new ObjectMapper().readValue(decoded, Map.class);
//解析authorization中的token,构造USER_ID_IN_HEADER
String userId = (String) properties.get(SecurityConstants.USER_ID_IN_HEADER);

RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, userId);
} catch (Exception e) {
LOGGER.error("Failed to customize header for the request", e);
}
}
} else {
//为了适配,设置匿名头部
RequestContext.getCurrentContext().addZuulRequestHeader(SecurityConstants.USER_ID_IN_HEADER, ANONYMOUS_USER_ID);
}

filterChain.doFilter(servletRequest, servletResponse);
}

@Override
public void destroy() {

}

//...

}

上面代码列出了头部增强的基本处理流程,将isPermitAllUrl的请求进行直接传递,否则判断是不是符合规范的头部,然后解析authorization中的token,构造USER_ID_IN_HEADER。最后为了适配,设置匿名头部。

需要注意的是,HeaderEnhanceFilter也要进行注册。Spring 提供了FilterRegistrationBean类,此类提供setOrder方法,可以为filter设置排序值,让spring在注册web filter之前排序后再依次注册。

2.3 资源服务器配置

利用资源服务器的配置,控制哪些是暴露端点不需要进行身份合法性的校验,直接路由转发,哪些是需要进行身份loadAuthentication,调用auth服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

//...
//配置permitAll的请求pattern,依赖于permitAllUrlProperties对象
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers().antMatchers("/**")
.and()
.authorizeRequests()
.antMatchers(permitAllUrlProperties.getPermitallPatterns()).permitAll()
.anyRequest().authenticated();
}

//通过自定义的CustomRemoteTokenServices,植入身份合法性的相关验证
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
CustomRemoteTokenServices resourceServerTokenServices = new CustomRemoteTokenServices();
//...
resources.tokenServices(resourceServerTokenServices);
}
}

资源服务器的配置大家看了笔者之前的文章应该很熟悉,此处不过多重复讲了。关于ResourceServerSecurityConfigurer配置类,之前的安全系列文章已经讲过,ResourceServerTokenServices接口,当时我们也用到了,只不过用的是默认的DefaultTokenServices。这边通过自定义的CustomRemoteTokenServices,植入身份合法性的相关验证。

当然这个配置还要引入Spring Cloud Security oauth2的相应依赖。

1
2
3
4
5
6
7
8
9
复制代码        <dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

2.4 自定义RemoteTokenServices实现

ResourceServerTokenServices接口其中的一个实现是RemoteTokenServices。

Queries the /check_token endpoint to obtain the contents of an access token.
If the endpoint returns a 400 response, this indicates that the token is invalid.

RemoteTokenServices主要是查询auth服务的/check_token端点以获取一个token的校验结果。如果有错误,则说明token是不合法的。笔者这边的的CustomRemoteTokenServices实现就是沿用该思路。需要注意的是,笔者的项目基于Spring cloud,auth服务是多实例的,所以这边使用了Netflix Ribbon获取auth服务进行负载均衡。Spring Cloud Security添加如下默认配置,对应auth服务中的相应端点。

1
2
3
4
5
6
7
8
9
复制代码security:
oauth2:
client:
accessTokenUri: /oauth/token
clientId: gateway
clientSecret: gateway
resource:
userInfoUri: /user
token-info-uri: /oauth/check_token

至于具体的CustomRemoteTokenServices实现,可以参考上面讲的思路以及RemoteTokenServices,很简单,此处略去。

至此,网关服务的增强完成,下面看一下我们对auth服务和后端backend服务的实现。

强调一下,为什么头部传递的userId等信息需要在网关构造?读者可以自己思考一下,结合安全等方面,😆笔者暂时不给出答案。

  1. auth整合

auth服务的整合修改,其实没那么多,之前对于user、role以及permission之间的定义和关系没有给出实现,这部分的sql语句已经在auth.sql中。所以为了能给出一个完整的实例,笔者把这部分实现给补充了,主要就是user-role,role、role-permission的相应接口定义与实现,实现增删改查。

读者要是想参考整合项目进行实际应用,这部分完全可以根据自己的业务进行增强,包括token的创建,其自定义的信息还可以在网关中进行统一处理,构造好之后传递给后端服务。

这边的接口只是列出了需要的几个,其他接口没写(因为懒。。)

这两个接口也是给backend项目用来获取相应的userId权限。

1
2
3
4
5
6
7
8
9
复制代码//根据userId获取用户对应的权限
@RequestMapping(method = RequestMethod.GET, value = "/api/userPermissions?userId={userId}",
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
List<Permission> getUserPermissions(@RequestParam("userId") String userId);

//根据userId获取用户对应的accessLevel(好像暂时没用到。。)
@RequestMapping(method = RequestMethod.GET, value = "/api/userAccesses?userId={userId}",
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
List<UserAccess> getUserAccessList(@RequestParam("userId") String userId);

好了,这边的实现已经讲完了,具体见项目中的实现。

  1. backend项目实现

本节是进行实现一个backend的实例,后端项目主要实现哪些功能呢?我们考虑一下,之前网关服务和auth服务所做的准备:

  • 网关构造的头部userId(可能还有其他信息,这边只是示例),可以在backend获得
  • 转发到backend服务的请求,都是经过身份合法性校验,或者是直接对外暴露的接口
  • auth服务,提供根据userId进行获取相应的权限的接口

根据这些,笔者绘制了一个backend的通用流程图:

bf

backend流程图

上面的流程图其实已经非常清晰了,首先经过filter过滤器,填充SecurityContextHolder的上下文。其次,通过切面来实现注解,是否需要进入切面表达式处理。不需要的话,直接执行接口内的方法;否则解析注解中需要的权限,判断是否有权限执行,有的话继续执行,否则返回403 forbidden。

4.1 filter过滤器

Filter过滤器,和上面网关使用一样,拦截客户的HttpServletRequest。

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 class AuthorizationFilter implements Filter {

@Autowired
private FeignAuthClient feignAuthClient;

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("过滤器正在执行...");
// pass the request along the filter chain
String userId = ((HttpServletRequest) servletRequest).getHeader(SecurityConstants.USER_ID_IN_HEADER);

if (StringUtils.isNotEmpty(userId)) {
UserContext userContext = new UserContext(UUID.fromString(userId));
userContext.setAccessType(AccessType.ACCESS_TYPE_NORMAL);

List<Permission> permissionList = feignAuthClient.getUserPermissions(userId);
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
for (Permission permission : permissionList) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority();
authority.setAuthority(permission.getPermission());
authorityList.add(authority);
}

CustomAuthentication userAuth = new CustomAuthentication();
userAuth.setAuthorities(authorityList);
userContext.setAuthorities(authorityList);
userContext.setAuthentication(userAuth);
SecurityContextHolder.setContext(userContext);
}
filterChain.doFilter(servletRequest, servletResponse);
}

//...
}

上述代码主要实现了,根据请求头中的userId,利用feign client获取auth服务中的该user所具有的权限集合。之后构造了一个UserContext,UserContext是自定义的,实现了Spring Security的UserDetails, SecurityContext接口。

4.2 通过切面来实现@PreAuth注解

基于Spring的项目,使用Spring的AOP切面实现注解是比较方便的一件事,这边我们使用了自定义的注解@PreAuth

1
2
3
4
5
6
7
复制代码@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
String value();
}

Target用于描述注解的使用范围,超出范围时编译失败,可以用在方法或者类上面。在运行时生效。不了解注解相关知识的,可以自行Google。

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
复制代码@Component
@Aspect
public class AuthAspect {


@Pointcut("@annotation(com.blueskykong.auth.demo.annotation.PreAuth)")
private void cut() {
}

/**
* 定制一个环绕通知,当想获得注解里面的属性,可以直接注入该注解
*
* @param joinPoint
* @param preAuth
*/
@Around("cut()&&@annotation(preAuth)")
public Object record(ProceedingJoinPoint joinPoint, PreAuth preAuth) throws Throwable {
//取出注解中的表达式
String value = preAuth.value();
//Spring EL 对value进行解析
SecurityExpressionOperations operations = new CustomerSecurityExpressionRoot(SecurityContextHolder.getContext().getAuthentication());
StandardEvaluationContext operationContext = new StandardEvaluationContext(operations);
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(value);
//获取表达式判断的结果
boolean result = expression.getValue(operationContext, boolean.class);
if (result) {
//继续执行接口内的方法
return joinPoint.proceed();
}
return "Forbidden";
}
}

因为Aspect作用在bean上,所以先用Component把这个类添加到容器中。@Pointcut定义要拦截的注解。@Around定制一个环绕通知,当想获得注解里面的属性,可以直接注入该注解。切面表达式内主要实现了,利用Spring EL对value进行解析,将SecurityContextHolder.getContext()转换成标准的操作上下文,然后解析注解中的表达式,最后获取对表达式判断的结果。

1
2
3
4
5
6
复制代码public class CustomerSecurityExpressionRoot extends SecurityExpressionRoot {

public CustomerSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
}

CustomerSecurityExpressionRoot继承的是抽象类SecurityExpressionRoot,而我们用到的实际表达式是定义在SecurityExpressionOperations接口,SecurityExpressionRoot又实现了SecurityExpressionOperations接口。不过这里面的具体判断实现,Spring Security 调用的也是Spring EL。

4.3 controller接口

下面我们看看最终接口是怎么用上面实现的注解。

1
2
3
4
5
复制代码    @RequestMapping(value = "/test", method = RequestMethod.GET)
@PreAuth("hasAuthority('CREATE_COMPANY')") // 还可以定义很多表达式,如hasRole('Admin')
public String test() {
return "ok";
}

@PreAuth中,可以定义的表达式很多,可以看SecurityExpressionOperations接口中的方法。目前笔者只是实现了hasAuthority()表达式,如果你想支持其他所有表达式,只需要构造相应的SecurityContextHolder即可。

4.4 为什么这样设计?

有些读者看了上面的设计,既然好多用到了Spring Security的工具类,肯定会问,为什么要引入这么复杂的工具类?

其实很简单,首先因为SecurityExpressionOperations接口中定义的表达式足够多,且较为合理,能够覆盖我们在平时用到的大部分场景;其次,笔者之前的设计是直接在注解中指定所需权限,没有扩展性,且可读性查;最后,Spring Security 4 确实引入了@PreAuthorize,@PostAuthorize等注解,本来想用来着,自己尝试了一下,发现对于微服务架构这样的接口级别的操作权限校验不是很适合,十多个过滤器太过复杂,而且还涉及到的Principal、Credentials等信息,这些已经在auth系统实现了身份合法性校验。笔者认为这边的功能实现并不是很复杂,需要很轻量的实现,读者有兴趣可以试着这部分的实现封装成jar包或者Spring Boot的starter。

  1. 总结

如上,首先讲了整合的设计思路,主要包含三个服务:gateway、auth和backend demo。整合的项目,总体比较复杂,其中gateway服务扩充了好多内容,对于暴露的接口进行路由转发,这边引入了Spring Security 的starter,配置资源服务器对暴露的路径进行放行;对于其他接口需要调用auth服务进行身份合法性校验,保证到达backend的请求都是合法的或者公开的接口;auth服务在之前的基础上,补充了role、permission、user相应的接口,供外部调用;backend demo是新起的服务,实现了接口级别的操作权限的校验,主要用到了自定义注解和Spring AOP切面。

由于实现的细节实在有点多,本文限于篇幅,只对部分重要的实现进行列出与讲解。如果读者有兴趣实际的应用,可以根据实际的业务进行扩增一些信息,如auth授权的token、网关拦截请求构造的头部信息、注解支持的表达式等等。

可以优化的地方当然还有很多,整合项目中设计不合理的地方,各位同学可以多多提意见。

推荐阅读

  1. 微服务网关netflix-zuul
  2. 认证鉴权与API权限控制在微服务架构中的设计与实现(一)
  3. 认证鉴权与API权限控制在微服务架构中的设计与实现(二)
  4. 认证鉴权与API权限控制在微服务架构中的设计与实现(三)
  5. 认证鉴权与API权限控制在微服务架构中的设计与实现(四)

源码

**网关、auth权限服务和backend服务的整合项目地址为:

GitHub:https://github.com/keets2012/microservice-integration

或者 码云:https://gitee.com/keets/microservice-integration**

订阅最新文章,欢迎关注我的公众号

微信公众号

本文转载自: 掘金

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

JDK不同操作系统的FileSystem(unix-like

发表于 2017-12-10

前言

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

关于FileSystem类

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

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

这里分成两个系列分析 JDK 对两种(Windows 和 unix-like )操作系统的文件系统的实现类,前面已经讲了 Windows操作系统,对应为 WinNTFileSystem 类。这里接着讲 unix-like 操作系统,对应为 UnixFileSystem 类。篇幅所限,分为上中下篇,此为下篇。

继承结构

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

类定义

1
复制代码class UnixFileSystem extends FileSystem

主要属性

  • slash 表示斜杠符号。
  • colon 表示冒号符号。
  • javaHome 表示Java Home目录。
  • cache 用于缓存标准路径。
  • javaHomePrefixCache 用于缓存标准路径前缀。
1
2
3
4
5
复制代码    private final char slash;
private final char colon;
private final String javaHome;
private ExpiringCache cache = new ExpiringCache();
private ExpiringCache javaHomePrefixCache = new ExpiringCache();

主要方法

list方法

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

  1. 获取 java/lang/String类对象,并检查不能为NULL。
  2. 通过 opendir 函数打开指定路径目录,为空的话则返回空。
  3. 开辟 sizeof(struct dirent64) + (PATH_MAX + 1) 大小的空间,PATH_MAX为1024。
  4. 通过 NewObjectArray 开辟一个初始大小为 maxlen 即16的字符串数组,然后使用循环不断调用 readdir64_r 函数获取目录下的元素,并存放到字符串数组中。这个过程中如果数组满了,则按照原先数组大小成倍扩扩展,扩展后将原数组的值复制到新数组中。
  5. 完成后关闭目录并释放 dirent64。
  6. 执行完上述操作,代表目录下的文件和目录的字符串数组大小就已经确定了,接着按照该大小再一次用 NewObjectArray 开辟一个字符串数组,并把原来数组的值复制过来,这就是最终的字符串数据了。
  7. 中途遇到错误则会跳转到 error 标签处,执行关闭目录和释放资源,并返回空。
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
复制代码private native boolean delete0(File f);

JNIEXPORT jobjectArray JNICALL
Java_java_io_UnixFileSystem_list(JNIEnv *env, jobject this,
jobject file)
{
DIR *dir = NULL;
struct dirent64 *ptr;
struct dirent64 *result;
int len, maxlen;
jobjectArray rv, old;
jclass str_class;

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

WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
dir = opendir(path);
} END_PLATFORM_STRING(env, path);
if (dir == NULL) return NULL;

ptr = malloc(sizeof(struct dirent64) + (PATH_MAX + 1));
if (ptr == NULL) {
JNU_ThrowOutOfMemoryError(env, "heap allocation failed");
closedir(dir);
return NULL;
}

len = 0;
maxlen = 16;
rv = (*env)->NewObjectArray(env, maxlen, str_class, NULL);
if (rv == NULL) goto error;

while ((readdir64_r(dir, ptr, &result) == 0) && (result != NULL)) {
jstring name;
if (!strcmp(ptr->d_name, ".") || !strcmp(ptr->d_name, ".."))
continue;
if (len == maxlen) {
old = rv;
rv = (*env)->NewObjectArray(env, maxlen <<= 1, str_class, NULL);
if (rv == NULL) goto error;
if (JNU_CopyObjectArray(env, rv, old, len) < 0) goto error;
(*env)->DeleteLocalRef(env, old);
}
#ifdef MACOSX
name = newStringPlatform(env, ptr->d_name);
#else
name = JNU_NewStringPlatform(env, ptr->d_name);
#endif
if (name == NULL) goto error;
(*env)->SetObjectArrayElement(env, rv, len++, name);
(*env)->DeleteLocalRef(env, name);
}
closedir(dir);
free(ptr);

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;

error:
closedir(dir);
free(ptr);
return NULL;
}

createDirectory方法

该方法用来创建目录,本地方法很简单,就是获取 File 对象对应的路径,再通过 mkdir 函数创建目录。其中0777,表示文件所有者、文件所有者所在的组的用户、其他用户,都有权限进行读、写、执行的操作。

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

JNIEXPORT jboolean JNICALL
Java_java_io_UnixFileSystem_createDirectory(JNIEnv *env, jobject this,
jobject file)
{
jboolean rv = JNI_FALSE;

WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
if (mkdir(path, 0777) == 0) {
rv = JNI_TRUE;
}
} END_PLATFORM_STRING(env, path);
return rv;
}

rename方法

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

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

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

本地方法主要调用了 rename 函数,根据 Java 层传入的两个 File 对象对应的路径进行重命名。

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

WITH_FIELD_PLATFORM_STRING(env, from, ids.path, fromPath) {
WITH_FIELD_PLATFORM_STRING(env, to, ids.path, toPath) {
if (rename(fromPath, toPath) == 0) {
rv = JNI_TRUE;
}
} END_PLATFORM_STRING(env, toPath);
} END_PLATFORM_STRING(env, fromPath);
return rv;
}

setLastModifiedTime方法

该方法用来设置文件或目录的最后修改时间。本地方法是先获取 File 对象对应的路径,再用 stat64 函数获取指定文件或目录的属性,接着通过 st_atime 成员得到文件最后访问时间,这个时间不用改,要改的是最后修改时间,所以根据 Java 层传入的时间作为最后修改时间,最后通过 utimes 函数设置文件的最后修改时间。

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

JNIEXPORT jboolean JNICALL
Java_java_io_UnixFileSystem_setLastModifiedTime(JNIEnv *env, jobject this,
jobject file, jlong time)
{
jboolean rv = JNI_FALSE;

WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
struct stat64 sb;

if (stat64(path, &sb) == 0) {
struct timeval tv[2];

tv[0].tv_sec = sb.st_atime;
tv[0].tv_usec = 0;

tv[1].tv_sec = time / 1000;
tv[1].tv_usec = (time % 1000) * 1000;

if (utimes(path, tv) == 0)
rv = JNI_TRUE;
}
} END_PLATFORM_STRING(env, path);

return rv;
}

setReadOnly方法

该方法用于将指定文件设置成只读。本地方法逻辑是先通过 statMode 函数获取文件的属性,再去掉 S_IWUSR、S_IWGRP 和 S_IWOTH 标识,分别表示用户写权限、用户组用户写权限和非所有者和用户组用户写权限。最后通过 chmod 函数完成文件只读属性设置。

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

JNIEXPORT jboolean JNICALL
Java_java_io_UnixFileSystem_setReadOnly(JNIEnv *env, jobject this,
jobject file)
{
jboolean rv = JNI_FALSE;

WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
int mode;
if (statMode(path, &mode)) {
if (chmod(path, mode & ~(S_IWUSR | S_IWGRP | S_IWOTH)) >= 0) {
rv = JNI_TRUE;
}
}
} END_PLATFORM_STRING(env, path);
return rv;
}

listRoots方法

该方法用于获取可用的文件系统的根文件对象的数组,对于 unix-like 来说,也就是只有一个根目录了,这之前还会用安全管理器检查下是否有根目录的权限。

1
2
3
4
5
6
7
8
9
10
11
复制代码public File[] listRoots() {
try {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead("/");
}
return new File[] { new File("/") };
} catch (SecurityException x) {
return new File[0];
}
}

getSpace方法

该方法用于获取所挂载的文件系统(包含该方法指定的路径)的空间大小,包括总空间大小、空闲空间大小和可用空间大小,Java 层分别用 SPACE_TOTAL = 0 SPACE_FREE = 1 SPACE_USABLE = 2标识。要查询某个文件的根目录的某某空间大小则将对应的标识传入,通过 getSpace0 本地方法获得。

可以看到本地方法使用了 statfs 或 statvfs64 函数来获取文件系统的信息,进而通过块数量乘以块大小得到总空间大小、空闲空间大小和可用空间大小。

至于为什么分别用 statfs 或 statvfs64 函数,其中 statfs 函数属于特定系统的,而 statvfs64 函数符合 POSIX 标准。一般最好优先使用 statvfs,以前的 JDK(1.7) 实现也是通过statvfs,而这里用 #ifdef MACOSX进行判断,应该是因为 MACOSX 系统对 statvfs64 函数支持不好,

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
复制代码JNIEXPORT jlong JNICALL
Java_java_io_UnixFileSystem_getSpace(JNIEnv *env, jobject this,
jobject file, jint t)
{
jlong rv = 0L;

WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
#ifdef MACOSX
struct statfs fsstat;
#else
struct statvfs64 fsstat;
#endif
memset(&fsstat, 0, sizeof(fsstat));
#ifdef MACOSX
if (statfs(path, &fsstat) == 0) {
switch(t) {
case java_io_FileSystem_SPACE_TOTAL:
rv = jlong_mul(long_to_jlong(fsstat.f_bsize),
long_to_jlong(fsstat.f_blocks));
break;
case java_io_FileSystem_SPACE_FREE:
rv = jlong_mul(long_to_jlong(fsstat.f_bsize),
long_to_jlong(fsstat.f_bfree));
break;
case java_io_FileSystem_SPACE_USABLE:
rv = jlong_mul(long_to_jlong(fsstat.f_bsize),
long_to_jlong(fsstat.f_bavail));
break;
default:
assert(0);
}
}
#else
if (statvfs64(path, &fsstat) == 0) {
switch(t) {
case java_io_FileSystem_SPACE_TOTAL:
rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
long_to_jlong(fsstat.f_blocks));
break;
case java_io_FileSystem_SPACE_FREE:
rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
long_to_jlong(fsstat.f_bfree));
break;
case java_io_FileSystem_SPACE_USABLE:
rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
long_to_jlong(fsstat.f_bavail));
break;
default:
assert(0);
}
}
#endif
} END_PLATFORM_STRING(env, path);
return rv;
}

getNameMax方法

该方法用于获取系统允许的最大文件名长度,直接通过 getNameMax0 本地获取最大长度,然后判断不能超过整型的最大值。

1
2
3
4
5
6
7
复制代码public int getNameMax(String path) {
long nameMax = getNameMax0(path);
if (nameMax > Integer.MAX_VALUE) {
nameMax = Integer.MAX_VALUE;
}
return (int)nameMax;
}

本地方法通过 pathconf 函数来获取指定路径下的文件名长度限制值,传入 _PC_NAME_MAX 即可得到。

1
2
3
4
5
6
7
8
9
10
复制代码JNIEXPORT jlong JNICALL
Java_java_io_UnixFileSystem_getNameMax0(JNIEnv *env, jobject this,
jstring pathname)
{
jlong length = -1;
WITH_PLATFORM_STRING(env, pathname, path) {
length = (jlong)pathconf(path, _PC_NAME_MAX);
} END_PLATFORM_STRING(env, path);
return length != -1 ? length : (jlong)NAME_MAX;
}

compare方法

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

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

hashCode方法

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

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

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

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

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

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

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

相关阅读:

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

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

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

JDK不同操作系统的FileSystem(unix-like)上篇

JDK不同操作系统的FileSystem(unix-like)中篇

欢迎关注:

这里写图片描述

本文转载自: 掘金

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

Laravel 路由中不固定数量参数,是如何实现的?

发表于 2017-12-10

最近在读 Laravel 源码的时候,发现了一个段特别有趣的代码,大家请看:

Laravel 源码

... 这三个点是做什么用的呢?我查了 PHP 的手册后发现,这个东西叫做可变数量的参数列表。

这个是干啥用的呢?PHP 手册是这么解释的。

... 是用户自定义函数中支持可变数量的参数列表。

… 存在于 PHP 5.6 及以上的版本中。 在 PHP 5.5 及更早版本中,使用函数 func_num_args(),func_get_arg(),和 func_get_args() 。

可变数量的参数列表,这个概念可能你会觉得很抽象。

我们可以这么来理解,我们自定义了一个函数或者某个 function,但是这个 function 的参数数量是不固定的,这也就是可变数量的参数列表。

关于可变数量的参数列表,让我们来看两个示例;

1
2
3
4
5
6
7
8
9
10
11
复制代码<?php
function sum(...$numbers) {
$acc = 0;
foreach ($numbers as $n) {
$acc += $n;
}
return $acc;
}

echo sum(1, 2, 3, 4);
?>

以上例程会输出:

10

可变数量参数将被传递到 function 中,给定的参数变量会作为数组。

我们再看一个示例:

1
2
3
4
5
6
7
复制代码<?php
function add($a, $b) {
return $a + $b;
}

echo add(...[1, 2])
?>

以上例程会输出:

3

可变数量参数将被传递到 function 中,给定的数组会作为参数变量。


这个可变数量参数,又和 Laravel 路由有什么关系呢?

在 Laravel 中,我们自定义路由是非常自由的,比如是这样的:

1
2
3
4
5
6
7
复制代码Route::get('user/{id}', 'UsersController@filter');

//路由对应的方法
public function filter($id)
{
# code...
}

或许有可能这样的:

1
2
3
4
5
6
7
复制代码Route::get('user/{id}/{name}', 'UsersController@filter');

//路由对应的方法
public function filter($id, $name)
{
# code...
}

Laravel 路由中这样不固定数量参数,在代码中是如何实现的呢?使用的就是可变数量参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码// */vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php

/**
* Dispatch a request to a given controller and method.
*
* @param \Illuminate\Routing\Route $route
* @param mixed $controller
* @param string $method
* @return mixed
*/
public function dispatch(Route $route, $controller, $method)
{
$parameters = $this->resolveClassMethodDependencies(
$route->parametersWithoutNulls(), $controller, $method
);

if (method_exists($controller, 'callAction')) {
return $controller->callAction($method, $parameters);
}

return $controller->{$method}(...array_values($parameters));
}

不得不让人佩服 Laravel 作者泰勒脑路清奇啊!

最近有人留言问我,Laravel 的路由原理是什么?Laravel 的路由是如何分配到 Controller 中的方法中呢?

下一篇文章,我会讲解 Laravel 路由的实现过程以及原理。

大家可以关注一下我的公众号哦! :joy:

file

本文转载自: 掘金

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

Java泛型的局限和问题 泛型的局限 泛型的常用经验 参考资

发表于 2017-12-10

这篇文章主要总结泛型的一些局限和实际的使用经验

泛型的局限

  1. 任何基本类型不能作为类型参数

经过类型擦除后,List
中包含的实际上还是Object的域,而在Java类型系统中Object和基本类型是两套体系,需要通过“自动装包、拆包机制”来进行交互。

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

public static void main(String[] args) {
//(1)通过自动装包和拆包,在泛型中和基本类型进行交互
List<Integer> li = new ArrayList<>();
for (int i = 0; i < 5; i++) {
//发生自动装包
li.add(i);
}

//发生自动拆包
for (int i: li) {
System.out.println(i);
}

//(2)错误示例
List<int> list = new ArrayList<>();
}
}

​
2. 任何在运行时需要知道确切类型信息的操作都无法工作。

由于Java的泛型是编译期泛型(在进入运行时后没有泛型的概念),因此运行时的类型转换和类型判定等操作都没有效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class Erased<T> {

public void f(Object[] args) {
//(1)不能在类型参数上使用instanceof操作, instanceof右边必须是某个原生类型或数组
if (args instanceof T) {}

//(2)不能直接实例化类型参数
T var = new T();

//(3)不能这么定义泛型数组,原因同上
T[] array = new T[100];

//(4)先定义一个Object数组,再强转成T[]数组,绕过泛型检查,但是会收到一个告警
T[] array2 = (T[])new Object[100];
}
}

​
3. 冲突1:方法名一样,参数列表是同一个类型参数的两个泛型方法,重载将产生相同的函数签名;

1
2
3
4
5
6
7
8
复制代码package org.java.learn.generics;

import java.util.List;

public class UserList<W, T> {
void f(List<T> v) {}
void f(List<W> v) {}
}

从图中的提示可以看出,在泛型擦除后,这两个方法签名完全相同,产生冲突;

泛型导致的重载冲突
4. 冲突2:使用泛型接口时,需要避免重复实现同一个接口

1
2
3
4
5
复制代码interface Payable<T> {}

class Employee implements Payable<Employee> {}

class Hourly extends Employee implements Payable<Hourly> {}

IDEA编辑器给出如下所示——“Payable不能被不同的类型参数继承,即不能重复实现同一个接口”

使用泛型接口时的冲突

​
5. 不能在静态域或方法中引用类型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class Erased<T> {

public static void f(Object[] args) {
//不能在静态域中引用类型参数
if (args instanceof T) {}


T var = new T();


T[] array = new T[100];


T[] array2 = (T[])new Object[100];
}
}

这个例子跟问题2基本相同,唯一是在方法的签名里多了一个static关键字,然后引发编译错误的原因就变成了:在静态域中无法引用类型变量(入下图所示)。

2017-12-0920.31.09.png

泛型的常用经验

  1. 尽量消除异常,初学者容易写出使用原生类型的代码,或者使用泛型不当的代码,现在编辑器非常先进,尽量消除提示的异常;对于开发者自己确认不需要消除切可以工作的代码,可以使用@SuppressWarnings("unchecked")屏蔽掉异常;
  2. 能用泛型类(或接口)的时候尽量使用;能用泛型方法的时候尽量使用泛型方法;
  3. 定义API时,尽量使用泛型;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class PlainResult<T> extends BaseResult {
private static final long serialVersionUID = -xxxxxx;

private T data;

public PlainResult() {
}

public T getData() {
return this.data;
}

public void setData(T data) {
this.data = data;
}
}

​
4. 编写基础工具类时,尽量使用泛型;

* 例子1:通用的返回值对象
1
2
3
4
5
6
7
8
9
10
11
12
复制代码//使用泛型类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DataListPageInfo<T> {
int page;
int pageSize;
int totalCount;
List<T> items = new ArrayList<>();

}
* 例子2:缓存操作工具类,这里使用了
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
复制代码@Component
public class RedisTemplateService {

private static final Logger LOGGER = LoggerFactory.getLogger(RedisTemplateService.class);

@Resource
private RedisTemplate redisTemplate;

private String getKey(String key, Object... params) {
if (params == null || params.length <= 0) {
return key;
}

return String.format(key, params);
}

//这里使用了泛型方法
public <T> T getFromJsonCache(Class<T> type, String keyPrefix, Object... params) {
try {
String ret = getString(keyPrefix, params);
return JSON.parseObject(ret, type);
} catch (Exception e) {
LOGGER.error("json parse error, type={},keyPrefix={},params={}", type, keyPrefix, params, e);
}

return null;
}

……
}

​

参考资料

  1. 《Java编程思想》
  2. 《Java核心技术》
  3. 《Effective Java》

本文转载自: 掘金

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

Laravel框架blade模板超新手级教程&小技巧

发表于 2017-12-10

作者:不洗碗工作室 - Hanxiao

版权归作者所有,转载请注明出处

为什么要使用blade模板

  • 容易理解,思路清晰
  • 方便,直接可以在框架里使用,可以直观的观察到自己写的代码的样子
  • 对后台数据的处理方便,blade模板和框架之间的参数传递实在是太方便了,可以说是随心所欲
  • 文档齐全,有问题基本可以查到
  • 适合单人开发,在完成后端实现后可以直接在blade模板里调用,写起来顺手
  • 可以使用bootstrap等样式来渲染页面,可以达到一些基本的美化需求

局限性

  • 只能做一些简易的页面,复杂的页面还是前后端分离更加适合
  • 想不出来了,它是真的好用啊!作为一个小白写一个小型项目用这个东西真的是个神器

对模板使用的一些心得和理解

我们不去谈模板的功能实现方式,我们只讲使用

  1. 设计方式: 我们可以按照最简单的思路来设计自己的blade模板,也就是看页面,比方说我们设计一个个人博客,我们的导航栏和footer几乎是不会变的,所以我们就可以先把这种每个页面都通用的地方独立出来生成一个模板,然后让别的页面继承这个独立出来的模板,可以理解为别的页面都都是基于这个页面来布置的
  2. 数据显示: 重点来了,blade模板的参数传递实在是方便的很,在看文档的时候我们不难看出,后台在return view的时候可以给页面附带参数,然后我们就可以在页面中以变量的形式直接调用数据。
  3. 例如文档中给的例子: image
  4. 这是最简单的用法,我们可以对这个方法进行拓展,我们返回的可以是一个简单的变量,也可以是一个复杂的变量。
  5. 我们还可以在一个页面调用一些方法,比方说框架自带的用户系统,我们可以在模板中直接使用方法Auth::check() image
  6. 这样就对文章的评论模块进行了实现,是不是很简单!
  7. 当然了,如果你想加入一些个人的服务,可以使用服务注入(inject)image
  8. 比方说我在创建一篇文章时要选择文章所属类别,就可以将类别服务注入进来

小技巧

  • 上面两条是对基本操作的简要概述,下面我来分享一下我个人在使用blade时总结的几个小窍门
    • 懒人写法
      • 想要写一个逻辑清晰的模板往往需要我们在创建目录的时候下点功夫,比如使用合适的目录结构与命名等等,但是这个过程往往让我们很痛苦(其实这样是值得的,因为这样做后期维护会很方便),因为本来不是很大的一个项目,我却把时间浪费在了给文件取名字和目录结构构思上。所以我结合blade模板的if语句和参数传递,想出了一个可以在一个blade文件中完成多个页面的写法
      • 具体实现如下:在return view时,我们多加一个参数,route参数,然后在blade模板文件里对route的值进行判断,不同的方法返回的route值不同,这样我们就可以通过if语句来进行判断,从而选择显示不同的视图。 image抽象一些就是这样,左边是常用写法,右边是我们的懒人写法
    • 路由与参数结合
      • 举个最简单的例子就是博客文章系统,具体实现:我们可以这样设置路由
      • Route::get('/article-{article_id}','ArticleController@showArticle');
      • 在页面中这样布置对应的文章链接<a href="{url('/article/'.$article->id)}">{ $article->title }</a>

小结

blade模板与Laravel框架碰撞可以产生了令人意向不到的火花,它俩一定还有很多很多有趣的用法,这需要我们的想象力。有好多的需求和场景不一定需要我们掌握多么高难的技术,灵活运用手中已有的工具往往可以解决很多问题。

1
2
复制代码PS:懒人写法仅试用小项目以及写着玩的时候,被领导或同伴发现是要被打死的
希望我举的例子能够引起你的一些思考,给予你一些灵感∩_∩

本文转载自: 掘金

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

淘宝直播弹幕爬虫

发表于 2017-12-10

背景说明

公司有通过淘宝直播间短链接来爬取直播弹幕的需求, 奈何即便google上面也仅找到一个相关的话题, 还没有答案. 所以只能自食其力了.

爬虫的github仓库地址在文末, 我们先看一下爬虫的最终效果:

下面我们来抽丝剥茧, 重现一下调研过程.

页面分析

直播间地址在分享直播时可以拿到:

弹幕一般不是websocket就是socket. 我们打开dev tools过滤ws的请求即可看到websocket地址:

提一下斗鱼: 它走的是flash的socket, 我们就算打开dev tools也是懵逼, 好在斗鱼官方直接开放了socket的API.

我们继续查看收到的消息, 发现消息的压缩类型compressType有两种: COMMON和GZIP. data的值肯定就是目标消息了, 看起来像经过了base64编码, 解密过程后面再说.

现在我们首先要解决的问题是如何拿到websocket地址. 分析一下html source, 发现可以通过其中不变的部分查找到脚本:

然鹅, 拿到这块整个的脚本格式化之后发现, 原始代码明显是模块化开发的, 经过了打包压缩. 所以我们只能分析模块内一小块代码, 这是没有意义的.
但是我们可以观察到不同的直播间websocket地址唯一不同的只有token, 所以我们可以想办法拿到token. 当然这是很恶心的环节, 完全没有头绪, 想到的各种可能性都失败了. 后面像无头苍蝇一样看页面发起的请求, 竟然给找到了…

token是通过api请求获取的, api地址是:

1
复制代码http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/

好了那websocket地址的问题解决了, 我们开始写爬虫吧.

编写爬虫

看看api的query string那一堆动态参数, 普通爬虫就别想了, 我们祭出神器: puppeteer.

puppeteer是谷歌推出的开放Node API的无头浏览器, 理论上可以可编程化地控制浏览器的各种行为, 对于我们的场景来说就是:
直播页面加载完之后, 拦截获取websocket token的api请求, 解析结果拿到token. 这部分的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码    const browser = await puppeteer.launch()
const page = (await browser.pages())[0]
await page.setRequestInterception(true)
const api = 'http://h5api.m.taobao.com/h5/mtop.mediaplatform.live.encryption/1.0/'
const { url } = message

// intercept request obtaining the web socket token
page.on('request', req => {
if (req.url.includes(api)) {
console.log(`[${url}] getting token`)
}
req.continue()
})
page.on('response', async res => {
if (!res.url.includes(api)) return

const data = await res.text()
const token = data.match(/"result":"(.*?)"/)[1]
const url = `ws://acs.m.taobao.com/accs/auth?token=${token}`
})

// open the taobao live page
await page.goto(url, { timeout: 0 })
console.log(`[${url}] page loaded`)

这里有个性能优化的小技巧. puppeteer官方示例中获取page实例会打开一个新页面: const page = await browser.newPage(), 实际上浏览器启动本来就默认有个about:blank页面打开, 我们的代码中直接是获取这个打开的实例来跳转直播页面, 这样就可以少一个进程.

可以ps ax|grep puppeteer观察启动的进程数来进行对比, 默认有两个主进程, 剩余的都是页面进程.

获取到websocket地址就可以建立连接拉取消息了:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码    const url = `ws://acs.m.taobao.com/accs/auth?token=${token}`
const ws = new WebSocket(url)

ws.on('open', () => {
console.log(`\nOPEN: ${url}\n`)
})
ws.on('close', () => {
console.log('DISCONN')
})
ws.on('message', msg => {
console.log(msg)
})

消息解密

现在我们能持续拉取消息了, 这样会方便分析. 前面我们分析页面的时候发现compressType有两种: COMMON和GZIP. 经过尝试, COMMON的可以直接得到明文, 而GZIP的需要再经过一次gunzip解码. 解码结果大致如下, 里面已经可以看到昵称和弹幕内容了:

然鹅, 一切才刚刚开始…内容里面是有乱码的, 基于这样的内容做正则匹配无果. 如果尝试直接保存buffer或者buffer.toString()到文件会发现文件根本打不开, 内容是无法解析的:

没办法, 我们只能分析原始buffer array的utf8编码了. 这里开了脑洞, 直接将buffer array做join得到的string拿来分析其规律 (分析代码见analyze.js文件):

几个样本的分析结果如下, 其中不变的部分做了高亮:

这些值可能是由有效字符编码按一定规则换算过来, 但谁又能猜得到呢, 也没必要.

这样我们就可以通过一个正则表达式解析出nick和barrage了:

1
复制代码/.*,[0-9]+,0,18,[0-9]+,(.*?),32,[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+,44,50,2,116,98,[0-9]+,0,10,[0-9]+,(.*?),18,20,10,12/

当然这个pattern同样能匹配到关注主播的弹幕, 这不是我们想要的. 我们可以通过一串确定的buffer字符串提前过滤掉这种消息:

1
复制代码const followedPattern = '226,129,130,226,136,176,226,143,135,102,111,108,108,111,119'

至此我们已经可以解析出干干净净的昵称+弹幕了. 完整解密代码如下:

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
复制代码function decode(msg) {
// base64 decode
let buffer = Buffer.from(msg.data, 'base64')
if (msg.compressType === 'GZIP') {
// gzip decode
buffer = zlib.gunzipSync(buffer)
}
const bufferStr = buffer.join(',')

// [followed] notifications are ignored
const followedPattern = '226,129,130,226,136,176,226,143,135,102,111,108,108,111,119'
if (bufferStr.includes(followedPattern)) {
return
}

// // print for debugging
// console.log(bufferStr)
// console.log(buffer.toString())

// first match is nick name and second match is barrage content
const barragePattern = /.*,[0-9]+,0,18,[0-9]+,(.*?),32,[0-9]+,[0-9]+,[0-9]+,[0-9]+,[0-9]+,44,50,2,116,98,[0-9]+,0,10,[0-9]+,(.*?),18,20,10,12/
const matched = bufferStr.match(barragePattern)
if (matched) {
const nick = parseStr(matched[1])
const barrage = parseStr(matched[2])
console.log(`${nick}: ${barrage}`)
}
}

当然可能还存在一个问题, 是关于上面分析结果表里的barrage前, 有连续的5位固定不变, 实际上刚开始是连同前面一位共6位不变的, 结果过了一天之后前面那位从130变到了131, 而再往前的几位变化频率则特别高. 所以我怀疑这些值有可能是跟当前时间有关.

可能不确定的一段时间之后这5位固定值也会变掉吧, 到时正则就得调整了, 但应该可以正常运行很久了. 如有哪些同仁感兴趣, 可以找找规律.

进程维护

实际使用时流程大致应该是这样的: 收到请求之后主进程fork一个爬虫子进程来获取websocket url, 子进程返回结果给主进程, 在使用方建立websocket连接(抢过连接)之后, 子进程便可自杀释放资源, 自杀的同时browser.close()杀死puppeteer相关进程.

之所以这样做是因为测试下来: websocket断开连接不久token会失效.

Github仓库

记得star啊😉

github.com/xiaozhongli…

本文转载自: 掘金

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

1…911912913…956

开发者博客

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