248 Matching Annotations
  1. Mar 2025
    1. Conditionally Disable Tests with JUnit 5 The last feature is useful when you want to avoid tests being executed based on different conditions. There might be tests that don’t run on different operating systems or require different environment variables to be present. JUnit 5 comes with some built-in conditions that you can use for your tests, e.g.: Java @Test @DisabledOnOs(OS.LINUX) public void disabledOnLinux() { assertEquals(42, 40 + 2); } @Test @DisabledIfEnvironmentVariable(named = "FLAKY_TESTS", matches = "false") public void disableFlakyTest() { assertEquals(42, 40 + 2); } 1234567891011 @Test@DisabledOnOs(OS.LINUX)public void disabledOnLinux() {  assertEquals(42, 40 + 2);} @Test@DisabledIfEnvironmentVariable(named = "FLAKY_TESTS", matches = "false")public void disableFlakyTest() {  assertEquals(42, 40 + 2);} On the other side, writing a custom condition is pretty straightforward. Let’s consider you don’t want to execute a test around midnight: Java @Test @DisabledOnMidnight public void disabledOnMidNight() { assertEquals(42, 40 + 2); } 12345 @Test@DisabledOnMidnightpublic void disabledOnMidNight() {  assertEquals(42, 40 + 2);} All you have to do is to implement ExecutionCondition and add your own condition: Java public class DisabledOnMidnightCondition implements ExecutionCondition { private static final ConditionEvaluationResult ENABLED_BY_DEFAULT = enabled("@DisabledOnMidnight is not present"); private static final ConditionEvaluationResult ENABLED_DURING_DAYTIME = enabled("Test is enabled during daytime"); private static final ConditionEvaluationResult DISABLED_ON_MIDNIGHT = disabled("Disabled as it is around midnight"); @Documented @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(DisabledOnMidnightCondition.class) public @interface DisabledOnMidnight { } @Override public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { Optional<DisabledOnMidnight> optional = findAnnotation(context.getElement(), DisabledOnMidnight.class); if (optional.isPresent()) { LocalDateTime now = LocalDateTime.now(); if (now.getHour() == 23 || now.getHour() <= 1) { return DISABLED_ON_MIDNIGHT; } else { return ENABLED_DURING_DAYTIME; } } return ENABLED_BY_DEFAULT; } } 1234567891011121314151617181920212223242526272829303132 public class DisabledOnMidnightCondition implements ExecutionCondition {   private static final ConditionEvaluationResult ENABLED_BY_DEFAULT =    enabled("@DisabledOnMidnight is not present");   private static final ConditionEvaluationResult ENABLED_DURING_DAYTIME =    enabled("Test is enabled during daytime");   private static final ConditionEvaluationResult DISABLED_ON_MIDNIGHT =    disabled("Disabled as it is around midnight");   @Documented  @Target({ElementType.TYPE, ElementType.METHOD})  @Retention(RetentionPolicy.RUNTIME)  @ExtendWith(DisabledOnMidnightCondition.class)  public @interface DisabledOnMidnight {  }   @Override  public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {    Optional<DisabledOnMidnight> optional = findAnnotation(context.getElement(), DisabledOnMidnight.class);    if (optional.isPresent()) {      LocalDateTime now = LocalDateTime.now();      if (now.getHour() == 23 || now.getHour() <= 1) {        return DISABLED_ON_MIDNIGHT;      } else {        return ENABLED_DURING_DAYTIME;      }    }    return ENABLED_BY_DEFAULT;  }}

      还可以使用 Conditional Disable 特性,比如测试不在 Linux 上跑(@DisabledOnOS)比如不在某个环境变量的具体值下跑(@DisabledIfEnvironmentVariable),或者还可以自己实现 ExecutionCondition 接口自己定义。

    2. While you might have configured this in the past with the corresponding Maven or Gradle plugin, you can now configure this as an experimental feature with JUnit (since version 5.3). This gives you more fine-grain control on how to parallelize the tests. A basic configuration can look like the following: Java junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent 12 junit.jupiter.execution.parallel.enabled = truejunit.jupiter.execution.parallel.mode.default = concurrent This enables parallel execution for all your tests and set the execution mode to concurrent. Compared to same_thread,  concurrent does not enforce to execute the test in the same thread of the parent. For a per test class or method mode configuration, you can use the @Execution annotation.

      Junit 5 自带了 Test 并行的功能,可以设置。

      可以通过多种方式来设置,比如在 pom.xml 中的 surefire plugin 中,或者在 src/test/resources 下面建立一个 junit-platform.properties 文件。

      只建议在单元测试下进行配置,具体配置可以查看 Junit 5 文档。https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution

    3. JUnit 5 offers parameter injection for test constructor and method arguments. There are built-in parameter resolvers you can use to inject an instance of TestReport, TestInfo, or RepetitionInfo (in combination with a repeated test):

      JUnit 5 提供了一个 parameter injection 的功能,可以给 test constructor 或者 method arguments 使用。比如默认有一些 parameter resolvers:TestReport, TestInfo, RepetitionInfo。

    4. Usually, you test different business requirements inside the same test class. With Java and JUnit 5, you write them one after the other and add new tests to the bottom of the class. While this is working for a small number of tests inside a class, this approach gets harder to manage for bigger test suites. Consider you want to adjust tests that verify a common scenario. As there is no defined order or grouping inside your test class you end up scrolling and searching them. The following JUnit 5 feature allows you to counteract this pain point of a growing test suite: nested tests. You can use this feature to group tests that verify common functionality. This does not only improves maintainability but also reduces the time to understand what the class under test is responsible for:

      使用 @Nested + @DisplayName 来标注测试类里的内部类,更好地 group 测试用例。

    5. With @TestMethodOrder(MethodOrderer.OrderAnnotation.class) you basically opt-out from the JUnit 5 default ordering. The actual order is then specified with @Order. A lower value implies a higher priority.

      使用 @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 和 @Order 可以自定义运行顺序,数字越小越靠前。

      最佳实践是测试不应该依赖于执行顺序,但是有的时候还是需要能够配置的。

    6. While JUnit 5 has the following default-test execution order: …, test methods will be ordered using an algorithm that is deterministic but intentionally nonobvious. This ensures that subsequent runs of a test suite execute test methods in the same order, thereby allowing for repeatable builds.

      JUnit 默认提供了一个运行顺序,保证可以产生可运行的结果。

    1. The main workflow when using Mockito for our tests is usually the following:Create mocks for collaborators of our class under test (e.g., using @Mock)Stub the behavior of the mocks as they'll otherwise return a default value (when().thenReturn())(Optionally) Verify the interaction of our mocks (verify())

      我们使用 Mockito 的流程通常是: - 定义一个 mock 作为依赖 - stub 它的返回值 - 验证 interaction

    2. JUnit 5 was launched in 2017 and is composed of several modules:JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit VintageJUnit Platform: Foundation to launch testing frameworks & it defines the TestEngine API for developing testing frameworksJUnit Jupiter: New programming model and extension model and provides the JupiterTest Engine that implements the TestEngine interface to run tests on the JUnit PlatformJUnit Vintage: Provides a TestEngine to run both JUnit 3 and JUnit 4 tests

      JUnit 5 在 2017 年发布,包含三个 modules: - JUnit Platform:基础,定义了 TestEngine API - JUnit Jupiter:新的编程模型和拓展模型,提供了 JupiterTest Engine,是一个实现了 TestEngine 的接口 - JUnit Vintage:提供了运行老的 Junit 3 4 的 TestEngine

    1. If the git status command is too vague for you — you want to know exactly what you changed, not just which files were changed — you can use the git diff command. We’ll cover git diff in more detail later, but you’ll probably use it most often to answer these two questions: What have you changed but not yet staged? And what have you staged that you are about to commit? Although git status answers those questions very generally by listing the file names, git diff shows you the exact lines added and removed — the patch, as it were.

      git status 能展示 general 的更改信息,能看到哪个文件被修改了,在此之上 git diff 能展示更多的信息。通常来说,git diff 能够回答: - 更改了但是没有 staged 的内容 - staged 但是没有提交的内容

      案例:起始状态是,当前有一个更改并 staged 的文件,一个只更改没有 staged 的文件

      《图1》

      如果想要查看没有 staged 的更改,直接使用 git diff(比较的是工作目录和 staging area)

      图2

      如果想要知道 staged 了什么可以提交,使用 git diff --staged(比较的是 staged files 和上一次提交)

      图3

      重要的是记住,git diff 本身比较的是工作目录和暂存区,如果所有更改都 staged 了,git diff 也就没有啥输出了。

      如果更改了文件的一部分,staged 了,然后再更改一部分,就会看到这样的输出:

      图4

      然后使用 git diff 查看没有 staged 的部分,使用 git diff --cached (等同于 --staged) 查看 staged 的那部分。

    2. Git Diff in an External Tool We will continue to use the git diff command in various ways throughout the rest of the book. There is another way to look at these diffs if you prefer a graphical or external diff viewing program instead. If you run git difftool instead of git diff, you can view any of these diffs in software like emerge, vimdiff and many more (including commercial products). Run git difftool --tool-help to see what is available on your system.

      我们还可以使用外部的工具,或者图形化界面来查看 git diff,比如利用 vim 的提供的 diff tool。

      使用 git difftool --tool-help 查看可选的工具。 得到的由 nvimdiff, vimdiff 以及 mac 上有一款 FileMerge 软件。

      使用 git difftool 来替代 git diff 就可以使用。

    3. In the simple case, a repository might have a single .gitignore file in its root directory, which applies recursively to the entire repository. However, it is also possible to have additional .gitignore files in subdirectories. The rules in these nested .gitignore files apply only to the files under the directory where they are located. The Linux kernel source repository has 206 .gitignore files. It is beyond the scope of this book to get into the details of multiple .gitignore files; see man gitignore for the details.

      在项目根目录下的 .gitignore 文件会递归地应用到整个根目录下。但是也可以在子目录下添加 .gitignore 文件,这种情况只应用在那个子目录。

      例如 Linux kernel 源代码就有 206 个 .gitignore 文件。可以使用 man gitignore 来查看详情,略。

    4. GitHub maintains a fairly comprehensive list of good .gitignore file examples for dozens of projects and languages at https://github.com/github/gitignore if you want a starting point for your project.

      GitHub 维护了一个各个语言和项目的 .gitignore 参考文件 repo,可以作为起手式模版。

    5. The rules for the patterns you can put in the .gitignore file are as follows: Blank lines or lines starting with # are ignored. Standard glob patterns work, and will be applied recursively throughout the entire working tree. You can start patterns with a forward slash (/) to avoid recursivity. You can end patterns with a forward slash (/) to specify a directory. You can negate a pattern by starting it with an exclamation point (!). Glob patterns are like simplified regular expressions that shells use. An asterisk (*) matches zero or more characters; [abc] matches any character inside the brackets (in this case a, b, or c); a question mark (?) matches a single character; and brackets enclosing characters separated by a hyphen ([0-9]) matches any character between them (in this case 0 through 9). You can also use two asterisks to match nested directories; a/**/z would match a/z, a/b/z, a/b/c/z, and so on. Here is another example .gitignore file: # ignore all .a files *.a # but do track lib.a, even though you're ignoring .a files above !lib.a # only ignore the TODO file in the current directory, not subdir/TODO /TODO # ignore all files in any directory named build build/ # ignore doc/notes.txt, but not doc/server/arch.txt doc/*.txt # ignore all .pdf files in the doc/ directory and any of its subdirectories doc/**/*.pdf

      .gitignore 的规则 - 空行和 # 忽略 - 标准的 glob 模式(会递归地应用于整个文件树) - 使用 / 开头来避免递归 - 以 / 结尾来声明文件 - 使用 ! 来 negate 一个模式

      Glob 模式可以理解为一个 shell 可以使用的简单正则表达式 - * 代表 0 个或多个字符 - [abc] 可以匹配 a b c 任意一个 - ? 匹配任意单一字符 - [0-9] 匹配 0~9 - a/**/z 可以匹配 a/z, a/b/z, a/b/c/z。即 ** 可以匹配任意中间路径

      案例文件

      忽略所有 .a 结尾的文件

      *.a

      但是还想要追踪 lib.a

      !lib.a

      只忽略当前文件夹下的 TODO 文件,但是不包括子目录下的 /TODO

      忽略所有在 build 文件夹下的文件

      build/

      忽略 doc/notes.txt,但是不要忽略 doc/server/arch.txt

      doc/*.txt

      忽略 doc/ 下所有 .pdf 文件(包括子文件下的)

      doc/*/.pdf

    6. Often, you’ll have a class of files that you don’t want Git to automatically add or even show you as being untracked. These are generally automatically generated files such as log files or files produced by your build system. In such cases, you can create a file listing patterns to match them named .gitignore. Here is an example .gitignore file: $ cat .gitignore *.[oa] *~ The first line tells Git to ignore any files ending in “.o” or “.a” — object and archive files that may be the product of building your code. The second line tells Git to ignore all files whose names end with a tilde (~), which is used by many text editors such as Emacs to mark temporary files. You may also include a log, tmp, or pid directory; automatically generated documentation; and so on. Setting up a .gitignore file for your new repository before you get going is generally a good idea so you don’t accidentally commit files that you really don’t want in your Git repository.

      有时候我们不想让 Git 来自动添加、甚至显示 untracted,就可以使用 .gitignore 文件来,在里面声明 pattern 来让 Git 忽略。

      需要被忽略的文件通常是日志文件、或者是由 build 系统产生的文件。

    7. Short Status While the git status output is pretty comprehensive, it’s also quite wordy. Git also has a short status flag so you can see your changes in a more compact way. If you run git status -s or git status --short you get a far more simplified output from the command: $ git status -s M README MM Rakefile A lib/git.rb M lib/simplegit.rb ?? LICENSE.txt New files that aren’t tracked have a ?? next to them, new files that have been added to the staging area have an A, modified files have an M and so on. There are two columns to the output — the left-hand column indicates the status of the staging area and the right-hand column indicates the status of the working tree. So for example in that output, the README file is modified in the working directory but not yet staged, while the lib/simplegit.rb file is modified and staged. The Rakefile was modified, staged and then modified again, so there are changes to it that are both staged and unstaged.

      git status 有一个短命令版本:git status -s 或 git status --short(输出正如在 RStudio 里看到的一样)。

      ?? - 新文件还没有被 track A - 新文件,并且已经被添加到 staging area M - tracked 文件,有新的修改,但是还没添加 M - tracked 文件,已经修改,也已经添加 MM - tracked 文件,一部分修改添加了,然后又修改了(没添加)

    8. Both files are staged and will go into your next commit. At this point, suppose you remember one little change that you want to make in CONTRIBUTING.md before you commit it. You open it again and make that change, and you’re ready to commit. However, let’s run git status one more time: $ vim CONTRIBUTING.md $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git reset HEAD <file>..." to unstage) new file: README modified: CONTRIBUTING.md Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: CONTRIBUTING.md What the heck? Now CONTRIBUTING.md is listed as both staged and unstaged. How is that possible? It turns out that Git stages a file exactly as it is when you run the git add command. If you commit now, the version of CONTRIBUTING.md as it was when you last ran the git add command is how it will go into the commit, not the version of the file as it looks in your working directory when you run git commit.

      如果 git add 了一个被 tracked 的文件(即 stage 修改为下一次提交),然后再修改文件,那么 git status 会将文件列出在两个区域:Changes to be committed(上一次 git add 时候那个准确版本)、Changes not staged for commit(刚刚的修改)。

      总之,git add 之后的更改,如果想包含进下一次提交,需要 git add again。

    9. The CONTRIBUTING.md file appears under a section named “Changes not staged for commit” — which means that a file that is tracked has been modified in the working directory but not yet staged. To stage it, you run the git add command. git add is a multipurpose command — you use it to begin tracking new files, to stage files, and to do other things like marking merge-conflicted files as resolved. It may be helpful to think of it more as “add precisely this content to the next commit” rather than “add this file to the project”.

      如果已经提交过的文件修改一下,它会出现在 "Changes not staged for commit",意思是文件已经被 Git 追踪了 + 已经被修改了 + 但是还没有 staged。

      同样是使用 git add 来 stage 更改。

      理解 git add 的正确心智模型是:把当下这个文件的状态添加到下一次提交中,因为 git add 可以用于多种用途:追踪新文件、添加修改到 stage、标记冲突文件为解决(待学习)--> 不仅仅是「添加文件到项目中」。

    10. If you run your status command again, you can see that your README file is now tracked and staged to be committed: $ git status On branch master Your branch is up-to-date with 'origin/master'. Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: README You can tell that it’s staged because it’s under the “Changes to be committed” heading. If you commit at this point, the version of the file at the time you ran git add is what will be in the subsequent historical snapshot.

      git add <xxx> 之后,被添加的内容就来到了 Changes to be committed 区域,可以 refer Figure 8,从 untracked 来到 staged。

    11. The git add command takes a path name for either a file or a directory; if it’s a directory, the command adds all the files in that directory recursively.

      git add 命令(or subcommand)接受一个 path name,可以是文件或者是文件夹,如果是后者,就递归地添加所有的文件。

    12. $ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working tree clean This means you have a clean working directory; in other words, none of your tracked files are modified. Git also doesn’t see any untracked files, or they would be listed here. Finally, the command tells you which branch you’re on and informs you that it has not diverged from the same branch on the server. For now, that branch is always master, which is the default; you won’t worry about it here. Git Branching will go over branches and references in detail.

      查看仓库文件的状态 git status

      $ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working tree clean

      的输出代表:所有 tracked 文件都没有被更改,git 也没有看见 untracked 文件,同时本地分支和服务器上的远端分支也没有区别。

    13. Remember that each file in your working directory can be in one of two states: tracked or untracked. Tracked files are files that were in the last snapshot, as well as any newly staged files; they can be unmodified, modified, or staged. In short, tracked files are files that Git knows about. Untracked files are everything else — any files in your working directory that were not in your last snapshot and are not in your staging area. When you first clone a repository, all of your files will be tracked and unmodified because Git just checked them out and you haven’t edited anything.

      Git 仓库中的文件有两种状态 tracked 和 untracked。

      Tracked 文件是被包含在上一个 snapshot 的文件,或者是新添加到 staged area 的文件。它们的状态可以是 unmodified, modified 或者 staged。Git 知道的文件就是 tracked 文件。

      Untracked files 是即没有在上一个快照,也没有添加到 staging area 的。

      通过这个图来查看文件的状态的变迁。

    1. Running Integration Tests With the Maven Failsafe Plugin Unlike the Maven Surefire Plugin, the Maven Failsafe Plugin is not a core plugin and hence won’t be part of our project unless we manually include it. As already outlined, the Maven Failsafe plugin is used to run our integration test. In contrast to our unit tests, the integration tests usually take more time, more setup effort (e.g., start Docker containers for external infrastructure with Testcontainers), and test multiple components of our application together. We integrate the Maven Failsafe Plugin by adding it to the build section of our pom.xml: XHTML <project> <!-- other dependencies --> <build> <!-- further plugins --> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>3.0.0-M5</version> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> 123456789101112131415161718192021 <project>    <!-- other dependencies -->   <build>      <!-- further plugins -->      <plugin>        <artifactId>maven-failsafe-plugin</artifactId>        <version>3.0.0-M5</version>        <executions>          <execution>            <goals>              <goal>integration-test</goal>              <goal>verify</goal>            </goals>          </execution>        </executions>      </plugin>    </plugins>  </build></project> As part of the executions configuration, we specify the goals of the Maven Failsafe plugin we want to execute as part of our build process. A common pitfall is to only execute the integration-test goal. Without the verify goal, the plugin will run our integration tests but won’t fail the build if there are test failures.

      Maven 自带的 core plugin 不包括 Failsafe,如果需要使用,需要我们自己引入。

      引入的方式通过在 pom.xml 里面的 build 标签下加入这个 plugin。

      引入的时候需要指定 goal,需要特别注意要引入 integration-test 和 verify 两个 goal,一个是运行集成测试,一个是检查加过。

      这样这个插件在 verify 阶段就会被调用。

    2. Maven is built around the concept of build lifecycles. There are three built-in lifecycles: default: handling project building and deployment clean: project cleaning site: the creation of our project’s (documentation) site Each of the three built-in lifecycles has a list of build phases. For our testing example, the default lifecycle is important. The default lifecycle compromises a set of build phases to handle building, testing, and deploying our Java project. Each phase represents a stage in the build lifecycle with a central responsibility: In short, the several phases have the following responsibilities: validate: validate that our project setup is correct (e.g., we have the correct Maven folder structure) compile: compile our source code with javac test: run our unit tests package: build our project in its distributable format (e.g., JAR or WAR) verify: run our integration tests and further checks (e.g., the OWASP dependency check) install: install the distributable format into our local repository (~/.m2 folder) deploy: deploy the project to a remote repository (e.g., Maven Central or a company hosted Nexus Repository/Artifactory) These build phases represent the central phases of the default lifecycle.

      Maven 中比较重要的概念是生命周期,三个内置的周期是 default, clean, site 三个生命周期。default 是用来处理项目的构建和部署的,clean 是项目清理,site 是构建项目的(文档)站点。

      其中 default 生命周期比较重要,有一系列 phases,各个都代表构建的一个阶段: - validata 验证项目设置正确(文件夹结构) - compile 编译源代码 - test 运行单元测试 - package 打包 JAR 或 WAR - verify 执行集成测试或其他检查(比如 OWASP) - install 把分发包安装到本地仓库 - deploy 部署项目到远程仓库

    3. The Maven Failsafe Plugin, designed to run our integration tests, detects our integration tests by the following default patterns: **/IT*.java **/*IT.java **/*ITCase.java

      Maven 的 Failsafe 插件用来运行集成测试,其判定一个类是集成测试的 pattern 如下: - /IT*.java - /IT.java - /ITCase.java

    4. The Maven Surefire Plugin, more about this plugin later, is designed to run our unit tests. The following patterns are the defaults so that the plugin will detect a class as a test: **/Test*.java **/*Test.java **/*Tests.java **/*TestCase.java

      Maven 的 Surefire 插件是用来运行单元测试的,插件默认会根据文件名的 patterns 来检测一个类是测试: - /Test*.java - /Test.java - /Tests.java - */TestCase.java

    5. target/test-classes At this location, Maven places our compiled test classes (.class files) and test resources whenever the Maven compiler compiles our test sources. We can explicitly trigger this with mvn test-compile and add a clean if we want to remove the existing content of the entire target folder first.

      target/test-classes 是 maven 用来放置编译好的测试文件和资源的地方。这就是 Maven 为我们做编译的体现,如果是自己编译的话,不会有这样的结构。

    6. src/test/resources As part of this folder, we store static files that are only relevant for our test. This might be a CSV file to import test customers for an integration test, a dummy JSON response for testing our HTTP clients, or a configuration file.

      src/text/resources 文件夹里的是存放关于测试的静态文件的,比如集成测试的数据、mock JSON 数据等。

    7. As a general recommendation, we should try to mirror the package structure of our production code (src/main/java). Especially if there’s a direct relationship between the test and a source class. The corresponding CustomerServiceTest for a CustomerService class inside the package com.company.customer should be placed in the same package within src/test/java. This improves the likelihood that our colleagues (and our future us) locate the corresponding test for a particular Java class without too many facepalms.

      Maven 项目,测试文件放在 src/text/java 文件夹下。作为一种约定和推荐,测试的目录结构和 Java 的要一致,这样方便找到(但实际上 Maven 是通过文件名来检测文件的)。

    8. If we want to run our integration tests manually, we can do so with the following command: Java mvn failsafe:integration-test failsafe:verify 1 mvn failsafe:integration-test failsafe:verify For scenarios where we don’t want to run our integration test (but still our unit tests), we can add -DskipITs to our Maven execution: Java mvn verify -DskipITs 1 mvn verify -DskipITs Similar to the Maven Surefire Plugin, we can also run a subset of our integration tests: Java mvn -Dit.test=MainIT failsafe:integration-test failsafe:verify mvn -Dit.test=MainIT#firstTest failsafe:integration-test failsafe:verify 123 mvn -Dit.test=MainIT failsafe:integration-test failsafe:verify mvn -Dit.test=MainIT#firstTest failsafe:integration-test failsafe:verify When using the command above, make sure the test classes have been compiled previously, as otherwise, there won’t be any test execution. There’s also a property available to entirely skip the compilation of test classes and avoid running any tests when building our project (not recommended): Java mvn verify -Dmaven.test.skip=true 1 mvn verify -Dmaven.test.skip=true

      一些命令

      可以单独运行 failsafe 的某个 goal,可以指定运行某个测试文件

      可以跳过所有的测试。

    9. Whenever we want to skip our unit tests when building our project, we can use an additional parameter: Java mvn package -DskipTests 1 mvn package -DskipTests We can also explicitly run only one or multiple tests: Java mvn test -Dtest=MainTest mvn test -Dtest=MainTest#testMethod mvn surefire:test -Dtest=MainTest 12345 mvn test -Dtest=MainTest mvn test -Dtest=MainTest#testMethod mvn surefire:test -Dtest=MainTest

      如果想跳过 test 使用 -DskipTests

      如果想跑指定的测试,用 -Dtest=xxx 来做

    10. For the example above, we’re running one unit test with JUnit 5 (testing provider). There’s no need to configure the testing provider anywhere, as with recent Surefire versions, the plugin will pick up the correct test provider by itself. The Maven Surefire Plugin integrates both JUnit and TestNG as testing providers out-of-the-box.

      JUnit 是 testing provider,surefire 插件能够自动使用它,同时也支持 TestNG。

    11. The Maven Surefire is responsible for running our unit tests. We must either follow the default naming convention of our test classes, as discussed above, or configure a different pattern that matches our custom naming convention. In both cases, we have to place our tests inside src/test/java folder for the plugin to pick them up.

      测试文件必须放在 src/test/java 里,推荐 follow 源代码的目录结构,可以遵循插件命名规范或者自己定义。

    12. Whenever we execute a build phase, our project will go through all build phases and sequentially until the build phase we specified. To phrase it differently, when we run mvn package, for example, Maven will execute the default lifecycle phases up to package in order: Java validate -> compile -> test -> package 1 validate -> compile -> test -> package If one of the build phases in the chain fails, the entire build process will terminate. Imagine our Java source code has a missing semicolon, the compile phase would detect this and terminate the process. As with a corrupt source file, there’ll be no compiled .class file to test. When it comes to testing our Java project, both the test and verify build phases are of importance. As part of the test phase, we’re running our unit tests with the Maven Surefire Plugin, and with verify our integration tests are executed by the Maven Failsafe Plugin.

      Maven 运行某一个 phase 会把之前的 phase 全部运行。

      和测试相关的两个 phase 是 test 和 verify,一个是单元测试,一个是集成测试。

    1. 可能与绝大多数人心中的认知会有差异,“使用多个独立的分布式服务共同构建一个更大型系统”的设想与实际尝试,反而要比今天大家所了解的大型单体系统出现的时间更早。 在 20 世纪 70 年代末期到 80 年代初,计算机科学刚经历了从以大型机为主向以微型机为主的蜕变,计算机逐渐从一种存在于研究机构、实验室当中的科研设备,转变为存在于商业企业中的生产设备,甚至是面向家庭、个人用户的娱乐设备。此时的微型计算机系统通常具有 16 位寄存器、不足 5MHz 时钟频率的处理器,不超过 1 MB 的最大内存和 64KB 单段偏移地址。譬如著名的英特尔处理器的鼻祖,Intel 8086 处理器 就是在 1978 年研制成功,流行于 80 年代中期,甚至一直持续到 90 年代初期仍有生产销售。 当时计算机硬件局促的运算处理能力,已直接妨碍到了在单台计算机上信息系统软件能够达到的最大规模。为突破硬件算力的限制,各个高校、研究机构、软硬件厂商开始分头探索,寻找使用多台计算机共同协作来支撑同一套软件系统运行的可行方案。这一阶段是对分布式架构最原始的探索,从结果来看,历史局限决定了它不可能一蹴而就地解决分布式的难题,但仅从过程来看,这个阶段的探索称得上成绩斐然。

      独立的分布式服务来构建系统,早于,大型单体系统出现的时间。

      最早是因为硬件的限制,开始了探索:多台计算机共同协作来支撑起系统。

    1. 在 Run Dashboard 中先启动“bookstore-microservices-platform-configuration”微服务,然后可一次性启动其余六个子模块的微服务。

      使用 Java 11 可以成功运行,17 不行。

      所以所谓的向后兼容,并不是那么简单,依赖的库也需要兼容。

    2. 工程中预留了一些的环境变量,便于配置和扩展,譬如,想要在非容器的单机环境中模拟热点模块的服务扩容,就需要调整每个服务的端口号。预留的这类环境变量包括:

      这里以后必须实际操作一下

    3. 由于在命令行启动多个服务、通过容器实现各服务隔离、扩展等都较繁琐,笔者提供了一个docker-compose.dev.yml文件,便于开发期调试使用:

      这里需要自己实验一下,这里的 docker compose file 的 build 是做什么的,从 Jar 到镜像?

    4. 由于笔者已经在配置文件中设置好了各个微服务的默认的地址和端口号,以便于本地调试。如果要在同一台机运行这些服务,并且每个微服务都只启动一个实例的话,那不加任何配置、参数即可正常以 Maven 编译、以 Jar 包形式运行。由于各个微服务需要从配置中心里获取具体的参数信息,因此唯一的要求只是“配置中心”的微服务必须作为第一个启动的服务进程,其他就没有别的前置要求了。具体的操作过程如下所示:

      以 Maven 编译、Jar 包运行的话,需要先启动配置中心服务。

      熟悉如何用 raw 方式编译、打包、运行服务

    5. 尽管 Netflix 套件的使用人数很多,但考虑到 Spring Cloud Netflix 已进入维护模式,笔者均列出了上述组件的代替品。这些组件几乎都是声明式的,这确保了它们的替代成本相当低廉,只需要更换注解,修改配置,无需改动代码。你在阅读源码时也会发现,三个“platform”开头的服务,基本上没有任何实际代码的存在。

      啊?那这三个服务到底是为什么存在,没有代码?

    6. 在笔者设定的演示案例中,准备把单体的 Fenix's Bookstore拆分成为“用户”、“商品”、“交易”三个能够独立运行的子系统,它们将在一系列非功能性技术模块(认证、授权等)和基础设施(配置中心、服务发现等)的支撑下互相协作,以统一的 API 网关对外提供与原来单体系统功能一致的服务,应用视图如下图所示:

      单体架构拆分成微服务的架构图

    7. 在单体架构下,没有什么有效阻断错误传播的手段,系统中“整体”与“部分”的关系没有物理的划分,系统质量只能靠研发与项目管理措施来尽可能地保障,少量的技术专家很难阻止大量螺丝钉式的程序员或者不熟悉原有技术架构的外包人员在某个不起眼的地方犯错并产生全局性的影响,并不容易做出整体可靠的大型系统。

      单体架构在面临人的因素的不足,也就是说,架构还有一个好处是,能够最小化开发人员水平参差不齐对系统可靠性的影响。

    8. 为异构能力进行的分布式部署

      为异构能力进行的分布式部署,指的是需要采用分布式架构将不同的组件部署到多个节点或服务器上,以实现高可用性、可拓展性和容错能力。

      这些独立运行的模块,需要通过网络来协作。

    9. 技术异构的需求从可选渐渐成为必须。Fenix's Bookstore 的单体版本是以目前应用范围最广的 Java 编程语言来开发,但依然可能遇到很多想做 Java 却不擅长的事情。譬如想去做人工智能,进行深度学习训练,发现大量的库和开源代码都离不开 Python;想要引入分布式协调工具时,发现近几年 ZooKeeper 已经有被后起之秀 Golang 的 Etcd 蚕食替代的趋势;想要做集中式缓存,发现无可争议的首选是 ANSI C 编写的 Redis,

      技术异构,简单理解,就是指的是一个系统用了多种编程语言和技术栈,整个系统不是由单一类型的技术组件构成的。

    10. 直至现在,由不同编程语言、不同技术框架所开发的微服务系统中,基于 Spring Cloud 的解决方案仍然是最为主流的选择。这个结果既是 Java 在服务端应用所积累的深厚根基的体现,也是 Spring 在 Java 生态系统中统治地位的体现。从 Spring Boot 到 Spring Cloud 的过渡,令现存数量极为庞大的、基于 Spring 和 Spring Boot 的单体系统得以平滑地迁移到微服务架构中,令这些系统的大部分代码都能够无需修改,或少量修改即可保留重用。微服务时代的早期,Spring Cloud 就集成了Netflix OSS (以及 Spring Cloud Netflix 进入维护期后对应的替代组件)成体系的微服务套件,基本上也能算“半透明地”满足了在微服务环境中必然会面临的服务发现、远程调用、负载均衡、集中配置等非功能性的需求。 笔者个人是一直不太倾向于 Spring Cloud Netflix 这种以应用代码去解决基础设施功能问题的“解题思路”,以自顶向下的视角来看,这既是虚拟化的微服务基础设施完全成熟之前必然会出现的应用形态,也是微服务进化过程中必然会被替代的过渡形态。不过,笔者的看法如何无关重要,基于 Spring Cloud Netflix 的微服务在当前是主流,直至未来不算短的一段时间内仍会是主流,而且以应用的视角来看,能自底向上观察基础设施在微服务中面临的需求和挑战,能用我们最熟悉的 Java 代码来解释分析问题,也有利于对微服务的整体思想的深入理解,所以将它作为我们了解的第一种微服务架构的实现是十分适合的。

      需要重读

    1. Fenix's Bookstore 单体架构后端参考(并未完全遵循)了 DDD 的分层模式和设计原则,整体分为以下四层: Resource:对应 DDD 中的 User Interface 层,负责向用户显示信息或者解释用户发出的命令。请注意,这里指的“用户”不一定是使用用户界面的人,可以是位于另一个进程或计算机的服务。由于本工程采用了 MVVM 前后端分离模式,这里所指的用户实际上是前端的服务消费者,所以这里以 RESTful 中的核心概念“资源”(Resource)来命名。 Application:对应 DDD 中的 Application 层,负责定义软件本身对外暴露的能力,即软件本身可以完成哪些任务,并负责对内协调领域对象来解决问题。根据 DDD 的原则,应用层要尽量简单,不包含任何业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作,这一点在代码上表现为 Application 层中一般不会存在任何的条件判断语句。在许多项目中,Application 层都会被选为包裹事务(代码进入此层事务开始,退出此层事务提交或者回滚)的载体。 Domain:对应 DDD 中的 Domain 层,负责实现业务逻辑,即表达业务概念,处理业务状态信息以及业务规则这些行为,此层是整个项目的重点。 Infrastructure:对应 DDD 中的 Infrastructure 层,向其他层提供通用的技术能力,譬如持久化能力、远程服务通讯、工具集,等等。

      这里详细解释了单体架构的后端分层,以及和传统 DDD 的对应。

    2. Fenix's Bookstore 单体架构后端尽可能采用标准的技术组件进行构建,不依赖于具体的实现

      这里周志明老师真的是体现了高水准,告诉读者「依赖于标准(接口),而不是具体实现」,同时给出了标准和实现上的详细说明。

      作为 Java 新人来说,我开始迈向了理解标准的那一步了。

    3. 如果希望使用 HSQLDB 的文件模式,或者其他非嵌入式的独立的数据库支持的话,也是很简单的。以常用的 MySQL/MariaDB 为例,程序中也已内置了 MySQL 的表结构初始化脚本,你可以使用环境变量PROFILES来激活 Spring Boot 中针对 MySQL 所提供的配置,命令如下所示: $ docker run -d -p 8080:8080 --name bookstore icyfenix/bookstore:monolithic -e PROFILES=mysql 此时你需要通过 Docker link、Docker Compose 或者直接在主机的 Host 文件中提供一个名为mysql_lan的 DNS 映射,使程序能顺利链接到数据库,关于数据库的更多配置,可参考源码中的application-mysql.yml 。

      任务:测试一下如何启动其他的数据库模式,并且理解发生了什么

    1. The issue is, you have to write and configure all these individual pieces yourself. Spring Boot, on the other hand, takes these single pieces and bundles them up together. Example: Always and automatically look for application.properties files in various places and read them in. Always booting up an embedded Tomcat so you can immediately see the results of writing your @RestControllers. Automatically configuring everything for you to send/receive JSON, without needing to worry a ton about specific Maven/Gradle dependencies.

      Spring Boot 提供的就是,一系列约定,一系列方便的工具链,一系列依赖包。

    2. So, to sum it up, Spring framework’s goal is to 'springify' available Java functionality, preparing it for dependency injection and therefore making the APIs easier to use in a Spring context.

      Spring 提供了很多模块,它们的功能在 Java 中都能实现,但是 Spring 提供了更简便的方式,并且 Spring 把这些功能都提供成了可以注入的 Bean,可以直接拿来用。

    3. You can inject properties into your beans, similarly like you would inject a dependency with the @Autowired annotation. But for properties, you need to use the @Value annotation.

      我们通过 @Value 注解来注入属性,Spring 会在 Environment 里寻找正确的属性。

    4. In a nutshell, an environment consists of one to many property sources.

      Spring 中的 Environment 包含了一个或多个 property sources,也就是属性源,比如 Spring MVC 应用包含了 ServletConfig/Context parameter, JNDI and JVM system property sources,它们之间是有层级关系的,可以覆盖。

      我们也可以定义自己的 Property Source,通过 @PropertySources 注解,其中声明的一个个 @PropertySource 就是类似于定义了一个个的 Resource,可以通过多种形式声明。

    5. Spring tries to make it easy for you to register and automatically find properties across all these different sources, through its environment abstraction.

      Spring 应用读取属性配置,可能来源于多个地方,Spring 提供了一个 environment abstraction 来让我们方便地从不同的地方获取到。

    6. In short, Spring gives you the ability to access resources via a nice little syntax. The resource interface has a couple of interesting methods:

      Spring 提供了一个 Resources 资源的抽象接口,可以通过简单的语法来链接到各种资源(classpath,文件系统,网络资源)

    7. Spring’s @Transactional Your UserService implementation above could look a bit like this: import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @Component public class UserService { @Transactional // (2) public User activateUser(Integer id) { // (1) // execute some sql // send an event // send an email } } We wrote an activateUser method, which, when called, needs to execute some SQL to update the User’s state in the database, maybe send an email or a messaging event. @Transactional on that method signals Spring that you need an open database connection/transaction for that method to work and that said transaction should also be committed at the end. And that Spring needs to do this. The problem: While Spring can create your UserService bean through the applicationContext configuration, it cannot rewrite your UserService. It cannot simply inject code in there that opens a database connection and commits a database transaction. But what it can do, is to create a proxy around your UserService that is transactional. So, only the proxy needs to know about how to open up and close a database connection and can then simply delegate to your UserService in between. Let’s have a look at that innocent ContextConfiguration again. @Configuration @EnableTransactionManagement // (1) public class MyApplicationContextConfiguration { @Bean public UserService userService() { // (2) return new UserService(); } } We added an annotation signaling Spring: Yes, we want @Transactional support, which automatically enables Cglib proxies under the hood. With the above annotation set, Spring does not just create and return your UserService here. It creates a Cglib proxy of your bean, that looks, smells and delegates to your UserService, but actually wraps around your UserService and gives its transaction management features. This might seem a bit unintuitive first, but most Spring developers encounter proxies very soon in debugging sessions. Because of the proxies, Spring stacktraces can get rather long and unfamiliar: When you step inside a method, you could very well step inside the proxy first - which scares people off. It is, however, completely normal and expected behavior.

      Spring AOP 的一个例子就是 @Transactional 注解,它会在方法的执行中开启一个事务,在方法结束后提交事务,这就是通过一个 proxy 来在原始类前后添加功能,让它成为 transactional 的。

    8. Are there alternatives to CGlib proxies? Proxies are the default choice when programming AOP with Spring. You are however not restricted to using proxies, you could also go the full AspectJ route, that modifies your actual bytecode, if wanted. Covering AspectJ is however outside the scope of this guide. AspectJ allows you to change actual bytecode through load-time-weaving or compile-time-weaving. This gives you a lot more possibilities, in exchange for a lot more complexity. You can however configure Spring to use AspectJ’s AOP, instead of its default, proxy-based AOP. Here are a couple of links if you want to get more information on this topic: AspectJ Homepage Spring AOP vs AspectJ Spring AOP official documentation

      除了默认的 CGlib 代理,Spring 还可以使用 AspectJ 的 AOP,它可以通过运行时织入和编译时织入来修改字节码,更强大但更复杂。

    9. More specifically, Spring will, by default, create dynamic Cglib proxies, that do not need an interface for proxying to work (like JDK’s internal proxy mechanism): Instead, Cglib can proxy classes through subclassing them on the fly. ( If you are unsure about the individual proxy patterns, read more about the proxies on Wikipedia. )

      Spring 默认用 Cglib 动态代理,不同于 Java 自己的代理机制(需要接口),Cglib 能够动态地生成子类(字节码技术)。

    10. Because under the hood, any Spring @Bean method can return you something that looks and feels like (in your case) a UserService, but actually isn’t. It can return you a proxy. The proxy will at some point delegate to the UserService you wrote, but first, will execute its own functionality.

      Spring 可能注入的不是你给他的 Bean,而是一个增强了的代理:这个代理会调用我们的 Bean 来做事情,但是也会做一些其他的。

    11. There have been a great many debates online, whether constructor injection or field injection is better, with a number of strong voices even claiming that field injection is harmful. To not add further noise to these arguments, the gist of this article is: I have worked with both styles, constructor injection and field injection in various projects over the recent years. Based solely on personal experience, I do not truly favor one style over the other. Consistency is king: Do not use constructor injection for 80% of your beans, field injection for 10% and method injection for the remaining 10%. Spring’s approach from the official documentation seems sensible: Use constructor injection for mandatory dependencies and setter/field injection for optional dependencies. Be warned: Be really consistent with that. Most importantly, keep in mind: The overall success of your software project will not depend on the choice of your favorite dependency injection method (pun intended).

      构造器注入还是字段注入,谁更好有很大争议,但是目前 Spring 推荐使用 constructor 对于 mandatory dependencies,使用 setter/field 注入对于 optional dependencies。

      这里有一篇外链。

    12. I have been lying to you a tiny bit in the previous section. In earlier Spring versions (pre 4.2, see history), you needed to specify @Autowired in order for constructor injection to work. With newer Spring versions, Spring is actually smart enough to inject these dependencies without an explicit @Autowired annotation in the constructor. So this would also work. @Component public class UserDao { private DataSource dataSource; public UserDao(DataSource dataSource) { this.dataSource = dataSource; } } Why did I mention @Autowired then? Because it does not hurt, i.e. makes things more explicit and because you can use @Autowired in many other different places, apart from constructors. Let’s have a look at different ways of dependency injection - constructor injection just being one many.

      在新版本的 Spring 中,不需要在构造器上标注 @Autowire 了,Spring 也能够正确注入依赖。

    13. Now, Spring has all the information it needs to create UserDAO beans: UserDAO is annotated with @Component → Spring will create it UserDAO has an @Autowired constructor argument → Spring will automatically inject the DataSource that is configured via your @Bean method Should there be no DataSources configured in any of your Spring configurations, you will receive a NoSuchBeanDefinition exception at runtime.

      Spring 通过 @Component 注解扫描 Bean,然后通过 @Autowire 注解来通过已有的 Bean 注入依赖。如果没有找到依赖就报错 NoSuchBeanDefinition。

    14. We added the @ComponentScan annotation. Note, that the UserDAO definition is now missing from the context configuration! What this @ComponentScan annotation does, is tell Spring: Have a look at all Java classes in the same package as the context configuration if they look like a Spring Bean! This means if your MyApplicationContextConfiguration lives in package com.marcobehler, Spring will scan every package, including subpackages, that starts with com.marcobehler for potential Spring beans. How does Spring know if something is a Spring bean? Easy: Your classes need to be annotated with a marker annotation, called @Component.

      通过 @ComponentScan 注解,可以让 Spring 扫描包下所有的 class,找到 Bean(比如 @Component 类)。

    15. One question: Why do you have to explicitly call new UserDao() with a manual call to dataSource()? Cannot Spring figure all of this out itself?

      用 Java config 的写法(@Configuration + @Bean 方法)来创建 bean,依然有一点疑问,里面还是需要手动地调用构造器,Spring 可以自己解决吗?

    16. The gist: Most Spring applications almost entirely consist of singleton beans, with the occasional other bean scope (prototype, request, session, websocket etc.) sprinkled in.

      Spring 中 Bean 的 scope。

    17. This approach works, but has two drawbacks: What happens if we want to create a new ProductDAO class, which also executes SQL statements? Your ProductDAO would then also have a DataSource dependency, which now is only available in your UserDAO class. You would then have another similar method or extract a helper class that contains your DataSource. We are creating a completely new DataSource for every single SQL query. Consider that a DataSource opens up a real, socket connection from your Java program to your database. This takes time and is rather expensive. It would be much nicer if we opened just one DataSource and re-used it, instead of opening and closing tons of them. One way of doing this could be by saving the DataSource in a private field in our UserDao, so it can be reused between methods - but that does not help with the duplication between multiple DAOs.

      对于作者给出的 DAO 类,需要依赖 data source 类来查询数据这个例子来说,使用 new 方法会带来如下弊端:很多 DAO. 类都需要 data source,每次 new 一个 data source 开销大。

    18. These instances that those factory methods create are called beans. It is a fancy word for saying: I (the Spring container) created them and they are under my control. But this leads to the question: How many instances of a specific bean should Spring create?

      在配置类里通过 @Bean 标注的方法就相当于工厂方法,Spring 通过这些方法创建 bean。

    19. Are there alternatives to AnnotationConfigApplicationContext? There are many ways to construct a Spring ApplicationContext, for example through XML files, annotated Java configuration classes or even programmatically. To the outside world, this is represented through the single ApplicationContext interface. Look at the MyApplicationContextConfiguration class from above. It is a Java class that contains Spring-specific annotations. That is why you would need to create an Annotation ConfigApplicationContext. If, instead, you wanted to create your ApplicationContext from XML files, you would create a ClassPathXmlApplicationContext. There are also many others, but in a modern Spring application, you will usually start out with an annotation-based application context.

      Spring 有多种方式来构建 Application Context,比如 XML、配置类、编程方式,构建出来的都是 ApplicationContext 接口的子类。

    20. What you really want to pass into the ApplicationContext constructor, is a reference to a configuration class, which should look like this:

      我们给 Spring ApplicationContext 传递的是一个「配置信息」,声明了需要的 Bean。比如通过一个配置类。

    21. That someone, who has control over all your classes and can manage them appropriately (read: create them with the necessary dependencies), is called ApplicationContext in the Spring universe.

      Spring 框架的核心就是一个「依赖注入容器」,这个控制类和类依赖组装的东西,在 Spring 里叫做 ApplicationContext。

    22. That someone is a dependency injection container and is exactly what Spring framework is all about.

      但是程序员依旧需要自己组装,而 dependency injection container 就是一个工具,来帮助我们组装好类及其依赖,Spring Framework 就是。

    23. Whenever a caller creates a new UserDao through its constructor, the caller also has to pass in a valid DataSource. The findByX methods will then simply use that DataSource. From the UserDao perspective this reads much nicer. It doesn’t know about the application class anymore, or how to construct DataSources itself. It only announces to the world "if you want to construct (i.e. use) me, you need to give me a datasource". But imagine you now want to run your application. Whereas you could call "new UserService()" previously, you’ll now have to make sure to call new UserDao(dataSource).

      解决问题的第一步是通过 Inversion of Control,即 DAO 不负责创建依赖,只是通过实例和构造器声明(我需要一个依赖),由调用者来负责传递依赖。

    24. There are however still several drawbacks to this solution: The UserDAO actively has to know where to get its dependencies from, it has to call the application class → Application.INSTANCE.dataSource(). If your program gets bigger, and you get more and more dependencies, you will have one monster Application.java class, which handles all your dependencies. At which point you’ll want to try and split things up into more classes/factories etc.

      即便是我们用了枚举单例,解决了 1)不需要 DAO 自己创建,2)全局单例,但是还是有问题。

      因为作为 DAO 来说,它依然需要主动地去获取 data source。设想以后程序变大了,依赖情况变多了,那么就会有一个巨大的全局类来 hold 住所有的依赖。

    25. import com.mysql.cj.jdbc.MysqlDataSource; public enum Application { INSTANCE; private DataSource dataSource; public DataSource dataSource() { if (dataSource == null) { MysqlDataSource dataSource = new MysqlDataSource(); dataSource.setUser("root"); dataSource.setPassword("s3cr3t"); dataSource.setURL("jdbc:mysql://localhost:3306/myDatabase"); this.dataSource = dataSource; } return dataSource; } }

      这里利用了枚举来实现单例。据说这是真正的单例,由 JVM 保证的。

    26. At its core, Spring framework is really just a dependency injection container, with a couple of convenience layers (think: database access, proxies, aspect-oriented programming, RPC, a web mvc framework) added on top.

      Spring 的核心就是一个 dependency injection container。

    27. Because it is essential to learn that Spring Framework is the basis for all other projects. Spring Boot, Spring Data, Spring Batch all build on top of Spring.

      所有的 Spring 项目都是基于 Spring 核心框架的,因此理解它很重要。

    1. If you want to get a copy of an existing Git repository — for example, a project you’d like to contribute to — the command you need is git clone. If you’re familiar with other VCSs such as Subversion, you’ll notice that the command is "clone" and not "checkout". This is an important distinction — instead of getting just a working copy, Git receives a full copy of nearly all data that the server has. Every version of every file for the history of the project is pulled down by default when you run git clone. In fact, if your server disk gets corrupted, you can often use nearly any of the clones on any client to set the server back to the state it was in when it was cloned (you may lose some server-side hooks and such, but all the versioned data would be there — see Getting Git on a Server for more details).

      Clone 一个已有的仓库,使用 git clone 命令。

      不同于 Subversion,我们将获得一个几乎完整的 copy,项目历史上所有文件的每个版本的信息都获取到了。

      如果 git 服务器损坏,我们可以用任意一个 copy 来恢复服务器(可能会丢失一些 git hooks,暂时不知道是啥)

      git clone 的时候提供一个 url,可以使用 https:// 协议,也可以通过 git://user@server:path/to/repo,git 的形式使用 ssh 协议,后续介绍。

    2. This creates a new subdirectory named .git that contains all of your necessary repository files — a Git repository skeleton. At this point, nothing in your project is tracked yet. See Git Internals for more information about exactly what files are contained in the .git directory you just created.

      git init 将一个文件夹初始化为一个 git 仓库,命令会创建一个 .git 的子文件夹,来包含所有必要的仓库文件。

      初始化之后,是没有文件被 track 的,具体什么文件会被包含在 .git,这涉及到 git 的内部细节。

    1. If you ever need help while using Git, there are three equivalent ways to get the comprehensive manual page (manpage) help for any of the Git commands: $ git help <verb> $ git <verb> --help $ man git-<verb> For example, you can get the manpage help for the git config command by running this: $ git help config

      可以使用如下三种方式查看 Git 的帮助 manual: - git help <verb> - git <verb> --help - man git-verb

      如果只是想查看一下命令,可以使用 git <verb> -h 来查看简短版本。

    2. These commands are nice because you can access them anywhere, even offline. If the manpages and this book aren’t enough and you need in-person help, you can try the #git, #github, or #gitlab channels on the Libera Chat IRC server, which can be found at https://libera.chat/. These channels are regularly filled with hundreds of people who are all very knowledgeable about Git and are often willing to help.

      可以通过 Libra Chat IRC 来提问,有空研究一下 IRC

    1. 不过,在开发期就进行的前后端联合在现今许多企业之中仍是主流形式,由一个人“全栈式”地开发某个功能时更是如此,因此,当要在开发模式中进行联调时,需要修改项目根目录下的 main.js 文件,使其不导入 Mock.js,即如下代码所示的条件语句判断为假:

      这里描述的应该是,理论上前后端应该分别开发,不存在联调,但是真实情况还是要前后端在开发期间联调。

    2. 同样出于前后端分离的目的,理论上后端通常只应当依据约定的服务协议(接口定义、访问传输方式、参数及模型结构、服务水平协议等)提供服务,并以此为依据进行不依赖前端的独立测试,最终集成时使用的是编译后的前端产品。

      Deepseek 前后端分离时,服务协议的要点可以简洁概括为以下几点:

      接口定义:明确后端提供的API接口及其功能。 访问方式:规定API的调用方法(如HTTP方法:GET、POST等)。 参数及模型:定义请求和响应的数据结构(如JSON格式)。 传输协议:确定通信协议(如HTTP/HTTPS)。 服务水平协议(SLA):约定性能指标(如响应时间、可用性)。 这些要点确保前后端能独立开发和测试,最终无缝集成。

    3. 同时,你也应当注意到,以纯前端方式运行的时候,所有对数据的修改请求实际都是无效的。譬如用户注册,无论你输入何种用户名、密码,由于请求的响应是静态预置的,所以最终都会以同一个预设的用户登陆。也是因此,我并没有提供”默认用户“、”默认密码“一类的信息供用户使用,你可以随意输入即可登陆。 不过,那些只维护在前端的状态依然是可以变动的,典型的如对购物车、收藏夹的增删改。让后端服务保持无状态,而把状态维持在前端中的设计,对服务的伸缩性和系统的鲁棒性都有着极大的益处,多数情况下都是值得倡导的良好设计。而其伴随而来的状态数据导致请求头变大、链路安全性等问题,都会在服务端部分专门讨论和解决。

      这里讨论的有的操作,是需要后端的,而有的操作只维护在前端,并且这种设计是好的设计,但也会伴随一些问题。

      这个可以解决我的一类问题「一个需求应该放在前端还是后端来实现」。

    4. 也许你已注意到,以上这些运行方式,均没有涉及到任何的服务端、数据库的部署。现代软件工程里,基于 MVVM 的工程结构使得前、后端的开发可以完全分离,只要互相约定好服务的位置及模型即可。Fenix's Bookstore 以开发模式运行时,会自动使用 Mock.js 拦截住所有的远程服务请求,并以事先准备好的数据来完成对这些请求的响应。

      Mock.js 作为远程服务访问的 mock

    1. 尤其是在国内特殊的网络环境下,无法直接访问到 Google 等国外的代码仓库,以至于不得不通过手工预载镜像或者代理的方式来完成环境搭建

      fuck GFW

    2. 笔者认为对于一个技术人员,成长主要的驱动力是实践,在开发程序、解决问题中增长自身的知识,再将知识归纳、总结、升华成为理论的,所以笔者将这部分安排到了整部文档的末尾,也是希望大家能先去实践,再谈理论。

      对于技术人员,在实践中学习,然后再总结理论很重要。

    3. 同时,笔者也认为对于一名研究人员,或者企业中真正能决定技术方向的决策者,理论与实践都不可缺少,涉及决策的场景中,成体系的理论知识甚至比实践经验还要关键,因为执行力再强也必须用在正确的方向上才有价值。如果你对自己的规划是有朝一日要从一名技术人员发展成研究或者管理角色,补充这部分知识是必不可少的。

      对于研究人员和决策者,有成体系的理论知识很重要。

    4. “不可变基础设施”这个概念由来已久。2012 年 Martin Fowler 设想的“凤凰服务器 ”与 2013 年 Chad Fowler 正式提出的“不可变基础设施 ”,都阐明了基础设施不变性所能带来的益处。在云原生基金会 (Cloud Native Computing Foundation,CNCF)所定义的“云原生”概念中,“不可变基础设施”提升到了与微服务平级的重要程度,此时它的内涵已不再局限于方便运维、程序升级和部署的手段,而是升华为向应用代码隐藏分布式架构复杂度、让分布式架构得以成为一种可普遍推广的普适架构风格的必要前提。在云原生时代、后微服务时代中,软件与硬件之间的界线已经彻底模糊,无论是基础设施的运维人员,抑或技术平台的开发人员,都有必要深入理解基础设施不变性的目的、原理与实现途径。

      关键词:不可变基础设施,云原生,屏蔽软硬件

    5. 不同的架构风格,其区别是到底要在技术规范上提供统一的解决方案,还是由应用系统自行去解决,又或者在基础设施层面将一类问题隔离掉,这部分将会讨论这类问题的解决思路、方法和常见工具。

      分布式中的各种问题,不同的架构似乎是问题放在哪一层的取舍问题。

    6. 而是聚焦于贴近一线研发人员的技术方案设计者。这部分将介绍作为一个架构师,你应该在做架构设计时思考哪些问题,有哪些主流的解决方案和行业标准做法,各种方案有什么优点、缺点,不同的解决方法会带来什么不同的影响,等等。以达到将“架构设计”这种听起来抽象的工作具体化、具象化的目的。

      很期待看到这部分的内容

    1. 举个例子,譬如某企业中应用的单体架构的 Java 系统,其更新、升级都必须要有固定的停机计划,必须在特定的时间窗口内才能按时开始,必须按时结束。如果出现了非计划的宕机,那便是生产事故。但是软件的缺陷不会遵循领导定下的停机计划来“安排时间出错”,为了应对缺陷与变化,做到不停机地检修,Java 曾经搞出了 OSGi 和 JVMTI Instrumentation 等这样复杂的 HotSwap 方案,以实现给奔跑中的汽车更换轮胎这种匪夷所思却又无可奈何的需求;而在微服务架构的视角下,所谓系统检修,不过只是一次在线服务更新而已,先停掉 1/3 的机器,升级新的软件版本,再有条不紊地导流、测试、做金丝雀发布,一切都是显得如此理所当然、平淡寻常;而在无服务架构的视角下,我们甚至都不可能去关心服务所运行的基础设施,连机器是哪台都不必知道,停机升级什么的就根本无从谈起了。

      这个例子让我明白了架构的重要性,如果只是单体架构的话,在线更新就要考虑 Hot Swap 这种方案;而在微服务和无服务的架构中,更新就变成了服务实例的替换而已。这就是架构带来的一种不同。

    2. 可是,如果不拘泥于特定系统或特定某个问题,以更宏观的角度来看,前面所列这种种好处却都只能算是“锦上添花”、是属于让系统“活得更好”的动因,肯定比不上系统如何“确保生存”的需求来得关键、本质。在笔者看来,架构演变最重要的驱动力,或者说这种“从大到小”趋势的最根本的驱动力,始终都是为了方便某个服务能够顺利地“死去”与“重生”而设计的,个体服务的生死更迭,是关系到整个系统能否可靠续存的关键因素。

      系统稳定的关键是,一个部分死去了之后能够重生来保持整个系统的稳定,而不是那些所谓的特点。

    3. 这个问题也并非杞人忧天庸人自扰式的瞎操心,计算机之父冯·诺依曼(John von Neumann)在 1940 年代末期,曾经花费了大约两年时间,研究这个问题并且得出了一门理论《自复制自动机 》(Theory of Self-Reproducing Automata),这个理论以机器应该如何从基本的部件中构造出与自身相同的另一台机器引出,其目的并不是想单纯地模拟或者理解生物体的自我复制,也并不是简单想制造自我复制的计算机,他的最终目的就是想回答一个理论问题:如何用一些不可靠的部件来构造出一个可靠的系统。 当时自复制机的艺术表示(图片来自维基百科) 自复制机恰好就是一个最好的用不可靠部件构造的可靠的系统例子。这里,“不可靠部件”可以理解为构成生命的大量细胞、甚至是分子。由于热力学扰动、生物复制差错等因素干扰,这些分子本身并不可靠。但是生命系统之所以可靠的本质,恰是因为它可以使用不可靠的部件来完成遗传迭代。这其中的关键点便是承认细胞等这些零部件可能会出错,某个具体的零部件可能会崩溃消亡,但在存续生命的微生态系统中一定会有其后代的出现,重新代替该零部件的作用,以维持系统的整体稳定。在这个微生态里,每一个部件都可以看作一只不死鸟(Phoenix),它会老迈,而之后又能涅槃重生。

      如何构建大规模的稳定系统,冯诺伊曼有过「自复制机器」的研究。这种建立在局部不稳定的大规模稳定是可靠的,比如人体就是一个例子。

    4. 这两者的理解路径和抽象程度是不一样的。如何学习一项具体的语言、框架、工具,譬如 Java、Spring、Vue.js……都是相对具象的,不论其蕴含的内容多少,复杂程度高低,它是至少能看得见摸得着。而如何学习某一种风格的架构方法,譬如单体、微服务、服务网格、无服务、云原生……则是相对抽象的,谈论它们可能要面临着“一百个人眼中有一百个哈姆雷特”的困境。谈这方面的话题,若要言之有物,就不能是单纯的经验陈述。笔者想来,回到这些架构根本的出发点和问题上,真正去使用这些不同风格的架构方法来实现某些需求,解决某些问题,然后在实践中观察它们的异同优劣,会是一种很好的,也许是最好的讲述方式。

      编码能力与软件架构的关系

    1. You'll find answers to all those questions above and further insights into how Maven works as part of this article: Maven Setup For Testing Java Applications.

      bookmark - Maven article

    2. I prefer Maven over Gradle. Even though the release cadence and the set of advanced features might not hold up to what Gradle provides, Maven is good enough for most backend projects. In the past, I (personally) had more headaches when trying to understand the Gradle setup than I had with Maven.

      对于 Maven 和 Gradle 来说,后者提供更多的 feature 且更新更快,但是 Maven 可以胜任大多数任务。

    1. I can give you a 99,99% guarantee, that you’d rather want to read the What is Spring Framework? article first, if…​:

      作者还有一个似乎很棒的 Spring 教程

    1. Whereas JSONAssert helps writing assertions for entire JSON documents, JsonPath enables us to extract specific parts of our JSON while using a JsonPath expression.

      JsonPath 是用来提取 JSON 数据的特定部分的,使用 JsonPath 语法来定位 JSON 数据的结构。

    2. JSONAssert helps writing unit tests for JSON data structures. This can be really helpful when testing the API endpoints of our Spring Boot application.

      JSONAssert 是一个断言 JSON 数据的库(org.skyscreamer),测试 API 常用。

      它包含一个 boolean 参数来设置 strictness。

    3. AssertJ is another assertion library that allows writing fluent assertions for Java tests. It follows a similar approach you already saw with Hamcrest as it makes the assertion more readable.

      AssertJ 是另一个断言库,它的特点是流式写法。

    4. Even though JUnit ships its own assertions within the package, org.junit.jupiter.api.Assertions we can still use another assertion library. Hamcrest is such an assertion library. The assertions we write with Hamcrest follow a more sentence-like approach which makes it more readable.

      JUnit 包含了自带的 assertions,Hamcrest 是一个其他的 assertion library 提供了可读性更好的断言语句,以及一些便利的断言方法。

    5. The JUnit team invested a lot in this refactoring to have a more platform-based approach with a comprehensive extension model.

      这里提到的 Junit 5 的 comprehensive extension model 还不太明白,GPT 说是插件系统,这是更高阶的内容。

      比如 Mockito 就需要使用 @ExtendWith(MockitoExtension.class)

    6. Nevertheless, migrating from JUnit 4 to 5 requires effort. All annotations, like @Test, now reside in the package org.junit.jupiter.api , and some annotations were renamed or dropped and have to be replaced. A short overview of the differences between both framework versions is the following: Assertions reside in org.junit.jupiter.api.Assertions Assumptions reside in org.junit.jupiter.api.Assumptions @Before and @After no longer exist; use @BeforeEach and @AfterEach instead. @BeforeClass and @AfterClass no longer exist; use @BeforeAll and @AfterAll instead. @Ignore no longer exists, use @Disabled or one of the other built-in execution conditions instead @Category no longer exists, use @Tag instead @Rule and @ClassRule no longer exist; superseded by @ExtendWith and @RegisterExtension @RunWith no longer exists, superseded by the extension model using @ExtendWith

      JUnit 4 到 5 的主要变化

    7. If for some reason, we want  a different version of a dependency coming from this starter, we can override it in our properties section of our pom.xml:

      如果想要覆盖某一个依赖的版本,可以使用 <properties> 标签,其名称和 spring-boot-dependencies 一样

    8. When using this starter, we don’t need to update the versions of all the dependencies manually. The Spring Boot parent POM handles all dependency versions, and the Spring Boot team ensures the different testing dependencies work properly together.

      spring-boot-starter-parent 这个 <parent> 定义了 parent POM 文件,然后我们可以从 .m2 文件夹里看到,里面的 pom 文件定义了各种依赖版本,因此 springboot test 的版本我们不需要指定

    9. This starter includes Spring-specific dependencies and dependencies for auto-configuration and a set of testing libraries. This includes JUnit, Mockito, Hamcrest, AssertJ, JSONassert, and JsonPath. These libraries all serve a specific purpose, and some can be replaced by each other, which we’ll later see on. Nevertheless, this opinionated selection of testing tools is all we need for unit testing. For writing integration tests, we might want to include additional dependencies (e.g., WireMock, Testcontainers, or Selenium) depending on our application setup.

      spring-boot-starter-test 包含了一系列预定义好的依赖:JUnit Mockito 等,还有自动配置,Spring 等的依赖,通过 mvn dependency:tree 可以打印依赖树

    1. That's because Spring Boot always includes the Spring Boot Starter Test in every project.

      springboot 默认就带上了 spring-boot-starter-test 依赖

    1. Generate a new Spring Boot project using the Spring Initializr

      使用 curl 来从 spring initializr 生成项目可以做一个命令行工具

    2. The mvnw file is a shell script for Linux and MacOS while mvnw.cmd is for Windows. The benefit of this Maven wrapper is that we can easily switch projects that depend on different Maven versions without much setup on our local machine. While Maven is quite mature and most versions are backward-compatible (especially since Maven 3), things are completely different when using Gradle, where this wrapper concept was inspired from.

      wrapper 的概念是 maven 从 gradle 那里借鉴过来的

    1. If you're on Mac or Linux, I can highly recommend jEnv to manage multiple Java versions on your machine.

      我现在用的 sdkman 也挺好用的,有空对比一下区别

    1. 它们实际上是数据的展示格式,分别按英国时区、中国时区、纽约时区对同一个时刻进行展示。而这个“同一个时刻”在计算机中存储的本质上只是一个整数,我们称它为Epoch Time。

      日期和时间在计算机中存储的本质 -- Epoch Time

    2. 编译器会把上述字符串(程序源码就是一个字符串)编译成字节码。在程序的运行期,变量n指向的内存实际上是一个4字节区域:

      4字节在内存中就是这样的想象

    1. 因为时区的存在,东八区的2019年11月20日早上8:15,和西五区的2019年11月19日晚上19:15,他们的时刻是相同的

      时刻是唯一的概念

    2. GMT和UTC可以认为基本是等价的,只是UTC使用更精确的原子钟计时,每隔几年会有一个闰秒,我们在开发程序的时候可以忽略两者的误差,因为计算机的时钟在联网的时候会自动与时间服务器同步时间。

      Ma Dawei 有一篇讲计算机时间的文章,介绍了服务器同步时间。

  2. Sep 2024
    1. Each language server can be installed via a package manager - but often in a different way. For example, the language servers for HTML/CSS/JS are provided by Microsoft, and are installed via npm. Whereas the language server for say, Rust, might be managed by rustup (or Rust itself). Keep this in mind, as you will need to have the relevant package manager installed based on what language servers you need. Microsoft keeps a list of LSP servers where you can check the repositories to see which package manager (if any) you will need to install it.

      不同的 language server 需要通过不同的 package manager 安装。

    2. Mason is an easy way to install these language servers for the programming languages of your choice if you don’t want to install and maintain them yourself.

      Mason 是安装 LSP language servers 的方便工具。

    3. It’s worth noting that these features don’t technically ship directly with Neovim when the language server is configured. It enables the use of them, and you have to get plugins for each feature you want (unix philosophy, baby).

      想要获得这些 servers 提供的特性,需要安装插件。

    4. These servers make it possible to get features such as go-to-definition, auto-complete and syntax errors (like if you spell something wrong or miss a semi-colon - so you don’t rip your hair out trying to find it).

      这些 servers 提供了诸如自动补全、语法错误等特性。

    5. Neovim ships with a built-in LSP client,

      neovim 自带一个 LSP client

    6. When picking a Neovim colorscheme make sure that it’s tree-sitter supported/compatible - Awesome Neovim provides a list of themes which you can use.

      soga,原来 colorscheme 要和 treesitter 兼容

    1. For example, creating a file called lazy.lua and then requiring it in your init.lua with require("lazy") is going to cause some problems... Remember the snippet above does the exact same thing to find lazy on the runtimepath, but now there are two modules named lazy - one in your config, and one in the Neovim data directory. This will essentially cause your entire configuration to break.

      在 /lua 文件夹里使用 lazy 模块要注意冲突

    1. I hope that you at least found this post useful though in understanding the runtimepath and which files are automatically ran on startup, why people structure their lua/ folders in different ways, and how you can begin structuring your configuration.

      虽然 /plugin 和 /lua 文件夹都是 runtimepath,但是 plugin 里的 lua 文件就不需要 require 就能用运行,lua 文件夹里的文件都需要在外面的 init.lua 中 require

    2. The reason you tend to see most users throwing their entire config in the lua/ directory is because you can use Lua modules to organize your config in a nice, neat way.

      推荐使用 /lua 文件夹来进行 neovim 配置是因为可以使用 Lua modules 来组织配置。

    1. Control characters other than tab (U+0000 to U+0008, U+000A to U+001F, U+007F) are not permitted in comments.

      疑问

      不明白什么是 control characters

    2. Newline means LF (0x0A) or CRLF (0x0D 0x0A).
    3. TOML is designed to map unambiguously to a hash table.

      问题

      以前这是个问题吗?

    1. There are also other use cases for refs than accessing React components.

      要点

      React

      其他一些 useRef 的使用场景

    2. The function that creates the component is wrapped inside of a forwardRef function call. This way the component can access the ref that is assigned to it. The component uses the useImperativeHandle hook to make its toggleVisibility function available outside of the component. We can now hide the form by calling noteFormRef.current.toggleVisibility() after a new note has been created:

      要点

      React

      在被引用的组件里,组件定义被 forwardRef 包裹,同时里面使用 useImperativeHandle 来定义能被外面获取到的内容。

    3. The useRef hook is used to create a noteFormRef reference, that is assigned to the Togglable component containing the creation note form. The noteFormRef variable acts as a reference to the component. This hook ensures the same reference (ref) that is kept throughout re-renders of the component.

      要点

      React

      useRef hooks 创建了一个引用,可以用来引用 component

    4. React documentation says the following about where to place the state: Sometimes, you want the state of two components to always change together. To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props. This is known as lifting state up, and it’s one of the most common things you will do writing React code. If we think about the state of the forms, so for example the contents of a new note before it has been created, the App component does not need it for anything. We could just as well move the state of the forms to the corresponding components.

      要点

      关于 React 中组件定义的位置。注意 lifting stat up 和把状态放到子组件中两种情况。

    5. The new and interesting part of the code is props.children, which is used for referencing the child components of the component. The child components are the React elements that we define between the opening and closing tags of a component.

      要点

      在 React 中,定义在一个 component 开闭标签之间的内容就是这个组件的 child components

    6. If loginVisible is false, then display will not receive any value related to the visibility of the component.

      要点

      如果 display: "" 的话,就是显示

    1. No matter how the validity of tokens is checked and ensured, saving a token in the local storage might contain a security risk if the application has a security vulnerability that allows Cross Site Scripting (XSS) attacks. An XSS attack is possible if the application would allow a user to inject arbitrary JavaScript code (e.g. using a form) that the app would then execute. When using React sensibly it should not be possible since React sanitizes all text that it renders, meaning that it is not executing the rendered content as JavaScript. If one wants to play safe, the best option is to not store a token in local storage. This might be an option in situations where leaking a token might have tragic consequences. It has been suggested that the identity of a signed-in user should be saved as httpOnly cookies, so that JavaScript code could not have any access to the token. The drawback of this solution is that it would make implementing SPA applications a bit more complex. One would need at least to implement a separate page for logging in. However, it is good to notice that even the use of httpOnly cookies does not guarantee anything. It has even been suggested that httpOnly cookies are not any safer than the use of local storage. So no matter the used solution the most important thing is to minimize the risk of XSS attacks altogether.

      延伸

      Cross Site Scripting 注入攻击 httpOnly cookies -> 让 SPA 不好实现

    2. The empty array as the parameter of the effect ensures that the effect is executed only when the component is rendered for the first time.

      要点

      useEffect 中如果设定 empty array 的话,就代表只有第一次渲染的时候执行 effect

    3. Values saved to the storage are DOMstrings, so we cannot save a JavaScript object as it is. The object has to be parsed to JSON first, with the method JSON.stringify. Correspondingly, when a JSON object is read from the local storage, it has to be parsed back to JavaScript with JSON.parse.

      要点

      local storage 里存储的是 DOMString,因此 JS 对象的存取,需要 stringify 和 parse

    4. Values in the local storage are persisted even when the page is re-rendered. The storage is origin-specific so each web application has its own storage.

      要点

      local storage 是 origin-specific 的,因此每一个 web app 都有一个 storage。

    5. This problem is easily solved by saving the login details to local storage. Local Storage is a key-value database in the browser.

      要点

      local storage 是浏览器中的 key-value 数据库

      API: window.localStorage.setItem / getItem / removeItem

    6. The noteService module contains a private variable called token. Its value can be changed with the setToken function, which is exported by the module. create, now with async/await syntax, sets the token to the Authorization header. The header is given to axios as the third parameter of the post method.

      要点

      axios 中设置 header 是通过第三个参数,传递一个对象

    7. A slightly odd looking, but commonly used React trick is used to render the forms conditionally:

      延伸

      React 中的条件渲染技巧

    1. This is most likely useful when doing the fix.

      要点

      在 .post() 方法后面使用 .set('Authorization', 'Bear xxx') 来设置测试中的请求头

    2. the field blog.user does not contain a string, but an object. So if you want to compare the ID of the object fetched from the database and a string ID, a normal comparison operation does not work. The ID fetched from the database must be parsed into a string first.

      重点

      mongoose 关联的 ref 类型是 ObjectId,因此要比较 ID 内容,使用 toString 方法。

    3. Both the new blog creation and blog deletion need to find out the identity of the user who is doing the operation. The middleware tokenExtractor that we did in exercise 4.20 helps but still both the handlers of post and delete operations need to find out who the user holding a specific token is.

      重点

      这里就是后端的常见操作,当寻找用户的操作要在 API 里完成的时候,就可以抽出来放到中间件里去做。

    4. As can be seen, this happens by chaining multiple middlewares as the arguments of the function use. It would also be possible to register a middleware only for a specific operation:

      重点

      middleware 可以针对某一个 endpoint 使用,以及某一个 route 使用。

    5. NB if you decide to define tests on multiple files, you should note that by default each test file is executed in its own process (see Test execution model in the documentation). The consequence of this is that different test files are executed at the same time. Since the tests share the same database, simultaneous execution may cause problems, which can be avoided by executing the tests with the option --test-concurrency=1, i.e. defining them to be executed sequentially.

      重点

      咋不早说... 每个测试文件都是在自己的进程中运行的,因此会并行跑,使用 --test-concurrency=1 来串行跑

    6. Usernames, passwords and applications using token authentication must always be used over HTTPS. We could use a Node HTTPS server in our application instead of the HTTP server (it requires more configuration). On the other hand, the production version of our application is in Fly.io, so our application stays secure: Fly.io routes all traffic between a browser and the Fly.io server over HTTPS.

      拓展

      Node 也可以配置使用 HTTPS 服务器来运行。

    7. Limiting creating new notes to logged-in users

      拓展

      • 用户 token 过期之后,重新登陆
      • 使用数据库在后端存 token -> 使用 redis 存 token
        • 后端 token 没有个人信息
      • 使用 cookie 传递 token
    8. It is also quite usual that instead of using Authorization-header, cookies are used as the mechanism for transferring the token between the client and the server.

      重点

      关于 token-based authentication 的问题

      还有一种常见的操作是,不实用 Authorization header,而是使用 cookie 来作为在 server 和 client 之间传递 token 的机制。

    9. When server-side sessions are used, the token is quite often just a random string, that does not include any information about the user as it is quite often the case when jwt-tokens are used. For each API request, the server fetches the relevant information about the identity of the user from the database.

      重点

      关于 token-based authentication 的问题

      使用 server-side sessions 的话,token 就只是 random 字符串,不像 jwt 一样包含用户的个人信息。

    10. The negative aspect of server-side sessions is the increased complexity in the backend and also the effect on performance since the token validity needs to be checked for each API request to the database. Database access is considerably slower compared to checking the validity of the token itself. That is why it is quite common to save the session corresponding to a token to a key-value database such as Redis, that is limited in functionality compared to eg. MongoDB or a relational database, but extremely fast in some usage scenarios.

      重点

      关于 token-based authentication 的问题

      server-side session 的问题是增加了后端的复杂性,而且每个 API 请求都需要走数据库进行校验,这很慢。因此通常会把 token 存到键值对数据库 redis 中。

    11. The other solution is to save info about each token to the backend database and to check for each API request if the access rights corresponding to the tokens are still valid. With this scheme, access rights can be revoked at any time. This kind of solution is often called a server-side session.

      重点

      关于 token-based authentication 的问题

      另一种解决方案是把 token 的信息存到后端,然后每个 API 请求都检查 token 的权限是否有效,这就是 server-side session。

    12. Once the token expires, the client app needs to get a new token. Usually, this happens by forcing the user to re-login to the app. The error handling middleware should be extended to give a proper error in the case of an expired token

      重点

      关于 token-based authentication 的问题

      第一种方法是给一个过期时间,时间到了,通常来说要迫使用户重新登陆(这个是需要实现的还是浏览器自动就有的?猜测是前者)

      但这样的做法实际上是用户体验不好的。

    13. Token authentication is pretty easy to implement, but it contains one problem. Once the API user, eg. a React app gets a token, the API has a blind trust to the token holder. What if the access rights of the token holder should be revoked?

      重点

      关于 token-based authentication 的

      最简单的实现问题在于 API 对于有 token 的人完全信任,如果遇到需要撤销 token 的情况怎么办?

    14. If the application has multiple interfaces requiring identification, JWT's validation should be separated into its own middleware. An existing library like express-jwt could also be used.

      #拓展 如果有多个接口(几乎是一定的)需要验证身份,那么 JWT 验证应该被放到自己的中间件中。

      有一个库提供这个功能 express-jwt

    15. The helper function getTokenFrom isolates the token from the authorization header. The validity of the token is checked with jwt.verify. The method also decodes the token, or returns the Object which the token was based on.

      重点

      原来从 header 中取得 token 就是操作字符串。 jwt.verify() 方法来校验 token

    16. There are several ways of sending the token from the browser to the server. We will use the Authorization header. The header also tells which authentication scheme is used. This can be necessary if the server offers multiple ways to authenticate. Identifying the scheme tells the server how the attached credentials should be interpreted.

      重点

      我们在进行 authentication 的时候,浏览器通过 Authorization header 来给服务器发送 token。这个 header 不仅包含了 token,还有使用了何种 Scheme。

    17. If the password is correct, a token is created with the method jwt.sign. The token contains the username and the user id in a digitally signed form.

      重点

      如果用户密码正确,就返回一个 jwt token,里面包含了 username user_id,数字签名的形式。

      签名的过程中使用了一个 SECRET.

    18. ecause the passwords themselves are not saved to the database, but hashes calculated from the passwords, the bcrypt.compare method is used to check if the password is correct:

      重点

      bcrypt.compare() 方法用来检查密码是否正确

    19. Let's first implement the functionality for logging in. Install the jsonwebtoken library, which allows us to generate JSON web tokens.

      重点

      我们使用 jsonwebtoken 这个库来生成 jwt token

    20. The principles of token-based authentication are depicted in the following sequence diagram:

      重点

      整个 authentication 的过程如下图 登陆请求的返回有一个 token,浏览器保存下来这个 token

    21. We will now implement support for token-based authentication to the backend.

      拓展阅读

      这里实现的是 token-based authentication

    1. NOTE: At this stage, firstly, some tests will fail. We will leave fixing the tests to a non-compulsory exercise.

      拓展

      这里花了不少时间,但是两个测试文件会相互影响,如何让 Node 不并行运行测试文件,或者更好地准备测试数据,是后续可以做的。

      还有一个问题是,test_helper 里面的函数没有像实现一样,应用 populate 方法改变返回,这里也是测试脆弱的一点。

    2. We could also implement other validations into the user creation. We could check that the username is long enough, that the username only consists of permitted characters, or that the password is strong enough. Implementing these functionalities is left as an optional exercise.

      拓展练习

      实现用户创建的更多校验:用户名长度,包含字符,密码强度

    3. However, we want to be careful when using the uniqueness index. If there are already documents in the database that violate the uniqueness condition, no index will be created. So when adding a uniqueness index, make sure that the database is in a healthy state! The test above added the user with username root to the database twice, and these must be removed for the index to be formed and the code to work.

      延伸阅读

      使用 Schema 上字段的 unique 选项来保证一个字短的唯一性的时候,需要确保数据库状态是好的,如果现有数据违反了唯一性,索引不会被建立起来。

    4. The fundamentals of storing passwords are outside the scope of this course material. We will not discuss what the magic number 10 assigned to the saltRounds variable means, but you can read more about it in the linked material.

      延伸阅读

      这里介绍了关于存储密码以及 salt 的知识。

    5. It's important to understand that the database does not know that the ids stored in the user field of the notes collection reference documents in the user collection. The functionality of the populate method of Mongoose is based on the fact that we have defined "types" to the references in the Mongoose schema with the ref option:

      再一次,数据库并不知道 collection 之间是关联的,是因为我们在 schema 定义中指定了它们之间的关联。

    6. We can use the populate method for choosing the fields we want to include from the documents. In addition to the field id we are now only interested in content and important. The selection of fields is done with the Mongo syntax:

      关联查询的时候怎么指定返回的内容,参考 Mongo 语法

    7. The argument given to the populate method defines that the ids referencing note objects in the notes field of the user document will be replaced by the referenced note documents.

      这句话着重理解:也就是传给 populate 方法的参数的含义是,我在查询 user 时候,user notes 字段里面关联的 id,都会被替代为真实的 notes document。

    8. Mongoose library can do some of these joins for us. Mongoose accomplishes the join by doing multiple queries, which is different from join queries in relational databases which are transactional, meaning that the state of the database does not change during the time that the query is made. With join queries in Mongoose, nothing can guarantee that the state between the collections being joined is consistent, meaning that if we make a query that joins the user and notes collections, the state of the collections may change during the query.

      Mongoose 提供的 join 是利用多次查询实现的。一个问题是不保证 transactional,进行查询的时候,数据库的状态可能会变。

    9. Mongoose validations do not provide a direct way to check the uniqueness of a field value. However, it is possible to achieve uniqueness by defining uniqueness index for a field. The definition is done as follows:

      Mongoose 中如何使某一个 field unique check

    10. In stark contrast to the conventions of relational databases, references are now stored in both documents: the note references the user who created it, and the user has an array of references to all of the notes created by them.

      这里表明,在传统的关系型数据库中,reference 只存储在一边。

    11. The type of the field is ObjectId that references note-style documents. Mongo does not inherently know that this is a field that references notes, the syntax is purely related to and defined by Mongoose.

      定义不同 schema 之间的 ref,Mongo 并不知道,是由 Mongoose 管理的。

    12. The structure and schema of the database are not as self-evident as it was with relational databases. The chosen schema must support the use cases of the application the best. This is not a simple design decision to make, as all use cases of the applications are not known when the design decision is made. Paradoxically, schema-less databases like Mongo require developers to make far more radical design decisions about data organization at the beginning of the project than relational databases with schemas. On average, relational databases offer a more or less suitable way of organizing data for many applications.

      文档数据库的结构相比起关系型数据库会更灵活,因此其实需要开发者在开始的时候就做出更重要的设计决定。这些决定不简单是因为 use case 在设计的时候不是明确的。

    1. The then-chain is alright, but we can do better. The generator functions introduced in ES6 provided a clever way of writing asynchronous code in a way that "looks synchronous". The syntax is a bit clunky and not widely used.

      延伸阅读

      接下来 ES6 中的 generator function 再提升了一点(不了解)

    2. The documentation for supertest says the following: if the server is not already listening for connections then it is bound to an ephemeral port for you so there is no need to keep track of ports. In other words, supertest takes care that the application being tested is started at the port that it uses internally.

      supertest 控制了应用启动在指定的 port,因此我们只需要引入 app.js,不用自己在指定端口启动。

    3. The Promise.all method can be used for transforming an array of promises into a single promise, that will be fulfilled once every promise in the array passed to it as an argument is resolved. The last line of code await Promise.all(promiseArray) waits until every promise for saving a note is finished, meaning that the database has been initialized.

      Promise.all() 的原理。

    4. Promise.all executes the promises it receives in parallel. If the promises need to be executed in a particular order, this will be problematic. In situations like this, the operations can be executed inside of a for...of block, that guarantees a specific execution order.

      但是 Promise.all() 方法不保证执行顺序,如果需要顺序的话,就得在循环里,一次一次等待了。

    5. The problem is that every iteration of the forEach loop generates an asynchronous operation, and beforeEach won't wait for them to finish executing. In other words, the await commands defined inside of the forEach loop are not in the beforeEach function, but in separate functions that beforeEach will not wait for.

      如果 async 方法里有 async 操作,不做任何处理的情况下,外层的 await 是不会等到内层的异步操作结束的。

    6. Because of the library, we do not need the next(exception) call anymore. The library handles everything under the hood. If an exception occurs in an async route, the execution is automatically passed to the error-handling middleware.

      这个库自动 handle 了捕获异常,以及 pass to next 的情况。

    7. One starts to wonder if it would be possible to refactor the code to eliminate the catch from the methods? The express-async-errors library has a solution for this.

      express-async-errors 库,可以让我们移除针对 async 代码的 try catch 写法。

    8. The reason for this is that strictEqual uses the method Object.is to compare similarity, i.e. it compares whether the objects are the same. In our case, it is enough to check that the contents of the objects, i.e. the values of their fields, are the same. For this purpose deepStrictEqual is suitable.

      Node 中的 equals 比较,strictEqual 比较引用,deepStrictEqual 比较对象内容。

    9. When code gets refactored, there is always the risk of regression, meaning that existing functionality may break.

      当代码重构的时候,有回归的风险

    10. The await keyword can't be used just anywhere in JavaScript code. Using await is possible only inside of an async function.

      await 关键字只能在 async 函数里使用

    11. The async and await keywords introduced in ES7 bring the same functionality as the generators, but in an understandable and syntactically cleaner way to the hands of all citizens of the JavaScript world.

      ES7 中的 async await 更好理解。

    12. By chaining promises we could keep the situation somewhat under control, and avoid callback hell by creating a fairly clean chain of then method calls. We have seen a few of these during the course. To illustrate this, you can view an artificial example of a function that fetches all notes and then deletes the first one:

      其次 promise chaining 提升了一点可读性

    13. All of the code we want to execute once the operation finishes is written in the callback function. If we wanted to make several asynchronous function calls in sequence, the situation would soon become painful. The asynchronous calls would have to be made in the callback. This would likely lead to complicated code and could potentially give birth to a so-called callback hell.

      对于异步代码的书写,首先是 callback hell

    14. The --tests-by-name-pattern option can be used for running tests with a specific name:

      使用 ---test-name-pattern 可以根据测试的 describe 信息来筛选需要运行的测试

    15. There are a few different ways of accomplishing this, one of which is the only method. With this method we can define in the code what tests should be executed:

      使用 test.only 方法,配合 npm test -- --test-only 命令,可以只运行指定测试。

  3. Aug 2024
    1. Both tests store the response of the request to the response variable, and unlike the previous test that used the methods provided by supertest for verifying the status code and headers, this time we are inspecting the response data stored in response.body property. Our tests verify the format and content of the response data with the method strictEqual of the assert-library.

      之前使用 supertest 库的方法来验证 HTTP 的状态码和头信息,这里是用 node 的 assert 库方法来验证内容。

    2. The problem here, however, is that when using a string, the value of the header must be exactly the same. For the regex we defined, it is acceptable that the header contains the string in question.

      在 superagent 对象的 expect 方法中, api 调用的 expect 方法中使用 string 就是精准匹配,使用正则表达式就是包含匹配。

    3. The config module that we have implemented slightly resembles the node-config package. Writing our implementation is justified since our application is simple, and also because it teaches us valuable lessons.

      node.js 中关于 configuration 的一个库 node-config

    4. The convention in Node is to define the execution mode of the application with the NODE_ENV environment variable. In our current application, we only load the environment variables defined in the .env file if the application is not in production mode.

      Node 中的约定是根据 NODE_ENV 环境变量来确定应用的运行模式。

      当后端运行在 host server 的时候,是 production mode。

      我们的应用在 non production mode 的时候才加载 .env 中的变量(规定是这样?)

    5. Since our application's backend is still relatively simple, we will decide to test the entire application through its REST API, so that the database is also included. This kind of testing where multiple components of the system are being tested as a group is called integration testing.

      一次测试系统的多个组件(比如后端逻辑+数据库)的测试叫做集成测试。

    6. In some situations, it can be beneficial to implement some of the backend tests by mocking the database instead of using a real database. One library that could be used for this is mongodb-memory-server.

      MERN 这边后端也存在 mock 数据库进行测试的库,mongodb-memory-server 是一个。

    1. VS Code has a handy feature that allows you to see where your modules have been exported. This can be very helpful for refactoring. For example, if you decide to split a function into two separate functions, your code could break if you don't modify all the usages. This is difficult if you don't know where they are. However, you need to define your exports in a particular way for this to work. If you right-click on a variable in the location it is exported from and select "Find All References", it will show you everywhere the variable is imported. However, if you assign an object directly to module.exports, it will not work. A workaround is to assign the object you want to export to a named variable and then export the named variable. It also will not work if you destructure where you are importing; you have to import the named variable and then destructure, or just use dot notation to use the functions contained in the named variable. The nature of VS Code bleeding into how you write your code is probably not ideal, so you need to decide for yourself if the trade-off is worthwhile.

      Neovim 里怎么来找一个变量的 reference

    2. There is no strict directory structure or file naming convention that is required for Express applications. In contrast, Ruby on Rails does require a specific structure. Our current structure simply follows some of the best practices that you can come across on the internet.

      express 应用的命名和结构没有严格规定,但是对比来说,Ruby on Rails 的文件需要指定的结构。

    3. The responsibility of establishing the connection to the database has been given to the app.js module. The note.js file under the models directory only defines the Mongoose schema for notes.

      由 app 建立连接,models 文件只定义 schema

    4. The index.js file only imports the actual application from the app.js file and then starts the application. The function info of the logger-module is used for the console printout telling that the application is running.

      index.js 的作用仅仅是导入 application 开始运行

    5. Extracting logging into its own module is a good idea in several ways. If we wanted to start writing logs to a file or send them to an external logging service like graylog or papertrail we would only have to make changes in one place.

      把 logger 抽到一个 util 的好处是,如果更改了 log 的存储位置,我们全局只需要修改一个地方。

    6. Before we move into the topic of testing, we will modify the structure of our project to adhere to Node.js best practices. Once we make the changes to the directory structure of our project, we will end up with the following structure:

      Node 关于 package structure 有最佳实践

    1. 只有在后端的所有内容都经过验证并正常工作后,才是测试前端与后端是否协同工作的好时机。仅通过前端进行测试效率极低。

      单独测试后端!