fatfoo

fatfoo 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

fatfoo 发布了文章 · 2020-09-23

入行 10 年总结,作为开发必须知道的 Maven 实用技巧

Maven 介绍

什么是 Maven

Maven 是基于项目对象模型(POM project object model),可以通过一小段描述信息(配置)来管理项目的构建,报告和文档的软件项目管理工具,简单的说就是用来管理项目所需要的依赖且管理项目构建的工具。

Maven 的安装与配置

  1. Maven 官网下载压缩包,解压到本地。
  2. 配置环境变量 MAVEN_HOME 为 Maven 的解压目录。
  3. 添加 Maven 目录下的 bin 目录到环境变量 PATH 中。
  4. 可以在 Maven 目录下的 conf/setting.xml 文件中,通过 <localRepository /> 来指定本地仓库路径。
  5. 打开终端,输入 mvn -version 验证时是否成功。

Idea 中配置本地安装的 Maven

打开 Idea 的配置面板,找到 Maven 配置页。

  1. Maven home directory:设置为本地的 Maven 路径
  2. User settings file:勾选后面的 Override 可以自定义 settings 文件,可以指向 Maven 路径下的 conf/settings.xml
  3. Local repository:勾选 Override 同样可以自定义仓库路径,默认会从配置的 settings 文件中读取。

Maven 坐标与依赖

坐标

Maven 通过 groupId、artifactId、version 三个变量来唯一确定一个具体的依赖,俗称 GAV。

依赖

在 pom.xml 中我们通过 dependency 来声明坐标信息(GAV),如我们需要声明对 4.2.6.RELEASE 版本 spring-core 包的依赖。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-core</artifactId>
  <version>4.2.6.RELEASE</version>
</dependency>

依赖 scope

  1. compile:编译依赖范围,在编译,测试,运行时都需要,依赖范围默认值
  2. test:测试依赖范围,测试时需要。编译和运行不需要,如 junit
  3. provided:已提供依赖范围,编译和测试时需要。运行时不需要,如 servlet-api
  4. runtime:运行时依赖范围,测试和运行时需要。编译不需要,例如面向接口编程,JDBC 驱动实现 jar
  5. system:系统依赖范围。本地依赖,不在 Maven 中央仓库,结合 systemPath 标签使用

依赖排除

使用 <exclusions> 标签下的 <exclusion> 标签指定 GA 信息来排除,例如:排除 xxx.jar 传递依赖过来的 yyy.jar

<dependency>
  <groupId>com.xxx</groupId>
  <artifactId>xxx</artifactId>
  <version>x.version</version>
  <exclusions>
    <exclusion>
      <groupId>com.xxx</groupId>
      <artifactId>yyy</artifactId>
    </exclusion>
  </exclusions>
</dependency>

依赖关系查看

进入工程根目录,在命令行中运行

  1. mvn dependency:tree 命令会列出依赖关系树及各级依赖关系
  2. mvn dependency:analyze 分析依赖关系

Maven 项目的结构

Maven 项目有其他标准的目录组织结构,如上图所示:

|- project: 项目目录
  |- src: 源码目录
    |- src/main/java: 项目 java 源码文件存放目录
    |- src/main/resources: 项目资源文件存放目录
    |- src/test/java: 项目单元测试源码文件存放目录
    |- src/test/resources: 项目单元测试资源文件存放目录
  |- target: 项目编译/打包后存放的目标路径
  |- pom.xml: 项目依赖管理配置文件

Maven 的生命周期

Maven 有 3 套生命周期,每套生命周期都包含了一些阶段,这些阶段是有序的,后面的阶段依赖前面的阶段。这三套生命周期是相互独立的,可以仅仅调用 clean 生命周期的某个阶段, 或者调用 default 生命周期的某个阶段,而不会对其他生命周期产生任何影响。

  1. clean:清理项目
  2. default:构建项目

    • compile:编译项目的源代码
    • package:打包编译好的代码
    • install:将包安装至本地仓库,提供给其他项目依赖
    • deploy:将最终的包复制到远程的仓库,提供给其他开发人员和项目共享。
  3. site:建立项目站点

生命周期的执行:

# 清理项目
mvn clean
# 打包项目
mvn package
# 打包并安装到本地仓库
mvn install

可以组合各阶段进行执行:

# 清理项目后打包并发布到远程仓库
mvn clean package deploy

settings 文件详解

settings 文件的作用

settings 是用来设置 Maven 参数的配置文件,并且,settings.xml 是 Maven 的全局配置文件。settings.xml 中包含类似本地仓库、远程仓库和联网使用的代理信息等配置。

settings 文件的位置

全局配置:${MAVEN_HOME}/conf/settings.xml

用户配置:${user.home}/.m2/settings.xml

settings 文件配置优先级

其实相对于多用户的 PC 机而言,在 Maven 安装目录的 conf 子目录下面的 settings.xml 才是真正的全局的配置。而用户目录的 .m2 子目录下面的 settings.xml 的配置只是针对当前用户的。当这两个文件同时存在的时候,那么对于相同的配置信息用户目录下面的 settings.xml 中定义的会覆盖 Maven 安装目录下面的 settings.xml 中的定义。用户目录下的 settings.xml 文件一般是不存在的,但是 Maven 允许我们在这里定义我们自己的 settings.xml,如果需要在这里定义我们自己的 settings.xml 的时候就可以把 Maven 安装目录下面的 settings.xml 文件拷贝到用户目录的 .m2 目录下,然后改成自己想要的样子。

settings.xml 元素

顶级元素概览

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                          https://maven.apache.org/xsd/settings-1.0.0.xsd">
  <localRepository/>
  <interactiveMode/>
  <usePluginRegistry/>
  <offline/>
  <pluginGroups/>
  <servers/>
  <mirrors/>
  <proxies/>
  <profiles/>
  <activeProfiles/>
</settings>

LocalRepository

该值表示构建系统本地仓库的路径。其默认值:~/.m2/repository

Servers

一般,仓库的下载和部署是在 pom.xml 文件中的 repositories 和 distributionManagement 元素中定义的。然而,一般类似用户名、密码(有些仓库访问是需要安全认证的)等信息不应该在 pom.xml 文件中配置,这些信息可以配置在 settings.xml 中。

<!--配置服务端的一些设置。一些设置如安全证书不应该和pom.xml一起分发。这种类型的信息应该存在于构建服务器上的settings.xml文件中。 -->
<servers>
  <!--服务器元素包含配置服务器时需要的信息 -->
  <server>
    <!--这是server的id(注意不是用户登陆的id),该id与distributionManagement中repository元素的id相匹配。 -->
    <id>server001</id>
    <!--鉴权用户名。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。 -->
    <username>my_login</username>
    <!--鉴权密码 。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。密码加密功能已被添加到2.1.0 +。详情请访问密码加密页面 -->
    <password>my_password</password>
    <!--鉴权时使用的私钥位置。和前两个元素类似,私钥位置和私钥密码指定了一个私钥的路径(默认是${user.home}/.ssh/id_dsa)以及如果需要的话,一个密语。将来passphrase和password元素可能会被提取到外部,但目前它们必须在settings.xml文件以纯文本的形式声明。 -->
    <privateKey>${usr.home}/.ssh/id_dsa</privateKey>
    <!--鉴权时使用的私钥密码。 -->
    <passphrase>some_passphrase</passphrase>
    <!--文件被创建时的权限。如果在部署的时候会创建一个仓库文件或者目录,这时候就可以使用权限(permission)。这两个元素合法的值是一个三位数字,其对应了unix文件系统的权限,如664,或者775。 -->
    <filePermissions>664</filePermissions>
    <!--目录被创建时的权限。 -->
    <directoryPermissions>775</directoryPermissions>
  </server>
</servers>

Mirrors

用于定义一系列的远程仓库的镜像。对于一个 Maven 项目,如果没有特别声明,默认使用 Maven 的 central 库,url 为 http://repo.maven.apache.org/...。但是这些远程库往往需要连接互联网访问,由于访问互联网的限制或安全控制的需要,在企业内部往往需要建立对远程库的镜像,即远程库的 mirror。

注意

  1. 定义多个远程仓库镜像时,只有当前一个 mirror 无法连接的时候,才会去找后一个,类似于备份和容灾。
  2. mirror 也不是按 settings.xml 中写的那样的顺序来查询的。所谓的第一个并不一定是最上面的那个。当有 id 为 B、A、C 的顺序的 mirror 在 mirrors 节点中,Maven 会根据字母排序来指定第一个,所以不管怎么排列,一定会找到 A 这个mirror来进行查找,当A无法连接,出现意外的情况下,才会去B查询。
Mirror

