工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式,可以将对象的创建与使用代码分离,提供一种统一的接口来创建不同类型的对象。

在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。

提供封装在工厂类中的静态方法,以隐藏对象初始化逻辑并使客户端代码专注于对象的使用而不是初始化。

解释

现实世界的例子:

想象一位炼金术士即将制造硬币。炼金术士必须能够制造金币和银币,并且必须能够在不修改现有源代码的情况下在它们之间进行切换。工厂模式通过提供静态构造方法来实现这一点,可以使用相关参数进行调用。

维基百科说:

工厂是一个用于创建其他对象的对象 - 正式而言,工厂是一个函数或方法,返回不同原型或类的对象。

代码示例:

我们有一个接口Coin和两个实现类GoldCoinSliverCoin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface Coin {

String getDescription();

}

public class GoldCoin implements Coin {

static final String DESCRIPTION = "这是金硬币!";

@Override
public String getDescription() {
return DESCRIPTION;
}

}

public class SliverCoin implements Coin {

static final String DESCRIPTION = "这是银硬币!";

@Override
public String getDescription() {
return DESCRIPTION;
}

}

下面的枚举代表我们支持的货币类型(GoldCoinSliverCoin)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum CoinType {

GOLD(GoldCoin::new),
SLIVER(SliverCoin::new);

private final Supplier<Coin> constructor;

CoinType(Supplier<Coin> constructor) {
this.constructor = constructor;
}

public Supplier<Coin> getConstructor() {
return constructor;
}

}

然后我们有封装了静态方法getCoin的工厂类CoinFactory来创建硬币对象。

1
2
3
4
5
6
7
public class CoinFactory {

public static Coin getCoin(CoinType coinType) {
return coinType.getConstructor().get();
}

}

现在,在客户端代码上,我们可以使用工厂类创建不同类型的硬币。

1
2
3
4
5
6
public static void main(String[] args) {
Coin goldCoin = CoinFactory.getCoin(CoinType.GOLD);
Coin sliverCoin = CoinFactory.getCoin(CoinType.SLIVER);
System.out.println(goldCoin.getDescription());
System.out.println(sliverCoin.getDescription());
}

程序输出:

1
2
这是金硬币!
这是银硬币!

随着需求的改变,现在炼金术士还必须能够制造铜币。我们只需要新增铜币的类CopperCoin并现实Coin接口。

1
2
3
4
5
6
7
8
9
public class CopperCoin implements Coin {

static final String DESCRIPTION = "这是铜硬币!";

@Override
public String getDescription() {
return DESCRIPTION;
}
}

然后在枚举类中新增铜币类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum CoinType {

GOLD(GoldCoin::new),
SLIVER(SliverCoin::new),
COPPER(copperCoin::new);

private final Supplier<Coin> constructor;

CoinType(Supplier<Coin> constructor) {
this.constructor = constructor;
}

public Supplier<Coin> getConstructor() {
return constructor;
}

}

现在我们可以直接调用工厂类的静态方法来创建铜币。

1
2
3
4
public static void main(String[] args) {
Coin copperCoin = CoinFactory.getCoin(CoinType.COPPER);
System.out.println(copperCoin.getDescription());
}

程序输出:

1
这是铜硬币!

适用性:

当您只关心对象的创建和使用,而不关心它如何创建和管理时,请使用工厂模式。

优点

  • 允许将所有对象创建保留在一处,并避免传播“新”对象跨代码库的关键字。
  • 允许编写松散耦合的代码。它的一些主要优点包括更好的可测试性、易于理解的代码、可交换的组件、可扩展性和独立的功能。

缺点

  • 代码变得比应有的更加复杂。

已知使用:

  • java.util.Calendar#getInstance()
  • java.util.ResourceBundle#getBundle()
  • java.text.NumberFormat#getInstance()
  • java.nio.charset.Charset#forName()
  • java.net.URLStreamHandlerFactory#createURLStreamHandler(String)(根据协议返回不同的单例对象)
  • java.util.EnumSet#of()
  • javax.xml.bind.JAXBContext#createMarshaller()以及其他类似的方法。

意图

抽象工厂模式是一种创建型设计模式, 它能创建一系列相关的对象, 而无需指定其具体类。

问题

要创建一个咖啡店,我们需要具有共同主题的对象。现代风格的咖啡店需要现代风格的椅子、现代风格的沙发和现代风格的咖啡桌,而维多利亚风格的咖啡店需要维多利亚风格的椅子、维多利亚风格的沙发和维多利亚风格的咖啡桌。咖啡店中的对象之间存在依赖关系。

假设你正在开发一款咖啡店模拟器。 你的代码中包括一些类, 用于表示:

  1. 一系列相关产品, 例如椅子Chair 、 沙发Sofa和 咖啡桌Coffee­Table
  2. 系列产品的不同变体。 例如, 你可以使用现代Modern 、 维多利亚Victorian等风格生成椅子、沙发和咖啡桌。

你需要设法单独生成每件家具对象, 这样才能确保其风格一致。 如果顾客收到的家具风格不一样, 他们可不会开心。

此外, 你也不希望在添加新产品或新风格时修改已有代码。 家具供应商对于产品目录的更新非常频繁, 你不会想在每次更新时都去修改核心代码的。

解决方案

首先, 抽象工厂模式建议为系列中的每件产品明确声明接口 (例如椅子、 沙发或咖啡桌)。 然后, 确保所有产品变体都继承这些接口。 例如, 所有风格的椅子都实现 椅子接口; 所有风格的咖啡桌都实现 咖啡桌接口, 以此类推。

接下来, 我们需要声明抽象工厂——包含系列中所有产品构造方法的接口。 例如 create­Chair创建椅子 、 createSofa创建沙发和 create­Coffee­Table创建咖啡桌 。 这些方法必须返回抽象产品类型, 即我们之前抽取的那些接口: 椅子沙发咖啡桌等等。

那么该如何处理产品变体呢? 对于系列产品的每个变体, 我们都将基于 抽象工厂接口创建不同的工厂类。 每个工厂类都只能返回特定类别的产品, 例如, 现代家具工厂Modern­Furniture­Factory只能创建 现代椅子Modern­Chair 、 现代沙发Modern­Sofa和 现代咖啡桌Modern­Coffee­Table对象。

抽象工厂模式结构

程序示例

首先,我们为咖啡店中的对象提供了一些接口和实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public interface Chair {
String getDescription();
}

public interface Sofa {
String getDescription();
}

public interface CoffeeTable {
String getDescription();
}

// Modern implementations ->
public class ModernChair implements Chair {
static final String DESCRIPTION = "这是现代风格的椅子!";
@Override
public String getDescription() {
return DESCRIPTION;
}
}
public class ModernSofa implements Sofa {
static final String DESCRIPTION = "这是现代风格的沙发!";
@Override
public String getDescription() {
return DESCRIPTION;
}
}
public class ModernCoffeeTable implements CoffeeTable {
static final String DESCRIPTION = "这是现代风格的咖啡桌!";
@Override
public String getDescription() {
return DESCRIPTION;
}
}

// Victorian implementations similarly -> ...

然后我们有了家具工厂的抽象和实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public interface FurnitureFactory {
Chair createChair();
Sofa createSofa();
CoffeeTable createCoffeeTable();
}

public class ModernFurnitureFactory implements KingdomFactory {
public Chair createChair() {
return new ModernChair();
}
public Sofa createSofa() {
return new ModernSofa();
}
public CoffeeTable createCoffeeTable() {
return new ModernCoffeeTable();
}
}

public class VictorianFurnitureFactory implements KingdomFactory {
public Chair createChair() {
return new VictorianChair();
}
public Sofa createSofa() {
return new VictorianSofa();
}
public CoffeeTable createCoffeeTable() {
return new VictorianCoffeeTable();
}
}

现在,我们可以为不同的家具工厂设计工厂。 在此示例中,我们创建了FactoryMaker,负责返回ModernFurnitureFactoryVictorianFurnitureFactory的实例。 客户可以使用FactoryMaker来创建所需的具体工厂,该工厂随后将生产不同的具体对象(椅子,沙发,咖啡桌)。 在此示例中,我们还使用了一个枚举来参数化客户要求的家具工厂类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Getter
@Setter
public class CoffeeStore {

private Chair chair;

private Sofa sofa;

private CoffeeTable coffeeTable;

public static class FactoryMaker {

public enum CoffeeStoreStyle {
Victorian, Modern
}

public static FurnitureFactory makeFactory(CoffeeStoreStyle style) {
FurnitureFactory factory;
switch (style) {
case Victorian:
factory = new VictorianFurnitureFactory();
break;
case Modern:
factory = new ModernFurnitureFactory();
break;
default: throw new IllegalArgumentException("FurnitureType not supported.");
};
return factory;
}

}

}

现在我们有了抽象工厂,使我们可以制作相关对象的系列,即现代风格工厂创建了现代风格椅子,现代风格沙发和现代风格咖啡桌等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {

CoffeeStore coffeeStore = new CoffeeStore();
System.out.println("Victorian Style");
FurnitureFactory victorianFactory = CoffeeStore.FactoryMaker.makeFactory(CoffeeStore.FactoryMaker.CoffeeStoreStyle.Victorian);
coffeeStore.setChair(victorianFactory.createChair());
coffeeStore.setSofa(victorianFactory.createSofa());
coffeeStore.setCoffeeTable(victorianFactory.createCoffeeTable());
System.out.println(coffeeStore.getChair().getDescription());
System.out.println(coffeeStore.getSofa().getDescription());
System.out.println(coffeeStore.getCoffeeTable().getDescription());

System.out.println("Modern Style");
FurnitureFactory ModernFactory = CoffeeStore.FactoryMaker.makeFactory(CoffeeStore.FactoryMaker.CoffeeStoreStyle.Modern);
coffeeStore.setChair(ModernFactory.createChair());
coffeeStore.setSofa(ModernFactory.createSofa());
coffeeStore.setCoffeeTable(ModernFactory.createCoffeeTable());
System.out.println(coffeeStore.getChair().getDescription());
System.out.println(coffeeStore.getSofa().getDescription());
System.out.println(coffeeStore.getCoffeeTable().getDescription());

}

程序输出:

1
2
3
4
5
6
7
8
Victorian Style
这是维多利亚风格的椅子!
这是维多利亚风格的沙发!
这是维多利亚风格的咖啡桌!
Modern Style
这是现代风格的椅子!
这是现代风格的沙发!
这是现代风格的咖啡桌!

适用性

在以下情况下使用抽象工厂模式

  • 该系统应独立于其产品的创建,组成和表示方式
  • 系统应配置有多个产品系列之一
  • 相关产品对象系列旨在一起使用,你需要强制执行此约束
  • 你想提供产品的类库,并且只想暴露它们的接口,而不是它们的实现。
  • 从概念上讲,依赖项的生存期比使用者的生存期短。
  • 你需要一个运行时值来构建特定的依赖关系
  • 你想决定在运行时从系列中调用哪种产品。
  • 你需要提供一个或更多仅在运行时才知道的参数,然后才能解决依赖关系。
  • 当你需要产品之间的一致性时
  • 在向程序添加新产品或产品系列时,您不想更改现有代码。

示例场景

  • 在运行时在FileSystemAcmeServiceDatabaseAcmeServiceNetworkAcmeService中选择并调用一个
  • 单元测试用例的编写变得更加容易
  • 适用于不同操作系统的UI工具

影响

  • Java中的依赖注入会隐藏服务类的依赖关系,这些依赖关系可能导致运行时错误,而这些错误在编译时会被捕获。

  • 虽然在创建预定义对象时模式很好,但是添加新对象可能会很困难。

  • 由于引入了许多新的接口和类,因此代码变得比应有的复杂。

已知使用

  • javax.xml.parsers.DocumentBuilderFactoryopen in new window
  • javax.xml.transform.TransformerFactoryopen in new window
  • javax.xml.xpath.XPathFactoryopen in new window

意图:

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

问题:

  1. 保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。

    它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。

    注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。

  2. 为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。

    和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。

    还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。

解决方案:

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符。
  • 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。

适用性:

当满足以下情况时,使用单例模式:

  • 确保一个类只有一个实例,并且客户端能够通过一个众所周知的访问点访问该实例。
  • 唯一的实例能够被子类扩展, 同时客户端不需要修改代码就能使用扩展后的实例。

一些典型的单例模式用例包括:

  • logging
  • 管理与数据库的链接
  • 文件管理器(File manager

已知使用:

  • java.lang.Runtime#getRuntime()
  • java.awt.Desktop#getDesktop()
  • java.lang.System#getSecurityManager()

代码示例:

饿汉式:

饿汉式的单例实现比较简单,其在类加载的时候,静态实例instance 就已创建并初始化好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Singleton {

private Singleton() {

}

private static final Singleton INSTANCE = new Singleton();


public static Singleton getInstance() {
return INSTANCE;
}

}

测试:

1
2
3
4
5
6
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println("singleton1=" + singleton1);
System.out.println("singleton2=" + singleton2);
}

结果:

1
2
singleton1=com.design.patterns.Singleton@74a14482
singleton2=com.design.patterns.Singleton@74a14482

懒汉式(线程安全的懒加载方式):

与饿汉式对应的是懒汉式,懒汉式为了支持延时加载,将对象的创建延迟到了获取对象的时候,但为了线程安全,不得不为获取对象的操作加锁,这就导致了低性能。

并且这把锁只有在第一次创建对象时有用,而之后每次获取对象,这把锁都是一个累赘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ThreadSafeLazyLoadedSingleton {

/**
* declared as volatile to ensure atomic access by multiple threads.
*/
private static volatile ThreadSafeLazyLoadedSingleton instance;

/**
* Private constructor to prevent instantiation from outside the class.
*/
private ThreadSafeLazyLoadedSingleton() {
// Protect against instantiation via reflection
if (instance != null) {
throw new IllegalStateException("Already initialized.");
}
}

/**
* The instance doesn't get created until the method is called for the first time.
*/
public static synchronized ThreadSafeLazyLoadedSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeLazyLoadedSingleton();
}
return instance;
}

}

测试:

1
2
3
4
ThreadSafeLazyLoadedSingleton instance1 = ThreadSafeLazyLoadedSingleton.getInstance();
ThreadSafeLazyLoadedSingleton instance2 = ThreadSafeLazyLoadedSingleton.getInstance();
System.out.println("instance1=" + instance1);
System.out.println("instance2=" + instance2);

结果:

1
2
instance1=com.design.patterns.singleton.ThreadSafeLazyLoadedSingleton@74a14482
instance2=com.design.patterns.singleton.ThreadSafeLazyLoadedSingleton@74a14482

双重检测:

饿汉式和懒汉式的单例都有缺点,双重检测的实现方式解决了这两者的缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class ThreadSafeDoubleCheckSingleton {

/**
* declared as volatile to ensure atomic access by multiple threads.
*/
private static volatile ThreadSafeDoubleCheckSingleton instance;

/**
* Private constructor to prevent instantiation from outside the class.
*/
private ThreadSafeDoubleCheckSingleton() {
// Protect against instantiation via reflection
if (instance != null) {
throw new IllegalStateException("Already initialized.");
}
}

/**
* To be called by user to obtain instance of the class.
*/
public static ThreadSafeDoubleCheckSingleton getInstance() {
// local variable increases performance by 25 percent
ThreadSafeDoubleCheckSingleton result = instance;
// Check if singleton instance is initialized.
if (instance == null) {
synchronized (ThreadSafeDoubleCheckSingleton.class) {
result = instance;
if (result == null) {
result = new ThreadSafeDoubleCheckSingleton();
instance = result;
}
}
}
return result;
}

}

测试:

1
2
3
4
5
6
public static void main(String[] args) {
ThreadSafeDoubleCheckSingleton instance1 = ThreadSafeDoubleCheckSingleton.getInstance();
ThreadSafeDoubleCheckSingleton instance2 = ThreadSafeDoubleCheckSingleton.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}

结果:

1
2
com.design.patterns.singleton.ThreadSafeDoubleCheckSingleton@74a14482
com.design.patterns.singleton.ThreadSafeDoubleCheckSingleton@74a14482

静态内部类:

这种方式能达到双检锁方式一样的功效,但实现更简单。

这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class StaticInnerClassSingleton {

/**
* Private constructor to prevent instantiation from outside the class.
*/
private StaticInnerClassSingleton() {

}

/**
* The InstanceHolder is a static inner class, and it holds the Singleton instance.
* It is not loaded into memory until the getInstance() method is called.
*/
private static class InstanceHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}

/**
* When this method is called, the InstanceHolder is loaded into memory
* and creates the Singleton instance. This method provides a global access point
* for the singleton instance.
*/
public static StaticInnerClassSingleton getInstance() {
return InstanceHolder.INSTANCE;
}

}

枚举:

一个只有一个元素的枚举类型是实现单例模式的最佳方式。

1
2
3
4
5
6
7
8
public enum EnumSingleton {
INSTANCE;

@Override
public String toString() {
return getDeclaringClass().getCanonicalName() + "@" + hashCode();
}
}

测试:

1
2
3
4
5
6
public static void main(String[] args) {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.INSTANCE;
System.out.println(instance1);
System.out.println(instance2);
}

结果:

1
2
com.design.patterns.singleton.EnumSingleton@1956725890
com.design.patterns.singleton.EnumSingleton@1956725890

影响

  • 通过控制实例的创建和生命周期,违反了单一职责原则(SRP)。
  • 鼓励使用全局共享实例,组织了对象及其使用的资源被释放。
  • 代码变得耦合,给客户端的测试带来难度。
  • 单例模式的设计可能会使得子类化(继承)单例变得几乎不可能

设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。

共有 23 种设计模式。这些模式可以分为三大类:

  • 创建型模式(Creational Patterns)- 这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。

    • 工厂模式(Factory Pattern)
    • 抽象工厂模式(Abstract Factory Pattern)
    • 单例模式(Singleton Pattern)
    • 建造者模式(Builder Pattern)
    • 原型模式(Prototype Pattern)
  • 结构型模式(Structural Patterns)- 这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。

    • 适配器模式(Adapter Pattern)
    • 桥接模式(Bridge Pattern)
    • 组合模式(Composite Pattern)
    • 装饰器模式(Decorator Pattern)
    • 外观模式(Facade Pattern)
    • 享元模式(Flyweight Pattern)
    • 代理模式(Proxy Pattern)
  • 行为型模式(Behavioral Patterns)- 这些设计模式特别关注对象之间的通信。

    • 责任链模式(Chain of Responsibility Pattern)
    • 命令模式(Command Pattern)
    • 迭代器模式(Iterator Pattern)
    • 中介者模式(Mediator Pattern)
    • 备忘录模式(Memento Pattern)
    • 观察者模式(Observer Pattern)
    • 状态模式(State Pattern)
    • 策略模式(Strategy Pattern)
    • 模板模式(Template Pattern)
    • 访问者模式(Visitor Pattern)

