fatfoo

fatfoo 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

fatfoo 发布了文章 · 9月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 后端与架构的干货等你一起学习与交流。

查看原文

赞 10 收藏 8 评论 0

fatfoo 发布了文章 · 9月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 发布了文章 · 9月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

实用至上

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

关注 1002

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

phodal @phodal

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

关注 1785

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 方面经常被人问到的问题:

(待补充)

关注 104719

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

android

Android(安卓或安致)是一种以 Linux 为基础的开放源码操作系统,主要使用于便携设备。2005 年由 Google 收购注资,并拉拢多家制造商组成开放手机联盟开发改良,逐渐扩展到到平板电脑及其他领域上。

简介

  Android一词的本义指“机器人”,同时也是Google于2007年11月5日宣布的基于Linux平台的开源手机操作系统的名称,该平台由操作系统、中间件、用户界面和应用软件组成。 

  系统架构

  android的系统架构和其操作系统一样,采用了分层的架构。从架构图看,android分为四个层,从高层到低层分别是应用程序层、应用程序框架层、系统运行库层和linux核心层。

  应用程序

  Android会同一系列核心应用程序包一起发布,该应用程序包包括客户端,SMS短消息程序,日历,地图,浏览器,联系人管理程序等。所有的应用程序都是使用JAVA语言编写的。

  应用程序框架

  开发人员也可以完全访问核心应用程序所使用的API框架。该应用程序的架构设计简化了组件的重用;任何一个应用程序都可以发布它的功能块并且任何其它的应用程序都可以使用其所发布的功能块(不过得遵循框架的安全性)。同样,该应用程序重用机制也使用户可以方便的替换程序组件。

  隐藏在每个应用后面的是一系列的服务和系统, 其中包括;

  丰富而又可扩展的视图(Views),可以用来构建应用程序, 它包括列表(lists),网格(grids),文本框(text boxes),按钮(buttons), 甚至可嵌入的web浏览器。

  内容提供器(Content Providers)使得应用程序可以访问另一个应用程序的数据(如联系人数据库), 或者共享它们自己的数据

  资源管理器(Resource Manager)提供 非代码资源的访问,如本地字符串,图形,和布局文件( layout files )。

  通知管理器 (Notification Manager) 使得应用程序可以在状态栏中显示自定义的提示信息。

  活动管理器( Activity Manager) 用来管理应用程序生命周期并提供常用的导航回退功能。

  有关更多的细节和怎样从头写一个应用程序,请参考 如何编写一个 Android 应用程序。

  系统运行库

  Android 包含一些C/C++库,这些库能被Android系统中不同的组件使用。它们通过 Android 应用程序框架为开发者提供服务。以下是一些核心库:

  * 系统 C 库 - 一个从BSD继承来的标准 C 系统函数库( libc ), 它是专门为基于 embedded linux的设备定制的。

  * 媒体库 - 基于PacketVideo OpenCORE;该库支持多种常用的音频、视频格式回放和录制,同时支持静态图像文件。编码格式包括MPEG4, H.264, MP3, AAC, AMR, JPG, PNG 。

  * Surface Manager - 对显示子系统的管理,并且为多个应用程序提 供了2D和3D图层的无缝融合。

  * LibWebCore - 一个最新的web浏览器引擎用,支持Android浏览器和一个可嵌入的web视图。

  应用程序组件

  Android开发四大组件分别是:活动(Activity): 用于表现功能。服务(Service): 后台运行服务,不提供界面呈现。广播接收器(BroadcastReceiver):用于接收广播。内容提供商(Content Provider): 支持在多个应用中存储和读取数据,相当于数据库。

  活动

  Android 中,Activity 是所有程序的根本,所有程序的流程都运行在Activity 之中,Activity可以算是开发者遇到的最频繁,也是Android 当中最基本的模块之一。在Android的程序当中,Activity 一般代表手机屏幕的一屏。如果把手机比作一个浏览器,那么Activity就相当于一个网页。在Activity 当中可以添加一些Button、Check box 等控件。可以看到Activity 概念和网页的概念相当类似。

  一般一个Android 应用是由多个Activity 组成的。这多个Activity 之间可以进行相互跳转,例如,按下一个Button 按钮后,可能会跳转到其他的Activity。和网页跳转稍微有些不一样的是,Activity 之间的跳转有可能返回值,例如,从Activity A 跳转到Activity B,那么当Activity B 运行结束的时候,有可能会给Activity A 一个返回值。这样做在很多时候是相当方便的。

  当打开一个新的屏幕时,之前一个屏幕会被置为暂停状态,并且压入历史堆栈中。用户可以通过回退操作返回到以前打开过的屏幕。我们可以选择性的移除一些没有必要保留的屏幕,因为Android会把每个应用的开始到当前的每个屏幕保存在堆栈中。

  服务

  Service 是android 系统中的一种组件,它跟Activity 的级别差不多,但是他不能自己运行,只能后台运行,并且可以和其他组件进行交互。Service 是没有界面的长生命周期的代码。Service 是一种程序,它可以运行很长时间,但是它却没有用户界面。这么说有点枯燥,来看个例子。打开一个音乐播放器的程序,这个时候若想上网了,那么,我们打开Android 浏览器,这个时候虽然我们已经进入了浏览器这个程序,但是,歌曲播放并没有停止,而是在后台继续一首接着一首的播放。其实这个播放就是由播放音乐的Service进行控制。当然这个播放音乐的Service也可以停止,例如,当播放列表里边的歌曲都结束,或者用户按下了停止音乐播放的快捷键等。service 可以在和多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD 卡上文件的变化,再或者在后台记录你地理信息位置的改变等等,总之服务嘛,总是藏在后头的。

  开启service有两种方式:

  (1) Context.startService():Service会经历onCreate -> onStart(如果Service还没有运行,则android先调用onCreate()然后调用onStart();如果Service已经运行,则只调用onStart(),所以一个Service的onStart方法可能会重复调用多次 );stopService的时候直接onDestroy,如果是调用者自己直接退出而没有调用stopService的话,Service会一直在后台运行。该Service的调用者再启动起来后可以通过stopService关闭Service。 注意,多次调用Context.startservice()不会嵌套(即使会有相应的onStart()方法被调用),所以无论同一个服务被启动了多少次,一旦调用Context.stopService()或者stopSelf(),他都会被停止。补充说明:传递给startService()的Intent对象会传递给onStart()方法。调用顺序为:onCreate --> onStart(可多次调用) --> onDestroy。

  (2) Context.bindService():Service会经历onCreate() --> onBind(),onBind将返回给客户端一个IBind接口实例,IBind允许客户端回调服务的方法,比如得到Service运行的状态或其他操作。这个时候把调用者(Context,例如Activity)会和Service绑定在一起,Context退出了,Srevice就会调用onUnbind --> onDestroyed相应退出,所谓绑定在一起就共存亡了。[20]

  广播接收器

  在Android 中,Broadcast 是一种广泛运用的在应用程序之间传输信息的机制。而BroadcastReceiver 是对发送出来的Broadcast进行过滤接受并响应的一类组件。可以使用BroadcastReceiver 来让应用对一个外部的事件做出响应。这是非常有意思的,例如,当电话呼入这个外部事件到来的时候,可以利用BroadcastReceiver 进行处理。例如,当下载一个程序成功完成的时候,仍然可以利用BroadcastReceiver 进行处理。BroadcastReceiver不能生成UI,也就是说对于用户来说不是透明的,用户是看不到的。BroadcastReceiver通过NotificationManager 来通知用户这些事情发生了。BroadcastReceiver 既可以在AndroidManifest.xml 中注册,也可以在运行时的代码中使用Context.registerReceiver()进行注册。只要是注册了,当事件来临的时候,即使程序没有启动,系统也在需要的时候启动程序。各种应用还可以通过使用Context.sendBroadcast () 将它们自己的intent broadcasts广播给其他应用程序。

  注册BroadcastReceiver有两种方式:

  (1)在AndroidManifest.xml进行注册。这种方法有一个特点即使你的应用程序已经关闭了,但这个BroadcastReceiver依然会接受广播出来的对象,也就是说无论你这个应用程序时开还是关都属于活动状态都可以接受到广播的事件;

  (2)在代码中注册广播。

  第一种俗称静态注册,第二种俗称动态注册,这两种注册Broadcast Receiver的区别:

  动态注册较静态注册灵活。实验证明:当静态注册一个Broadcast Receiver时,不论应用程序是启动与否。都可以接受对应的广播。

  动态注册的时候,如果不执行unregister Receiver();方法取消注册,跟静态是一样的。但是如果执行该方法,当执行过以后,就不能接受广播了。

  内容提供

  Content Provider 是Android提供的第三方应用数据的访问方案。

  在Android中,对数据的保护是很严密的,除了放在SD卡中的数据,一个应用所持有的数据库、文件等内容,都是不允许其他直接访问的。Andorid当然不会真的把每个应用都做成一座孤岛,它为所有应用都准备了一扇窗,这就是Content Provider。应用想对外提供的数据,可以通过派生Content Provider类, 封装成一枚Content Provider,每个Content Provider都用一个uri作为独立的标识,形如:content://com.xxxxx。所有东西看着像REST的样子,但实际上,它比REST 更为灵活。和REST类似,uri也可以有两种类型,一种是带id的,另一种是列表的,但实现者不需要按照这个模式来做,给你id的uri你也可以返回列表类型的数据,只要调用者明白,就无妨,不用苛求所谓的REST。

  另外,Content Provider不和REST一样只有uri可用,还可以接受Projection,Selection,OrderBy等参数,这样,就可以像数据库那样进行投影,选择和排序。查询到的结果,以Cursor(参见:reference/android/database/Cursor.html )的形式进行返回,调用者可以移动Cursor来访问各列的数据。

  Content Provider屏蔽了内部数据的存储细节,向外提供了上述统一的接口模型,这样的抽象层次,大大简化了上层应用的书写,也对数据的整合提供了更方便的途径。Content Provider内部,常用数据库来实现,Android提供了强大的Sqlite支持,但很多时候,你也可以封装文件或其他混合的数据。

  在Android中,Content Resolver是用来发起Content Provider的定位和访问的。不过它仅提供了同步访问的Content Provider的接口。但通常,Content Provider需要访问的可能是数据库等大数据源,效率上不足够快,会导致调用线程的拥塞。因此Android提供了一个AsyncQueryHandler(参见:reference/android/content/AsyncQueryHandler.html),帮助进行异步访问Content Provider。

  在各大组件中,Service和Content Provider都是那种需要持续访问的。Service如果是一个耗时的场景,往往会提供异步访问的接口,而Content Provider不论效率如何,都提供的是约定的同步访问接口。