当 Maven 需要到的依赖 jar 包不在本地仓库时,就需要到远程仓库下载。这个时候如果 settings.xml 中配置了镜像,而且镜像配置的规则中匹配到目标仓库时,Maven 认为目标仓库被镜像了,不会再去被镜像仓库下载依赖 jar包,而是直接去镜像仓库下载。简单而言,mirror 可以拦截对远程仓库的请求,改变对目标仓库的下载地址。

<mirrors>
  <!-- 给定仓库的下载镜像。 -->
  <mirror>
    <!-- 该镜像的唯一标识符。id 用来区分不同的 mirror元素。 -->
    <id>mirrorId</id>
    <!-- 镜像名称 -->
    <name>PlanetMirror Australia</name>
    <!-- 该镜像的URL。构建系统会优先考虑使用该URL,而非使用默认的服务器URL。 -->
    <url>http://downloads.planetmirror.com/pub/maven2</url>
    <!-- 被镜像的服务器的id。例如,如果我们要设置了一个 Maven 中央仓库(http://repo.maven.apache.org/maven2/)的镜像,就需要将该元素设置成 central。这必须和中央仓库的id central完全一致。 -->
    <mirrorOf>repositoryId</mirrorOf>
  </mirror>
</mirrors>
加速远程依赖的下载

使用镜像可以解决远程依赖下载慢的问题。

<mirrors>
  <!--国内阿里云提供的镜像,非常不错-->
  <mirror>
    <!--This sends everything else to /public -->
    <id>aliyun_nexus</id>
    <mirrorOf>central</mirrorOf> 
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
  </mirror>
</mirrors>

在 settings.xml 中配置如上 mirrors,远程的依赖就会从阿里云镜像中下载。

高级镜像配置

为了满足一些复杂的需求,Maven 还支持更高级的镜像配置:

  1. <mirrorOf>*</mirrorOf>:匹配所有远程仓库。
  2. <mirrorOf>external:*</mirrorOf>:匹配所有远程仓库,使用 localhost 的除外,使用 file:// 协议的除外。也就是说,匹配所有不在本机上的远程仓库。
  3. <mirrorOf>repo1,repo2</mirrorOf>:匹配仓库 repo1 和 repo2,使用逗号分隔多个远程仓库。
  4. <mirrorOf>*,!repo1</miiroOf>:匹配所有远程仓库,repo1 除外,使用感叹号将仓库从匹配中排除。
案例

个人的 Maven 配置了阿里的镜像,而项目中需要使用到一些第三方 jar 包,为了方便引入,已上传到192.168.0.201 的 nexus 私服下。但由于个人 Maven 阿里的镜像配置为 <mirrorOf>*</mirrorOf>,所有的仓库都被镜像,不会再去 192.168.0.201 下下载第三方 jar 包。

上传的第三方 jar 包目标路径:
http://192.168.0.201:8081/nexus/content/groups/public/com/alipay/sdk-java/20170615110434/sdk-java-20170615110434.pom
被镜像后路径:
http://maven.aliyun.com/nexus...

所以需要修改镜像的 mirrorOf 规则,避免默认从镜像中下载。

Maven的 conf/settings.xml

<mirrors>
  <!--国内阿里云提供的镜像,非常不错-->
  <mirror>
    <!--This sends everything else to /public -->
    <id>aliyun_nexus</id>
    <!--对所有仓库使用该镜像,除了一个名为maven_nexus_201的仓库除外-->
    <!--这个名为maven_nexus_201的仓库可以在javamaven项目中配置一个repository-->
    <mirrorOf>*,!maven_nexus_201</mirrorOf> 
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
  </mirror>
</mirrors>

Maven 项目下的 pom.xml 配置一个远程仓库

<repositories>
  <!-- 192.168.0.201远程仓库 -->
  <repository>
    <id>maven_nexus_201</id>
    <name>maven_nexus_201</name>
    <layout>default</layout>
    <url>http://192.168.0.201:8081/nexus/content/groups/public/</url>
    <snapshots>  
      <enabled>true</enabled>  
    </snapshots>
  </repository>
</repositories>

pom 文件详解

顶级元素概览

<project xmlns="http://maven.apache.org/POM/4.0.0" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd ">
  <parent />
  <!-- 声明项目描述符遵循哪一个 POM 模型版本。模型本身的版本很少改变,虽然如此,但它仍然是必不可少的,
         这是为了当 Maven 引入了新的特性或者其他模型变更的时候,确保稳定性。 -->
  <modelVersion />
  <groupId />
  <artifactId />
  <version />
  <packaging />
  <name />
  <url />
  <description />
  <prerequisites />
  <issueManagement />
  <ciManagement />
  <inceptionYear />
  <mailingLists />
  <developers />
  <contributors />
  <licenses />
  <scm />
  <organization />
  <build />
  <profiles />
  <modules />
  <repositories />
  <pluginRepositories />
  <dependencies />
  <reports />
  <reporting />
  <dependencyManagement />
  <distributionManagement />
  <properties />
</project>  

parent

<!-- 父项目的坐标。如果项目中没有规定某个元素的值,那么父项目中的对应值即为项目的默认值。
         坐标包括group ID,artifact ID和 version。 --> 
<parent> 
  <!-- 被继承的父项目的构件标识符 --> 
  <artifactId>xxx</artifactId>

  <!-- 被继承的父项目的全球唯一标识符 -->
  <groupId>xxx</groupId> 

  <!-- 被继承的父项目的版本 --> 
  <version>xxx</version>

  <!-- 父项目的pom.xml文件的相对路径。相对路径允许你选择一个不同的路径。默认值是../pom.xml。
             Maven首先在构建当前项目的地方寻找父项目的pom,其次在文件系统的这个位置(relativePath位置),
             然后在本地仓库,最后在远程仓库寻找父项目的pom。 --> 
  <relativePath>xxx</relativePath> 
</parent>

groudId、artifactId、version、packaging

<!-- 项目的全球唯一标识符,通常使用全限定的包名区分该项目和其他项目。并且构建时生成的路径也是由此生成, 
         如com.mycompany.app生成的相对路径为:/com/mycompany/app --> 
<groupId>xxx</groupId> 

<!-- 构件的标识符,它和group ID一起唯一标识一个构件。换句话说,你不能有两个不同的项目拥有同样的artifact ID
和groupID;在某个特定的group ID下,artifact ID也必须是唯一的。构件是项目产生的或使用的一个东西,Maven
为项目产生的构件包括:JARs,源码,二进制发布和WARs等。 --> 
<artifactId>xxx</artifactId> 

<!-- 项目当前版本,格式为:主版本.次版本.增量版本-限定版本号 --> 
<version>1.0-SNAPSHOT</version>

<!-- 项目产生的构件类型,例如jar、war、ear、pom。插件可以创建他们自己的构件类型,所以前面列的不是全部构件类型 --> 
<packaging>jar</packaging> 

<packaging> 默认构件类型为 jar,当作为父级 Maven 项目的时候,构件类型为 pom。

modules

<!-- 模块(有时称作子项目) 被构建成项目的一部分。列出的每个模块元素是指向该模块的目录的相对路径 --> 
<modules>
  <!--子项目相对路径-->
  <module></module>
</modules>

通过 <modules> Maven 项目之间可以形成父子关系,可以有多个层级。

repositories

<!-- 发现依赖和扩展的远程仓库列表。 --> 
<repositories> 
  <!-- 包含需要连接到远程仓库的信息 --> 
  <repository> 
    <!-- 如何处理远程仓库里发布版本的下载 --> 
    <releases> 
      <!-- true 或者 false 表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 --> 
      <enabled><enabled> 

      <!-- 该元素指定更新发生的频率。Maven 会比较本地 POM 和远程 POM 的时间戳。
             这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 --> 
      <updatePolicy></updatePolicy> 

        <!-- 当Maven验证构件校验文件失败时该怎么做:ignore(忽略),fail(失败),或者warn(警告)。 --> 
      <checksumPolicy></checksumPolicy> 
    </releases> 

    <!-- 如何处理远程仓库里快照版本的下载。有了 releases 和 snapshots 这两组配置,POM 就可以在每个单独的仓库中,
         为每种类型的构件采取不同的策略。例如,可能有人会决定只为开发目的开启对快照版本下载的支持。 --> 
    <snapshots> 
      <enabled><enabled>
      <updatePolicy></updatePolicy>
      <checksumPolicy></checksumPolicy> 
    </snapshots> 

    <!-- 远程仓库唯一标识符。可以用来匹配在 settings.xml 文件里配置的远程仓库 --> 
    <id>banseon-repository-proxy</id> 

    <!-- 远程仓库名称 --> 
    <name>banseon-repository-proxy</name> 

    <!-- 远程仓库 URL,按 protocol://hostname/path 形式 --> 
    <url>http://192.168.1.169:9999/repository/</url> 

    <!-- 用于定位和排序构件的仓库布局类型-可以是 default(默认)或者legacy(遗留)。Maven 2为其仓库提供了一个默认
         的布局;然而,Maven 1.x 有一种不同的布局。我们可以使用该元素指定布局是 default(默认)还是 legacy(遗留)。 --> 
    <layout> default </layout> 
  </repository> 
</repositories>

<id> 可以配合 settings.xml 中的远程仓库配置进行使用,见上一节中的案例

properties

<!-- 以值替代名称,Properties 可以在整个 POM 中使用,也可以作为触发条件。格式是<name>value</name>。 --> 
<properties>
  <name>value</name>
</properties>

dependencies

<!-- 该元素描述了项目相关的所有依赖。 这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。 --> 
<dependencies> 
  <dependency> 
    <!-- 依赖的group ID --> 
    <groupId>org.apache.maven</groupId> 

    <!-- 依赖的artifact ID --> 
    <artifactId>maven-artifact</artifactId> 

    <!-- 依赖的版本号。 在 Maven 2 里,也可以配置成版本号的范围。 --> 
    <version>3.8.1</version> 

    <!-- 依赖类型,默认类型是jar。它通常表示依赖的文件的扩展名,但也有例外。一个类型可以被映射成另外一个扩展
                 名或分类器。类型经常和使用的打包方式对应,尽管这也有例外。一些类型的例子:jar,war,ejb-client和test-jar。
                 如果设置extensions为 true,就可以在plugin里定义新的类型。所以前面的类型的例子不完整。 --> 
    <type>jar</type> 

    <!-- 依赖的分类器。分类器可以区分属于同一个POM,但不同构建方式的构件。分类器名被附加到文件名的版本号后面。例如,
                 如果你想要构建两个单独的构件成JAR,一个使用Java 1.4编译器,另一个使用Java 6编译器,你就可以使用分类器来生
                 成两个单独的JAR构件。 --> 
    <classifier></classifier> 

    <!-- 依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来。欲知详情请参考依赖机制。 
                - compile :默认范围,用于编译 
                - provided:类似于编译,但支持你期待jdk或者容器提供,类似于classpath 
                - runtime: 在执行时需要使用 
                - test: 用于test任务时使用 
                - system: 需要外在提供相应的元素。通过systemPath来取得 
                - systemPath: 仅用于范围为system。提供相应的路径 
                - optional: 当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用 --> 
    <scope>test</scope> 

    <!-- 仅供system范围使用。注意,不鼓励使用这个元素,并且在新的版本中该元素可能被覆盖掉。该元素为依赖规定了文件
                 系统上的路径。需要绝对路径而不是相对路径。推荐使用属性匹配绝对路径,例如${java.home}。 --> 
    <systemPath></systemPath> 

    <!-- 当计算传递依赖时,从依赖构件列表里,列出被排除的依赖构件集。即告诉 Maven 你只依赖指定的项目,不依赖项目的
                 依赖。此元素主要用于解决版本冲突问题 --> 
    <exclusions> 
      <exclusion> 
        <artifactId>spring-core</artifactId> 
        <groupId>org.springframework</groupId> 
      </exclusion> 
    </exclusions> 

    <!-- 可选依赖,如果你在项目 B 中把 C 依赖声明为可选,你就需要在依赖于 B 的项目(例如项目 A)中显式的引用对 C 的依赖。
                 可选依赖阻断依赖的传递性。 --> 
    <optional>true</optional> 
  </dependency> 
</dependencies>

dependencyManagement

  1. 只能出现在父 pom 里
  2. 用于统一版本号
  3. 只是依赖声明,并不直接依赖,需要时在子项目中在声明要使用依赖的 GA 信息,V 信息可以省略。
<!-- 继承自该项目的所有子项目的默认依赖信息。这部分的依赖信息不会被立即解析,而是当子项目声明一个依赖
     (必须描述 groupId 和 artifactId 信息),如果 group ID 和 artifact ID 以外的一些信息没有
     描述,则通过 group ID 和 artifact ID 匹配到这里的依赖,并使用这里的依赖信息。 --> 
<dependencyManagement> 
  <dependencies> 
    <!-- 参见dependencies/dependency元素 --> 
    <dependency> 
    </dependency> 
  </dependencies> 
</dependencyManagement>

build

<build> 
  <!-- 该元素设置了项目源码目录,当构建项目的时候,构建系统会编译目录里的源码。
       该路径是相对于 pom.xml 的相对路径。 --> 
  <sourceDirectory></sourceDirectory> 

  <!-- 该元素设置了项目单元测试使用的源码目录,当测试项目的时候,构建系统会编译目录里的源码。
       该路径是相对于 pom.xml 的相对路径。 --> 
  <testSourceDirectory></testSourceDirectory> 

  <!-- 被编译过的应用程序 class 文件存放的目录。 --> 
  <outputDirectory></outputDirectory> 

  <!-- 被编译过的测试 class 文件存放的目录。 --> 
  <testOutputDirectory></testOutputDirectory> 

  <!-- 这个元素描述了项目相关的所有资源路径列表,例如和项目相关的属性文件,
       这些资源被包含在最终的打包文件里。 --> 
  <resources> 
    <!-- 这个元素描述了项目相关或测试相关的所有资源路径 --> 
    <resource> 

      <!-- 是否使用参数值代替参数名。参数值取自 properties 元素或者文件里配置的属性,文件在 filters 元素里列出。 --> 
      <filtering></filtering>

      <!-- 描述存放资源的目录,该路径相对 pom.xml 路径 --> 
      <directory></directory>

      <!-- 包含的模式列表,例如**/*.xml. --> 
      <includes>
        <include></include>
      </includes>

      <!-- 排除的模式列表,例如**/*.xml -->
      <excludes>
        <exclude></exclude>
      </excludes>
    </resource> 
  </resources> 

  <!-- 子项目可以引用的默认插件信息。该插件配置项直到被引用时才会被解析或绑定到生命周期。
       给定插件的任何本地配置都会覆盖这里的配置 --> 
  <pluginManagement> 
    <!-- 参见 dependencies 元素 -->
    <plugins>  
    </plugins> 
  </pluginManagement> 

  <!-- 该项目使用的插件列表 。 --> 
  <plugins> 
    <!-- plugin 元素包含描述插件所需要的信息。 --> 
    <plugin> 
      <!-- 插件在仓库里的 group ID --> 
      <groupId></groupId> 

      <!-- 插件在仓库里的 artifact ID --> 
      <artifactId></artifactId> 

      <!-- 被使用的插件的版本(或版本范围) --> 
      <version></version> 

      <!-- 在构建生命周期中执行一组目标的配置。每个目标可能有不同的配置。 --> 
      <executions> 
        <!-- execution元素包含了插件执行需要的信息 --> 
        <execution> 
          <!-- 执行目标的标识符,用于标识构建过程中的目标,或者匹配继承过程中需要合并的执行目标 --> 
          <id></id>

          <!-- 绑定了目标的构建生命周期阶段,如果省略,目标会被绑定到源数据里配置的默认阶段 --> 
          <phase></phase>

          <!-- 配置的执行目标 --> 
          <goals></goals> 

          <!-- 作为DOM对象的配置 --> 
          <configuration></configuration>
        </execution> 
      </executions> 

      <!-- 项目引入插件所需要的额外依赖 --> 
      <dependencies>
        <!-- 参见dependencies/dependency元素 --> 
        <dependency> 
        </dependency> 
      </dependencies> 

      <!-- 作为 DOM 对象的配置 --> 
      <configuration></configuration> 
    </plugin> 
  </plugins>
</build> 

使用 Nexus 搭建 Maven 私服

Nexus 是一个强大的 Maven 仓库管理器,它极大的简化了本地内部仓库的维护和外部仓库的访问。

Docker 搭建 Nexus

我们采用 Docker 方式来安装:

1. 拉取 nexus3 镜像

# 拉取 nexus3 镜像
docker pull sonatype/nexus3:3.16.0

2. 启动 nexus3 容器

# 通过镜像启动容器
docker run -d --name nexus -p 8081:8081 -v /Users/linfuyan/Develop/nexus-data:/nexus-data sonatype/nexus3:3.16.0
-d:后台模式运行容器

--name:容器命名为 nexus

-p:映射本地 8081 端口到容器内的 8081 端口

-v:将容器内的 /nexus-data 目录挂载到本地目录

3. 浏览器中访问 localhost:8081 就可以看到 Nexus 页面了。

注意

3.6.0 版本的 sonatype/nexus3 无法查看到 upload 页面。

3.27.0 版本的 sonatype/nexus3 在我电脑上跑不起来,而且初始密码存在 admin.password 文件中。

最终选择了 3.16.0 版本。

Nexus 基础使用

通过右上角的 sign in 按钮,输入默认账号密码 admin/admin123 可以对仓库等进行管理。

创建远程仓库

可选 Maven2 group、hosted、proxy 类型。

hosted:本地仓库,通常我们会部署自己的构件到这一类型的仓库。比如公司的第二方库。

proxy:代理仓库,它们被用来代理远程的公共仓库,如 Maven 中央仓库。

group:仓库组,用来合并多个hosted/proxy仓库,当你的项目希望在多个 repository 使用资源时就不需要多次引用了,只需要引用一个 group 即可。

新建 hosted 类型仓库

把 craft4j 添加到 maven-public 中。

创建具有上传权限的角色

创建具有上传权限的角色的用户

上传 jar 组件到 Nexus

通过网页手动上传第三方 jar 到 Nexus

上传成功以后,就可以在对应的远程仓库中查看到上传完成的组件信息。

通过 mvn deploy:deploy-file 上传到 Nexus

➜  ron-jwt git:(master) ✗ mvn deploy:deploy-file -DgroupId=io.craft4j -DartifactId=checkstyle -Dversion=1.0.1-SNAPSHOT -Dpackaging=jar -Dfile=/Users/linfuyan/Code/java-lab/airplan-java-server/codestyle/checkstyle-7.0-all.jar -DrepositoryId=craft4j -Durl=http://127.0.0.1:8081/repository/craft4j/
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] --- maven-deploy-plugin:2.7:deploy-file (default-cli) @ standalone-pom ---
Downloading from remote-repository: http://127.0.0.1:8081/repository/craft4j/io/craft4j/checkstyle/1.0.1-SNAPSHOT/maven-metadata.xml
Downloaded from remote-repository: http://127.0.0.1:8081/repository/craft4j/io/craft4j/checkstyle/1.0.1-SNAPSHOT/maven-metadata.xml (770 B at 3.7 kB/s)
Uploading to remote-repository: http://127.0.0.1:8081/repository/craft4j/io/craft4j/checkstyle/1.0.1-SNAPSHOT/checkstyle-1.0.1-20200910.081245-2.jar
Uploading to remote-repository: http://127.0.0.1:8081/repository/craft4j/io/craft4j/checkstyle/1.0.1-SNAPSHOT/checkstyle-1.0.1-20200910.081245-2.pom

