那些绕不过去的 Java 知识点(一)

关于本文

虽然接触 Java 已经 8 年之久,可惜学习之初的笔记文档没能很好地保存下来。本文是近几年工作学习中遇到的一些零散的知识点,包括了 基础概念、实用的编程技巧、代码可读性、设计模式、性能优化(工具&编码)、测试相关、JVM 相关、常用的工具和常见问题。本着好记性不如烂笔头的初衷,在不断地踩坑和爬坑的过程中,慢慢地记录成文。期待着本文能起到抛砖引玉的作用,以看到大家的真知灼见。

基础知识

注解

GuardedBy

@GuardedBy 注解可以作用于某一个属性或者方法,约定在访问这些被注解标记的资源时,能被同步代码块保护着。简单的使用案例如下:

@GuardedBy("obj")
private ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
private final Object obj = new Object();

public void put(String k, String v) {
    synchronized (obj) {
        map.put(k, v);
    }
}

/**
 * If you use `error prone` tool to check this, this annotation should be `@SuppressWarnings("GuardedBy")`
 * {@see https://errorprone.info/bugpattern/GuardedBy}
 * {@see https://github.com/apache/incubator-druid/pull/6868#discussion_r249639199}
 */
@SuppressWarnings("FieldAccessNotGuarded")
public void remove(String k) {
    map.remove(k);
}

@Override
public String toString() {
    synchronized (obj) {
        return "GuardedByExample{" +
                "map=" + map +
                '}';
    }
}

Tips: Code Example from Apache Druid;另外,error-prone 工具支持对多种版本@GuardedBy 进行检查

InterfaceStability

  • @InterfaceStability.Stable
    主版本是稳定的,不同主版本间,可能不兼容

  • @InterfaceStability.Evolving
    不断变化中,不同的次版本间,可能不兼容

  • @InterfaceStability.Unstable
    不对可靠性和健壮性做任何保证

InterfaceAudience

  • @InterfaceAudience.Public
    对所有工程可用

  • @InterfaceAudience.LimitedPrivate
    仅限特定的工程,如 HBaseZookeeperHDFS 等(以 Hadoop 为例)

  • @InterfaceAudience.Private
    仅限于工程内部使用

序列化

transient

给实现了 Serializable 接口的类中的字段,增加 transient 修饰符,则可以让该字段跳过序列化的过程

Tips: Full code is here and here.

类中包含没有实现 Serializable 接口的字段

需要自己实现 serializedeserialize 方法

Tips: Full code is here and here.

实用技巧

Collection 内元素类型转换

// 对集合里面的元素类型进行转换(A -> B)
List<B> variable = (List<B>)(List<?>) collectionOfListA;

集合的差集、交集、并集

// 使用 Guava 中封装的 Sets 类
import com.google.common.collect.Sets;

Sets.difference(set1, set2)
Sets.intersection(set1, set2)
Sets.union(set1, set2)

数组转为 Set

// JDK8
new HashSet<>(Arrays.asList(str.trim().split(",")))
// JDK9+
Set.of(str.trim().split(","));

TransmittableThreadLocal 解决跨父子线程和线程池缓存问题

编码

String tlMsg = "tl";
String ttlMsg = "ttl";

final ThreadLocal<String> tl = new ThreadLocal<>();
tl.set(tlMsg);
final TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
ttl.set(ttlMsg);

assertEquals(tl.get(), tlMsg);
assertEquals(ttl.get(), ttlMsg);

new Thread(() -> {
    assertNull(tl.get());
    assertEquals(ttl.get(), ttlMsg);
}).start();

Tips: Full code is here.

参考

可读性

魔法数字

编码过程中,应该避免出现没有声明含义的纯数字

反面示例

// org.apache.kafka.connect.runtime.distributed.DistributedHerder#stop
@Override
public void stop() {
  if (!forwardRequestExecutor.awaitTermination(10000L, TimeUnit.MILLISECONDS))
    forwardRequestExecutor.shutdownNow();
}

正面示例