概述

“Null sucks.” - Doug Lea

Doug Lea 是一位美国的计算机科学家,他是 Java 平台的并发和集合框架的主要设计者之一。他在 2014 年的一篇文章中说过:“Null sucks.”,意思是 null 很糟糕。他认为 null 是一种不明确的表示,它既可以表示一个值不存在,也可以表示一个值未知,也可以表示一个值无效。这样就会导致很多逻辑错误和空指针异常,给程序员带来很多麻烦。他建议使用 Optional 类来封装可能为空的值,从而提高代码的可读性和健壮性。

null 引用是一种表示一个对象变量没有指向任何对象的方式,它是 Java 语言中的一个特殊值,也是导致空指针异常(NullPointerException)的主要原因。虽然 null 引用可以用来表示一个值不存在或未知,也可以用来节省内存空间。但是它也不符合面向对象的思想,因为它不是一个对象,不能调用任何方法或属性。

可以看到,null 引用并不好,我们应该尽量避免使用 null,那么我们该怎么避免 null 引用引起的逻辑错误和运行时异常嘞?

其实这个问题 Java 的设计者也知道,于是他们在 Java8 之后设计引入了 Optional 类解决这个问题,本文将给大家详细介绍下 Optional 类的设计目的以及使用方法。


Optional 类是什么?

Optional 类是 java 8 中引入的一个新的类,它的作用是封装一个可能为空的值,从而避免空指针异常(NullPointerException)。Optional 类可以看作是一个容器,它可以包含一个非空的值,也可以为空。Optional 类提供了一些方法,让我们可以更方便地处理可能为空的值,而不需要显式地进行空值检查或者使用 null。

Optional 类的设计

Optional 类的设计是基于函数式编程的思想,它借鉴了 Scala 和 Haskell 等语言中的 Option 类型。Optional 类实现了 java.util.function 包中的 Supplier、Consumer、Predicate、Function 等接口,这使得它可以和 lambda 表达式或者方法引用一起使用,形成更简洁和优雅的代码。

Optional 类被 final 修饰,因此它是一个不可变的类,它有两个静态方法用于创建 Optional 对象。

Optional.empty()

Optional.empty 表示一个空的 Optional 对象,它不包含任何值。

1
2
// 创建一个空的 Optional 对象
Optional<String> empty = Optional.empty();

Optional.of(T value)

Optional.of 表示一个非空的 Optional 对象,它包含一个非空的值。

1
2
// 创建一个非空的 Optional 对象
Optional<String> hello = Optional.of("Hello");

Optional.ofNullable(T value)

注意,如果我们使用 Optional.of 方法传入一个 null 值,会抛出 NullPointerException。如果我们不确定一个值是否为空,可以使用 Optional.ofNullable 方法,它会根据值是否为空,返回一个相应的 Optional 对象。例如:

1
2
// 创建一个可能为空的 Optional 对象
Optional<String> name = Optional.ofNullable("Hello");

Optional 对象的使用方法

Optional 对象提供了一些方法,让我们可以更方便地处理可能为空的值,而不需要显式地进行空值检查或者使用 null。以下是一些常用的方法。

isPresent()

1
2
3
4
5
6
Optional<Person> personOptional = daoService.selectOne(person1);
if (personOptional.isPresent()) {
System.out.println("查询不为空");
} else {
System.out.println("查询为空");
}

get()

如果 Optional 对象包含一个非空的值,返回该值,否则抛出 NoSuchElementException 异常。

1
2
3
4
5
6
7
Optional<Person> personOptional = daoService.selectOne(person1);
if (personOptional.isPresent()) {
Person person = personOptional.get();
System.out.println(person);
} else {
System.out.println("查询为空");
}

ifPresent(Consumer<? super T> action)

1
2
Optional<Person> personOptional = daoService.selectOne(person1);
personOptional.ifPresent(person -> System.out.println(person.getName()));

orElse(T other)

如果 Optional 对象包含一个非空的值,返回该值,否则返回给定的默认值。

1
2
Optional<Person> personOptional = daoService.selectOne(person1);
Person person = personOptional.orElse(new Person());

orElseGet(Supplier<? extends T> supplier)

如果 Optional 对象包含一个非空的值,返回该值,否则返回由给定的供应者操作生成的值。

orElse的参数是间接计算得来的时候,使用orElse可能导致NullPointerException,当orElse的参数是间接计算得来的时候。虽然这种说法有点牵强(因为并不是orElse导致了空指针异常),但是使用orElseGet确实可以避免这种情况。

两者的明显(也是唯一)区别是前者需要传递的参数是一个值(通常是为空时的默认值),后者传递的是一个函数。我们看一下源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Return the value if present, otherwise return {@code other}.
*/
public T orElse(T other) {
return value != null ? value : other;
}

/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
*/
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

简单解释为,我们使用Optional包装的变量如果不为空,返回它本身,否则返回我们传递进去的值。orElseGet参数为Supplier接口,它是一个函数式接口,它的形式是这样的:() -> { return computedResult },即入参为空,有返回值(任意类型的)

我们可能考虑的问题是:何时使用orElse和何时使用orElseGet?看起来可以使用orElseGet的时候,使用orElse也可以代替(因为Supplier接口没有入参),而且使用orElseGet还需要将计算过程额外包装成一个 lambda 表达式。

一个关键的点是,使用Supplier能够做到懒计算,即使用orElseGet时。它的好处是,只有在需要的时候才会计算结果。具体到我们的场景,使用orElse的时候,每次它都会执行计算结果的过程,而对于orElseGet,只有Optional中的值为空时,它才会计算备选结果。这样做的好处是可以避免提前计算结果的风险

1
2
3
4
5
6
7
8
9
Person person1 = new Person();
person1.setName("zhangsan");
Person person2 = null;

String orElse = Optional.ofNullable(person1.getName()).orElse(person2.getName()); // 报错,空指针

String orElseGet = Optional.ofNullable(person1.getName()).orElseGet(() -> person2.getName()); // 不报错

System.out.println(orElseGet);

map(Function<? super T,? extends U> mapper)

如果 Optional 对象包含一个非空的值,对该值应用给定的映射函数,返回一个包含映射结果的 Optional 对象,否则返回一个空的 Optional 对象。

1
2
3
4
5
6
7
8
9
Person person1 = new Person();
person1.setName("zhangsan");
Optional<Person> person = Optional.ofNullable(person1);
Optional<String> optionalS = person.map(p -> p.getName().toUpperCase());
if (optionalS.isPresent()) {
String upperName = optionalS.get();
System.out.println(upperName);
}
// ZHANGSAN

flatMap(Function<? super T,Optional<U>> mapper)

map和flatMap的区别:

map会将传入的Function函数的结果进行封装,先看源码:

1
2
3
4
5
6
7
8
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Optional.ofNullable(mapper.apply(value));//会使用Optional的ofNullable方法包装Function函数返回的值
}
}

flatMap会直接返回Function函数执行的结果,看源码:

1
2
3
4
5
6
7
8
9
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));//直接返回Function执行的结果

}
}

所以在使用的时候,下面这段代码会报错。

1
2
3
4
5
6
7
public static String getStudentName(School school){
return Optional.ofNullable(school)
.map(School::getTearch)
.map(Tearch::getStudent)
.map(Student::getName)
.orElse("false");
}

而下面这段代码不会报错。

1
2
3
4
5
6
public static String getStudentName(School school){
return Optional.ofNullable(school)
.flatMap(School::getTearch)
.flatMap(Tearch::getStudent)
.map(Student::getName).orElse("false");
}

filter(Predicate<? super T> predicate)

如果 Optional 对象包含一个非空的值,并且该值满足给定的谓词条件,返回包含该值的 Optional 对象,否则返回一个空的 Optional 对象。

1
2
3
4
5
6
7
8
9
Person person1 = new Person();
person1.setName("zhangsan");
Optional<Person> optionalPerson = Optional.of(person1);
Optional<Person> optional = optionalPerson.filter(person -> "ZHANGSAN".equals(person.getName()));)
if (optional.isPresent()){
System.out.println(optional);
} else {
System.out.println("optional为空");
}

每个SpringBoot程序都有一个主入口,就是main()方法,在main()方法中调用SpringApplication.run()来启动整个程序。

1
2
3
4
5
6
7
@SpringBootApplication
public class Application {

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

SpringApplication.class

1
2
3
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class[]{primarySource}, args);
}
1
2
3
4
5
6
7
8
9
10
11
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 根据classpath的类推断并设置webApplicationType
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
  1. 调用静态run()方法时,我们首先创建一个SpringApplication的对象实例。在创建实例时,进行了一些基本的初始化操作。大体如下:
    1. 根据classpath的类推断并设置webApplicationType,根据源码可以看到包含三种容器REACTIVENONESERVLET,默认用的是WebApplicationType.SERVLET容器
    2. META-INF/spring.factories中获取BootstrapRegistryInitializer并放入集合bootstrapRegistryInitializers
    3. 加载所有的ApplicationContextInitializer并放入集合initializers
    4. 加载所有的ApplicationListener并放入集合listeners
    5. 推断mainApplicationClass并赋值给this.mainApplicationClass

初始化完成后,执行run()方法。

1
2
3
4
5
6
7
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 查找并加载所有的SpringApplicationRunListener
SpringApplicationRunListeners listeners = getRunListeners(args);
// 通知所有的listeners程序启动
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
if (context.isRunning()) {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
}
}
catch (Throwable ex) {
if (ex instanceof AbandonedRunException) {
throw ex;
}
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
  1. run()方法中首先调用方法getRunListeners()查找并加载所有的SpringApplicationRunListener(监听器),放入到SpringApplicationRunListeners这个集合类里面来进行统一管理。然后调用他们的starting()来通知所有的listeners(监听器)程序启动。

getRunListeners方法:

SpringAppcation.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private SpringApplicationRunListeners getRunListeners(String[] args) {
ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
argumentResolver = argumentResolver.and(String[].class, args);
List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class,
argumentResolver);
SpringApplicationHook hook = applicationHook.get();
SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
if (hookListener != null) {
listeners = new ArrayList<>(listeners);
listeners.add(hookListener);
}
return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
}


private <T> List<T> getSpringFactoriesInstances(Class<T> type, ArgumentResolver argumentResolver) {
return SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);
}

SpringFactoriesLoader.class

1
2
3
public static SpringFactoriesLoader forDefaultResourceLocation(@Nullable ClassLoader classLoader) {
return forResourceLocation("META-INF/spring.factories", classLoader);
}

getRunListeners()方法从spring.factories中获取运行监听器。

我们debug跟踪一下。

可以看到,注册为SpringApplicationRunListener的实现类只有一个,就是EventPublishingRunListener。用来在SpringBoot的整个启动流程中的不同时间点发布不同类型的应用事件(SpringApplicationEvent)。EventPublishingRunListenerSpringApplicationRunListener 的子类,它会在应用程序启动期间发布多个事件。当应用程序上下文创建时,EventPublishingRunListener 会发布 ApplicationStartingEvent 事件;当应用程序运行时,它会发布 ApplicationStartedEvent 事件。这些事件可以被其他组件监听,例如自定义的事件监听器。通过监听这些事件,您可以在应用程序启动期间执行自定义的逻辑。


我们接着回到Run方法中

1
2
3
4
5

try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
// 创建并配置environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置 property sources 和 profiles
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 将environment.getPropertySources()放在第一个位置
ConfigurationPropertySources.attach(environment);
// 运行监听器通知所有监听器环境准备完成
listeners.environmentPrepared(bootstrapContext, environment);
// 将'defaultProperties' property source移到最后
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
// 将 environment 绑定到 {@link SpringApplication}
bindToSpringApplication(environment);
// 环境转换成StandardEnvironment
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
// 返回环境配置对象ConfigurableEnvironment
return environment;
}
1
2
3
4
5
6
7
8
9
10
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(this.webApplicationType);
if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.webApplicationType);
}
return (environment != null) ? environment : new ApplicationEnvironment();
}
  1. 通过prepareEnvironment()方法进行环境的准备(包括配置property和对应的profile信息,将其放入environment变量),然后返回可配置环境environment

继续查看run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try {
......
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}
  1. 调用printBanner()方法打印 banner信息
1
2
3
4
5
6
7
8
9
10
11
12
13
private Banner printBanner(ConfigurableEnvironment environment) {
if (this.bannerMode == Mode.OFF) {
return null;
} else {
ResourceLoader resourceLoader = this.resourceLoader != null ? this.resourceLoader : new DefaultResourceLoader((ClassLoader)null);
SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter((ResourceLoader)resourceLoader, this.banner);
return this.bannerMode == Mode.LOG ? bannerPrinter.print(environment, this.mainApplicationClass, logger) : bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
}

protected ConfigurableApplicationContext createApplicationContext() {
return this.applicationContextFactory.create(this.webApplicationType);
}

我们可以通过在类路径下添加banner.txt文件自定义banner打印信息或将spring.banner.location属性设置为此类文件的位置来更改启动时打印的横幅。如果文件的编码不是 UTF-8,您可以设置spring.banner.charset

自定义banner示例:

banner.txt

打印效果:

继续查看run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
......
// 创建应用程序上下文对象
context = createApplicationContext();
// 设置下上下文对象的应用程序启动器
context.setApplicationStartup(this.applicationStartup);
// 准备上下文,配置容器的基本信息
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
// 设置容器的环境变量
context.setEnvironment(environment);
// 设置容器的ResourceLoader、ClassLoader、ConversionService
this.postProcessApplicationContext(context);
// 获取所有初始化器调用initialize()初始化
this.applyInitializers(context);
// 触发所有 SpringApplicationRunListener监听器的contextPrepared事件方法
listeners.contextPrepared(context);
bootstrapContext.close(context);
// 打印启动日志和启动应用的Profile
if (this.logStartupInfo) {
this.logStartupInfo(context.getParent() == null);
this.logStartupProfileInfo(context);
}
// Add boot specific singleton beans 添加启动特定的单例bean
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// 向beanFactory注册单例bean:命令行参数bean
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
// 向beanFactory注册单例bean:banner bean
beanFactory.registerSingleton("springBootBanner", printedBanner);
}

if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
((AbstractAutowireCapableBeanFactory)beanFactory).setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}

if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
// Load the sources,加载所有的资源
Set<Object> sources = this.getAllSources();
// 断言资源必须非空
Assert.notEmpty(sources, "Sources must not be empty");
// 将bean加载到应用上下文中
this.load(context, sources.toArray(new Object[0]));
// 触发所有SpringApplicationRunListener监听器contextLoaded方法
listeners.contextLoaded(context);
}

继续回到run()方法:

1
2
3
4
5
6
7
8
9
10
11
try {
......
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}

refreshContext

配置完容器基本信息后,刷新容器上下文refreshContext方法

1
2
3
4
5
6
7
private void refreshContext(ConfigurableApplicationContext context) {
if (this.registerShutdownHook) {
shutdownHook.registerApplicationContext(context);
}

this.refresh(context);
}
1
2
3
4
5
6
7
8
9
10
11
12
public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
} catch (RuntimeException var3) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}

throw var3;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    
public void refresh() throws BeansException, IllegalStateException {

synchronized(this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// 上下文准备刷新
this.prepareRefresh();
// 刷新bean工厂,并返回bean工厂
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
// 准备bean工厂,以便进行上下文的使用
this.prepareBeanFactory(beanFactory);
try {
// 允许在上下文子类中对 bean 工厂进行后置处理
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 在bean创建之前调用BeanFactoryPostProcessors后置处理方法
this.invokeBeanFactoryPostProcessors(beanFactory);
// 注册BeanPostProcessor
this.registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// 注册DelegatingMessageSource
this.initMessageSource();
// 注册一个applicationEventMulticaster的广播器
this.initApplicationEventMulticaster();
// 在特定上下文子类中初始化其他特殊 bean
this.onRefresh();
// 注册监听器Listeners
this.registerListeners();
// 实例化所有剩余的(非懒加载)单例。
this.finishBeanFactoryInitialization(beanFactory);
// 发布对应事件
this.finishRefresh();
} catch (BeansException var10) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
}
this.destroyBeans();
this.cancelRefresh(var10);
throw var10;
} finally {
this.resetCommonCaches();
contextRefresh.end();
}
}
}

刷新容器上下文refreshContext方法之后看到afterRefresh是一个空方法,主要用于开发者拓展使用。

run():

1
2
3
4
5
6
7
8
9
10
11
try {
......
refreshContext(context);
afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
callRunners(context, applicationArguments);
}

容器配置都完成之后,这时监听应用上下文启动完成所有的运行监听器调用 started() 方法,发布监听应用的启动事件

1
2
3
4
5
void started(ConfigurableApplicationContext context, Duration timeTaken) {
this.doWithListeners("spring.boot.application.started", (listener) -> {
listener.started(context, timeTaken);
});
}

后续继续执行callRunners方法遍历所有runner,调用run方法。

上述都完成之后到了最后一步,执行listener.running方法

1
2
3
4
5
6
7
8
9
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
// 所有的运行监听器调用running()方法,监听应用上下文
listeners.ready(context, timeTakenToReady);
return context;
} catch (Throwable var11) {
this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var11);
}

运行所有运行监听器,该方法执行以后SpringApplication.run()也就算执行完成了,那么SpringBootApplicationContext也就启动完成了。

流程图:

早期的Spring项目需要添加需要配置繁琐的xml,比如MVC、事务、数据库连接等繁琐的配置。Spring Boot的出现就无需这些繁琐的配置,因为Spring Boot基于约定大于配置的理念,在项目启动时候,将约定的配置类自动配置到IOC容器里。这些都因为Spring Boot有自动配置的特性。

main方法

Spring Boot都需要创建一个main启动类,而启动类都含有@SpringBootApplication注解,使用Spring Boot零配置就可以运行起来,这就是 Spring Boot 自动装配的能力了。

看看Spring Boot入口——main方法:

1
2
3
4
5
6
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}

核心注解

@SpringBootApplication 注解

Spring Boot 启动类上都有一个 @SpringBootApplication注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

我们需要关注的有以下几个:

  • @ComponentScan 组件扫描指令
  • @SpringBootConfiguration
  • @EnableAutoConfiguration

从源码声明可以看出,@SpringBootApplication相当于@SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan,因此我们直接拆开来分析。

@SpringBootConfiguration

@SpringBootConfiguration是继承自Spring@Configuration注解,@SpringBootConfiguration作用相当于@Configuration

1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

Spring 3.0中增加了@Configuration@Bean。可基JavaConfig形式对Spring容器中的bean进行更直观的配置。SpringBoot推荐使用基于JavaConfig的配置形式。

基于xml配置:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
default-lazy-init="true">
<bean id="myService" class="..myServiceImpl">
...
</bean>
</beans>