repositoryId 依赖 settings.xml 中的 server 配置,主要是用户名与密码。

通过 mvn deploy 方式上传到 Nexus

在上面的步骤中,已经将新建的 craft4j 仓库添加到 maven-public 仓库组中。

在 settings.xml 中修改配置:

<servers>
  <server>
    <id>nexus-releases</id>
    <username>dev</username>
    <password>dev123</password>
  </server>
  <server>
    <id>craft4j</id>
    <username>dev</username>
    <password>dev123</password>
  </server>
</servers>

在项目 pom.xml 中修改配置:

<distributionManagement>
  <repository>
    <id>maven-releases</id>
    <name>Nexus Release Repository</name>
    <url>http://127.0.0.1:8081/repository/maven-releases/</url>
  </repository>
  <snapshotRepository>
    <id>craft4j</id>
    <name>Nexus Snapshot Repository</name>
    <url>http://127.0.0.1:8081/repository/craft4j/</url>
  </snapshotRepository>
</distributionManagement>

pom.xml 中的 repository id 与 settings.xml 中的 server id 相对应,需要保持一致。

在需要发布的项目中执行 mvn deploy,看到如下日志,就说明发布成功了,同样可以在 Nexus 的仓库浏览页中查看。

➜  ron-jwt git:(master) ✗ mvn clean deploy
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------------< io.ron:ron-jwt >---------------------------
[INFO] Building ron-jwt 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] ...
[INFO] --- maven-deploy-plugin:2.7:deploy (default-deploy) @ ron-jwt ---
Downloading from craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/maven-metadata.xml
Uploading to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/ron-jwt-1.0-20200910.071640-1.jar
Uploaded to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/ron-jwt-1.0-20200910.071640-1.jar (15 kB at 21 kB/s)
Uploading to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/ron-jwt-1.0-20200910.071640-1.pom
Uploaded to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/ron-jwt-1.0-20200910.071640-1.pom (1.3 kB at 2.6 kB/s)
Downloading from craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/maven-metadata.xml
Uploading to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/maven-metadata.xml
Uploaded to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/1.0-SNAPSHOT/maven-metadata.xml (757 B at 2.0 kB/s)
Uploading to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/maven-metadata.xml
Uploaded to craft4j: http://127.0.0.1:8081/repository/craft4j/io/ron/ron-jwt/maven-metadata.xml (271 B at 1.0 kB/s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.243 s
[INFO] Finished at: 2020-09-10T15:16:42+08:00
[INFO] ------------------------------------------------------------------------