// 这里除了需要将 10000L 抽象成 FORWARD_REQUEST_SHUTDOWN_TIMEOUT_MS 静态变量
// 还可以进一步使用 10_000L 方便阅读
// 因为这里和时间有关,更进一步,可以使用 TimeUnit.SECONDS.toMillis(10) 来代替纯数字
private static final long FORWARD_REQUEST_SHUTDOWN_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);

@Override
public void stop() {
  if (!forwardRequestExecutor.awaitTermination(FORWARD_REQUEST_SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS))
    forwardRequestExecutor.shutdownNow();
}

Tips: Full code is here.

字符串判空

反面示例

// org.apache.kafka.connect.runtime.distributed.DistributedHerder#reconfigureConnector
leaderUrl.equals("")

正面示例

// 一方面,需要考虑不能将对象放在 equals 方法之前,避免空指针异常
// 另一方面,使用 `str.length() == 0` 的方式,效率会高一些
// 进一步,使用 isEmpty() 方法,则可以使得代码更加可读
leaderUrl == null || leaderUrl.trim().isEmpty()

Tips: Full code is here.

箭头型代码

反面示例

public void m(String s) {
    if (s != null) {
        if (s.trim().length() > 0) {
            if (s.contains("yuzhouwan")) {
                System.out.println("https://yuzhouwan.com");
            }
        }
    }
}

正面示例

public void m(String s) {
    if (s == null) {
        return;
    }
    if (s.trim().length() == 0) {
        return;
    }
    if (!s.contains("yuzhouwan")) {
        return;
    }
    System.out.println("https://yuzhouwan.com");
}

参考

函数式编程

Optional

反面示例
public Long deserialize(ByteArrayDataInput in) {
    return isNullByteSet(in) ? null : in.readLong();
}
正面示例
return Optional.ofNullable(in)
               .filter(InputRowSerde::isNotNullByteSet)
               .map(ByteArrayDataInput::readLong)
               .get();
参考
  • Write null byte when indexing numeric dimensions with Hadoop #7020

anyMatch

反面示例
private boolean isTaskPending(Task task) {
    for (TaskRunnerWorkItem workItem : taskRunner.getPendingTasks()) {
        if (workItem.getTaskId().equals(task.getId())) {
            return true;
        }
    }
    return false;
}
正面示例
final String taskId = task.getId();
return taskRunner.getPendingTasks()
                 .stream()
                 .anyMatch(t -> taskId.equals(t.getTaskId()));
参考
  • Run pending tasks when assigned a task that is already pending #6991

设计模式

里氏替换原则

描述

里氏替换原则Liskov substitution principle,LSP)强调的是 面向对象程序设计中的 可替代性,说明在计算机程序中,如果 S 是 T 的子类型,那么类型 T 的对象可以用类型 S 的对象替换(即 T 类型的对象可以被任何子类型 S 的对象替换),而不改变程序的任何期望属性(正确地执行的任务等)

参考

性能优化

工具层面

性能指标监控

<metrics.version>3.2.0</metrics.version>

<dependency>
  <groupId>io.dropwizard.metrics</groupId>
    <artifactId>metrics-core</artifactId>
  <version>${metrics.version}</version>
  <exclusions>
    <exclusion>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
    </exclusion>
  </exclusions>
</dependency>

JMH 基准测试

增加 Maven 依赖
<jmh.version>1.19</jmh.version>

<!-- JMH -->
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>${jmh.version}</version>
</dependency>
<dependency>
    <groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>${jmh.version}</version>
</dependency>
编写 JMH 测试案例
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class BenchmarkSimple {

    @Benchmark
    public void bench() {
        add(1, 1);
    }

    private static int add(int a, int b) {
        return a + b;
    }

    /*
    Benchmark               Mode    Cnt     Score        Error     Units
    BenchmarkSimple.bench   thrpt    5  13352311.603 ± 767137.272  ops/ms
     */
    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(BenchmarkSimple.class.getSimpleName())
                .forks(1)
                .warmupIterations(5)
                .measurementIterations(5)
                .threads(10)
                .build();
        new Runner(opt).run();
    }
}

Tips: Full code is here.

优势
  • 不和会 JUnit 冲突,不用担心 Jenkins 会自动跑 Benchmark 测试而影响效率(否则需要添加 @Ignore 让 CI 系统忽略掉性能相关的 JUnit 测试用例)
  • 支持 warm up,可以解决 JIT 预热问题