基于JavaConfig配置:

1
2
3
4
5
6
public class MyConfiguration {
@Bean
public MyService mockService(){
return new MyServiceImpl();
}
}

总结,@Configuration相当于一个springxml文件,配合@Bean注解,可以在里面配置需要Spring容器管理的bean


@ComponentScan

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
@AliasFor("basePackages")
String[] value() default {};

@AliasFor("value")
String[] basePackages() default {};

Class<?>[] basePackageClasses() default {};

Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

String resourcePattern() default "**/ *.class";

boolean useDefaultFilters() default true;

Filter[] includeFilters() default {};

Filter[] excludeFilters() default {};

boolean lazyInit() default false;

@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Filter {
FilterType type() default FilterType.ANNOTATION;

@AliasFor("classes")
Class<?>[] value() default {};

@AliasFor("value")
Class<?>[] classes() default {};

String[] pattern() default {};
}
}

相关属性解释:

  • basePackagesvalue: 指定扫描的包或包数组
  • basePackageClasses: 指定具体的要扫描的类
  • nameGenerator: 对应的bean名称的生成器 默认的是BeanNameGenerator
  • useDefaultFilters: 是否对带有@Component @Repository @Service @Controller注解的类开启检测,默认是开启的
  • includeFilters: 通过includeFilters加入扫描路径下没有以上注解的类加入spring容器
  • excludeFilters: 通过excludeFilters过滤出不用加入spring容器的类
  • lazyInit: 扫描到的类是都开启懒加载 ,默认是不开启的

@ComponentScan通常与@Configuration一起配合使用,用来告诉Spring需要扫描哪些包或类。如果不设值的话默认扫描@ComponentScan注解所在类所在的包及其所有子包,所以对于一个Spring Boot项目,一般会把入口类放在顶层目录中,这样就能够保证源码目录下的所有类都能够被扫描到。


@EnableAutoConfiguration

@EnableAutoConfigurationSpringBoot自动配置的核心注解

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

其中@Import({AutoConfigurationImportSelector.class}) 就是自动配置的核心入口。

@Import({AutoConfigurationImportSelector.class})可以自定义导入规则,主要就是AutoConfigurationImportSelector类的selectImports方法来选择需要自动配置的类进行配置。

1
2
3
4
5
6
7
8
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}

selectImports方法中主要是getAutoConfigurationEntry这个方法,而getAutoConfigurationEntry方法中最主要是getCandidateConfigurations这个方法。

1
2
3
4
5
6
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
1
2
3
4
5
6
7
8
9
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}

String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
1
2
3
4
5
6
7
8
9
10
11
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
Map<String, List<String>> result = new HashMap();

try {
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
......
}

SpringFactoriesLoader.loadFactoryNames()方法主要作用是读取spring-boot-autoconfigure.jar包下的META-INF/spring.factories中配置可以进行自动配置的类,这些类都是以JavaConfig形式进行导入的,在满足了@Conditional中定义的加载条件后,Spring会将这些类加载到IOC容器中。

spring.factories内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\

.... 省略了很多

Spring.factories定义了哪些类可以被自动配置,每个配置类定了在什么条件下可以被自动配置,并将这些类实例化被Spring容器管理。

这里以AopAutoConfiguration为例子,当类路径下存在EnableAspectJAutoProxy.classAspect.classAdvice.class等类时才会注入AopAutoConfiguration,并且默认是创建CglibAutoProxyConfiguration配置。

总的来说:

从上面查看的源码,可以知道Spring Boot自动配置主要是@EnableAutoConfiguration实现的,@EnableAutoConfiguration注解导入AutoConfigurationImportSelector类,通过selectImports方法调用SpringFactoriesLoader.loadFactoryNames()扫描所有含有META-INF/spring.factories文件的jar包,将所有jar包里面的spring.factories文件中@EnableAutoConfiguration对应的类注入到IOC容器中。

Spring Boot基于约定大于配置的理念,配置如果没有额外的配置的话,就给按照默认的配置使用约定的默认值,按照约定配置到IOC容器中,无需开发人员手动添加配置,加快开发效率。

基础知识

Maven 基于构建生命周期的中心概念。 这意味着构建和分发特定项的过程是明确定义的。

对于构建项目的人来说,这意味着只需要学习一小部分命令即可构建任何 Maven 项目,POM 将确保他们获得所需的结果。

共有三个内置构建生命周期:default, clean and sitedefault生命周期处理项目部署,clean生命周期处理项目清理,而 site生命周期处理项目网站的创建。

构建生命周期由多个阶段组成

每个构建生命周期都由不同的构建阶段列表定义,其中一个构建阶段代表生命周期中的一个阶段。

例如,默认生命周期包括以下阶段(有关生命周期阶段的完整列表,请参阅生命周期参考):

  • validate- 验证项目是否正确并且所有必要的信息均可用
  • compile- 编译项目的源代码,编译后的类被放置在 中${basedir}/target/classes
  • test- 使用合适的单元测试框架测试编译后的源代码。这些测试不应要求打包或部署代码
  • package- 将编译后的代码打包为可发布的格式,如 JAR
  • verify- 对集成测试的结果进行检查,确保符合质量标准
  • install- 将软件包(例如JAR包)安装到本地存储库(本地Repository)中,作为本地其他项目的依赖项使用
  • deploy- 将最终包复制到远程存储库以与其他开发人员和项目共享

这些生命周期阶段按顺序执行以完成默认生命周期。 考虑到上面的生命周期阶段,这意味着当使用默认生命周期时,Maven 将首先验证项目,然后尝试编译源代码,针对测试运行这些源代码,打包二进制文件(例如 jar),针对该项目运行集成测试 包,验证集成测试,将验证的包安装到本地存储库,然后将安装的包部署到远程存储库。


命令行调用

您应该选择与您的结果相匹配的阶段。 如果你想要你的jar,运行package。如果您想运行单元测试,请运行test

如果您不确定自己想要什么,首选调用的阶段是:

1
mvn verify

在执行verify之前,该命令会依次执行每个默认生命周期阶段(validate, compile, package等)。在大多数情况下,效果与打包相同。不过,如果有集成测试,这些测试也会被执行。在验证阶段,还可以进行一些额外的检查,例如,如果代码是按照预定义的检查样式规则编写的。

在构建环境中,使用下面的调用即可将工程干净利落地构建并部署到共享存储库中。

1
mvn clean deploy

同一命令可用于多模块场景(即具有一个或多个子项目的项目)。 Maven遍历每个子项目并执行clean,然后执行deploy(包括之前的所有构建阶段步骤)。

构建阶段由插件目标组成

不过,尽管构建阶段负责构建生命周期中的特定步骤,但其履行职责的方式可能会有所不同。这可以通过声明与这些构建阶段绑定的插件目标来实现。

一个插件目标代表一个特定的任务(比构建阶段更细),有助于项目的构建和管理。它可以与零个或多个构建阶段绑定。未与任何构建阶段绑定的目标可通过直接调用在构建生命周期之外执行。执行顺序取决于目标和构建阶段的调用顺序。

例如,考虑下面的命令。cleanpackage是构建阶段,而dependency:copy-dependencies是(插件的)目标。

1
mvn clean dependency:copy-dependencies package

如果要执行此操作,则将首先执行clean阶段(这意味着它将运行clean生命周期的所有前置阶段,加上clean阶段本身),然后是dependency:copy-dependencies目标,最后执行package阶段(以及package默认生命周期的所有先前构建阶段)。

此外,如果一个目标与一个或多个构建阶段绑定,那么该目标将在所有这些阶段中被调用。

设置项目以使用构建生命周期

构建生命周期简单易用,但在为项目构建 Maven 构建时,如何为每个构建阶段分配任务?

Packaging-打包

第一种,也是最常见的一种方法,是通过POM 元素<packaging>为项目设置打包。一些有效的<packaging>值包括 jarwarearpom。如果没有指定包装值,则默认为jar

每个packaging都包含一个目标列表,用于绑定到特定阶段。例如,jar packaging将把以下目标绑定到默认生命周期的构建阶段。

阶段 jar packagin的插件目标
process-resources resources:resources
compile compiler:compile
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test
package jar:jar
install install:install
deploy deploy:deploy

这几乎是一套标准的绑定。

Plugins-插件

将目标添加到生命周期阶段的第二种方法是在项目中配置插件。插件是为 Maven 提供目标的工程。此外,一个插件可能有一个或多个目标,其中每个目标代表该插件的一种能力。例如,编译器插件有两个目标:compiletestCompile。前者编译你的主代码的源代码,而后者编译你的测试代码的源代码。

在后面的章节中您将看到,插件可以包含指示将目标绑定到哪个生命周期阶段的信息。请注意,仅添加插件本身的信息是不够的,还必须指定要在构建过程中运行的目标。

配置的目标将添加到已绑定到所选包的生命周期的目标中。 如果多个目标绑定到特定阶段,则使用的顺序是首先执行打包中的目标,然后执行 POM 中配置的目标。 请注意,您可以使用<executions>元素来更好地控制特定目标的顺序。

为阶段添加目标的第二种方法是在项目中配置插件。插件是向 Maven 提供目标的工件。此外,一个插件可能有一个或多个目标,其中每个目标代表该插件的一种能力。例如,编译器插件有两个目标:compiletestCompile。前者编译主代码的源代码,后者编译测试代码的源代码。

在后面的章节中您将看到,插件可以包含指示将目标绑定到哪个生命周期阶段的信息。请注意,仅添加插件本身的信息是不够的,还必须指定要在构建过程中运行的目标。

配置的目标将添加到已绑定到所选包装生命周期的目标中。如果一个特定阶段绑定了多个目标,则使用的顺序是先执行包装中的目标,然后再执行 POM 中配置的目标。请注意,您可以使用<executions>元素对特定目标的顺序进行更多控制。

例如,Modello插件默认将其目标modello:java绑定到generate-sources阶段(注:modello:java目标生成 Java 源代码)。因此,要使用 Modello 插件并让它从模型生成源代码并将其纳入构建,您需要在<build><plugins>部分的 POM 中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<plugin>
<groupId>org.codehaus.modello</groupId>
<artifactId>modello-maven-plugin</artifactId>
<version>1.8.1</version>
<executions>
<execution>
<configuration>
<models>
<model>src/main/mdo/maven.mdo</model>
</models>
<version>4.0.0</version>
</configuration>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
</plugin>

您可能想知道为什么<executions>元素在那里。 这样您就可以根据需要使用不同的配置多次运行相同的目标。还可以根据一个给定 ID单独的执行,以便在继承或应用配置文件期间,您可以控制是否合并目标配置或将其转变为附加执行。

当多个执行与特定阶段相匹配时,它们将按照 POM 中指定的顺序执行,先执行继承的执行。

现在,就modello:java而言,只有在生成源代码阶段才有意义。但有些目标可以在多个阶段中使用,因此可能没有合理的默认值。对于这些目标,你可以自己指定阶段。例如,假设有一个目标display:time,它能在命令行中回声显示当前时间,你希望它在process-test-resources阶段运行,以显示测试何时开始。可以这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>com.mycompany.example</groupId>
<artifactId>display-maven-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<phase>process-test-resources</phase>
<goals>
<goal>time</goal>
</goals>
</execution>
</executions>
</plugin>

What is the POM?

POM 代表“项目对象模型”,即”Project Object Model”。它是 Maven 项目的 XML 表示形式,保存在名为pom.xml的文件中。当 Maven 人员在场时,谈论项目是在哲学意义上谈论项目,而不仅仅是包含代码的文件集合。一个项目包含配置文件、相关开发人员及其角色、缺陷、跟踪系统、组织和许可证、项目所在的 URL、项目的依赖关系,以及赋予代码生命的所有其他小部件。它是与项目有关的所有事项的一站式商店。事实上,在 Maven 的世界里,一个项目根本不需要包含任何代码,只需要一个pom.xml

Quick Overview

这是 POM 的项目元素下的直接元素列表。请注意,modelVersion的版本是4.0.0,这是目前唯一支持的 POM 版本,而且始终是必需的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- The Basics -->
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<packaging>...</packaging>
<dependencies>...</dependencies>
<parent>...</parent>
<dependencyManagement>...</dependencyManagement>
<modules>...</modules>
<properties>...</properties>

<!-- Build Settings -->
<build>...</build>
<reporting>...</reporting>

<!-- More Project Information -->
<name>...</name>
<description>...</description>
<url>...</url>
<inceptionYear>...</inceptionYear>
<licenses>...</licenses>
<organization>...</organization>
<developers>...</developers>
<contributors>...</contributors>

<!-- Environment Settings -->
<issueManagement>...</issueManagement>
<ciManagement>...</ciManagement>
<mailingLists>...</mailingLists>
<scm>...</scm>
<prerequisites>...</prerequisites>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<distributionManagement>...</distributionManagement>
<profiles>...</profiles>
</project>

POM文件基础信息

POM 包含项目的所有必要信息,以及构建过程中要使用的插件配置。它是 “who”, “what”, “where”的声明式表现,而构建生命周期是 “when” and “how”。这并不是说 POM 不能影响应用生命周期的流程–它可以。例如,通过配置maven-antrun-plugin,就可以在 POM 中嵌入 Apache Ant 任务。然而,这最终只是一个声明。 build.xml 准确地告诉 Ant 在运行时要做什么(过程式),而 POM 则说明其配置(声明式)。如果某种外力导致生命周期跳过 Ant 插件的执行,也不会阻止已执行的插件发挥其魔力。这与 build.xml 文件不同,在 build.xml 文件中,任务几乎总是依赖于在它之前执行的行。

1
2
3
4
5
6
7
8
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.codehaus.mojo</groupId>
<artifactId>my-project</artifactId>
<version>1.0</version>
</project>

Maven 坐标