Maven 构件的版本管理

使用 Maven 作为依赖管理工具,一般我们对于依赖的版本号,常见两种类型:一种以“-RELEASE”结尾,另一种以“-SNAPSHOT”结尾。

私服中,会存在 snapshot 快照仓库和 release 发布仓库,snapshot 快照仓库用于保存开发过程中的不稳定版本,release 正式仓库则是用来保存稳定的发行版本。

Maven 会根据模块的版本号(pom 文件中的 version)中是否带有“-SNAPSHOT”(注意这里必须是全部大写)来判断是快照版本还是正式版本。如果是快照版本,那么在 mvn deploy 时会自动发布到私服的快照版本库中;如果是正式发布版本,那么在 mvn deploy 时会自动发布到正式版本库中。

快照版本的依赖,Maven 编译打包的时候无论本地是否存在,都会去私服拉取最新的,而正式版本的依赖,如果本地仓库已经存在,Maven 不会去私服拉取最新的版本,所以我们要基于快照版本进行开发,但是上线的时候一定记得变成正式版,否则如果本地正在进行开发另一个功能,提交到私服的代码有可能会被误上线。

那我们有什么好的方法来避免这种情况呢?

1. 在 settings.xml 中修改私服配置,通过 updatePolicy 为 always 强制更新。

<profile>
    <id>nexus</id>
    <repositories>
        <repository>
            <id>nexus</id>
            <name>Nexus</name>
            <url>http://127.0.0.1:8081/repository/groups/maven-public/</url>
            <releases>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </releases>
            <snapshots>
                <enabled>true</enabled>
                <updatePolicy>always</updatePolicy>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
        </pluginRepository>
    </pluginRepositories>
</profile>

2. 在构建的时候加上“-U”参数,强制拉取所有依赖的最新代码

mvn clean install -U

3. 语义化版本

首先,我们在团队协作时,要定义好开发中的依赖一定不要忘记升级版本号,然后开发的过程中还要保持版本号以“-SNAPSHOT”结尾,来把该依赖作为快照版本进行开发,这样每次别人更新完上传到私服以后,你本地打包时会自动拉取最新代码,从而方便我们的开发和维护。

参考与致谢

带你深度解析Maven

史上最全的maven的pom.xml文件详解

setting.xml 配置详解

上传jar包到nexus私服

Maven私服:Docker安装nexus3

Maven版本号中隐藏的惊天大秘密

Maven全局配置文件settings.xml详解


如果你看完本文有收获,欢迎关注微信公众号:精进Java(ID: craft4j),更多 Java 后端与架构的干货等你一起学习与交流。

查看原文

赞 11 收藏 9 评论 0

fatfoo 发布了文章 · 2020-09-16

自定义 Starter 后,一行代码就可以实现 Jwt 登录认证,爽呆了

在上一篇《Spring Boot 集成 JWT 实现用户登录认证》中,我们已经基于 nimbus-jose-jwt 封装好了一个 jwt,并且只需要自定义 HandlerInterceptor 实现类和 WebMvcConfigurer 实现类就可以在项目中引入使用 jwt 做登录认证。

Spring Boot 中的 starter 是一种非常重要的机制,能够抛弃以前繁杂的配置,将其统一集成进 starter,应用只需要在 Maven 中引入 starter 依赖,Spring Boot 就能自动扫描到要加载的信息并启动相应的默认配置。starter 让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。Spring Boot 会自动通过 classpath 路径下的类发现需要的 Bean,并注册进 IOC 容器。Spring Boot 提供了针对日常企业应用研发各种场景的 spring-boot-starter 依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

接下来,我们就在之前封装的 ron-jwt 基础上自定义的一个 starter,项目只要在依赖中引入这个 starter,再定义简单的配置就可以使用 jwt 的实现登录认证。

新建工程并配置依赖

Spring Boot 提供的 starter 以 spring-boot-starter-xxx 的方式命名的。官方建议自定义的 starter 使用 xxx-spring-boot-starter 命名规则,以区分 Spring Boot 生态提供的 starter。所以我们新建工程 ron-jwt-spring-boot-starter。

在 pom.xml 中引入依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>io.ron</groupId>
  <artifactId>ron-jwt</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