BTrace

介绍

BTrace is a safe, dynamic tracing tool for the Java platform. BTrace can be used to dynamically trace a running Java program (similar to DTrace for OpenSolaris applications and OS). BTrace dynamically instruments the classes of the target application to inject tracing code ("bytecode tracing").

参考

LooseJar

介绍

分析没有被加载任何 class 的 jar 包,帮助删除工程中不必要的 jar 包。不过,需要注意的是,有些类是动态加载的(比如数据类型转换类的,只有加载数据时才会用到),需要尽可能地多测试,才能保证 LooseJar 分析准确

使用步骤
  • 下载
    在 LooseJar 的 release 页面,下载 loosejar-1.1.0.jar

  • 拷贝
    将 loosejar.jar 放到应用的 WEB-INF 下的 lib 目录中,比如说路径是 /yuzhouwan/yuzhouwan-site/yuzhouwan-site-web/src/main/webapp/WEB-INF/lib/loosejar.jar

  • 配置
    在 IDE 中的 installed JRES 里面的 JDK 处配置 -Dfile.encoding=uft8 -javaagent:/yuzhouwan/yuzhouwan-site/yuzhouwan-site-web/src/main/webapp/WEB-INF/lib/loosejar.jar

  • 启动
    启动应用,尽可能做到路径全覆盖地测试应用,让每段代码都被执行到

  • 查看结果
    运行 JDK 自带的 Jconsole 工具,选择 BootStrap 的那个端口,然后选择 MBean 下的 com.googlecode.loosejar,并点击 summary,即可看到分析结果

编码层面

并发相关

synchronized

详见,《如何运用 JVM 知识提高编程水平 - synchronized 的性能之争

StampedLock
特性

该类是一个读写锁的改进,它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写

参考

集合优化

HashMap
为什么 Java 8 版本中引入红黑树
  • 原因

    JDK8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布

    当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势

    针对这种情况,JDK8 中引入了 红黑树(查找时间复杂度为 O(\log n))来优化这个问题

  • 流程

    添加时,当桶中链表个数超过 8 时会转换成红黑树

    删除、扩容时,如果桶中结构为红黑树,并且树中元素个数太少的话,会进行修剪或者直接还原成链表结构

    查找时即使哈希函数设计不合理,大量元素集中在一个桶中,由于有红黑树结构,性能也不会差

Collections 类
空集合

Collections.emptyList() 重用一个对象而不是创建一个新对象,就像 Arrays.asList() 一样。不同的是,Collections.singletonList(something) 是不可变的,而 Arrays.asList(something) 是一个固定大小的 List,其中 List 和 Array 在 Heap 中已经连接。

参考
LinkedList vs. ArrayList
LinkedList
  • get(int index) is O(\frac{n}4) average
  • add(E element) is O(1)
  • add(int index, E element) is O(\frac{n}4) average, but O(1) when index = 0
  • remove(int index) is O(\frac{n}4) average
  • Iterator.remove() is O(1)
  • ListIterator.add(E element) is O(1)

Note: O(\frac{n}4) is average, O(1) best case (e.g. index = 0), O(\frac{n}2) worst case (middle of list)

ArrayList
  • get(int index) is O(1)
  • add(E element) is O(1) amortized, but O(n) worst-case since the array must be resized and copied
  • add(int index, E element) is O(\frac{n}2) average
  • remove(int index) is O(\frac{n}2) average
  • Iterator.remove() is O(\frac{n}2) average
  • ListIterator.add(E element) is O(\frac{n}2) average
参考
contains 方法
HashSet

时间复杂度 和 内存使用率 角度看,HashSet 为最佳之选

参考
toArray 方法
说明