软件开发

  Java方面

  Android支持使用Java作为编程语言来开发应用程序,而Android的Java开发方面从接口到功能,都有层出不穷的变化。考虑到Java虚拟机的效率和资源占用,谷歌重新设计了Android的Java,以便能提高效率和减少资源占用,因而与J2ME等不同。其中Activity等同于J2ME的MIDlet,一个 Activity 类(Class)负责创建视窗(Windows),一个活动中的Activity就是在 foreground(前景)模式,背景运行的程序叫做Service。两者之间通过由ServiceConnection和AIDL连结,达到复数程序同时运行效果。如果运行中的 Activity 全部画面被其他 Activity 取代时,该 Activity 便被停止(Stopped),甚至被系统清除(Kill)。

  View等同于J2ME的Displayable,程序人员可以通过 View 类与“XML layout”档将UI放置在视窗上,Android 1.5的版本可以利用 View 打造出所谓的 Widgets,其实Widget只是View的一种,所以可以使用xml来设计layout,HTC的Android Hero手机即含有大量的widget。至于ViewGroup 是各种layout 的基础抽象类(abstract class),ViewGroup之内还可以有ViewGroup。View的构造函数不需要再Activity中调用,但是Displayable的是必须的,在Activity 中,要通过findViewById()来从XML 中取得View,Android的View类的显示很大程度上是从XML中读取的。View 与事件(event)息息相关,两者之间通过Listener 结合在一起,每一个View都可以注册一个event listener,例如:当View要处理用户触碰(touch)的事件时,就要向Android框架注册View.OnClickListener。另外还有BitMap等同于J2ME的Image。   