spring-boot-configuration-processor 主要的作用是在编译时在 META-INF 下生成 spring-configuration-metadata.json 文件,该文件主要为IDE 使用,即可以通过在 application.properties 文件中通过 ctrl + 点击进入配置属性所在的类中。

spring-boot-autoconfigure 主要作用是提供自动装配功能。

spring-boot-starter-web 则是因为我们将会内置 HandlerInterceptor 实现类和 WebMvcConfigurer 实现类。

ron-jwt 是我们在上一篇中封装好的 jwt 库。

定义配置项管理类

我们定义 JwtProperties 来声明 starter 的使用者可使用哪些配置项。

@ConfigurationProperties(prefix = "ron.jwt")
public class JwtProperties {

    private String tokenName = JwtUtils.DEFAULT_TOKEN_NAME;

    private String hmacKey;

    private String jksFileName;

    private String jksPassword;

    private String certPassword;

    // 签发人
    private String issuer;

    // 主题
    private String subject;

    // 受众
    private String audience;

    private long notBeforeIn;

    private long notBeforeAt;

    private long expiredIn;

    private long expiredAt;
}

具体参数说明,可参考上一篇文章中的 JwtConfig。

@ConfigurationProperties 注解指定了所有配置项的前缀为 ron.jwt

@ConfigurationProperties 的基本用法非常简单:我们为每个要捕获的外部属性提供一个带有字段的类。请注意以下几点:

  • 前缀定义了哪些外部属性将绑定到类的字段上。
  • 根据 Spring Boot 宽松的绑定规则,类的属性名称必须与外部属性的名称匹配。
  • 我们可以简单地用一个值初始化一个字段来定义一个默认值。
  • 类本身可以是包私有的。
  • 类的字段必须有公共 setter 方法。

Spring Boot 宽松的绑定规则(relaxed binding):

Spring Boot 使用一些宽松的绑定属性规则。因此,以下变体都将绑定到 tokenName 属性上:

  • ron.jwt.tokenname=Authorization
  • ron.jwt.tokenName=Authorization
  • ron.jwt.token_name=Authorization
  • ron.jwt.token-name=Authorization

实现相关的功能

在上一篇中,我们把 HandlerInterceptor 实现类和 WebMvcConfigurer 实现类放在具体的业务项目中自己实现。实际上这部分也是项目中比较通用的逻辑,因此我们考虑将这些实现放置在 starter 中。项目中可以不做多余的自定义,直接通过引入 starter 就可以使用 jwt 认证的功能。

JwtInterceptor

public class JwtInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(JwtInterceptor.class);

    private static final String PREFIX_BEARER = "Bearer ";

    @Autowired
    private JwtProperties jwtProperties;

    @Autowired
    private JwtService jwtService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {

        // 如果不是映射到方法直接通过
        if(!(handler instanceof HandlerMethod)){
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 检查是否有 @AuthRequired 注解,有且 required() 为 false 则跳过
        if (method.isAnnotationPresent(AuthRequired.class)) {
            AuthRequired authRequired = method.getAnnotation(AuthRequired.class);
            if (!authRequired.required()) {
                return true;
            }
        }

        String token = request.getHeader(jwtProperties.getTokenName());

        logger.info("token: {}", token);

        if (StringUtils.isEmpty(token) || token.trim().equals(PREFIX_BEARER.trim())) {
            return true;
        }

        token = token.replace(PREFIX_BEARER, "");

        // 设置线程局部变量中的 token
        JwtContext.setToken(token);

        // 在线程局部变量中设置真实传递的数据,如当前用户信息等
        String payload = jwtService.verify(token);
        JwtContext.setPayload(payload);

        return onPreHandleEnd(request, response, handler, payload);
    }

    public boolean onPreHandleEnd(HttpServletRequest request, HttpServletResponse response,
                                  Object handler, String payload) throws Exception {
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        // 务必在线程结束前清理线程局部变量
        JwtContext.removeAll();
    }
}

onPreHandleEnd 方法是一个默认实现,如业务上有必要,也可以继承 JwtInterceptor,在这个方法中添加自定义的逻辑。一个可能的场景是将 JWT 的 token 放到 Redis 中进行超时管理。

JwtInterceptorConfig

public class JwtInterceptorConfig implements WebMvcConfigurer {

    private JwtInterceptor jwtInterceptor;

    public JwtInterceptorConfig(JwtInterceptor jwtInterceptor) {
        this.jwtInterceptor = jwtInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor).addPathPatterns("/**");
    }
}

这里默认拦截了所有的请求,在上一篇文章中,我们提到可以配合 @AuthRequired 来过滤不需要拦截的请求。

编写自动配置逻辑

JwtAutoConfiguration

@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class JwtAutoConfiguration {

    @Autowired
    private JwtProperties jwtProperties;

    @Bean
    public JwtConfig jwtConfig() {
        JwtConfig jwtConfig = new JwtConfig();
        BeanUtils.copyProperties(jwtProperties, jwtConfig);
        return jwtConfig;
    }

    @Bean
    public JwtService jwtService() {
        JwtConfig jwtConfig = jwtConfig();
        return JwtUtils.obtainJwtService(jwtConfig);
    }

    @Bean
    public JwtInterceptor jwtInterceptor() {
        return new JwtInterceptor();
    }

    @Bean
    public JwtInterceptorConfig jwtInterceptorConfig() {
        return new JwtInterceptorConfig(jwtInterceptor());
    }
}

@EnableConfigurationProperties 的作用是引入使用 @ConfigurationProperties 注解的类,并使其生效。

@EnableConfigurationProperties 文档中解释:当 @EnableConfigurationProperties 注解应用到你的@Configuration 时, 任何被 @ConfigurationProperties 注解的 beans 将自动被 Environment 属性配置。 这种风格的配置特别适合与 SpringApplication 的外部 YAML 配置进行配合使用。

集成 starter 使之生效

有两种方式可以让 starter 在应用中生效。

通过 SPI 机制加载 - 被动生效

通过 Spring Boot 的 SPI 的机制来去加载我们的starter。

在 resources 目录下新建 WEB-INF/spring.factories 文件。

META-INF/spring.factories 文件是 Spring Boot 框架识别并解析 starter 的核心文件。spring.factories 文件是帮助 Spring Boot 项目包以外的 Bean(即在 pom 文件中添加依赖中的 Bean)注册到 Spring Boot 项目的 Spring 容器。由于 @ComponentScan 注解只能扫描Spring Boot 项目包内的 Bean 并注册到 Spring 容器中,因此需要 @EnableAutoConfiguration 注解来注册项目包外的 Bean。而 spring.factories 文件,则是用来记录项目包外需要注册的 Bean 类名。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.ron.jwt.starter.JwtAutoConfiguration

自定义 Enable 注解引入 - 主动生效

在 starter 组件集成到我们的 Spring Boot 应用时需要主动声明启用该 starter 才生效,我们通过自定义一个 @Enable 注解然后在把自动配置类通过 Import 注解引入进来。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({JwtAutoConfiguration.class})
@Documented
@Inherited
public @interface EnableJwt {

}

如果使用主动生效的方式,那么实现被动生效的 META-INF/spring.factories 文件需要移除。

打包并发布 starter

使用 mvn install 可以打包并安装在本地;

使用 mvn deploy 可以发布到远程仓库。

后续我会写一篇关于 Maven 的文章,结合自己入行多年的经验,将开发人员会用到 Maven 技巧进行梳理。

欢迎关注我的公众号:精进Java(ID:craft4j)。

测试应用程序

可以复用上一篇中的项目,在 pom.xml 中修改依赖,引入 ron-jwt-spring-boot-starter。

<dependency>
  <groupId>io.ron</groupId>
  <artifactId>ron-jwt-spring-boot-starter</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

去掉 HandlerInterceptor 实现类和 WebMvcConfigurer 实现类。

在 application.yml 中配置 ron.jwt.hmac-key 的值,就可以提供 HMAC 算法实现的 JWT 签名与验证功能了。

如果 starter 采用的被动生效方式,现在就可以运行程序,然后使用 Postman 进行测试并观察结果。

如果 starter 采用的主动生效方式,需要在项目启动类上添加 @EnableJwt 注解将 jwt-starter 引入。

@SpringBootApplication
public class JwtStarterApplication {

    public static void main(String[] args) {
        SpringApplication.run(JwtStarterApplication.class, args);
    }
}

- End -


通过两篇文章,我们展示了基于 nimbus-jose-jwt 的 jwt 库使用,在此基础上封装了我们自己的基础 jwt 库,并介绍了 Spring Boot 自定义 starter 的步骤,以及自定义 @Enable 注解的实现。

如果你觉得有收获,请关注我的公众号:精进Java(ID:craft4j),第一时间获取 Java 后端与架构的知识动态。