There are two styles to convert a collection to an array: either using a pre-sized array (like c.toArray(new String[c.size()])) or using an empty array (like c.toArray(new String[0]).

In older Java versions using pre-sized array was recommended, as the reflection call which is necessary to create an array of proper size was quite slow. However since late updates of OpenJDK 6 this call was intrinsified, making the performance of the empty array version the same and sometimes even better, compared to the pre-sized version. Also passing pre-sized array is dangerous for a concurrent or synchronized collection as a data race is possible between the size and toArray call which may result in extra nulls at the end of the array, if the collection was concurrently shrunk during the operation.

This inspection allows to follow the uniform style: either using an empty array (which is recommended in modern Java) or using a pre-sized array (which might be faster in older Java versions or non-HotSpot based JVMs).

压测
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class ToArrayBenchmark {

    @Param({"1", "100", "1000", "5000", "10000", "100000"})
    private int n;

    private final List<Object> list = new ArrayList<>();

    @Setup
    public void populateList() {
        for (int i = 0; i < n; i++) {
            list.add(0);
        }
    }

    @Benchmark
    public Object[] preSize() {
        return list.toArray(new Object[n]);
    }

    @Benchmark
    public Object[] resize() {
        return list.toArray(new Object[0]);
    }

    /*
    Integer List:
    Benchmark                    (n)  Mode  Cnt       Score        Error  Units
    ToArrayBenchmark.preSize       1  avgt    3      41.552 ±    108.030  ns/op
    ToArrayBenchmark.preSize     100  avgt    3     216.449 ±    799.501  ns/op
    ToArrayBenchmark.preSize    1000  avgt    3    2087.965 ±   6027.778  ns/op
    ToArrayBenchmark.preSize    5000  avgt    3    9098.358 ±  14603.493  ns/op
    ToArrayBenchmark.preSize   10000  avgt    3   24204.199 ± 121468.232  ns/op
    ToArrayBenchmark.preSize  100000  avgt    3  188183.618 ± 369455.090  ns/op
    ToArrayBenchmark.resize        1  avgt    3      18.987 ±     36.449  ns/op
    ToArrayBenchmark.resize      100  avgt    3     265.549 ±   1125.008  ns/op
    ToArrayBenchmark.resize     1000  avgt    3    1560.713 ±   2922.186  ns/op
    ToArrayBenchmark.resize     5000  avgt    3    7804.810 ±   8333.390  ns/op
    ToArrayBenchmark.resize    10000  avgt    3   24791.026 ±  78459.936  ns/op
    ToArrayBenchmark.resize   100000  avgt    3  158891.642 ±  56055.895  ns/op

    Object List:
    Benchmark                    (n)  Mode  Cnt      Score       Error  Units
    ToArrayBenchmark.preSize       1  avgt    3     36.306 ±    96.612  ns/op
    ToArrayBenchmark.preSize     100  avgt    3     52.372 ±    84.159  ns/op
    ToArrayBenchmark.preSize    1000  avgt    3    449.807 ±   215.692  ns/op
    ToArrayBenchmark.preSize    5000  avgt    3   2080.172 ±  2003.726  ns/op
    ToArrayBenchmark.preSize   10000  avgt    3   4657.937 ±  8432.624  ns/op
    ToArrayBenchmark.preSize  100000  avgt    3  51980.829 ± 46920.314  ns/op
    ToArrayBenchmark.resize        1  avgt    3     16.747 ±    85.131  ns/op
    ToArrayBenchmark.resize      100  avgt    3     43.803 ±    28.704  ns/op
    ToArrayBenchmark.resize     1000  avgt    3    404.681 ±   132.986  ns/op
    ToArrayBenchmark.resize     5000  avgt    3   1972.649 ±   174.691  ns/op
    ToArrayBenchmark.resize    10000  avgt    3   4021.440 ±  1114.212  ns/op
    ToArrayBenchmark.resize   100000  avgt    3  44204.167 ± 76714.850  ns/op
     */
    public static void main(String[] args) throws Exception {
        Options opt = new OptionsBuilder()
                .include(ToArrayBenchmark.class.getSimpleName())
                .forks(1)
                .warmupIterations(1)
                .measurementIterations(3)
                .threads(1)
                .build();
        new Runner(opt).run();
    }
}

Tips: Full code is here.

instanceof
Operation Runtime in nanoseconds per operation Relative to instanceof
INSTANCEOF 39,598 ± 0,022 ns/op 100,00 %
GETCLASS 39,687 ± 0,021 ns/op 100,22 %
TYPE 46,295 ± 0,026 ns/op 116,91 %
OO 48,078 ± 0,026 ns/op 121,42 %
参考

字符串相关

repeat

借鉴 JDK11 中新增的 String#repeat 特性,实现高效的 repeat 工具方法

import java.nio.charset.StandardCharsets;

/**
 * Returns a string whose value is the concatenation of the
 * string {@code s} repeated {@code count} times.
 * <p>
 * If count or length is zero then the empty string is returned.
 * <p>
 * This method may be used to create space padding for
 * formatting text or zero padding for formatting numbers.
 *
 * @param count number of times to repeat
 * @return A string composed of this string repeated
 * {@code count} times or the empty string if count
 * or length is zero.
 * @throws IllegalArgumentException if the {@code count} is negative.
 * @link https://bugs.openjdk.java.net/browse/JDK-8197594
 */
public static String repeat(String s, int count) {
    if (count < 0) {
        throw new IllegalArgumentException("count is negative, " + count);
    }
    if (count == 1) {
        return s;
    }
    byte[] value = s.getBytes(StandardCharsets.UTF_8);
    final int len = value.length;
    if (len == 0 || count == 0) {
        return "";
    }
    if (len == 1) {
        final byte[] single = new byte[count];
        Arrays.fill(single, value[0]);
        return new String(single, StandardCharsets.UTF_8);
    }
    if (Integer.MAX_VALUE / count < len) {
        throw new OutOfMemoryError();
    }
    final int limit = len * count;
    final byte[] multiple = new byte[limit];
    System.arraycopy(value, 0, multiple, 0, len);
    int copied = len;
    for (; copied < limit - copied; copied <<= 1) {
        System.arraycopy(multiple, 0, multiple, copied, copied);
    }
    System.arraycopy(multiple, 0, multiple, copied, limit - copied);
    return new String(multiple, StandardCharsets.UTF_8);
}
参考

Fork / Join 思想

这里以查找最小数为例,具体实现如下:

import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

/**
 * Copyright @ 2019 yuzhouwan.com
 * All right reserved.
 * Function:Minimum Finder
 *
 * @author Benedict Jin
 * @since 2019/4/13
 */
public class MinimumFinder extends RecursiveTask<Integer> {

    private static final int JOIN_THRESHOLD = 5;

    private final int[] data;
    private final int start;
    private final int end;

    private MinimumFinder(int[] data, int start, int end) {
        this.data = data;
        this.start = start;
        this.end = end;
    }

    private MinimumFinder(int[] data) {
        this(data, 0, data.length);
    }

    @Override
    protected Integer compute() {
        final int len = end - start;
        if (len < JOIN_THRESHOLD) {
            return internal();
        }
        final int split = len / 2;
        final MinimumFinder left = new MinimumFinder(data, start, start + split);
        left.fork();
        final MinimumFinder right = new MinimumFinder(data, start + split, end);
        return Math.min(right.compute(), left.join());
    }

    private Integer internal() {
        System.out.println(Thread.currentThread() + " computing: " + start + " to " + end);
        int min = Integer.MAX_VALUE;
        for (int i = start; i < end; i++) {
            if (data[i] < min) {
                min = data[i];
            }
        }
        return min;
    }

    public static void main(String[] args) {
        final int[] data = new int[1000];
        final Random random = new Random(System.nanoTime());
        for (int i = 0; i < data.length; i++) {
            data[i] = random.nextInt(100);
        }
        final ForkJoinPool pool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
        final MinimumFinder finder = new MinimumFinder(data);
        System.out.println(pool.invoke(finder));
    }
}
参考

堆栈优化

ByteBuffer

通过 allocateDirect(int capacity) 方法可以避开堆栈,直接通过操作系统创建内存块作为缓冲区。该方式与操作系统能更好地耦合,因而能进一步提高 I/O 操作的速度。缺点是,分配直接缓冲区的系统开销很大。因此,只有在缓冲区较大并会长期存在,或者需要经常重用时,才使用这种缓冲区

位运算

奇偶数
public static boolean isEven(int i) {
    return (i & 1) == 0;
}
毫秒
public static final long SECOND_MASK = 0xFFFFFFFF00000000L;

public static boolean isMillis(long timestamp) {
    return (timestamp & SECOND_MASK) != 0;
}
参考

日志相关

log4j 开启 BufferedIO

参考

测试相关

参数驱动

利用 @Parameterized.Parameters 注解可以指定多个可能的传值,使得当前测试类下的所有测试用例可以被多次复用。但是该注解并不能让参数之间自行组合,所以严格来说,并不是参数驱动(后续介绍的 HttpRunner 框架则是严格意义上的参数驱动)

import com.yuzhouwan.compression.CompressionType;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.junit.runners.Parameterized;

import java.util.Arrays;

@FixMethodOrder(MethodSorters.JVM)
@RunWith(Parameterized.class)
public class CompressionTest {

    @Parameterized.Parameter()
    public CompressionType compressionType4ts;
    @Parameterized.Parameter(1)
    public CompressionType compressionType4longValue;
    @Parameterized.Parameter(2)
    public CompressionType compressionType4doubleValue;

    @Parameterized.Parameters
    public static Iterable<Object[]> getParameters() {
        return Arrays.asList(new Object[][]{
                {CompressionType.NONE, CompressionType.NONE, CompressionType.NONE},
                {CompressionType.SIMPLE8B, CompressionType.NONE, CompressionType.GORILLA},
                {CompressionType.SIMPLE8B_WITH_RLE, CompressionType.ZIGZAG_WITH_SIMPLE8B, CompressionType.NONE},
        });
    }

    /**
     * NONE - NONE - NONE
     * SIMPLE8B - NONE - GORILLA
     * SIMPLE8B_WITH_RLE - ZIGZAG_WITH_SIMPLE8B - NONE
     */
    @Test
    public void test() {
        System.out.println(compressionType4ts + " - " + compressionType4longValue + " - " + compressionType4doubleValue);
    }
}

测试先行

参考

自动生成 Test Case

参考

自动化测试

HttpRunner

介绍

HttpRunner™ 是一款面向 HTTP(S) 协议的通用测试框架,只需编写维护一份 YAML/JSON 脚本,即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。这里将以测试 OpenTSDB 为例,更加具象地介绍 HttpRunner

QuickStart
安装
$ pip install httprunner==1.5.15

$ hrun -V
  1.5.15

$ har2case -V
  0.2.0
启动 Flask
$ pip install flask

$ mkdir docs/data/
$ wget https://cn.httprunner.org/data/api_server.py -P docs/data/
$ export FLASK_APP=docs/data/api_server.py
$ export FLASK_ENV=development
$ flask run

$ curl localhost:5000                                          
  Hello World!
测试
$ wget https://cn.httprunner.org/data/demo-quickstart.har -P docs/data/

# 将 demo-quickstart.har 转换为 HttpRunner 的测试用例文件
# 默认输出 JSON 文件,加 `-2y` 参数,可以转化为 YAML
$ har2case docs/data/demo-quickstart.har
$ hrun docs/data/demo-quickstart.json
新建测试项目
# 新建目录
$ httprunner --startproject yuzhouwan

$ ls -sail
  12891420763 4 -rw-r--r--  1 benedictjin wheel   44 Feb  2 11:37 .env
  12891384628 0 drwxr-xr-x  2 benedictjin wheel   64 Feb  1 16:44 api/
  12891454305 4 -rw-r--r--  1 benedictjin wheel 2389 Feb  2 14:34 debugtalk.py
  12891454901 4 -rw-r--r--  1 benedictjin wheel 1452 Feb  2 15:21 locustfile.py
  12891454386 0 drwxr-xr-x  8 benedictjin wheel  256 Feb  2 15:30 reports/
  12891384629 0 drwxr-xr-x  7 benedictjin wheel  224 Feb  2 14:47 testcases/
  12891384630 0 drwxr-xr-x  2 benedictjin wheel   64 Feb  1 16:44 testsuites/
# .env         存放环境变量的 properties 文件
# testcases    存放所有 httprunner 的 json 测试实例
# debugtalk.py 存放所有 httprunner 测试实例中,需要用到自定义函数
# reports      生成的 html 结果页面
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容