关注 63478

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

node.js

图片描述
Node 是一个 Javascript 运行环境(runtime)。实际上它是对 Google V8 引擎(应用于 Google Chrome 浏览器)进行了封装。V8 引擎执行 Javascript 的速度非常快,性能非常好。Node 对一些特殊用例进行了优化,提供了替代的 API,使得 V8 在非浏览器环境下运行得更好。例如,在服务器环境中,处理二进制数据通常是必不可少的,但 Javascript 对此支持不足,因此,V8.Node 增加了 Buffer 类,方便并且高效地 处理二进制数据。因此,Node 不仅仅简单的使用了 V8,还对其进行了优化,使其在各环境下更加给力。

关注 81149

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

python

Python(发音:英[ˈpaɪθən],美[ˈpaɪθɑ:n]),是一种面向对象、直译式电脑编程语言,也是一种功能强大的通用型语言,已经具有近二十年的发展历史,成熟且稳定。它包含了一组完善而且容易理解的标准库,能够轻松完成很多常见的任务。它的语法非常简捷和清晰,与其它大多数程序设计语言不一样,它使用缩进来定义语句。

Python支持命令式程序设计、面向对象程序设计、函数式编程、面向切面编程、泛型编程多种编程范式。与Scheme、Ruby、Perl、Tcl等动态语言一样,Python具备垃圾回收功能,能够自动管理存储器使用。它经常被当作脚本语言用于处理系统管理任务和网络程序编写,然而它也非常适合完成各种高级任务。Python虚拟机本身几乎可以在所有的作业系统中运行。使用一些诸如py2exe、PyPy、PyInstaller之类的工具可以将Python源代码转换成可以脱离Python解释器运行的程序。

Python的主要参考实现是CPython,它是一个由社区驱动的自由软件。目前由Python软件基金会管理。基于这种语言的相关技术正在飞快的发展,用户数量快速扩大,相关的资源非常多。

关注 102700

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

关注 96502

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

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