如果你对项目的完整源码感兴趣,可以在公众号中回复 jwt 来获取。

查看原文

赞 5 收藏 2 评论 0

fatfoo 发布了文章 · 2020-09-14

Spring Boot 集成 JWT 实现用户登录认证

JWT 简介

什么是 JWT

JWT 是 JSON Web Token 的缩写,是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519)。定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT 可以使用 HMAC 算法或者是 RSA 的公私秘钥对进行签名。

JWT请求流程

JWT 请求流程

  1. 用户使用账号和密码发起 POST 请求;
  2. 服务器使用私钥创建一个 JWT;
  3. 服务器返回这个 JWT 给浏览器;
  4. 浏览器将该 JWT 串在请求头中像服务器发送请求;
  5. 服务器验证该 JWT;
  6. 返回响应的资源给浏览器。

JWT 的主要应用场景

身份认证在这种场景下,一旦用户完成了登录,在接下来的每个请求中包含 JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用 JWT 对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

JWT 数据结构

JWT 是由三段信息构成的,将这三段信息文本用 . 连接一起就构成了 JWT 字符串。

JWT 的三个部分依次为头部:Header,负载:Payload 和签名:Signature。

JWT 数据结构

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",
  "typ": "JWT"
}

上面代码中,alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT

最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。

Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的有效信息。有效信息包含三个部分:

  1. 标准中注册的声明
  2. 公共的声明
  3. 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss (issuer):签发人
  • exp (expiration time):过期时间,必须要大于签发时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号,JWT 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。

公共的声明 :

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明 :

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解码的,意味着该部分信息可以归类为明文信息。

这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符 +/=,在 URL 里面有特殊含义,所以要被替换掉:= 被省略、+ 替换成 -/ 替换成 _ 。这就是 Base64URL 算法。

JWT 的使用方式

客户端收到服务器返回的 JWT 之后需要在本地做保存。此后,客户端每次与服务器通信,都要带上这个 JWT。一般的的做法是放在 HTTP 请求的头信息 Authorization 字段里面。

Authorization: Bearer <token>

这样每个请求中,服务端就可以在请求头中拿到 JWT 进行解析与认证。

JWT 的特性

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

基于 nimbus-jose-jwt 简单封装

nimbus-jose-jwt 是最受欢迎的 JWT 开源库,基于Apache 2.0开源协议,支持所有标准的签名(JWS)和加密(JWE)算法。nimbus-jose-jwt 支持使用对称加密(HMAC)和非对称加密(RSA)两种算法来生成和解析 JWT 令牌。

下面我们对 nimbus-jose-jwt 进行简单的封装,提供以下功能的支持:

  1. 支持使用 HMAC 和 RSA 算法生成和解析 JWT 令牌
  2. 支持私有信息直接作为 Payload,以及标准信息+私有信息作为 Payload。内置支持后者。
  3. 提供工具类及可扩展接口,方便自定义扩展开发。

pom 中添加依赖

首先我们在 pom.xml 中引入 nimbus-jose-jwt 的依赖。

<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
  <version>8.20</version>
</dependency>

JwtConfig

这个类用于统一管理相关的参数配置。

public class JwtConfig {

    // JWT 在 HTTP HEADER 中默认的 KEY
    private String tokenName = JwtUtils.DEFAULT_TOKEN_NAME;

    // HMAC 密钥,用于支持 HMAC 算法
    private String hmacKey;

    // JKS 密钥路径,用于支持 RSA 算法
    private String jksFileName;

    // JKS 密钥密码,用于支持 RSA 算法
    private String jksPassword;

    // 证书密码,用于支持 RSA 算法
    private String certPassword;

    // JWT 标准信息:签发人 - iss
    private String issuer;

    // JWT 标准信息:主题 - sub
    private String subject;

    // JWT 标准信息:受众 - aud
    private String audience;

    // JWT 标准信息:生效时间 - nbf,未来多长时间内生效
    private long notBeforeIn;
    
    // JWT 标准信息:生效时间 - nbf,具体哪个时间生效
    private long notBeforeAt;

    // JWT 标准信息:过期时间 - exp,未来多长时间内过期
    private long expiredIn;

    // JWT 标准信息:过期时间 - exp,具体哪个时间过期
    private long expiredAt;
}  

hmacKey 字段用于支持 HMAC 算法,只要该字段不为空,则使用该值作为 HMAC 的密钥对 JWT 进行签名与验证。

jksFileNamejksPasswordcertPassword 三个字段用于支持 RSA 算法,程序将读取证书文件作为 RSA 密钥对 JWT 进行签名与验证。

其他几个字段用于设置 Payload 中需要携带的标准信息。

JwtService

JwtService 是提供 JWT 签名与验证的接口,内置了 HMACJwtServiceImpl 提供 HMAC 算法的实现和 RSAJwtServiceImpl 提供 RSA 算法的实现。两种算法在获取密钥的方式上是有差别的,这里也提出来成了接口方法。后续如果要自定义实现,只需要再写一个具体实现类。

public interface JwtService {

    /**
     * 获取 key
     *
     * @return
     */
    Object genKey();

    /**
     * 对信息进行签名
     *
     * @param payload
     * @return
     */
    String sign(String payload);

    /**
     * 验证并返回信息
     *
     * @param token
     * @return
     */
    String verify(String token);
}
public class HMACJwtServiceImpl implements JwtService {

    private JwtConfig jwtConfig;

    public HMACJwtServiceImpl(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
    }

    @Override
    public String genKey() {
        String key = jwtConfig.getHmacKey();
        if (JwtUtils.isEmpty(key)) {
            throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, new NullPointerException("HMAC need a key"));
        }
        return key;
    }

    @Override
    public String sign(String info) {
        return JwtUtils.signClaimByHMAC(info, genKey(), jwtConfig);
    }

    @Override
    public String verify(String token) {
        return JwtUtils.verifyClaimByHMAC(token, genKey(), jwtConfig);
    }
}
public class RSAJwtServiceImpl implements JwtService {

    private JwtConfig jwtConfig;

    private RSAKey rsaKey;

    public RSAJwtServiceImpl(JwtConfig jwtConfig) {
        this.jwtConfig = jwtConfig;
    }

    private InputStream getCertInputStream() throws IOException {
        // 读取配置文件中的证书路径
        String jksFile = jwtConfig.getJksFileName();
        if (jksFile.contains("://")) {
            // 从本地文件读取
            return new FileInputStream(new File(jksFile));
        } else {
            // 从 classpath 读取
            return getClass().getClassLoader().getResourceAsStream(jwtConfig.getJksFileName());
        }
    }

    @Override
    public RSAKey genKey() {
        if (rsaKey != null) {
            return rsaKey;
        }
        InputStream is = null;
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            is = getCertInputStream();
            keyStore.load(is, jwtConfig.getJksPassword().toCharArray());
            Enumeration<String> aliases = keyStore.aliases();
            String alias = null;
            while (aliases.hasMoreElements()) {
                alias = aliases.nextElement();
            }
            RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(alias, jwtConfig.getCertPassword().toCharArray());
            Certificate certificate = keyStore.getCertificate(alias);
            RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey();
            rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
            return rsaKey;
        } catch (IOException | CertificateException | UnrecoverableKeyException
                | NoSuchAlgorithmException | KeyStoreException e) {
            e.printStackTrace();
            throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, e);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public String sign(String payload) {
        return JwtUtils.signClaimByRSA(payload, genKey(), jwtConfig);
    }

    @Override
    public String verify(String token) {
        return JwtUtils.verifyClaimByRSA(token, genKey(), jwtConfig);
    }
}

JwtUtils