上面定义的 POM 是 Maven 允许的最低限度。groupId:artifactId:version都是必填字段(不过,如果 groupId 和 version 是继承自父版本,则无需明确定义,有关继承的内容稍后详述)。这三个字段的作用很像地址和时间戳。这标记了版本库中的一个特定位置,就像 Maven 项目的坐标系:

  • groupId:这通常在一个组织或项目中是唯一的。例如,所有核心 Maven 工程都(应该)使用的 groupIdorg.apache.mavengroupId不一定使用点符号,例如 junit 项目。请注意,点标记的 groupId 不必与项目包含的包结构相对应。 但是,这是一个值得遵循的好习惯。当存储在存储库中时,该组的行为与操作系统中的 Java 打包结构非常相似。 这些点被操作系统特定的目录分隔符(例如 Unix 中的“/”)替换,成为基础存储库的相对目录结构。 在给出的示例中,org.codehaus.mojo组位于目录$M2_REPO/org/codehaus/mojo中。
  • artifactIdartifactId 通常是项目的名称。虽然 groupId 很重要,但小组内的人在讨论中很少会提到 groupId(他们通常都是同一个 ID,例如 MojoHaus 项目的 groupId:org.codehaus.mojo。它与 groupId 一起创建了一个key,该key将此项目与世界上其他所有项目区分开来(至少应该如此:)。artifactId 配合 groupId , 完全定义了工程在版本库中的唯一地址。在上述项目中,my-project位于$M2_REPO/org/codehaus/mojo/my-project
  • version: groupId:artifactId 表示的是一个项目,但它们无法确定我们谈论的是该项目的哪个版本。我们想要 2018 年(4.12 版)的 junit:junit,还是 2007 年(3.8.2 版)的 junit:junit?简而言之:代码发生了变化,这些变化就应该进行版本化,而这个元素可以使这些版本保持一致。my-project 1.0 版本的文件存放在$M2_REPO/org/codehaus/mojo/my-project/1.0的目录结构中。

Packaging-打包

现在,我们有了groupId:artifactId:version的地址结构,还需要一个标准标签来提供真正完整的内容:那就是项目的包装。在我们的例子中,上面定义的org.codehaus.mojo:my-project:1.0的 POM 示例将打包为jar。我们可以通过声明不同的打包方式将其变为war

1
2
3
4
5
6
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<packaging>war</packaging>
...
</project>

如果没有声明packaging,Maven 默认的packaging为 jar。当前的核心打包方式有:pomjarmaven-pluginejbwarearrar

POM Relationships

Dependencies

POM 的基石是其依赖列表。大多数项目都需要依赖其他项目才能正确构建和运行。如果 Maven 为你所做的一切就是管理这个列表,那么你已经收获很多。Maven 下载并链接编译以及需要它们的其他目标的依赖项。作为额外的好处,Maven 引入了这些依赖项的依赖项(传递依赖项),让您的列表只关注项目所需的依赖项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<type>jar</type>
<scope>test</scope>
<optional>true</optional>
</dependency>
...
</dependencies>
...
</project>
  • groupId, artifactId, version:

    您会经常看到这些元素。 这个三位一体用于及时计算特定项目的 Maven 坐标,将其划分为该项目的依赖项。 此计算的目的是选择与所有依赖项声明匹配的版本(由于传递依赖项,同一工程可能有多个依赖项声明)。

  • type:

    对应于所选的依赖关系类型。 默认为jar。虽然它通常表示依赖项文件名的扩展名,但情况并非总是如此:类型可以映射到不同的扩展名和分类器。type通常与所使用的packaging相对应,但也并非总是如此。

  • scope:

    该元素指的是当前任务(编译和运行时、测试等)的类路径,以及如何限制依赖关系的传递性。有五种作用域可供选择:

    • compile- 这是scope,在未指定任何作用域时使用。编译依赖项在所有类路径中都可用。 此外,这些依赖关系会传播到依赖项目。
    • provided- 这与compile很相似,但表示您希望 JDK 或容器在运行时提供它。它只能在编译和测试的classpath 上使用,而且不具有传递性。
    • runtime- 此作用域表示编译时不需要该依赖关系,但执行时需要。它在运行时和测试类路径中,但不在编译类路径中。
    • test- 此范围表示应用程序的正常使用不需要依赖项,并且仅适用于测试编译和执行阶段。 它不具有传递性。
    • system- 此范围与提供的prvoide类似,只是您必须显式提供包含它的 JAR。 该工件始终可用,无需在资源库中查找。
  • systemPath:

    仅在<scope>范围为system时才使用。否则,如果设置了该元素,编译将失败。路径必须是绝对路径,因此建议使用属性指定特定机器的路径,如${java.home}/lib。它会假定系统依赖作用域具有优先权,因此 Maven 不会检查项目的远程资源库,而是检查系统路径该文件是否存在。如果不存在,Maven 会导致构建失败,并建议您手动下载和安装。

  • optional:

    当项目本身是依赖项时,标记为optional。例如,假设项目A需要依赖项目B来编译一部分代码,而这部分代码在运行时可能用不到,那么我们的所有项目可能都不需要项目B。因此,如果项目X将项目A作为自己的依赖项,那么 Maven 就根本不需要安装项目B。如果=>表示必须依赖,而-->表示可选,那么在构建A时会出现A=>B的情况,而构建X时可能会出现X=>A-->B的情况。

    简而言之,optional让其他项目知道,当您使用此项目时,不需要此依赖项即可正常工作。

依赖关系版本要求规范

依赖项的版本元素定义版本要求,用于计算依赖项版本。软性要求可以被依赖关系图中其他地方的同一工程的不同版本所替代。硬性要求规定了特定的一个或多个版本,并覆盖软性要求。如果没有任何依赖项版本可以满足该工程的所有硬要求,则构建会失败。

版本要求的语法如下:

  • 1.0: 1.0 的软要求。 如果依赖关系树中较早没有出现其他版本,则使用 1.0。
  • [1.0]:1.0 的硬性要求。 使用 1.0 并且仅使用 1.0。
  • (,1.0]: 任何<= 1.0 版本的硬性要求。
  • [1.2,1.3]: 1.2 和 1.3(含)之间任何版本的硬性要求。
  • [1.0,2.0): 1.0<= x< 2.0;硬性要求在 1.0(含)和 2.0(不含)之间的任何版本。
  • [1.5,): 任何大于或等于 1.5 版本的硬性要求。
  • (,1.0],[1.2,): 小于或等于 1.0 或大于或等于 1.2 的任何版本的硬性要求,但不包括 1.1。多个要求之间用逗号分隔。
  • (,1.1),(1.1,): 除 1.1 之外的任何版本的硬性需求;例如,因为 1.1 存在关键漏洞。
    Maven 挑选每个项目中满足该项目依赖项所有硬性要求的最高版本。如果没有任何版本满足所有硬性要求,则构建失败。

版本顺序规范

如果版本字符串在语法上是正确的语义版本控制 1.0.0 版本号,则在几乎所有情况下版本比较都遵循该规范中概述的优先规则。 这些版本是常见的字母数字 ASCII 字符串,例如 2.15.2-alpha。 更准确地说,如果要比较的两个版本号都与语义版本控制规范中的 BNF 语法中的“有效 semver”产生式相匹配,则情况成立。 Maven 不考虑该规范隐含的任何语义。

重要:这只适用于语义版本控制1.0.0。 Maven的版本顺序算法与语义版本控制2.0.0不兼容。 特别是,Maven不对加号进行特殊处理,也不考虑构建标识符。

当版本字符串不遵循语义版本控制时,需要一组更复杂的规则。 Maven 坐标被分割为点(“.”)、连字符(“-”)以及数字和字符之间的转换之间的标记。 分隔符被记录并将对顺序产生影响。 数字和字符之间的转换相当于连字符。 空标记将替换为“0”。 这给出了一系列版本号(数字标记)和带有“.”的版本限定符(非数字标记)。 或“-”前缀。

拆分和替换示例:

1-1.foo-bar1baz-.1 ->1-1.foo-bar-1-baz-0.1

然后,从版本末尾开始,修剪尾随的“null”值(0、“”、“final”、“ga”)。 从头到尾对每个剩余的连字符重复此过程。

修剪示例:

  • 1.0.0->1
  • 1.g->1
  • 1.fina->1
  • 1.0->1
  • 1.->1
  • 1-->1
  • 1.0.0-foo.0.0->1-foo
  • 1.0.0-0.0.0->1

版本顺序是这一前缀标记序列的字典顺序,较短的标记填充有足够的“空”值和匹配的前缀,以与较长的标记具有相同的长度。 填充的“null”值取决于其他版本的前缀:0 表示“.”,“”表示“-”。 前缀令牌顺序为:

如果前缀相同,则比较 token:

  • 数字标记具有自然顺序。
  • 非数字标记(“限定符”)按字母顺序排列,但以下标记除外(按此顺序排在第一位)
    • alpha“< “beta“< “milestone“< “rc“ = “cr“< “snapshot“< “” = “final“ = “ga“< “sp
    • 当直接跟数字时,“alpha”、“beta”和“milestone”限定符可以分别缩写为“a”、“b”和“m”。
      否则“.qualifier”=“-qualifier”<“-number”<“.number”
    • alpha = a<<<beta>>= b<<<milestone>>= m<<<rc>>= cr<<<snapshot>>’<<<>>’ = final = ga = release< sp

鼓励遵守 semver 规则,不鼓励使用某些限定词:

  • eaandpreview相比,更倾向使用alpha,betamilestone
  • 优先选择1.0.0-RC1,而不是1.0.0.0.RC1
  • 不鼓励使用CR修饰符。请使用RC
  • 不鼓励使用finalgarelease限定符。请勿使用限定符。
  • 不鼓励使用SP限定符。 而是增加补丁版本。

Exclusions-排除项

限制依赖项的传递依赖关系有时很有用。依赖项可能指定了不正确的作用域,或与项目中的其他依赖项发生冲突。Exclusions会告诉 Maven 不要在类路径中包含指定的artifact,即使该artifact是本项目一个或多个依赖项的依赖项(传递依赖项)。例如,maven-embedder依赖于maven-core。假设您想依赖maven-embedder,但不想在 classpath 中包含maven-core或其依赖项。那么就在声明依赖maven-embedder的元素中添加maven-core作为排除项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-embedder</artifactId>
<version>3.9.4</version>
<exclusions>
<exclusion>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
</exclusion>
</exclusions>
</dependency>
...
</dependencies>
...
</project>

这只会从maven-embedder依赖项中删除到maven-core的路径。 如果maven-core在 POM 中的其他位置显示为直接或传递依赖项,它仍然可以添加到类路径中。

通配符排除可以轻松排除依赖项的所有传递依赖项。 在下面的情况下,您可能正在使用 maven-embedder 并且想要管理您使用的依赖项,因此您排除所有传递依赖项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-embedder</artifactId>
<version>3.8.6</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
...
</dependencies>
...
</project>
  • exclusions: Exclusions包含一个或多个<exclusion>,每个元素都包含表示要排除的依赖关系的groupIdartifactId。与optional不同的是,exclusions会主动将artifacts从依赖关系树中移除,而optional可能会也可能不会被安装和使用。

Inheritance-继承

Maven 为构建管理带来的一个强大功能是项目继承概念。虽然在 Ant 等构建系统中可以模拟继承,但 Maven 在项目对象模型中明确了项目继承。

1
2
3
4
5
6
7
8
9
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.codehaus.mojo</groupId>
<artifactId>my-parent</artifactId>
<version>2.0</version>
<packaging>pom</packaging>
</project>

父项目和聚合(多模块)项目的packaging类型需要为pom。这些类型定义了与一组生命周期阶段绑定的目标。例如,如果打包类型为jar,那么打包阶段将执行jar:jar目标。现在,我们可以为父 POM 添加值,这些值将由其子 POM 继承。父 POM 中的大多数元素都会被其子 POM 继承,其中包括:

  • groupId
  • version
  • description
  • url
  • inceptionYear
  • organization
  • licenses
  • developers
  • contributors
  • mailingLists
  • scm
  • issueManagement
  • ciManagement
  • properties
  • dependencyManagement
  • dependencies
  • repositories
  • pluginRepositories
  • build
    • plugin executions with matching ids
    • plugin configuration
    • etc.
  • reporting
  • profiles

不继承的重要元素包括:

  • artifactId
  • name
  • prerequisites
1
2
3
4
5
6
7
8
9
10
11
12
13
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.codehaus.mojo</groupId>
<artifactId>my-parent</artifactId>
<version>2.0</version>
<relativePath>../my-parent</relativePath>
</parent>

<artifactId>my-project</artifactId>
</project>

注意relativePath元素。它不是必需的,但可以用来指示 Maven 在搜索本地和远程版本库之前,首先搜索该项目父节点的路径。

要查看继承的实际效果,只需查看 ASF 或 Maven 父 POM 即可。

The Super POM

与面向对象编程中的对象继承类似,扩展父 POM 的 POM 会从该父 POM 继承某些值。 此外,正如 Java 对象最终继承自java.lang.Object一样,所有项目对象模型都继承自基础 Super POM。 下面的代码片段是 Maven 3.5.4 的 Super POM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
<project>
<modelVersion>4.0.0</modelVersion>

<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>

<build>
<directory>${project.basedir}/target</directory>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<finalName>${project.artifactId}-${project.version}</finalName>
<testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
</testResources>
<pluginManagement>
<!-- NOTE: These plugins will be removed from future versions of the super POM -->
<!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) -->
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-beta-5</version>
</plugin>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.8</version>
</plugin>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.5.3</version>
</plugin>
</plugins>
</pluginManagement>
</build>

<reporting>
<outputDirectory>${project.build.directory}/site</outputDirectory>
</reporting>

<profiles>
<!-- NOTE: The release profile will be removed from future versions of the super POM -->
<profile>
<id>release-profile</id>

<activation>
<property>
<name>performRelease</name>
<value>true</value>
</property>
</activation>

<build>
<plugins>
<plugin>
<inherited>true</inherited>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<inherited>true</inherited>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<inherited>true</inherited>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<updateReleaseInfo>true</updateReleaseInfo>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>

</project>

您可以创建一个最小的pom.xml,并在命令行中执行:mvn help:effective-pom来查看超级 POM 如何影响您的项目对象模型。

Dependency Management-依赖管理

除了继承某些顶层元素外,父类还拥有为子 POM 和传递依赖关系配置值的元素。其中一个元素就是依赖关系管理dependencyManagement

  • dependencyManagement: POM 使用它来帮助管理其所有子项的依赖关系信息。如果my-parent项目使用dependencyManagement定义了对junit:junit:4.12的依赖项,则从该项目继承的 POM 设置其依赖项时,可以仅给出groupId=junitartifactId=junit即可,Maven 将自动填写父依赖项设置的版本 。这种方法的好处显而易见。依赖关系的详细信息可以在一个中心位置设置,并传播到所有继承的 POM。

    需要注意的是,从传递依赖关系中合并进来的artifacts的版本和范围也受依赖关系管理部分中的版本规范控制。这可能会导致意想不到的后果。 考虑这样一种情况,您的项目使用两个依赖项:dep1 和 dep2。 dep2 反过来也使用 dep1,并且需要特定的最低版本才能运行。 如果您随后使用 dependencyManagement 指定旧版本,dep2 将被迫使用旧版本,并失败。 所以,你必须小心检查整个依赖树以避免这个问题; mvn dependency:tree 很有帮助。


聚合多个模块

包含多个模块的项目称为多模块项目或聚合项目。模块是该 POM 列出的项目,并作为一个组执行。pom 打包项目可以通过将一组项目列为模块来聚合一组项目的构建,这些模块是这些项目的目录或 POM 文件的相对路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.codehaus.mojo</groupId>
<artifactId>my-parent</artifactId>
<version>2.0</version>
<packaging>pom</packaging>

<modules>
<module>my-project</module>
<module>another-project</module>
<module>third-project/pom-example.xml</module>
</modules>
</project>

在列出模块时,您无需考虑模块间的依赖关系,即 POM 给出的模块顺序并不重要。Maven 会对模块进行拓扑排序,使依赖模块总是先于被依赖模块构建。

关于继承与聚合的最后一点说明

继承和聚合为通过单一的高级 POM 控制构建创造了良好的动态效果。你经常会看到既是父项目又是聚合项目的项目。例如,整个 Maven 核心通过单个基础 POMorg.apache.maven:maven运行,因此构建 Maven 项目可以通过单个命令来执行:mvn compile。不过,聚合项目和父项目都是 POM 项目,它们并不相同,不能混为一谈。POM 项目可以从其聚合的任何模块继承,但不一定拥有这些模块。相反,一个 POM 项目可以聚合不从它继承的项目。

Propertie-属性

属性是理解 POM 基础知识的最后一个必需部分。Maven 属性是值占位符,就像 Ant 中的属性一样。它们的值可以通过使用符号${X}在 POM 中的任何位置访问,其中X是属性。 或者它们可以被插件用作默认值,例如:

1
2
3
4
5
6
7
8
9
10
11
12
<project>
...
<properties>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<!-- Following project.-properties are reserved for Maven in will become elements in a future POM definition. -->
<!-- Don't start your own properties properties with project. -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
...
</project>

它们有五种不同的风格:

  1. env.X:在变量前添加“env”前缀。 将返回 shell 的环境变量。 例如,${env.PATH} 包含 PATH 环境变量。注意:虽然 Windows 环境变量本身不区分大小写,但属性的查找却区分大小写。换句话说,Windows shell 会返回 %PATH% 和 %Path% 的相同值,而 Maven 则会区分 ${env.PATH} 和 ${env.Path}。为可靠起见,环境变量的名称统一为大写。
  2. project.x:POM 中的点 (.) 表示的路径将包含相应元素的值。 例如:<project><version>1.0</version></project>可通过${project.version}访问。
  3. settings.xml 中的点 (.) 符号路径将包含相应元素的值。例如<settings><offline>false</offline></settings>可通过${settings.offline}访问。
  4. 所有通过java.lang.System.getProperties()访问的属性都可作为 POM 属性使用,如${java.home}
  5. x: 设置在 POM 中的<properties />元素中。<properties><someVar>value</someVar></properties>的值可用作${someVar}

Build Settings

除了上面给出的 POM 基础知识之外,在声明 POM 的基本能力之前,还必须了解两个要素。它们分别是build元素和reporting元素,前者负责处理诸如声明项目目录结构和管理插件等事务,后者则在很大程度上反映了用于报告目的的构建元素。

Build

根据 POM 4.0.0 XSD,build元素在概念上分为两部分:有一个BaseBuild类型,其中包含两个构建元素通用的元素集(项目下的顶级构建元素和配置文件下的构建元素) ,如下所述); 还有Build类型,它包含BaseBuild集以及顶级定义的更多元素。 让我们首先分析两者之间的共同点。

注:这些不同的构建元素可分别称为 “项目构建 “和 “配置文件构建”。

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<!-- "Project Build" contains more elements than just the BaseBuild set -->
<build>...</build>

<profiles>
<profile>
<!-- "Profile Build" contains a subset of "Project Build"s elements -->
<build>...</build>
</profile>
</profiles>
</project>

The BaseBuild Element Set

BaseBuild顾名思义:POM 中<build></build>之间的基本元素集。

1
2
3
4
5
6
7
8
9
<build>
<defaultGoal>install</defaultGoal>
<directory>${basedir}/target</directory>
<finalName>${artifactId}-${version}</finalName>
<filters>
<filter>filters/filter1.properties</filter>
</filters>
...
</build>
  • defaultGoal: 如果没有给出,则执行的默认目标或阶段。如果给出了目标,则应按照命令行中的方式进行定义(例如jar:jar)。 如果定义了阶段(例如install),情况也是如此。
  • directory: 这是项目构建后输出文件的目录,或者用 Maven 的话说,就是构建的目标。 它适当地默认为${basedir}/target
  • finalName: 这是最终构建时项目的名称(不带文件扩展名,例如:my-project-1.0.jar)。 默认为${artifactId}-${version}。不过,”finalName “这个术语有点名不副实,因为构建捆绑项目的插件完全有权忽略/修改这个名称(但它们通常不会这样做)。例如,如果maven-jar-plugin被配置为给 jar 提供一个 test 分类器,那么上面定义的实际 jar 将被构建为my-project-1.0-test.jar
  • filter: 定义*.properties文件,其中包含一些列的属性值适用于资源文件列表。 换句话说,过滤器文件中定义的“name=value”对将替换构建时资源中的${name}字符串。上面的示例定义了filters/目录下的filter1.properties文件。 Maven的默认过滤器目录是${basedir}/src/main/filters/

Resources

build元素的另一个功能是指定资源文件在项目中存在的位置。资源(通常)不是代码。 它们不会被编译,而是要捆绑在项目中或出于各种其他原因(例如代码生成)而使用的项目。