JwtService 的实现类中比较简洁,因为主要的方法都在 JwtUtils 中提供了。如下是 Payload 中只包含私有信息时,两种算法的签名与验证实现。可以使用这些方法方便的实现自己的扩展。

   /**
     * 使用 HMAC 算法签名信息(Payload 中只包含私有信息)
     *
     * @param info
     * @param key
     * @return
     */
    public static String signDirectByHMAC(String info, String key) {
        try {
            JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
                    .type(JOSEObjectType.JWT)
                    .build();

            // 建立一个载荷 Payload
            Payload payload = new Payload(info);

            // 将头部和载荷结合在一起
            JWSObject jwsObject = new JWSObject(jwsHeader, payload);

            // 建立一个密匙
            JWSSigner jwsSigner = new MACSigner(key);

            // 签名
            jwsObject.sign(jwsSigner);

            // 生成 token
            return jwsObject.serialize();
        } catch (JOSEException e) {
            e.printStackTrace();
            throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
        }
    }

    /**
     * 使用 RSA 算法签名信息(Payload 中只包含私有信息)
     *
     * @param info
     * @param rsaKey
     * @return
     */
    public static String signDirectByRSA(String info, RSAKey rsaKey) {
        try {
            JWSSigner signer = new RSASSASigner(rsaKey);
            JWSObject jwsObject = new JWSObject(
                    new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
                    new Payload(info)
            );
            // 进行加密
            jwsObject.sign(signer);

            return jwsObject.serialize();
        } catch (JOSEException e) {
            e.printStackTrace();
            throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e);
        }
    }

    /**
     * 使用 HMAC 算法验证 token(Payload 中只包含私有信息)
     *
     * @param token
     * @param key
     * @return
     */
    public static String verifyDirectByHMAC(String token, String key) {
        try {
            JWSObject jwsObject = JWSObject.parse(token);
            // 建立一个解锁密匙
            JWSVerifier jwsVerifier = new MACVerifier(key);
            if (jwsObject.verify(jwsVerifier)) {
                return jwsObject.getPayload().toString();
            }
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
        } catch (JOSEException | ParseException e) {
            e.printStackTrace();
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
        }
    }

    /**
     * 使用 RSA 算法验证 token(Payload 中只包含私有信息)
     *
     * @param token
     * @param rsaKey
     * @return
     */
    public static String verifyDirectByRSA(String token, RSAKey rsaKey) {
        try {
            RSAKey publicRSAKey = rsaKey.toPublicJWK();
            JWSObject jwsObject = JWSObject.parse(token);
            JWSVerifier jwsVerifier = new RSASSAVerifier(publicRSAKey);
            // 验证数据
            if (jwsObject.verify(jwsVerifier)) {
                return jwsObject.getPayload().toString();
            }
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null"));
        } catch (JOSEException | ParseException e) {
            e.printStackTrace();
            throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e);
        }
    }

JwtException

定义统一的异常类,可以屏蔽 nimbus-jose-jwt 以及其他诸如加载证书错误抛出的异常,并且在其他项目集成我们封装好的库的时候,方便的进行异常处理。

在 JwtService 实现的不同阶段,我们封装了不同的 JwtException 子类,来方便外部根据需要做对应的处理。如异常是 KeyGenerateException,则处理成服务器处理错误;如异常是 TokenVerifyException,则处理成 Token 验证失败,无权限。

JwtContext

JWT 用于用户认证,经常在 Token 验证完成后,程序中需要获取到当前登录的用户信息, JwtContext 中提供了通过线程局部变量保存信息的方法。

public class JwtContext {

    private static final String KEY_TOKEN = "token";
    private static final String KEY_PAYLOAD = "payload";

    private static ThreadLocal<Map<Object, Object>> context = new ThreadLocal<>();

    private JwtContext() {}

    public static void set(Object key, Object value) {
        Map<Object, Object> locals = context.get();
        if (locals == null) {
            locals = new HashMap<>();
            context.set(locals);
        }
        locals.put(key, value);
    }

    public static Object get(Object key) {
        Map<Object, Object> locals = context.get();
        if (locals != null) {
            return locals.get(key);
        }
        return null;
    }

    public static void remove(Object key) {
        Map<Object, Object> locals = context.get();
        if (locals != null) {
            locals.remove(key);
            if (locals.isEmpty()) {
                context.remove();
            }
        }
    }

    public static void removeAll() {
        Map<Object, Object> locals = context.get();
        if (locals != null) {
            locals.clear();
        }
        context.remove();
    }

    public static void setToken(String token) {
        set(KEY_TOKEN, token);
    }

    public static String getToken() {
        return (String) get(KEY_TOKEN);
    }

    public static void setPayload(Object payload) {
        set(KEY_PAYLOAD, payload);
    }

    public static Object getPayload() {
        return get(KEY_PAYLOAD);
    }
}

@AuthRequired

在项目实战中,并不是所有 Controller 中的方法都必须传 Token,通过 @AuthRequired 注解来区分方法是否需要校验 Token。

/**
 * 应用于 Controller 中的方法,标识是否拦截进行 JWT 验证
 */
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AuthRequired {

    boolean required() default true;
}

Spring Boot 集成 JWT 实例

有了上面封装好的库,我们在 SpringBoot 项目中集成 JWT。创建好 Spring Boot 项目后,我们编写下面主要的类。

JwtDemoInterceptor

在 Spring Boot 项目中,通过自定义 HandlerInterceptor 的实现类可以对请求和响应进行拦截,我们新建 JwtDemoInterceptor 类进行拦截。

public class JwtDemoInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(JwtDemoInterceptor.class);

    private static final String PREFIX_BEARER = "Bearer ";

    @Autowired
    private JwtConfig jwtConfig;

    @Autowired
    private JwtService jwtService;

    /**
     * 预处理回调方法,实现处理器的预处理(如检查登陆),第三个参数为响应的处理器,自定义 Controller
     * 返回值:
     * true 表示继续流程(如调用下一个拦截器或处理器);
     * false 表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过 response 来产生响应。
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果不是映射到方法直接通过
        if(!(handler instanceof HandlerMethod)){
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 检查是否有 @AuthRequired 注解,有且 required() 为 false 则跳过
        if (method.isAnnotationPresent(AuthRequired.class)) {
            AuthRequired authRequired = method.getAnnotation(AuthRequired.class);
            if (!authRequired.required()) {
                return true;
            }
        }

        String token = request.getHeader(jwtConfig.getTokenName());

        logger.info("token: {}", token);

        if (StringUtils.isEmpty(token) || token.trim().equals(PREFIX_BEARER.trim())) {
            return true;
        }

        token = token.replace(PREFIX_BEARER, "");

        String payload = jwtService.verify(token);

        // 设置线程局部变量中的 token
        JwtContext.setToken(token);
        JwtContext.setPayload(payload);
        return true;
    }

    /**
     * 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过 modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView 也可能为null。
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    /**
     * 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于 try-catch-finally 中的 finally
     * 但仅调用处理器执行链中 preHandle 返回 true 的拦截器的 afterCompletion。
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        JwtContext.removeAll();
    }
}

preHandlepostHandleafterCompletion 三个方法的具体作用,可以看代码上的注释。

preHandle 中这段代码中的逻辑如下:

  1. 拦截被 @AuthRequired 注解的方法,只要不是 required = false 都会进行 Token 的校验。
  2. 从请求中解析出 Token,对 Token 进行验证。如果验证异常,会在方法中抛出异常。
  3. Token 验证通过,会在线程局部变量中设置相关信息,以便后续程序获取处理。

afterCompletion 中这段代码对线程变量进行了清理。

InterceptorConfig

定义 InterceptorConfig,通过 @Configuration 注解,Spring 会加载该类,并完成装配。

addInterceptors 方法中设置拦截器,并拦截所有请求。

jwtDemoConfig 方法中注入 JwtConfig,并设置了 HMACKey。

jwtDemoService 方法会根据注入的 JwtConfig 配置,生成具体的 JwtService,这里是 HMACJwtServiceImpl。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtDemoInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public JwtDemoInterceptor jwtDemoInterceptor() {
        return new JwtDemoInterceptor();
    }

    @Bean
    public JwtConfig jwtDemoConfig() {
        JwtConfig jwtConfig = new JwtConfig();
        jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");

        return jwtConfig;
    }

    @Bean
    public JwtService jwtDemoService() {
        return JwtUtils.obtainJwtService(jwtDemoConfig());
    }

}

编写测试 Controller

@RestController
public class UserController {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private JwtService jwtService;

    @GetMapping("/sign")
    @AuthRequired(required = false)
    public String sign() throws JsonProcessingException {

        UserDTO userDTO = new UserDTO();
        userDTO.setName("fatfoo");
        userDTO.setPassword("112233");
        userDTO.setSex(0);

        String payload = objectMapper.writeValueAsString(userDTO);

        return jwtService.sign(payload);
    }

    @GetMapping("/verify")
    public UserDTO verify() throws IOException {
        String payload = (String) JwtContext.getPayload();
        return objectMapper.readValue(payload, UserDTO.class);
    }
}

sign 方法对用户信息进行签名并返回 Token;由于 @AuthRequired(required = false) 拦截器将不会对其进行拦截。

verify 方法在 Token 通过验证后,获取解析出的信息并返回。

用 Postman 进行测试

访问 sign 接口,返回签名 Token。

在 Header 中添加 Token 信息,请求 verify 接口,返回用户信息。

测试 RSA 算法实现

上面我们只设置了 JwtConfig 的 hmacKey 参数,使用的是 HMAC 算法进行签名和验证。本节我们演示 RSA 算法进行签名和验证的实现。

生成签名文件

使用 Java 自带的 keytool 工具可以方便的生成证书文件。

➜  resources git:(master) ✗ keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
输入密钥库口令:
密钥库口令太短 - 至少必须为 6 个字符
输入密钥库口令: ronjwt
再次输入新口令: ronjwt
您的名字与姓氏是什么?
  [Unknown]:  ron
您的组织单位名称是什么?
  [Unknown]:  ron
您的组织名称是什么?
  [Unknown]:  ron
您所在的城市或区域名称是什么?
  [Unknown]:  Xiamen
您所在的省/市/自治区名称是什么?
  [Unknown]:  Fujian
该单位的双字母国家/地区代码是什么?
  [Unknown]:  CN
CN=ron, OU=ron, O=ron, L=Xiamen, ST=Fujian, C=CN是否正确?
  [否]:  是

输入 <jwt> 的密钥口令
    (如果和密钥库口令相同, 按回车):

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

文件生成后,复制到项目的 resource 目录下。

设置 JwtConfig 参数

修改上节 InterceptorConfig 中的 jwtDemoConfig 方法,这是 jksFileName、jksPassword、certPassword 3 个参数。

@Bean
public JwtConfig jwtDemoConfig() {
    JwtConfig jwtConfig = new JwtConfig();
//        jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb");

    jwtConfig.setJksFileName("jwt.jks");
    jwtConfig.setJksPassword("ronjwt");
    jwtConfig.setCertPassword("ronjwt");
    return jwtConfig;
}

不要设置 hmacKey 参数,否则会加载 HMACJwtServiceImpl。因为 JwtUtils#obtainJwtService 方法实现如下:

/**
 * 获取内置 JwtService 的工厂方法。
 *
 * 优先采用 HMAC 算法实现
 *
 * @param jwtConfig
 * @return
 */
public static JwtService obtainJwtService(JwtConfig jwtConfig) {
    if (!JwtUtils.isEmpty(jwtConfig.getHmacKey())) {
        return new HMACJwtServiceImpl(jwtConfig);
    }

    return new RSAJwtServiceImpl(jwtConfig);
}

这样就可以进行 RSA 算法签名与验证的测试了。运行程序并使用 Postman 测试,请自行查看区别。

- End -


本文只是 Spring Boot 集成 JWT 的第一篇,在后续我们还将继续对这个库进行封装,构建 spring-boot-starter,自定义 @Enable 注解来方便在项目中引入。

请关注我的公众号:精进Java(ID:craft4j),第一时间获取知识动态。

如果你对项目的完整源码感兴趣,可以在公众号中回复 jwt 来获取。

查看原文

赞 13 收藏 10 评论 0

fatfoo 关注了专栏 · 2019-06-05

实用至上

记录和分享我在开发过程中的心得与感悟

关注 1008

fatfoo 关注了用户 · 2018-07-12

phodal @phodal

待我代码编成,娶你为妻可好 @花仲马

关注 1795

fatfoo 关注了标签 · 2018-07-12

javascript

JavaScript 是一门弱类型的动态脚本语言,支持多种编程范式,包括面向对象和函数式编程,被广泛用于 Web 开发。

一般来说,完整的JavaScript包括以下几个部分:

  • ECMAScript,描述了该语言的语法和基本对象
  • 文档对象模型(DOM),描述处理网页内容的方法和接口
  • 浏览器对象模型(BOM),描述与浏览器进行交互的方法和接口

它的基本特点如下:

  • 是一种解释性脚本语言(代码不进行预编译)。
  • 主要用来向HTML页面添加交互行为。
  • 可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。

JavaScript常用来完成以下任务:

  • 嵌入动态文本于HTML页面
  • 对浏览器事件作出响应
  • 读写HTML元素
  • 在数据被提交到服务器之前验证数据
  • 检测访客的浏览器信息

《 Javascript 优点在整个语言中占多大比例?

关注 167723

fatfoo 关注了标签 · 2018-07-12

java

Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

Java编程语言的风格十分接近 C++ 语言。继承了 C++ 语言面向对象技术的核心,Java舍弃了 C++ 语言中容易引起错误的指針,改以引用取代,同时卸载原 C++ 与原来运算符重载,也卸载多重继承特性,改用接口取代,增加垃圾回收器功能。在 Java SE 1.5 版本中引入了泛型编程、类型安全的枚举、不定长参数和自动装/拆箱特性。太阳微系统对 Java 语言的解释是:“Java编程语言是个简单、面向对象、分布式、解释性、健壮、安全与系统无关、可移植、高性能、多线程和动态的语言”。

版本历史

重要版本号版本代号发布日期
JDK 1.01996 年 1 月 23 日
JDK 1.11997 年 2 月 19 日
J2SE 1.2Playground1998 年 12 月 8 日
J2SE 1.3Kestrel2000 年 5 月 8 日
J2SE 1.4Merlin2002 年 2 月 6 日
J2SE 5.0 (1.5.0)Tiger2004 年 9 月 30 日
Java SE 6Mustang2006 年 11 月 11 日
Java SE 7Dolphin2011 年 7 月 28 日
Java SE 8JSR 3372014 年 3 月 18 日
最新发布的稳定版本:
Java Standard Edition 8 Update 11 (1.8.0_11) - (July 15, 2014)
Java Standard Edition 7 Update 65 (1.7.0_65) - (July 15, 2014)

更详细的版本更新查看 J2SE Code NamesJava version history 维基页面

新手帮助

不知道如何开始写你的第一个 Java 程序?查看 Oracle 的 Java 上手文档

在你遇到问题提问之前,可以先在站内搜索一下关键词,看是否已经存在你想提问的内容。

命名规范

Java 程序应遵循以下的 命名规则,以增加可读性,同时降低偶然误差的概率。遵循这些命名规范,可以让别人更容易理解你的代码。

  • 类型名(类,接口,枚举等)应以大写字母开始,同时大写化后续每个单词的首字母。例如:StringThreadLocaland NullPointerException。这就是著名的帕斯卡命名法。
  • 方法名 应该是驼峰式,即以小写字母开头,同时大写化后续每个单词的首字母。例如:indexOfprintStackTraceinterrupt
  • 字段名 同样是驼峰式,和方法名一样。
  • 常量表达式的名称static final 不可变对象)应该全大写,同时用下划线分隔每个单词。例如:YELLOWDO_NOTHING_ON_CLOSE。这个规范也适用于一个枚举类的值。然而,static final 引用的非不可变对象应该是驼峰式。

Hello World

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

编译并调用:

javac -d . HelloWorld.java
java -cp . HelloWorld

Java 的源代码会被编译成可被 Java 命令执行的中间形式(用于 Java 虚拟机的字节代码指令)。

可用的 IDE

学习资源

常见的问题

下面是一些 SegmentFault 上在 Java 方面经常被人问到的问题:

(待补充)

关注 135135

fatfoo 关注了标签 · 2018-07-12

前端

Web前端开发是从网页制作演变而来的,名称上有很明显的时代特征。在互联网的演化进程中,网页制作是Web 1.0时代的产物,那时网站的主要内容都是静态的,用户使用网站的行为也以浏览为主。2005年以后,互联网进入Web 2.0时代,各种类似桌面软件的Web应用大量涌现,网站的前端由此发生了翻天覆地的变化。网页不再只是承载单一的文字和图片,各种富媒体让网页的内容更加生动,网页上软件化的交互形式为用户提供了更好的使用体验,这些都是基于前端技术实现的。

Web前端优化
  1. 尽量减少HTTP请求 (Make Fewer HTTP Requests)
  2. 减少 DNS 查找 (Reduce DNS Lookups)
  3. 避免重定向 (Avoid Redirects)
  4. 使得 Ajax 可缓存 (Make Ajax Cacheable)
  5. 延迟载入组件 (Post-load Components)
  6. 预载入组件 (Preload Components)
  7. 减少 DOM 元素数量 (Reduce the Number of DOM Elements)
  8. 切分组件到多个域 (Split Components Across Domains)
  9. 最小化 iframe 的数量 (Minimize the Number of iframes)
  10. 杜绝 http 404 错误 (No 404s)

关注 185724

fatfoo 关注了标签 · 2018-07-12

html5

HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

关注 89909

fatfoo 关注了标签 · 2018-07-12

vue.js

Reactive Components for Modern Web Interfaces.

Vue.js 是一个用于创建 web 交互界面的。其特点是

  • 简洁 HTML 模板 + JSON 数据,再创建一个 Vue 实例,就这么简单。
  • 数据驱动 自动追踪依赖的模板表达式和计算属性。
  • 组件化 用解耦、可复用的组件来构造界面。
  • 轻量 ~24kb min+gzip,无依赖。
  • 快速 精确有效的异步批量 DOM 更新。
  • 模块友好 通过 NPM 或 Bower 安装,无缝融入你的工作流。

官网:https://vuejs.org
GitHub:https://github.com/vuejs/vue

关注 129126

认证与成就

  • 获得 247 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-11
个人主页被 1.7k 人浏览