例如,Plexus 项目需要在 META-INF/plexus 目录中放置一个configuration.xml文件(该文件指定了容器的组件配置)。虽然我们可以很容易地将该文件放在src/main/resources/META-INF/plexus目录中,但我们还是想让 Plexus 拥有自己的src/main/plexus目录。为了让 JAR 插件正确地捆绑资源,您需要指定与下面类似的资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
...
<resources>
<resource>
<targetPath>META-INF/plexus</targetPath>
<filtering>false</filtering>
<directory>${basedir}/src/main/plexus</directory>
<includes>
<include>configuration.xml</include>
</includes>
<excludes>
<exclude>**/*.properties</exclude>
</excludes>
</resource>
</resources>
<testResources>
...
</testResources>
...
</build>
</project>
  • resources:是资源元素的列表,每个元素都描述了与此项目相关的文件的包含内容和位置。
  • targetPath:指定用于放置构建资源集的目录结构。目标路径默认为基本目录。对于将打包到 JAR 中的资源,通常指定的目标路径是 META-INF。
  • filteringtruefalse,表示是否对该资源启用过滤。请注意,不必定义过滤器*.properties文件即可进行过滤,资源还可以使用 POM 中默认定义的属性(例如 ${project.version})。使用以下命令传递到命令行 “-D”标志(例如“-Dname=value”)或由属性元素显式定义。 过滤器文件已在上面介绍过。
  • directory:该元素的值定义在哪里可以找到资源文件。 构建的默认目录是${basedir}/src/main/resources
  • includes: 包括:一组文件模式,指定要作为指定目录下的资源包含的文件,使用 * 作为通配符。
  • excludes: 与includes相同的结构,指定要忽略哪些文件。 在includesexcludes发生冲突时,excludes获胜。
  • testResources: 它们的定义与resources类似,但自然是在测试阶段使用。唯一的区别是项目的默认(Super POM 定义的)测试资源目录是${basedir}/src/test/resources。测试资源不会被部署。

Plugins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<build>
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<extensions>false</extensions>
<inherited>true</inherited>
<configuration>
<classifier>test</classifier>
</configuration>
<dependencies>...</dependencies>
<executions>...</executions>
</plugin>
</plugins>
</build>
</project>

除了groupId:artifactId:version的标准坐标外,还有一些元素用于配置插件或构建与插件的交互。

  • extensions:truefalse,是否加载该插件的扩展。 默认情况下为false。 本文档稍后将介绍扩展。
  • inherited:truefalse,表示此插件配置是否应用于继承自此插件的 POM。默认值为true
  • configuration: 这是单个插件的特定属性。无需太深入地了解插件如何工作的机制,只需说明插件 Mojo 可能期望的任何属性(这些是 Java Mojo bean 中的 getter 和 setter)都可以在这里指定。在上面示例中,我们在maven-jar-plugin中将classifier设置为test。值得注意的是,所有配置元素,无论它们位于 POM 中的什么位置,都是为了将值传递到另一个底层系统,例如插件。 换句话说:POM 模式永远不会明确要求配置元素中的值,但插件目标完全有权要求配置值。

如果您的 POM 声明了一个parent,它将从父类的build/pluginspluginManagement部分继承插件配置。

default configuration inheritance:

为了说明这一点,请考虑父 POM 中的以下片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>my.group</groupId>
<artifactId>my-plugin</artifactId>
<configuration>
<items>
<item>parent-1</item>
<item>parent-2</item>
</items>
<properties>
<parentKey>parent</parentKey>
</properties>
</configuration>
</plugin>

下面是使用该父级作为parent的项目中的插件配置:

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>my.group</groupId>
<artifactId>my-plugin</artifactId>
<configuration>
<items>
<item>child-1</item>
</items>
<properties>
<childKey>child</childKey>
</properties>
</configuration>
</plugin>

默认行为是根据元素名称合并配置元素的内容。 如果子 POM 具有特定元素,则该值将成为有效值。 如果子 POM 没有元素,但父 POM 有,则父值将成为有效值。 请注意,这纯粹是对 XML 的操作; 不涉及插件本身的代码或配置。 仅涉及元素,而不涉及其值。

将这些规则应用到示例中,Maven 得出:

1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>my.group</groupId>
<artifactId>my-plugin</artifactId>
<configuration>
<items>
<item>child-1</item>
</items>
<properties>
<childKey>child</childKey>
<parentKey>parent</parentKey>
</properties>
</configuration>
</plugin>

高级配置继承:combine.childcombine.self

你可以控制子POM如何从父POM继承配置通过向配置元素的子元素添加属性。这些属性是combine.childrencombine.self。在子 POM 中使用这些属性来控制 Maven 如何将父 POM 中的插件配置与子 POM 中的显式配置相结合。

以下是带有这两个属性说明的子配置:

1
2
3
4
5
6
7
8
9
10
<configuration>
<items combine.children="append">
<!-- combine.children="merge" is the default -->
<item>child-1</item>
</items>
<properties combine.self="override">
<!-- combine.self="merge" is the default -->
<childKey>child</childKey>
</properties>
</configuration>

现在,有效结果如下:

1
2
3
4
5
6
7
8
9
10
<configuration>
<items combine.children="append">
<item>parent-1</item>
<item>parent-2</item>
<item>child-1</item>
</items>
<properties combine.self="override">
<childKey>child</childKey>
</properties>
</configuration>

combine.children="append "的结果是按顺序连接父元素和子元素。而combine.self="override"则会完全抑制父元素的配置。您不能在一个element上同时使用combine.self="override"combine.children="append"; 如果你尝试,override将会获胜。

请注意,这些属性只适用于它们所声明的配置元素,不会传播到嵌套元素。也就是说,如果子 POM 中项目元素的内容是复杂结构而非文本,那么其子元素仍将采用默认的合并策略,除非它们本身也标有属性。

merge.*属性从父 POM 继承到子 POM。 将这些属性添加到父 POM 时要小心,因为这可能会影响子 POM 或孙 POM。

  • dependencies:dependencies在 POM 中经常出现,它是所有plugins元素块下的一个元素。dependencies有着与基础构建相同的结构和功能。这种情况下的主要区别在于依赖项不再作为项目的依赖项,而是作为其所在插件的依赖项。其强大之处在于可以更改插件的依赖项列表,可能是通过排除删除未使用的运行时依赖项,或者更改所需依赖项的版本。 有关更多信息,请参阅上面的依赖项。
  • executions: 需要注意的是,一个plugin可能有多个goals。每个目标都可能有单独的配置,甚至可能将插件的目标完全绑定到不同的阶段。executions配置了一个插件goals的执行。

例如,假设要antrun:run目标绑定到verify阶段。我们希望该任务能呼应构建目录,并通过将inherited设置为false来避免将此配置传递给其子任务(假设它是父任务)。执行结果如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<id>echodir</id>
<goals>
<goal>run</goal>
</goals>
<phase>verify</phase>
<inherited>false</inherited>
<configuration>
<tasks>
<echo>Build Dir: /home/jenkins/82467a7c/workspace/aven_maven-box_maven-site_master/target</echo>
</tasks>
</configuration>
</execution>
</executions>

</plugin>
</plugins>
</build>
</project>
  • id: 不言自明。它指定了所有其他执行块之间的这个执行块,该运行阶段时,将以下形式显示:[plugin:goal execution: id]。在此示例中:antrun:run execution: echodir
  • goals: 它包含了一系列元素,在本例中,是该执行块指定的插件目标列表。
  • phase: 这是一系列goals列表将执行的阶段。这是一个非常强大的选项,允许将任何目标绑定到构建生命周期中的任何阶段,从而改变 Maven 的默认行为。
  • inherited: 与上面的继承元素一样,将其设置为 false 将禁止 Maven 将此执行传递给其子元素。 该元素仅对父 POM 有意义。
  • configuration: 与上面相同,但将配置限制到这个特定的目标列表,而不是插件下的所有目标。

Plugin Management

pluginManagement是一个与plugins一起出现的元素。插件管理以大致相同的方式包含插件元素,不同之处在于它不是为特定项目构建配置插件信息,而是用于配置继承自此项目构建的项目。不过,这只能配置子项目或当前 POM 中插件元素实际引用的插件。子项目完全有权覆盖pluginManagement定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
...
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>pre-process-classes</id>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<classifier>pre-process</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
...
</build>
</project>

如果我们将这些规范添加到plugins元素中,它们将仅适用于单个POM。但是,如果我们在pluginManagement元素下应用它们,那么这个 POM 和所有在构建过程中继承 了maven-jar-plugin的POM也将获得预处理类的执行。因此,与其在每个子pom.xml中包含上述乱七八糟的内容,不如只需要以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
</plugins>
...
</build>
</project>

The Build Element Set

XSD 中的 “构建 “类型表示那些只适用于 “项目构建 “的元素。尽管额外元素的数量很多(6 个),但项目构建所包含的元素中只有两组是配置文件构建所缺少的:directoriesandextensions

Directories

目录元素集存在于父构建元素中,为整个 POM 设置了各种目录结构。由于它们不存在于配置文件构建中,因此不能被配置文件更改。

1
2
3
4
5
6
7
8
9
10
11
12
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
<sourceDirectory>${basedir}/src/main/java</sourceDirectory>
<scriptSourceDirectory>${basedir}/src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>${basedir}/src/test/java</testSourceDirectory>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<testOutputDirectory>${basedir}/target/test-classes</testOutputDirectory>
...
</build>
</project>

如果上述*Directory元素的值被设置为绝对路径(在展开其属性时),则使用该目录。否则,它将相对于基本构建目录:${basedir}。请注意,脚本源目录scriptSourceDirectory在 Maven 中已不再使用,并已过时。

Extensions

扩展是用于工程构建的一系列artifacts列表。它们将被包含在正在运行的构建的类路径中。他们可以扩展构建过程(例如为 Wagon 传输机制添加 ftp provider),以及激活插件以更改构建生命周期。简而言之,扩展是在构建期间激活的artifacts。扩展不需要做任何实际工作,也不包含 Mojo。因此,扩展非常适合指定一个通用插件接口的多个实现中的一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<build>
...
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ftp</artifactId>
<version>1.0-alpha-3</version>
</extension>
</extensions>
...
</build>
</project>

Reporting

报告包含专门对应网站生成阶段的元素。定义和配置在reporting元素下相当多的Maven 插件可以生成报告。例如:生成 Javadoc 报告。 与构建元素配置插件的能力非常相似,与构建元素配置插件的功能一样,报告元素也具有同样的功能。明显的区别在于,明显的区别在于,报告不是在执行块中对插件目标进行精细控制,而是在 reportSet 元素中配置目标。更微妙的区别在于,报告元素下的插件配置可作为构建插件配置使用,尽管相反情况并非如此(构建插件配置不会影响报告插件)。

对于了解构建元素的人来说,报告元素下唯一不熟悉的项目可能是可能就是布尔排除默认值元素Boolean excludeDefaults element。该元素向站点生成器表示排除通常默认生成的报告。当通过站点构建周期生成站点时,项目信息部分会放置在左侧菜单中,其中充满了报告,例如项目团队报告或依赖项列表报告。这些报告目标由 maven-project-info-reports-plugin 生成。作为一个和其他插件一样的插件,它也可以通过以下更冗长的方式被抑制,从而有效地关闭项目信息报告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<reporting>
<outputDirectory>${basedir}/target/site</outputDirectory>
<plugins>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>2.0.1</version>
<reportSets>
<reportSet></reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
...
</project>

另一个区别是plugin下的outputDirectory元素。 在报告的情况下,输出目录默认为${basedir}/target/site

Report Sets

重要的是要记住,单个插件可能有多个目标。 每个目标可能有单独的配置。报告集配置报告插件目标的执行。 这听起来是不是很熟悉——似曾相识?关于构建的执行元素也说了同样的事情,但有一个区别:您不能将报告绑定到另一个阶段。 对不起。例如,假设您想将 javadoc:javadoc 目标配置为链接到 “http://java.sun.com/j2se/1.5.0/docs/api/",但仅限于 javadoc 目标(而不是目标 maven-javadoc-plugin:jar)。我们还希望将此配置传递给其子级,并将inherited设置为 true。 reportSet类似于以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<reporting>
<plugins>
<plugin>
...
<reportSets>
<reportSet>
<id>sunlink</id>
<reports>
<report>javadoc</report>
</reports>
<inherited>true</inherited>
<configuration>
<links>
<link>http://java.sun.com/j2se/1.5.0/docs/api/</link>
</links>
</configuration>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
...
</project>

在buildexecutions和reportingreportSets之间,现在应该清楚它们存在的原因了。最简单的意义上讲,它们是对配置的深入研究。它们赋予了 POM 在控制其构建命运方面的最终粒度。


More Project Information

有几个元素并不影响构建,而是为了方便开发人员而记录项目。在生成项目网站时,这些元素中有许多都用于填写项目细节。不过,与所有 POM 声明一样,插件也可以将它们用于任何用途。以下是最简单的元素:

  • name: 项目往往都有一个会话式的名称,而不只是artifactId。Sun 公司的工程师们并没有把他们的项目称为 “java-1.5”,而是称之为 “Tiger”。这里是设置该值的位置。
  • description: 简短、可读的项目描述。尽管这不应取代正式文档,但对 POM 的任何读者进行快速评语总是有帮助的。
  • url: 项目主页。
  • inceptionYear: 项目首次创建的年份。

Licenses-许可证

1
2
3
4
5
6
7
8
<licenses>
<license>
<name>Apache-2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
<comments>A business-friendly OSS license</comments>
</license>
</licenses>

Licenses 是法律文件定义如何以及何时使用项目,项目应列出直接适用于本项目的许可证,而不应列出适用于本项目依赖关系的许可证。

  • name, url and comments: 不言自明,以前在其他情况下也遇到过。建议使用 SPDX 标识符作为许可证名称。第四个许可证元素是
  • distribution: 这描述了如何合法地分发项目。 两种指定的方法是repo(它们可以从Maven存储库下载)或manual(它们必须手动安装)。

Organization-组织

大多数项目都是由某种组织(企业、私人团体等)运作的。这里设置了最基本的信息。

1
2
3
4
5
6
7
8
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<organization>
<name>Codehaus Mojo</name>
<url>http://mojo.codehaus.org</url>
</organization>
</project>

Developers-开发者

所有项目都由某个人在某个时间创建的文件组成。 与围绕项目的其他系统一样,参与项目的人员也与项目息息相关。开发人员可能是项目的核心开发成员。需要注意的是,尽管一个组织可能有很多开发人员(程序员),但把他们都列为开发人员并不是好的做法,而只能是那些直接负责代码的人。一个好的经验法则是,如果不应该就项目与此人联系,就不需要在此列出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<developers>
<developer>
<id>jdoe</id>
<name>John Doe</name>
<email>jdoe@example.com</email>
<url>http://www.example.com/jdoe</url>
<organization>ACME</organization>
<organizationUrl>http://www.example.com</organizationUrl>
<roles>
<role>architect</role>
<role>developer</role>
</roles>
<timezone>America/New_York</timezone>
<properties>
<picUrl>http://www.example.com/jdoe/pic</picUrl>
</properties>
</developer>
</developers>
...
</project>
  • id, name, email: 这些信息对应于开发者的 ID(可能是整个组织的唯一 ID)、开发者的姓名和电子邮件地址。
  • organization, organizationUrl: 你可能猜到了,这分别是开发者的组织名称和 URL。
  • roles: 角色应明确规定该人负责的标准行动。就像一个人可以身兼数职一样,一个人可以扮演多个角色。
  • timezone: timezone:有效的时区 ID,例如 America/New_York 或 Europe/Berlin,或者是开发人员居住地与 UTC 的小时数(和分数)偏移量,例如 -5 或 +1。 时区 ID 是高度首选,因为它们不受 DST 和时区转换的影响。 有关官方时区数据库和维基百科中的列表,请参阅 IANA。
  • properties: 该元素用于放置有关个人的其他属性。例如,指向个人图像或即时消息句柄的链接。不同的插件可能会使用这些属性,也可能只是给阅读 POM 的其他开发人员使用。

Contributors-贡献者

贡献者就像开发人员一样,但在项目的生命周期中扮演辅助角色。 也许贡献者发送了错误修复,或者添加了一些重要的文档。 一个健康的开源项目可能会有比开发人员更多的贡献者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<contributors>
<contributor>
<name>Noelle</name>
<email>some.name@gmail.com</email>
<url>http://noellemarie.com</url>
<organization>Noelle Marie</organization>
<organizationUrl>http://noellemarie.com</organizationUrl>
<roles>
<role>tester</role>
</roles>
<timezone>America/Vancouver</timezone>
<properties>
<gtalk>some.name@gmail.com</gtalk>
</properties>
</contributor>
</contributors>
...
</project>

Environment Settings

Issue Management-问题管理

这定义了所使用的bug跟踪系统(Bugzilla、TestTrack、ClearQuest 等)。尽管没有什么可以阻止插件使用此信息进行某些操作,但它主要用于生成项目文档。

1
2
3
4
5
6
7
8
9
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<issueManagement>
<system>Bugzilla</system>
<url>http://127.0.0.1/bugzilla/</url>
</issueManagement>
...
</project>

Continuous Integration Management-持续集成管理

在过去几年中,基于触发器或定时(如每小时或每天)的持续集成构建系统比手动构建系统更受欢迎。随着构建系统越来越标准化,运行触发这些构建的系统也越来越标准化。虽然大部分配置取决于所使用的特定程序(Continuum、Cruise Control 等),但也有一些配置可能会在 POM 中进行。Maven 在通知器元素集中捕捉到了一些经常出现的重复设置。通知器是通知人们某些构建状态的方式。在以下示例中,此 POM 设置邮件类型的通知程序(即电子邮件),并配置要在指定触发器sendOnErrorsendOnFailure上使用的电子邮件地址,而不是sendOnSuccesssendOnWarning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<ciManagement>
<system>continuum</system>
<url>http://127.0.0.1:8080/continuum</url>
<notifiers>
<notifier>
<type>mail</type>
<sendOnError>true</sendOnError>
<sendOnFailure>true</sendOnFailure>
<sendOnSuccess>false</sendOnSuccess>
<sendOnWarning>false</sendOnWarning>
<configuration><address>continuum@127.0.0.1</address></configuration>
</notifier>
</notifiers>
</ciManagement>
...
</project>

Mailing Lists-邮件列表

邮件列表是与项目相关人员保持联系的绝佳工具。大多数邮件列表都面向开发人员和用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<mailingLists>
<mailingList>
<name>User List</name>
<subscribe>user-subscribe@127.0.0.1</subscribe>
<unsubscribe>user-unsubscribe@127.0.0.1</unsubscribe>
<post>user@127.0.0.1</post>
<archive>http://127.0.0.1/user/</archive>
<otherArchives>
<otherArchive>http://base.google.com/base/1/127.0.0.1</otherArchive>
</otherArchives>
</mailingList>
</mailingLists>
...
</project>
  • subscribe, unsubscribe: 些元素指定用于执行相关操作的电子邮件地址。要订阅上面的用户列表,用户将向 user-subscribe@127.0.0.1 发送电子邮件。
  • archive: 此元素指定旧邮件列表电子邮件的存档的 URL(如果存在)。 如果有镜像存档,可以在otherArchives下指定。
  • post: 用于发布到邮件列表的电子邮件地址。 请注意,并非所有邮件列表都能够发帖(例如构建失败列表)。

SCM-版本控制

SCM(软件配置管理,也称为源代码/控制管理,或者简单地说,版本控制)是任何健康项目不可或缺的一部分。 如果您的 Maven 项目使用 SCM 系统(确实如此,不是吗?),那么您可以在此处将该信息放入 POM 中。

1
2
3
4
5
6
7
8
9
10
11
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<scm>
<connection>scm:svn:http://127.0.0.1/svn/my-project</connection>
<developerConnection>scm:svn:https://127.0.0.1/svn/my-project</developerConnection>
<tag>HEAD</tag>
<url>http://127.0.0.1/websvn/my-project</url>
</scm>
...
</project>
  • connection, developerConnection: 这两个连接元素传达了如何通过 Maven 连接到版本控制系统。连接需要 Maven 的读取访问权限才能找到源代码(例如更新),而developerConnection 则需要提供写入访问权限的连接。Maven 项目衍生了另一个名为 Maven SCM 的项目,该项目为任何希望实现它的 SCM 创建了一个通用 API。 最流行的是 CVS 和 Subversion,但是,其他受支持的 SCM 的列表也在不断增加。 所有 SCM 连接都是通过通用 URL 结构建立的。

    1
    scm:[provider]:[provider_specific]
  • 其中provider是SCM系统的类型。 例如,连接到 CVS 存储库可能如下所示:

    1
    scm:cvs:pserver:127.0.0.1:/cvs/root:my-project
  • tag: 指定该项目所在的标签。 HEAD(即 SCM root)是默认值。

  • url: 可公开浏览的存储库。 例如,通过 ViewCVS。

Prerequisites-先决条件

POM 可能需要某些先决条件才能正确执行。在 POM 4.0.0 中,唯一作为先决条件存在的元素是maven元素,它需要一个最低版本号。

使用 Maven Enforcer Plugin 的requireMavenVersion规则或其他规则作为构建时的先决条件。对于打包的maven-plugin,运行时仍会使用该规则,以确保满足插件的最低 Maven 版本要求(但仅限于引用插件的 pom.xml)。

1
2
3
4
5
6
7
8
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<prerequisites>
<maven>2.0.6</maven>
</prerequisites>
...
</project>

Repositories-存储库

存储库是遵循 Maven 存储库目录布局的工程集合。要成为 Maven 资源库工程,POM 文件必须位于$BASE_REPO/groupId/artifactId/version/artifactId-version.pom结构中。$BASE_REPO可以是本地的(文件结构),也可以是远程的(基本 URL);其余布局将保持不变。资源库是收集和存储artifacts的地方。每当项目依赖于某个artifact时,Maven 将首先尝试使用指定工件的本地副本。 如果本地存储库中不存在该工件,它将尝试从远程存储库下载。POM 中的版本库元素指定了要搜索的备用版本库。

存储库是 Maven 社区最强大的功能之一。 默认情况下,Maven 搜索 https://repo.maven.apache.org/maven2/ 的中央存储库。 可以在 pom.xml repositories 元素中配置其他存储库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<repositories>
<repository>
<releases>
<enabled>false</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>warn</checksumPolicy>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
<checksumPolicy>fail</checksumPolicy>
</snapshots>
<name>Nexus Snapshots</name>
<id>snapshots-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<layout>default</layout>
</repository>
</repositories>
<pluginRepositories>
...
</pluginRepositories>
...
</project>
  • releases, snapshots: 这些是针对每种类型的工件(Release或者snapshot)的策略。有了这两套策略,POM 就能在单个版本库中改变每种类型的策略,而不受另一种策略的影响。例如,可能出于开发目的,有人会决定只启用快照下载。
  • enabled:truefalse,表示该版本库是否为相应类型(Release或者snapshot)启用。
  • updatePolicy: 该元素指定了尝试更新的频率。Maven 将比较本地 POM 的时间戳(存储在版本库的 maven-metadata 文件中)和远程 POM 的时间戳。可选项有:alwaysdaily(默认)、interval:X(X 是以分钟为单位的整数)或never(仅在本地版本库中不存在时下载)。
  • checksumPolicy: 当Maven将文件部署到存储库时,它也会部署相应的校验和文件。当校验和缺失或者不正确您的可选项是ignore,fail, orwarn
  • layout: 在上文对资源库的描述中,提到它们都遵循一个共同的布局。这基本上是正确的。Maven 2 引入的布局是 Maven 2 和 3 使用的版本库默认布局。不过,Maven 1.x 有不同的布局。使用此元素指定它是默认的还是旧的。

Plugin Repositories-插件存储库

存储库是两种主要类型artifacts的所在地。第一种类型是用作其他工程的依赖项,另一种类型的工程是插件。Maven 插件本身就是一种特殊类型的工程。因此,插件存储库可能与其他存储库分开(尽管,我还没有听到这样做的令人信服的论据)。 无论如何,pluginRepositories 元素块的结构与 repositories 元素类似。 每个pluginRepository 元素都指定Maven 可以找到新插件的远程位置。

Distribution Management-分发管理

分发管理的作用正如其听起来一样:它管理整个构建过程中生成的工件和支持文件的分发。 首先从最后一个元素开始:

1
2
3
4
5
6
7
8
9
10
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<distributionManagement>
...
<downloadUrl>http://mojo.codehaus.org/my-project</downloadUrl>
<status>deployed</status>
</distributionManagement>
...
</project>
  • downloadUrl: 是另一个 POM 可以指向的存储库的 URL,以便获取此 POM 的工件。 用最简单的话来说,我们告诉 POM 如何上传它(通过repository/url),但是公众可以从哪里下载它呢? 这个元素回答了这个问题。
  • status: 警告!就像巢中的雏鸟一样,status永远不应该被人类的手触碰!这样做的原因是,当项目被传输到存储库时,Maven 会自动设置项目的状态。 其有效类型如下。
    • none: 没有特殊的状态。 这是 POM 的默认设置。
    • converted: 存储库的管理员将此 POM 从早期版本转换为 Maven 2。
    • partner: 该artifact已与合作伙伴资源库同步。
    • deployed: :迄今为止最常见的状态,这意味着该工件是从 Maven 2 或 3 实例部署的。 这是使用命令行deploy手动部署时得到的结果。
    • verified: 该项目已经过验证,应视为已完成。

Repository-存储库

repositories 元素在 POM 中指定了 Maven 下载远程工件供当前项目使用的位置和方式,而 distributionManagement 则指定了该项目在部署时从哪里(以及如何)进入远程版本库。如果未定义快照存储库(snapshotRepository),存储库元素将用于快照分发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<distributionManagement>
<repository>
<uniqueVersion>false</uniqueVersion>
<id>corp1</id>
<name>Corporate Repository</name>
<url>scp://repo/maven2</url>
<layout>default</layout>
</repository>
<snapshotRepository>
<uniqueVersion>true</uniqueVersion>
<id>propSnap</id>
<name>Propellors Snapshots</name>
<url>sftp://propellers.net/maven</url>
<layout>legacy</layout>
</snapshotRepository>
...
</distributionManagement>
...
</project>
  • id, name:id用于在众多资源库中唯一标识该资源库,name是人类可读的形式。
  • uniqueVersion: 唯一版本,取值为truefalse,表示部署到该版本库的工件是应该获得唯一生成的版本号,还是使用定义为地址一部分的版本号。
  • url: 这是版本库元素的核心。它指定了用于将构建的工件(以及 POM 文件和校验和数据)传输到版本库的位置和传输协议。
  • layout: 这些与存储库元素中定义的布局元素具有相同的类型和用途。

Site Distribution-站点发布

分发管理distributionManagement不仅负责向资源库分发,还负责定义如何部署项目网站和文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<distributionManagement>
...
<site>
<id>mojo.website</id>
<name>Mojo Website</name>
<url>scp://beaver.codehaus.org/home/projects/mojo/public_html/</url>
</site>
...
</distributionManagement>
...
</project>
  • id, name, url: 这些元素与上面的 distributionManagement 资源库元素中的对应元素类似。

Relocation-搬迁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<project xmlns="http://maven.apache.org/POM/4.0.0"1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<distributionManagement>
...
<relocation>
<groupId>org.apache</groupId>
<artifactId>my-project</artifactId>
<version>1.0</version>
<message>We have moved the Project under Apache</message>
</relocation>
...
</distributionManagement>
...
</project>

项目不是一成不变的;它们是有生命的东西(或垂死的东西,视情况而定)。随着项目的发展壮大,一个常见的情况是项目被迫迁往更合适的地方。例如,当你的下一个大获成功的开源项目转移到 Apache 旗下时,最好能给用户提个醒,告诉他们项目将更名为org.apache:my-project:1.0。除了指明新地址外,提供一条解释原因的信息也是很好的形式。

Profiles

POM 4.0 的一个新功能是项目可以根据构建环境改变设置。一个profile元素既包含可选激活(配置文件触发器),也包含激活该配置文件后对 POM 所做的一系列更改。例如,为测试环境构建的项目可能与最终部署环境有着不同的数据库。或者,根据所使用的 JDK 版本不同,依赖关系也可能来自不同的资源库。The elements of profiles are as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<profiles>
<profile>
<id>test</id>
<activation>...</activation>
<build>...</build>
<modules>...</modules>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<dependencies>...</dependencies>
<reporting>...</reporting>
<dependencyManagement>...</dependencyManagement>
<distributionManagement>...</distributionManagement>
</profile>
</profiles>
</project>

Activation

ActivationsProfiles的关键。只有在特定情况下,配置文件才能修改基本 POM。这些情况通过激活一个activation元素来指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<profiles>
<profile>
<id>test</id>
<activation>
<activeByDefault>false</activeByDefault>
<jdk>1.5</jdk>
<os>
<name>Windows XP</name>
<family>Windows</family>
<arch>x86</arch>
<version>5.1.2600</version>
</os>
<property>
<name>sparrow-type</name>
<value>African</value>
</property>
<file>
<exists>${basedir}/file2.properties</exists>
<missing>${basedir}/file1.properties</missing>
</file>
</activation>
...
</profile>
</profiles>
</project>

自 Maven 3.2.2 以来,当满足所有指定条件时才会激活。

  • jdk:activation在 jdk 元素中具有内置的、以 Java 为中心的检查。 该值为以下三种类型之一:

    • 根据 maven-enforcer-plugin 定义的版本范围,[(,
    • 如果值以!开头,则为否定前缀
    • 一个(无负数)前缀,用于所有其他情况
  • 如果运行 Maven 所用的 JDK 版本以给定的前缀开始/不以给定的前缀开始(不包括潜在的前导 !),则前缀(否定)值匹配。如果运行 Maven 所用的 JDK 版本介于下限和上限之间(包含或排除),则值范围匹配。

  • os: os 元素可以定义一些特定于操作系统的属性,如上图所示。有关操作系统值的更多详情,请参阅 maven-enforcer-plugins RequireOS Rule。

  • property: 如果 Maven 检测到相应name=value对的系统属性或 CLI 用户属性(可以在 POM 中通过 ${name} 解除引用)与给定值(如果给定)匹配,则profile将激活。

  • file: 最后,给定的文件名可以通过文件存在或丢失来激活配置文件。 注意:此元素的插值仅限于 ${basedir}、系统属性和请求属性。


Maven依赖的优先原则

使用Maven的程序员都会遇到一个问题,那就是Maven依赖冲突的问题,这会导致ClassNotFound或者MethodNotFound这样的异常。其实只要明白Maven依赖的根本性的原则就不怕这样的问题了。

依赖路径最短优先

一个Demo项目依赖了两个jar包,其中A-B-C-X(1.0)A-D-X(2.0)。由于X(2.0)路径最短,所以项目使用的是X(2.0)

申明顺序优先

如果A-B-X(1.0)A-C-X(2.0) 这样的路径长度一样怎么办呢?这样的情况下,maven会根据pom文件声明的顺序加载,如果先声明了B,后声明了C,那就最后的依赖就会是X(1.0)

所以maven依赖原则总结起来就两条:路径最短,申明顺序其次。


Final

Maven POM 体积庞大。不过,它的大小也证明了它的多功能性。

至少可以说,将项目的所有方面抽象为单个工件的能力是强大的。 每个项目都有数十个不同的构建脚本和分散的文档的日子已经一去不复返了。

与 Maven 一起组成 Maven 星系的其他明星—定义明确的构建生命周期、易于编写和维护的插件、集中式存储库、系统范围和基于用户的配置,以及越来越多的工具来完成开发人员的工作 更容易维护复杂的项目 - POM 是大而明亮的中心。

介绍

pandas 是一个 Python 包,它提供快速、灵活且富有表现力的数据结构,旨在使“关系”或“标记”数据的处理变得简单直观。 它的目标是成为用 Python 进行实际、真实世界数据分析的基本高级构建块。 此外,它还有更广泛的目标,即成为任何语言中最强大、最灵活的开源数据分析/操作工具。 它已经在朝着这个目标前进。

pandas 非常适合下列这些类型的数据:

  • 具有异构类型列的表格数据,如 SQL 表或 Excel 电子表格
  • 有序和无序(不一定是固定频率)时间序列数据
  • 带有行和列标签的任意矩阵数据(同质类型或异构)
  • 任何其他形式的观察/统计数据集。数据根本不需要被标记就可以放入 pandas 数据结构中

主要特点

pandas 的两种主要数据结构Series(一维)和DataFrame(二维)处理金融、统计、社会科学和许多工程领域的绝大多数典型用例。对于 R 用户,DataFrame提供 R data.frame提供的一切以及更多。pandas 构建在NumPy之上,旨在与许多其他第三方库在科学计算环境中良好集成。

以下是 pandas 擅长的一些事情:

  • 轻松处理浮点和非浮点数据中的缺失数据(表示为 NaNNANaT
  • 大小可变性:可以从 DataFrame 和更高维对象中插入和删除列
  • 自动和精确的数据对齐:对象可以准确地与一组数据对齐,或者用户可以简单地忽略数据并让SeriesDataFrame等在计算中自动为您对齐数据
  • 强大、灵活的分组功能,可对数据集执行拆分-应用-组合操作,以聚合和转换数据
  • 可以轻松地将其他 PythonNumPy 数据结构中的不规则、不同索引的数据转换为 DataFrame 对象
  • 基于智能标签的切片、高级索引和大数据集的子集
  • 直观的合并和连接数据集
  • 灵活的重塑和翻转数据集
  • 坐标轴的分层标签(每个刻度可能有多个标签)
  • 强大的IO工具,用于从平面文件(CSV and delimited)、Excel文件、数据集以及从超快HDF5格式保存或加载的数据

安装

pandas 可以通过PyPI中的 pip 安装。

1
pip install pandas

pandas 处理什么样的数据?

导包

1
import pandas as pd

pandas 数据表表示

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd
df = pd.DataFrame(
{"Name": [
"张三",
"李四",
"王五",
],
"Age": [22, 24, 26],
"Sex": ["男", "男", "女"],
}
)
if __name__ == '__main__':
print(df)

输出:

在电子表格软件中,我们数据的表格表示看起来非常相似:

DataFrame中每一列都是一个 Series

如果只对Name这一列的数据感兴趣:

1
pring(df["Age"])

输出:

1
2
3
4
0    22
1 24
2 26
Name: Age, dtype: int64

当选择DataFrame中的单列时,结果是 Series。要选择列,请使用方括号之间的列标签[]

创建Series

1
2
ages = pd.Series([20, 30, 40], name="Age")
print(ages)

输出:

1
2
3
4
0    20
1 30
2 40
Name: Age, dtype: int64

使用 DataFrame 或 Series 做一些事情

我想知道谁的年龄最大?

DataFrame我们可以通过选择Age列并应用max()函数:

1
2
ages = pd.Series([20, 30, 40], name="Age")
print(ages.max())

输出:

1
40

DataFrameSeries还有许多其他函数供我们使用,可以通过下面链接进行查看。

Series

DataFrame

假如我们只对数据表的数值数据的一些基本统计感兴趣:

1
print(df.describe())

Out:

1
2
3
4
5
6
7
8
9
        Age
count 3.0
mean 24.0
std 2.0
min 22.0
25% 23.0
50% 24.0
75% 25.0
max 26.0

describe() 方法可快速浏览 DataFrame 中的数字数据。由于NameSex列是文本数据,因此 describe() 方法默认不考虑这两列。


如何读写表格数据?

数据集:

本教程使用成人人口普查收入数据集,存储为 csv 文件。如下所示:

Pandas提供了read_csv()函数将Excel文件读取为DataFrame对象。

pandas 支持许多不同的文件格式或开箱即用的数据源(csvexcelsqljsonparquet…),读取每个文件格式或数据源都使用带有前缀read_*的函数。

1
2
adult = pd.read_csv("data/adult.csv")
print(adult)

Out:

请确保读完数据之后总是有一个数据检查。显示 DataFrame 时,默认显示前 5 行和后 5 行。

要查看 DataFrame 的前 N 行,请使用该 head() 方法并以所需的行数作为参数。

我想查看 pandas DataFrame 的前 8 行。

1
print(adult.head(8))

Out:

1
2
3
4
5
6
7
8
9
   age  workclass  fnlwgt  ... hours.per.week  native.country income
0 90 ? 77053 ... 40 United-States <=50K
1 82 Private 132870 ... 18 United-States <=50K
2 66 ? 186061 ... 40 United-States <=50K
3 54 Private 140359 ... 40 United-States <=50K
4 41 Private 264663 ... 40 United-States <=50K
5 34 Private 216864 ... 45 United-States <=50K
6 38 Private 150601 ... 40 United-States <=50K
7 74 State-gov 88638 ... 20 United-States >50K

对最后 N 行感兴趣吗?pandas也提供了一种 tail() 方法。例如,adult.tail(10)将返回 DataFrame 的最后 10 行。

可以通过请求 pandas dtypes 属性来检查 pandas 如何解释每个列数据类型:

1
print(adult.dtypes)

Out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
age                int64
workclass object
fnlwgt int64
education object
education.num int64
marital.status object
occupation object
relationship object
race object
sex object
capital.gain int64
capital.loss int64
hours.per.week int64
native.country object
income object
dtype: object

对于每一列,都会列出所使用的数据类型。其中的数据类型DataFrame为整数 ( int64)和字符串 ( object)。

DataFrame保存为Excel文件:

1
adult.to_excel("adult.xlsx", sheet_name="person", index=False)

read_*函数常用于读取函数到Pandas中,而to_*函数常用于存储数据。

to_excel()方法将数据存储为 Excel 文件。在此示例中,sheet_name名为“person”而不是默认的“Sheet1”。通过设置 index=False行索引标签不会保存在电子表格中。

读取函数read_excel()会将数据重新加载到 DataFrame

1
2
adult = pd.read_excel("adult.xlsx")
print(adult)

Out:

1
2
3
4
5
6
7
8
9
10
11
12
       age workclass  fnlwgt  ... hours.per.week  native.country income
0 90 ? 77053 ... 40 United-States <=50K
1 82 Private 132870 ... 18 United-States <=50K
2 66 ? 186061 ... 40 United-States <=50K
3 54 Private 140359 ... 40 United-States <=50K
4 41 Private 264663 ... 40 United-States <=50K
... ... ... ... ... ... ... ...
32556 22 Private 310152 ... 40 United-States <=50K
32557 27 Private 257302 ... 38 United-States <=50K
32558 40 Private 154374 ... 40 United-States >50K
32559 58 Private 151910 ... 40 United-States <=50K
32560 22 Private 201490 ... 20 United-States <=50K

我对 DataFrame 的技术摘要感兴趣

1
print(adult.info())

Out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 32561 entries, 0 to 32560
Data columns (total 15 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 age 32561 non-null int64
1 workclass 32561 non-null object
2 fnlwgt 32561 non-null int64
3 education 32561 non-null object
4 education.num 32561 non-null int64
5 marital.status 32561 non-null object
6 occupation 32561 non-null object
7 relationship 32561 non-null object
8 race 32561 non-null object
9 sex 32561 non-null object
10 capital.gain 32561 non-null int64
11 capital.loss 32561 non-null int64
12 hours.per.week 32561 non-null int64
13 native.country 32561 non-null object
14 income 32561 non-null object
dtypes: int64(6), object(9)
memory usage: 3.7+ MB

info()方法提供了有关 DataFrame 的技术信息 ,因此让我们对输出作出更详细地解释:

  • 它确实是一个DataFrame
  • 有 32561 个entries,即 32561 行
  • 每行都有一个行标签(也称为index),其值范围为 0 到 32560
  • 该表有 15列。大多数列的每一行都有一个值(所有 32561 个值都是non-null
  • workclass education marital.status occupation relationship race sex native.country income列都是由文本数据组成(String 又称 Object)
  • 其他列是数值数据,其中一些是整数(又名integer
  • 不同列中的数据类型(字符、整数……)通过dtypes列出
  • 还提供了用于保存 DataFrame 的大致 RAM

选择DataFrame的子集

如何选择DataFrame的特定列?

假如我们只对Age感兴趣:

1
2
3
adult = pd.read_excel("adult.xlsx")
adult_subset = adult.head()
print(adult_subset["age"])

Out:

1
2
3
4
5
6
0    90
1 82
2 66
3 54
4 41
Name: age, dtype: int64

DataFrame 中的每一列都是一个Series。当选择单个列时,返回的对象是 pandas Series。我们可以通过检查输出的类型来验证这一点:

1
print(type(adult["age"]))

Out:

1
<class 'pandas.core.series.Series'>

看看shape输出:

1
print(adult["age"].shape)

Out:

1
(5,)

DataFrame.shapeSeriesDataFram 的一个属性。DataFrame包含行数和列数:(nrows, ncolumns)。pandas Series 是一维的,仅返回行数。

假如对年龄和性别感兴趣:

1
2
age_sex = adult[["age", "sex"]]
print(age_sex)

Out:

1
2
3
4
5
6
   age     sex
0 90 Female
1 82 Female
2 66 Female
3 54 Female
4 41 Female

要选择多个列,请在选择括号内使用列名称列表[]

返回的数据类型是 pandas DataFrame

1
print(type(age_sex))

Out:

1
<class 'pandas.core.frame.DataFrame'>

In:

1
print(age_sex.shape)

Out:

1
(5, 2)

返回的DataFrame具有5 行 2 列。


如何选择DataFrame的特定行?

假如对 60岁以上的客户感兴趣。

1
print(adult[adult["age"] > 60])

Out:

1
2
3
4
   age workclass  fnlwgt  ... hours.per.week  native.country income
0 90 ? 77053 ... 40 United-States <=50K
1 82 Private 132870 ... 18 United-States <=50K
2 66 ? 186061 ... 40 United-States <=50K

括号内的条件 adult["age"] > 60 检查哪些行 Age 列的值大于 60:

1
print(adult["age"] > 60)

Out:

1
2
3
4
5
6
0     True
1 True
2 True
3 False
4 False
Name: age, dtype: bool

条件表达式(也可以是 ==, !=, <, <=,… )的输出实际上是一组pandas Series的布尔值(TrueFalse),

其行数与原始 DataFrame 相同 。这样的一系列布尔值可用于将其放在选择括号 [] 之间来过滤 DataFrame, 仅选择值为 True 的行。

假如我们只对native.countryChinaIndia的用户感兴趣。

1
2
country = adult[adult["native.country"].isin(["China", "India"])]
print(country.head())

Out:

1
2
3
4
5
6
     age         workclass  fnlwgt  ... hours.per.week  native.country income
63 51 Self-emp-not-inc 160724 ... 40 China >50K
90 39 Private 198654 ... 67 India >50K
214 42 Self-emp-inc 23510 ... 60 India >50K
244 64 Private 149044 ... 60 China <=50K
377 30 Private 315640 ... 40 China >50K

与条件表达式类似,isin() 条件函数为匹配条件列表中的每一行返回 True。要根据此类函数过滤行,请使用括号内的条件函数,括号内的条件函数检查native.countryChinaIndia的行。

与下列等价:

1
2
country = adult[(adult["native.country"] == "China") | (adult["native.country"] == "India")]
print(country.head())

Out:

1
2
3
4
5
6
     age         workclass  fnlwgt  ... hours.per.week  native.country income
63 51 Self-emp-not-inc 160724 ... 40 China >50K
90 39 Private 198654 ... 67 India >50K
214 42 Self-emp-inc 23510 ... 60 India >50K
244 64 Private 149044 ... 60 China <=50K
377 30 Private 315640 ... 40 China >50K

组合多个条件语句时,每个条件必须用括号 () 括起来。 而且,不能使用 or and ,而需要使用或运算符 | 以及与运算符 &

假如我们只对年龄已知的用户感兴趣。

1
2
3
adult = pd.read_excel("adult.xlsx")
age_not_no = adult[adult["age"].notna()]
print(age_not_no.head())

Out:

1
2
3
4
5
6
7
8
   age workclass  fnlwgt  ... hours.per.week  native.country income
0 90 ? 77053 ... 40 United-States <=50K
1 82 Private 132870 ... 18 United-States <=50K
2 66 ? 186061 ... 40 United-States <=50K
3 54 Private 140359 ... 40 United-States <=50K
4 41 Private 264663 ... 40 United-States <=50K

[5 rows x 15 columns]

notna() 条件函数对于值不是 Null 值的每一行返回 True。 因此,可以结合选择括号[]来过滤数据表。

如何选择DataFrame的特定行和列?

假如对35 岁以上人员的性别感兴趣。

1
2
adult_names = adult.loc[adult["age"] > 35, "sex"]
print(adult_names.head())

在这种情况下,行和列的子集是一次性生成的,仅使用括号选择 [] 已经不够了。 选择括号 [] 前面需要 loc/iloc 运算符。 使用 loc/iloc 时,逗号之前的部分是您想要选择的行,逗号之后的部分是您想要选择的列。

使用列名、行标签或条件表达式时,请在选择括号 [] 前面使用 loc 运算符。 对于逗号之前和之后的部分,您可以使用单个标签、标签列表、标签切片、条件表达式或冒号。 使用冒号指定您要选择所有行或列。

假如对第 10 行到第 25 行和第 3 到第 5 列感兴趣。

1
print(adult.iloc[9:25, 2:5])

Out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    fnlwgt     education  education.num
9 70037 Some-college 10
10 172274 Doctorate 16
11 164526 Prof-school 15
12 129177 Bachelors 13
13 136204 Masters 14
14 172175 Doctorate 16
15 45363 Prof-school 15
16 172822 11th 7
17 317847 Masters 14
18 119592 Assoc-acdm 12
19 203034 Bachelors 13
20 188774 Bachelors 13
21 77009 11th 7
22 29059 HS-grad 9
23 153870 Some-college 10
24 135285 HS-grad 9

同样,行和列的子集是一次性生成的,仅使用选择括号 [] 已经不够了。 当根据表中的位置对某些行和/或列特别感兴趣时,请在选择括号 [] 前面使用 iloc 运算符。

当使用 loc 或 iloc 选择特定行和/或列时,我们对我们所选的特定行和/或列赋值。

例如,要将前三行人员的年龄统一改成20:

1
2
adult.iloc[0:3, 0] = 20
print(adult.head(5))

Out:

1
2
3
4
5
6
   age workclass  fnlwgt  ... hours.per.week  native.country income
0 20 ? 77053 ... 40 United-States <=50K
1 20 Private 132870 ... 18 United-States <=50K
2 20 ? 186061 ... 40 United-States <=50K
3 54 Private 140359 ... 40 United-States <=50K
4 41 Private 264663 ... 40 United-States <=50K

Remember:

  • 要选择数据子集时,使用方括号[]。
  • 在括号内,您可以使用单个列/行标签、列/行标签列表、标签切片、条件表达式或冒号。
  • 通过 loc 使用行名和列名选择特定的行和列。
  • 通过 iloc 使用坐标轴位置选择特定的行和列。
  • 你可以为 loc 或 iloc 选择的行或列赋新值。

如何在 pandas 中创建绘图?

导入相关类库:

本教程使用的数据:空气质量数据

在本教程中,使用了有关的空气质量数据,这些数据由 OpenAQ 提供并使用 py-openaq 包。 air_quality_no2.csv 数据集提供了分别位于巴黎、安特卫普和伦敦的测量站 FR04014、BETR801 和伦敦威斯敏斯特的值。

读取数据:

1
2
air_quality = pd.read_csv("data/air_quality_no2.csv", index_col=0, parse_dates=True)
print(air_quality.head())

Out:

1
2
3
4
5
6
7
                     station_antwerp  station_paris  station_london
datetime
2019-05-07 02:00:00 NaN NaN 23.0
2019-05-07 03:00:00 50.5 25.0 19.0
2019-05-07 04:00:00 45.0 27.7 19.0
2019-05-07 05:00:00 NaN 50.4 16.0
2019-05-07 06:00:00 NaN 61.9 NaN

read_csv 函数的 index_col参数表示将第一(第 0)列定义为结果 DataFrame 的索引。

read_csv 函数的 parse_dates参数表示将列中的日期转换为 Timestamp 对象。

通过视图快速的检查数据:

1
2
3
air_quality = pd.read_csv("data/air_quality_no2.csv", index_col=0, parse_dates=True)
air_quality.plot()
plt.show()

pandas 默认为DataFrame包含数字的每一列数创建一个线图。

我只想用巴黎的数据列绘制图形。

我想直观地比较NO2在伦敦和巴黎测量的值。

1
2
air_quality.plot.scatter(x="station_london", y="station_paris", alpha=0.5)
plt.show()

除了使用绘图功能时的默认线图之外,还可以使用许多其他图形来绘制数据。

绘图方法允许使用除默认线图之外的多种绘图样式,可以通过plot()方法的kind参数进行设置:

  • "bar" "barh" 绘制柱状图
  • "hist" 绘制直方图
  • "box" 绘制箱形图
  • "kde" "density" 绘制密度图
  • "area" 绘制面积图
  • "scatter" 绘制散点图
  • "hexbin" 绘制六边形箱图
  • "pie" 绘制饼状图

例如,可以通过以下方式创建箱线图:

1
2
air_quality.plot.box()
plt.show()

我希望每一列都在一个单独的子图中。

1
2
axs = air_quality.plot.area(figsize=(12, 4), subplots=True)
plt.show()

我想进一步定制、扩展或保存结果图。

pandas 创建的每个绘图对象都是 Matplotlib对象。由于 Matplotlib 提供了大量的选项来自定义绘图,因此使 pandasMatplotlib 之间的关联显式化,从而可以将 Matplotlib 的所有功能发挥到绘图上。


如何从现有列创建新列

教程数据如下:

假设我们想在表格中新增一列表示所有商场第一季度的收入:

1
2
market_sales["first_season_sales"] = market_sales.iloc[:, 0:4].sum(axis=1)
print(market_sales)

Out:

1
2
3
4
5
6
7
8
        sales_01  sales_02  sales_03  sales_04  first_season_sales
market
沃尔玛 10000 30000 80000 60000 180000
家乐福 20000 20000 20000 20000 80000
好又多 30000 50000 10000 10000 100000
大润发 40000 60000 50000 30000 180000
华润万家 50000 40000 60000 20000 170000
永辉 60000 10000 20000 10000 100000

其他数学运算符(+、-、*、/、…)或逻辑运算符(<、>、==、…)也按此逻辑工作。

如果您需要更高级的逻辑,您可以通过apply().

如果你想对某些列名进行重命名操作:

1
2
3
4
5
6
7
8
9
10
renamed_market_sales = market_sales.rename(
columns={

"sales_01": "SALES_FIRST_MONTH",
"sales_02": "SALES_SECOND_MONTH",
"sales_03": "SALES_THIRD_MONTH",
"sales_04": "SALES_FOURTH_MONTH"
}
)
print(renamed_market_sales)

Out:

1
2
3
4
5
6
7
8
        SALES_FIRST_MONTH  ...  SALES_FOURTH_MONTH
market ...
沃尔玛 10000 ... 60000
家乐福 20000 ... 20000
好又多 30000 ... 10000
大润发 40000 ... 30000
华润万家 50000 ... 20000
永辉 60000 ... 10000

rename() 函数可用于行和列。 你需要提供一个字典,其中包含当前名称的键和新名称的值,以更新相应的名称。

映射不应仅限于固定名称,也可以是映射函数。 例如,也可以使用函数将列名转换为小写字母:

1
2
renamed_market_sales = renamed_market_sales.rename(columns=str.lower)
print(renamed_market_sales)

Out:

1
2
3
4
5
6
7
8
        sales_first_month  ...  sales_fourth_month
market ...
沃尔玛 10000 ... 60000
家乐福 20000 ... 20000
好又多 30000 ... 10000
大润发 40000 ... 30000
华润万家 50000 ... 20000
永辉 60000 ... 10000

如何计算汇总统计数据

数据集:本教程使用成人人口普查收入数据集。

汇总统计数据

所有人员的平均年龄是多少?

1
2
adult = pd.read_excel("data/adult.xlsx")
print(adult["age"].mean())

Out:

1
38.58164675532078

可以使用不同的统计数据,并将其应用于具有数值数据的列。 一般情况下,操作会排除缺失数据并默认跨行(按列)操作。

我们可以使用median函数来计算一系列数值的中位数。这个函数可以用于计算 SeriesDataFrame 对象中的中位数。:

1
2
adult = pd.read_excel("data/adult.xlsx")
print(adult[["age", "hours.per.week"]].median())

Out:

1
2
3
age               37.0
hours.per.week 40.0
dtype: float64

可以使用 DataFrame.agg() 方法定义给定列的聚合统计信息的特定组合,而不是预定义的统计信息:

1
2
3
4
5
              age  hours.per.week
min 17.000000 1.000000
max 90.000000 99.000000
median 37.000000 40.000000
skew 0.558743 0.227643

聚合统计数据并按类别分组

统计所有男性和女性的平均年龄是多少?

1
print(adult[["age", "sex"]].groupby("sex").mean())

Out:

1
2
3
4
              age
sex
Female 36.858230
Male 39.433547

由于我们感兴趣的是每个性别的平均年龄,因此首先对这两列进行子选择:titanic[["Sex", "Age"]]。 接下来,对 “Sex” 列应用 groupby() 方法,为每个类别创建一个组。 计算并返回每个性别组的平均年龄。

如果我们只对每个性别的平均年龄感兴趣,则分组数据也支持列的选择(像往常一样的矩形括号 []):

1
print(adult.groupby("sex")["age"].mean())

Out:

1
2
3
4
sex
Female 36.858230
Male 39.433547
Name: age, dtype: float64

按类别统计记录数

统计每个性别的人数是多少:

1
print(adult["sex"].value_counts())

Out:

1
2
3
4
sex
Female 10771
Male 21790
Name: count, dtype: int64

该函数是一个快捷方式,因为它实际上是一个 groupby 操作,并结合了每组内记录数的计数:

1
print(adult.groupby("sex")["sex"].count())

size和两者都count可以与 结合使用 groupby。而size包含NaN值并仅提供行数(表的大小),count排除缺失值。在该value_counts方法中,使用dropna参数来包含或排除NaN值。


如何重塑表格布局

让我们使用空气质量数据集的一小部分。我们专注于 NO2数据并仅使用每个位置的前两个测量值。数据子集将被称为no2_subset

1
2
3
4
air_quality = pd.read_csv(
"data/air_quality_long.csv", index_col="date.utc", parse_dates=True
)
print(air_quality.head())

Out:

1
2
3
4
5
6
7
                                city country location parameter  value   unit
date.utc
2019-06-18 06:00:00+00:00 Antwerpen BE BETR801 pm25 18.0 µg/m³
2019-06-17 08:00:00+00:00 Antwerpen BE BETR801 pm25 6.5 µg/m³
2019-06-17 07:00:00+00:00 Antwerpen BE BETR801 pm25 18.5 µg/m³
2019-06-17 06:00:00+00:00 Antwerpen BE BETR801 pm25 16.0 µg/m³
2019-06-17 05:00:00+00:00 Antwerpen BE BETR801 pm25 7.5 µg/m³

空气质量数据集包含以下列:

  • city:使用传感器的城市,巴黎、安特卫普或伦敦
  • 国家:使用传感器的国家,FR、BE 或 GB
  • location:传感器的 ID,FR04014BETR801伦敦威斯敏斯特
  • 参数:传感器测量的参数,或者NO2 或颗粒物
  • value:测量值
  • 单位:测量参数的单位,在本例中为“μg/m³”

DataFrame的索引是datetime测量的日期时间。

对表行进行排序

根据年龄对表格数据进行排序

1
2
adult = pd.read_excel("adult.xlsx")
print(adult.sort_values("age").head())

Out:

1
2
3
4
5
6
7
8
       age workclass  fnlwgt  ... hours.per.week  native.country income
4159 17 Private 130125 ... 20 United-States <=50K
18810 17 Private 187308 ... 15 United-States <=50K
25709 17 Private 153021 ... 20 United-States <=50K
6046 17 ? 103810 ... 40 United-States <=50K
8682 17 ? 127003 ... 40 United-States <=50K

[5 rows x 15 columns]

根据年龄和受教育等级对所有人员进行排序。

1
2
adult_sorted = adult.sort_values(by=["age", "education.num"], ascending=False).head()
print(adult_sorted)
1
2
3
4
5
6
7
8
       age  workclass  fnlwgt  ... hours.per.week  native.country income
1742 90 Private 87372 ... 72 United-States >50K
1739 90 Local-gov 227796 ... 60 United-States >50K
8914 90 Private 51744 ... 50 United-States >50K
20621 90 Private 115306 ... 40 United-States <=50K
22166 90 Private 206667 ... 40 United-States >50K

[5 rows x 15 columns]

使用 DataFrame.sort_values(),表中的行根据定义的列进行排序。 索引将遵循行顺序。

从长到宽的表格格式

1
2
3
4
5
6
7
8
air_quality = pd.read_csv(
"data/air_quality_long.csv", index_col="date.utc", parse_dates=True
)
# filter for no2 data only
no2 = air_quality[air_quality["parameter"] == "no2"]
# use 2 measurements (head) for each location (groupby)
no2_subset = no2.sort_index().groupby(["location"]).head(2)
print(no2_subset)

Out:

1
2
3
4
5
6
7
8
9
                                city country  ... value   unit
date.utc ...
2019-04-09 01:00:00+00:00 Antwerpen BE ... 22.5 µg/m³
2019-04-09 01:00:00+00:00 Paris FR ... 24.4 µg/m³
2019-04-09 02:00:00+00:00 London GB ... 67.0 µg/m³
2019-04-09 02:00:00+00:00 Antwerpen BE ... 53.5 µg/m³
2019-04-09 02:00:00+00:00 Paris FR ... 27.4 µg/m³

[5 rows x 6 columns]

我希望三个站点的值作为彼此相邻的单独列。(即将某些行转换成列)

1
print(no2_subset.pivot(columns="location", values="value"))

Out:

1
2
3
4
5
location                   BETR801  FR04014  London Westminster
date.utc
2019-04-09 01:00:00+00:00 22.5 24.4 NaN
2019-04-09 02:00:00+00:00 53.5 27.4 67.0
2019-04-09 03:00:00+00:00 NaN NaN 67.0

数据透视表

我想要以表格形式获取每个站NO2PM2.5的平均浓度。

1
2
3
4
air_quality_mean = air_quality.pivot_table(
values="value", index="location", columns="parameter", aggfunc="mean"
)
print(air_quality_mean)

Out:

1
2
3
4
5
parameter                 no2       pm25
location
BETR801 26.950920 23.169492
FR04014 29.374284 NaN
London Westminster 29.740050 13.443568

数据透视表是电子表格软件中众所周知的概念。当对每个变量的行/列边距(小计)感兴趣时,请将参数margins设置为True

1
2
3
4
5
6
7
8
air_quality_mean = air_quality.pivot_table(
values="value",
index="location",
columns="parameter",
aggfunc="mean",
margins=True
)
print(air_quality_mean)

Out:

1
2
3
4
5
6
parameter                 no2       pm25        All
location
BETR801 26.950920 23.169492 24.982353
FR04014 29.374284 NaN 29.374284
London Westminster 29.740050 13.443568 21.491708
All 29.430316 14.386849 24.222743

从宽到长的表格格式

我们向DataFrame 加一个新索引reset_index()

1
2
no2_pivoted = no2.pivot(columns="location", values="value").reset_index()
print(no2_pivoted.head())

Out:

1
2
3
4
5
6
location                  date.utc  BETR801  FR04014  London Westminster
0 2019-04-09 01:00:00+00:00 22.5 24.4 NaN
1 2019-04-09 02:00:00+00:00 53.5 27.4 67.0
2 2019-04-09 03:00:00+00:00 54.5 34.2 67.0
3 2019-04-09 04:00:00+00:00 34.5 48.5 41.0
4 2019-04-09 05:00:00+00:00 46.5 59.5 41.0

我想在单列(长格式)中收集所有空气质量二氧化氮测量值。

1
2
no_2 = no2_pivoted.melt(id_vars="date.utc")
print(no_2)

Out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                      date.utc            location  value
0 2019-04-09 01:00:00+00:00 BETR801 22.5
1 2019-04-09 02:00:00+00:00 BETR801 53.5
2 2019-04-09 03:00:00+00:00 BETR801 54.5
3 2019-04-09 04:00:00+00:00 BETR801 34.5
4 2019-04-09 05:00:00+00:00 BETR801 46.5
... ... ... ...
5110 2019-06-20 20:00:00+00:00 London Westminster NaN
5111 2019-06-20 21:00:00+00:00 London Westminster NaN
5112 2019-06-20 22:00:00+00:00 London Westminster NaN
5113 2019-06-20 23:00:00+00:00 London Westminster NaN
5114 2019-06-21 00:00:00+00:00 London Westminster NaN

[5115 rows x 3 columns]

DataFrame 上的 pandas.melt() 方法将数据表从宽格式转换为长格式。 列标题成为新创建的列中的变量名称。

换句话说,也就是将多列合并成一列。

pandas.melt()可以更详细地定义传递给的参数:

1
2
3
4
5
6
7
no_2 = no2_pivoted.melt(
id_vars="date.utc",
value_vars=["BETR801", "FR04014", "London Westminster"],
value_name="NO_2",
var_name="id_location",
)
print(no_2)

上述参数的解释:

  • value_vars定义将哪些列融合在一起
  • value_name为列值提供自定义列名,而不是默认列名value
  • var_name为融合的列提供自定义列名称。否则它采用索引名称或默认值variable
  • id_vars中声明的列将不会被融合
  • id_vars声明的列不会被融合

因此,参数 value_namevar_name 只是两个生成列的用户定义名称。 要融合的列由 id_varsvalue_vars 定义。


如何合并多个表的数据

现有两个班的学生数据 class_01_students.xlsx class_02_students.xlsx 如下:

class_01_students.xlsx

1
2
class_01_students = pd.read_excel("data/class_01_students.xlsx")
print(class_01_students)

Out:

1
2
3
4
5
6
   name   age  sex   height blood
0 张三 20 男 175 O
1 李四 21 女 160 A
2 王五 50 男 170 B
3 赵六 30 男 173 AB
4 田七 40 男 180 A

class_02_students.xlsx:

1
2
class_02_students = pd.read_excel("data/class_02_students.xlsx")
print(class_02_students)

Out:

1
2
3
4
5
6
   name   age sex  height blood
0 小明 18 男 181 O
1 小黄 24 男 177 A
2 小红 17 女 170 B
3 小李 32 男 157 AB
4 小花 29 女 168 A

将两个具有相似结构的表合并成一个表:

1
2
3
4
class_01_students = pd.read_excel("data/class_01_students.xlsx")
class_02_students = pd.read_excel("data/class_02_students.xlsx")
students = pd.concat([class_01_students, class_02_students], axis=0)
print(students)

Out:

1
2
3
4
5
6
7
8
9
10
11
    name  age sex  height blood
0 张三 20 男 175 O
1 李四 21 女 160 A
2 王五 50 男 170 B
3 赵六 30 男 173 AB
4 田七 40 男 180 A
0 小明 18 男 181 O
1 小黄 24 男 177 A
2 小红 17 女 170 B
3 小李 32 男 157 AB
4 小花 29 女 168 A

concat() 函数沿其中一个轴(按行或按列)执行多个表的串联操作。

默认情况下,串联是沿着轴 0 进行的,因此生成的表组合了输入表的行。 让我们检查原始表和串联表的形状来验证操作:

1
2
3
4
5
6
print("shape of class_01_students:", class_01_students.shape)
# shape of class_01_students: (5, 5)
print("shape of class_02_students:", class_02_students.shape)
# shape of class_02_students: (5, 5)
print("shape of students:", students.shape)
# shape of students: (10, 5)

axis 参数出现在许多可沿轴应用的 pandas 方法中。 DataFrame 有两个相应的轴:第一个轴垂直向下跨行(轴 0),第二个轴水平跨列(轴 1)。 默认情况下,大多数操作(如串联或汇总统计)是跨行(轴 0)的,但也可以跨列应用。

根据年龄对表进行排序(正序):

1
2
sorted_students = students.sort_values("age")
print(sorted_students)

Out:

1
2
3
4
5
6
7
8
9
10
11
    name  age sex  height blood
2 小红 17 女 170 B
0 小明 18 男 181 O
0 张三 20 男 175 O
1 李四 21 女 160 A
1 小黄 24 男 177 A
4 小花 29 女 168 A
3 赵六 30 男 173 AB
3 小李 32 男 157 AB
4 田七 40 男 180 A
2 王五 50 男 170 B

倒序:

1
2
sorted_students = students.sort_values("age", ascending=False)
print(sorted_students)

Out:

1
2
3
4
5
6
7
8
9
10
11
    name  age sex  height blood
2 王五 50 男 170 B
4 田七 40 男 180 A
3 小李 32 男 157 AB
3 赵六 30 男 173 AB
4 小花 29 女 168 A
1 小黄 24 男 177 A
1 李四 21 女 160 A
0 张三 20 男 175 O
0 小明 18 男 181 O
2 小红 17 女 170 B

这些教程中没有提到同时存在多个行/列索引。 分层索引或 MultiIndex 是一种先进且强大的 pandas 功能,用于分析更高维度的数据。

多重索引超出了 pandas 介绍的范围。 目前,请记住函数reset_index可用于将任何级别的索引转换为列。

使用通用标识符连接表

现有学生基本信息表 class_01_students.xlsx 和学生成绩表 student_score.xlsx 如下:

基本信息表 class_01_students.xlsx :

1
2
class_01_students = pd.read_excel("data/class_01_students.xlsx")
print(class_01_students)

Out:

1
2
3
4
5
6
  	name  age sex  height blood  student_id
0 张三 20 男 175 O 1
1 李四 21 女 160 A 2
2 王五 50 男 170 B 3
3 赵六 30 男 173 AB 4
4 田七 40 男 180 A 5

学生成绩表 student_score.xlsx :

1
2
student_score = pd.read_excel("data/student_score.xlsx")
print(student_score)

Out:

1
2
3
4
5
   student_id  chinese  math  english  physics
0 1 89 76 88 90
1 2 76 67 50 89
2 3 57 82 91 88
3 4 23 43 54 76

根据student_id合并两个表格:

1
2
3
4
class_01_students = pd.read_excel("data/class_01_students.xlsx")
student_score = pd.read_excel("data/student_score.xlsx")
merged_table = pd.merge(class_01_students, student_score, how="left", on="student_id")
print(merged_table)

Out:

1
2
3
4
5
6
    name  age sex  height blood  student_id  chinese  math  english  physics
0 张三 20 男 175 O 1 89.0 76.0 88.0 90.0
1 李四 21 女 160 A 2 76.0 67.0 50.0 89.0
2 王五 50 男 170 B 3 57.0 82.0 91.0 88.0
3 赵六 30 男 173 AB 4 23.0 43.0 54.0 76.0
4 田七 40 男 180 A 5 NaN NaN NaN NaN

mege操作类似于数据库的join操作

现学生成绩表 student_score.xlsx 和学生基本信息表 class_01_students.xlsx 没有相同的key。

学生基本信息表 class_01_students.xlsx 的数据如下:

1
2
3
4
5
6
    name  age sex  student_id
0 张三 20 男 1
1 李四 21 女 2
2 王五 50 男 3
3 赵六 30 男 4
4 田七 40 男 5

学生成绩表 student_score.xlsx 的数据如下:

1
2
3
4
5
   stuid  chinese  math  english  physics
0 1 89 76 88 90
1 2 76 67 50 89
2 3 57 82 91 88
3 4 23 43 54 76

连接两个表:

1
2
3
4
class_01_students = pd.read_excel("data/class_01_students.xlsx")
student_score = pd.read_excel("data/student_score.xlsx")
merged_table = pd.merge(class_01_students, student_score, how="left", left_on="student_id", right_on="stuid")
print(merged_table)

与前面的示例相比,没有公共列名。然而,表 class_01_students.xlsx 中的student_id列和表student_score.xlsx 中的stuid字段以通用格式提供了学生Id信息,此处使用参数left_on 和参数right_on (而不仅仅是on)来建立两个表之间的链接。


如何轻松处理时间序列数据

本教程使用的数据:空气质量数据

使用 pandas 日期时间属性

将纯文本转换成日期时间对象:

1
2
3
air_quality = pd.read_csv("data/air_quality_no2_long.csv")
air_quality["date.utc"] = pd.to_datetime(air_quality["date.utc"])
print(air_quality["date.utc"].head())

最初,日期时间中的值是字符串,不提供任何日期时间操作(例如提取年份、星期几等)。 通过应用 to_datetime 函数,pandas 解释字符串并将其转换为日期时间(即 datetime64[ns, UTC])对象。 在 pandas 中,我们将这些类似于标准库中的 datetime.datetime 的日期时间对象称为 pandas.Timestamp

由于许多数据集的其中一列中确实包含日期时间信息,因此 pandas.read_csv()pandas.read_json()pandas 输入函数可以在使用 parse_dates 参数在读取数据时将一些列日期列转换成时间戳对象。

为什么这些pandas.Timestamp对象有用?让我们通过一些示例来说明附加值。

我们正在使用的时间序列数据集的开始日期和结束日期是哪天?

1
2
print(air_quality["date.utc"].min())
print(air_quality["date.utc"].max())

Out:

1
2
2019-05-07 01:00:00+00:00
2019-06-21 00:00:00+00:00

使用 pandas.Timestamp 作为日期时间使我们能够使用日期信息进行计算并使它们具有可比性。 因此,我们可以用它来获取时间序列的长度:

1
print(air_quality["date.utc"].max() - air_quality["date.utc"].min())

Out:

1
44 days 23:00:00

结果是一个 pandas.Timedelta 对象,类似于标准 Python 库中的 datetime.timedelta 并定义持续时间。

我想向 DataFrame 添加一个新列,仅包含月份

1
2
air_quality["month"] = air_quality["date.utc"].dt.month
print(air_quality.head())

Out:

1
2
3
4
5
6
7
8
    city country                  date.utc  ... value   unit  month
0 Paris FR 2019-06-21 00:00:00+00:00 ... 20.0 µg/m³ 6
1 Paris FR 2019-06-20 23:00:00+00:00 ... 21.8 µg/m³ 6
2 Paris FR 2019-06-20 22:00:00+00:00 ... 26.5 µg/m³ 6
3 Paris FR 2019-06-20 21:00:00+00:00 ... 24.9 µg/m³ 6
4 Paris FR 2019-06-20 20:00:00+00:00 ... 21.4 µg/m³ 6

[5 rows x 8 columns]

通过使用 Timestamp 对象来表示日期,pandas 提供了许多与时间相关的属性。 例如月份month,还有年份year、季度quarter……所有这些属性都可以通过 dt 访问器访问。

每个测量位置一周中每一天的平均二氧化氮浓度是多少?

1
2
print(air_quality.groupby(
[air_quality["date.utc"].dt.weekday, "location"])["value"].mean())

Out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
date.utc  location          
0 BETR801 27.875000
FR04014 24.856250
London Westminster 23.969697
1 BETR801 22.214286
FR04014 30.999359
London Westminster 24.885714
2 BETR801 21.125000
FR04014 29.165753
London Westminster 23.460432
3 BETR801 27.500000
FR04014 28.600690
London Westminster 24.780142
4 BETR801 28.400000
FR04014 31.617986
London Westminster 26.446809
5 BETR801 33.500000
FR04014 25.266154
London Westminster 24.977612
6 BETR801 21.896552
FR04014 23.274306
London Westminster 24.859155
Name: value, dtype: float64

还记得统计计算教程中 groupby 提供的分割-应用-组合模式吗?在这里,我们想要计算给定的统计数据(例如每周每个测量地的二氧化碳的平均浓度)。为了按工作日进行分组,我们使用 pandas Timestampdatetime 属性 weekday(星期一 = 0 和星期日 = 6),也可以通过 dt 访问器访问。 可以对地点和工作日进行分组,以拆分每个组合的平均值计算。

将所有站点的时间序列中一天中典型的 NO2 模式绘制在一起。 换句话说,一天中每个小时的平均值是多少?

1
2
3
4
5
6
7
fig, axs = plt.subplots(figsize=(12, 4))
air_quality.groupby(air_quality["date.utc"].dt.hour)["value"].mean().plot(
kind='bar', rot=0, ax=axs
)
plt.xlabel("Hour of the day") # custom x label using Matplotlib
plt.ylabel("$NO_2 (µg/m^3)$")
plt.show()

日期时间作为索引

在重塑教程中, pivot()介绍了如何重塑数据表,将每个测量位置作为单独的列:

1
2
no_2 = air_quality.pivot(index="date.utc", columns="location", values="value")
print(no_2.head())

Out:

1
2
3
4
5
6
7
location                   BETR801  FR04014  London Westminster
date.utc
2019-05-07 01:00:00+00:00 50.5 25.0 23.0
2019-05-07 02:00:00+00:00 45.0 27.7 19.0
2019-05-07 03:00:00+00:00 NaN 50.4 19.0
2019-05-07 04:00:00+00:00 NaN 61.9 16.0
2019-05-07 05:00:00+00:00 NaN 72.4 NaN

通过旋转数据,日期时间信息成为表的索引。一般来说,将某列设置为索引可以通过该set_index函数来实现。

使用日期时间索引(即DatetimeIndex)提供了强大的功能。例如,我们不需要dt访问器来获取时间序列属性,而是直接在索引上使用这些属性:

1
2
print(no_2.index.year)
print(no_2.index.month)

Out:

1
2
3
4
5
6
7
8
Index([2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019,
...
2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019, 2019],
dtype='int32', name='date.utc', length=1033)
Index([5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
...
6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
dtype='int32', name='date.utc', length=1033)

绘制5 月 20 日至 5 月 21 月底不同站点的 NO2 值。

1
2
3
no_2 = air_quality.pivot(index="date.utc", columns="location", values="value")
no_2["2019-05-20":"2019-05-21"].plot()
plt.show()

如何操作文本数据

本教程数据class_01_students.xlsx

1
2
class_01_students = pd.read_excel("data/class_01_students.xlsx")
print(class_01_students)

Out:

1
2
3
4
5
6
        name  age  sex  student_id
0 San Zhang 20 男 1
1 Si Li 21 女 2
2 Wu Wang 50 男 3
3 Liu Zhao 30 男 4
4 Qi Tian 40 男 5

name设为小写。

1
2
class_01_students = pd.read_excel("data/class_01_students.xlsx")
print(class_01_students["name"].str.lower())

Out:

1
2
3
4
5
6
0    san zhang
1 si li
2 wu wang
3 liu zhao
4 qi tian
Name: name, dtype: object

与时间序列教程中具有 dt 访问器的日期时间对象类似,使用 str 访问器时可以使用许多专用字符串方法。 这些方法通常具有与单个元素的等效内置字符串方法匹配的名称,但按元素应用到列的每个值。

通过提取逗号之前的部分来创建一个新列“姓氏”,其中包含乘客的姓氏。

1
2
3
class_01_students["surname"] = class_01_students["name"].str.split(" ").str.get(1);

print(class_01_students)

Out:

1
2
3
4
5
6
        name  age sex  student_id surname
0 San Zhang 20 男 1 Zhang
1 Si Li 21 女 2 Li
2 Wu Wang 50 男 3 Wang
3 Liu Zhao 30 男 4 Zhao
4 Qi Tian 40 男 5 Tian

使用该Series.str.split()方法,每个值都作为 2 个元素的列表返回。第一个元素是空格之前的部分,第二个元素是空格之后的部分。

假如我们只对姓名中带有”Tian”字的用户感兴趣:

1
print(class_01_students["name"].str.contains("Tian"))

Out:

1
2
3
4
5
6
0    False
1 False
2 False
3 False
4 True
Name: name, dtype: bool

方法Series.str.contains()检查列中的每个值(Name如果字符串包含单词Tian,并返回每个值TrueTianname的一部分)或 FalseTian不是name的一部分)。

支持更强大的字符串提取,因为 Series.str.contains()Series.str.extract()方法接受正则表达式,但超出了本教程的范围。

哪位用户的名字最长?

1
print(class_01_students["name"].str.len().idxmax())

Out:

1
0

为了获得最长的名称,我们首先必须获得列中每个名称的长度Name。通过使用 pandas 字符串方法,该 Series.str.len()函数将单独应用于每个名称(按元素)。

接下来,我们需要获取表中名称长度最大的对应位置,最好是索引标签。 idxmax() 方法正是这样做的。 它不是字符串方法,适用于整数,因此不使用 str

根据行 ( 0) 和列 ( name) 的索引名称,我们可以使用运算符进行选择loc:

1
print(class_01_students.loc[class_01_students["name"].str.len().idxmax(), "name"])

Out:

1
San Zhang

在“sex”列中,将“男”的值替换为“M”,将“女”的值替换为“F”。

1
2
class_01_students["sex"] = class_01_students["sex"].replace({"男":"M", "女":"F"})
print(class_01_students)

Out:

1
2
3
4
5
6
        name  age sex  student_id
0 San Zhang 20 M 1
1 Si Li 21 F 2
2 Wu Wang 50 M 3
3 Liu Zhao 30 M 4
4 Qi Tian 40 M 5

虽然replace()它不是字符串方法,但它提供了一种使用映射或词汇表来转换某些值的便捷方法。它需要dictionary来定义映射。{from : to}


关于Pandas的更多教程请访问:https://pandas.pydata.org/pandas-docs/stable/getting_started/tutorials.html。

0%