Spring Boot中的配置文件使用以及重新加载

概要

本教程将展示如何通过Java configuration和@PropertySource或XML和property-placeholder在Spring中设置和使用属性

通过Java Annotations注册配置文件

Spring 3.1起引入了新的@PropertySource注释,作为向环境中添加属性源的方便机制。
该注释将与基于java的配置和@Configuration注释一起使用:

@Configuration
@PropertySource("classpath:foo.properties")
public class PropertiesWithJavaConfig {//...
}

另一种非常有用的注册新属性文件的方法是使用占位符,它允许在运行时动态选择正确的文件:

@PropertySource({ "classpath:persistence-${envTarget:mysql}.properties"
})

${envTarget:mysql} , envTarget:环境变量名称 , mysql:默认值

定义多个属性文件

在Java 8中@PropertySource注释是可重复的。因此,我们可以使用这个注释来定义多个属性位置:


@PropertySource("classpath:foo.properties")
@PropertySource("classpath:bar.properties")
public class PropertiesWithJavaConfig {//...
}

当然也可以使用数组的形式:

@PropertySources({@PropertySource("classpath:foo.properties"),@PropertySource("classpath:bar.properties")
})
public class PropertiesWithJavaConfig {//...
}

注入属性值

  • 用@Value注释注入属性很简单
@Value( "${jdbc.url}" )
private String jdbcUrl;
  • 在注入的时候使用默认值
@Value( "${jdbc.url:aDefaultUrl}" )
private String jdbcUrl;
  • 使用Environment API获取属性值
@Autowired
private Environment env;
...
dataSource.setUrl(env.getProperty("jdbc.url"));

@Value使用

@Value注解可用于将值注入spring管理bean中的字段,并且可以应用于字段或构造函数/方法参数级别。

  • 注入硬编码值到字段中(没有任何意义)
@Value("string value")
private String stringValue;
  • 使用@PropertySource注释允许我们使用@Value注释处理来自属性文件的值。在下面的例子中,我们从分配给字段的文件中获取值:
@Value("${value.from.file}")
private String valueFromFile;
  • 我们也可以用相同的语法从系统属性中设置值,假设我们已经定义了一个系统属性systemValue:
@Value("${systemValue}")
private String systemValue;
  • 可以为未定义的属性提供默认值。一些默认值将被注入:
@Value("${unknown.param:some default}")
private String someDefault;

如果将相同的属性定义为系统属性以及属性文件中,则将优先应用系统属性,系统属性 > 配置文件属性

  • 数组属性设置
listOfValues = A,B,C
@Value("${listOfValues}")
private String[] valuesArray;
  • map 属性设置

我们还可以使用@Value注释来注入一个Map属性。首先,我们需要在属性文件的{key: ’ value’}中定义属性:

valuesMap={key1: '1', key2: '2', key3: '3'}
@Value("#{${valuesMap}}")
private Map<String, Integer> valuesMap;
  • 如果我们需要在映射中获取特定键的值,我们所要做的就是在表达式中添加键的名称:
@Value("#{${valuesMap}.key1}")
private Integer valuesMapKey1;
  • 安全的使用表达式
@Value("#{${valuesMap}['unknownKey']}")
private Integer unknownMapKey;
  • 为map设定默认值
@Value("#{${unknownMap : {key1: '1', key2: '2'}}}")
private Map<String, Integer> unknownMap;@Value("#{${valuesMap}['unknownKey'] ?: 5}")
private Integer unknownMapKeyWithDefaultValue;
  • 使用@Value注入所有的系统变量
@Value("#{systemProperties}")
private Map<String, String> systemPropertiesMap;
  • 使用SpEL设置属性
@Component
@PropertySource("classpath:values.properties")
public class CollectionProvider {private List<String> values = new ArrayList<>();@Autowiredpublic void setValues(@Value("#{'${listOfValues}'.split(',')}") List<String> values) {this.values.addAll(values);}// standard getter
}

Spring Boot 中配置属性

在Spring Boot工程目录src/main/resources中的application.properties会被自动加载,无需额外配置就可以使用

  • 1、在运行的时候指定配置文件
java -jar app.jar --spring.config.location=classpath:/another-location.properties
  • 2、在Spring Boot2.3后可以指定配置文件通配符
java -jar app.jar --spring.config.location=config/*/
  • 3、通过环境变量设置属性文件

如果我们需要针对不同的环境设置属性文件。可以简单地定义应用程序环境属性文件,然后设置一个具有相同环境名称的Spring配置文件。
例如,如果我们定义了一个“stage”环境,这意味着我们必须定义一个stage配置文件application-stage.properties。
此env文件将被加载,并优先于默认的属性文件。这时候,默认文件仍将被加载,只是当存在属性冲突时,使用环境的属性覆盖默认值。

  • 4、测试的时候使用属性文件

有时在测试应用程序时,我们可能还需要使用不同的属性值。
Spring引导通过在测试运行期间查看src/test/resources目录来为我们处理这个问题。同样,默认属性仍然可以正常注入,但是如果发生冲突,默认属性将被覆盖。

  • 5、单元测试中使用属性文件

如果我们需要对测试属性进行更细粒度的控制,那么我们可以使用@TestPropertySource注释。
这允许我们为特定的测试上下文设置测试属性,优先于默认的属性源:

@RunWith(SpringRunner.class)
@TestPropertySource("/foo.properties")
public class FilePropertyInjectionUnitTest {@Value("${foo}")private String foo;@Testpublic void whenFilePropertyProvided_thenProperlyInjected() {assertThat(foo).isEqualTo("bar");}
}
  • 6、如果不需要属性文件也可以直接注入配置项
@RunWith(SpringRunner.class)
@TestPropertySource(properties = {"foo=bar"})
public class PropertyInjectionUnitTest {@Value("${foo}")private String foo;@Testpublic void whenPropertyProvided_thenProperlyInjected() {assertThat(foo).isEqualTo("bar");}
}

把属性注入到对象中

我们可以使用@ConfigurationProperties注释,它将这些属性层次映射到Java对象中

  • 数据库链接属性
database.url=jdbc:postgresql:/localhost:5432/instance
database.username=foo
database.password=bar

或者YAML文件

database:url: jdbc:postgresql:/localhost:5432/instanceusername: foopassword: bar
secret: foo
  • 使用@ConfigurationProperties 注入属性
@ConfigurationProperties(prefix = "database")
public class Database {String url;String username;String password;// standard getters and setters
}

Spring Boot应用它的约定优于配置的规则,自动在属性名和它们对应的字段之间进行映射,我们需要提供的只是属性前缀

通过命令行设置属性

除了使用属性文件外,我们也可以在启动程序的时候使用命令行传入参数

java -jar app.jar --property="value"

传入环境变量参数

java -Dproperty.name="value" -jar app.jar

Spring Boot 会自动探测环境变量

export name=value
java -jar app.jar

@ConfigurationProperties 使用详解

简单属性映射

@Configuration
@ConfigurationProperties(prefix = "mail")
public class ConfigProperties {private String hostName;private int port;private String from;// standard getters and setters
}
  • 我们使用@Configuration,这样Spring就可以在应用程序上下文中创建一个Spring bean。

  • @ConfigurationProperties最适合使用具有相同前缀的层次属性;因此,我们添加一个mail前缀。Spring框架使用标准的Java bean setter,因此我们必须为每个属性声明setter方法。

  • 注意:如果我们在POJO中不使用**@Configuration**,那么我们需要在主Spring应用程序类中添加**@EnableConfigurationProperties(ConfigProperties.class)**来将属性绑定到POJO中:

@SpringBootApplication
@EnableConfigurationProperties(ConfigProperties.class)
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}

Spring将自动绑定属性文件中定义的任何属性,这些属性具有前缀mail,并且与ConfigProperties类中的一个字段同名

Spring Boot 自动/定制绑定属性

Spring Boot通过类路径扫描查找和注册@ConfigurationProperties类。因此,不需要用@Component(和其他元注释,如@Configuration)注释这样的类,甚至也不需要使用@EnableConfigurationProperties

@ConfigurationProperties(prefix = "mail")
public class ConfigProperties { private String hostName; private int port; private String from; // standard getters and setters
}

由@SpringBootApplication启用的类路径扫描器会找到ConfigProperties类,不需要做额外配置。

此外,我们可以使用@ConfigurationPropertiesScan注释来扫描配置属性类的自定义位置:

@SpringBootApplication
@ConfigurationPropertiesScan("com.baeldung.properties")
public class DemoApplication { public static void main(String[] args) {   SpringApplication.run(DemoApplication.class, args); }
}

上述例子中Spring Boot将只在com.baeldung中查找配置属性类

嵌套属性绑定

我们可以在列表、映射和类中使用嵌套属性

属性类

public class ConfigProperties {private String host;private int port;private String from;private List<String> defaultRecipients;private Map<String, String> additionalHeaders;private Credentials credentials;// standard getters and setters
}

属性文件示例

# 简单属性
mail.hostname=mailer@mail.com
mail.port=9000
mail.from=mailer@mail.com# List 属性
mail.defaultRecipients[0]=admin@mail.com
mail.defaultRecipients[1]=owner@mail.com# Map 属性
mail.additionalHeaders.redelivery=true
mail.additionalHeaders.secure=true# Object 对象属性
mail.credentials.username=john
mail.credentials.password=password
mail.credentials.authMethod=SHA1

@Bean 注释的方法上使用 @ConfigurationProperties

我们还可以在带有@ bean注释的方法上使用@ConfigurationProperties注释。
当我们想要将属性绑定到我们无法控制的第三方组件时,这种方法可能特别有用。

让我们创建一个简单的Item类,我们将在下一个例子中使用:

  • 属性类
public class Item {private String name;private int size;// standard getters and setters
}

现在让我们看看如何在@Bean方法上使用@ConfigurationProperties来将外部化的属性绑定到项目实例:

  • 属性文件
# 简单属性
item.name=mailer@mail.com
item.size=32
  • 通过@Configuration 构造类
@Configuration
public class ConfigProperties {@Bean@ConfigurationProperties(prefix = "item")public Item item() {return new Item();}
}

因此,任何带有itme前缀的属性都将映射到Spring上下文管理的item实例中

属性验证

@ConfigurationProperties使用JSR-303格式提供对属性的验证

  • 例如,让我们强制设置hostName属性:
@NotBlank
private String hostName;
  • 将authMethod属性设置为1到4个字符长:
@Length(max = 4, min = 1)
private String authMethod;
  • 端口属性从1025到65536:
@Min(1025)
@Max(65536)
private int port;
  • 从属性必须匹配的电子邮件地址格式:
@Pattern(regexp = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,6}$")
private String from;

这帮助我们减少了代码中的很多if-else条件判断,使代码看起来更干净、更简洁。

如果其中任何一个验证失败,则主应用程序将无法启动并抛出IllegalStateException异常。

Hibernate验证框架使用标准的Java bean getter和setter,因此为每个属性声明getter和setter非常重要。

属性自定义转换

我们还可以添加自己的自定义转换器来支持将属性转换为特定的类类型。

让我们添加一个简单的类Employee:

public class Employee {private String name;private double salary;
}

然后我们将创建一个自定义转换器来转换这个属性:

conversion.employee=john,2000

我们需要实现转换器接口,然后使用@ConfigurationPropertiesBinding注释来注册我们的自定义转换器:

@Component
@ConfigurationPropertiesBinding
public class EmployeeConverter implements Converter<String, Employee> {@Overridepublic Employee convert(String from) {String[] data = from.split(",");return new Employee(data[0], Double.parseDouble(data[1]));}
}

属性构造器绑定,不可变属性

从Spring Boot 2.2开始,我们可以使用@ConstructorBinding注释来绑定配置属性。
这本质上意味着@configurationproperty注释的类现在可能是不可变的。

@ConfigurationProperties(prefix = "mail.credentials")
@ConstructorBinding
public class ImmutableCredentials {private final String authMethod;private final String username;private final String password;public ImmutableCredentials(String authMethod, String username, String password) {this.authMethod = authMethod;this.username = username;this.password = password;}public String getAuthMethod() {return authMethod;}public String getUsername() {return username;}public String getPassword() {return password;}
}
  • 上述例子中,在使用@ConstructorBinding时,我们需要向构造函数提供我们想要绑定的所有参数。

  • 所有不变的向量的域都是最终的。而且,没有setter方法。

  • 此外,需要强调的是,要使用构造函数绑定,我们需要显式地使用@EnableConfigurationProperties或@ConfigurationPropertiesScan启用配置类

Spring Boot 重新加载属性文件

在Spring中我们有不同的访问属性的选项:

  • 1、Environment– 我们可以注入Environment,然后使用Environment#getProperty读取给定的属性。环境包含不同的属性源,如系统属性、-D参数和application.properties(.yml)。另外,可以使用@PropertySource向环境中添加额外的属性源。
  • 2、Properties– 我们可以将属性文件加载到Properties实例中,然后通过调用Properties#get(“property”)在bean中使用它。
  • 3、@Value– 我们可以通过@Value(${’ property’})注释在bean中注入一个特定的属性
  • 4、@ConfigurationProperties– 我们可以使用@ConfigurationProperties加载bean中的分层属性

从外部文件中重载配置文件

要在运行时更改文件中的属性,我们应该将该文件放在jar之外的某个地方。然后,我们将通过命令行参数-spring.config告诉Spring从哪里获取配置文件。spring.config.location=file://{path to file}。或者,我们可以把它放到application.properties中。

在基于文件的属性中,我们必须选择一种方法来重新加载文件。例如,我们可以开发一个端点或调度程序来读取文件和更新属性。

重新加载文件的一个类库是Apache的commons-configuration,我们可以使用不同重载策略PropertiesConfiguration。

增加 commons-configuration 依赖到 pom.xml文件中:

<dependency><groupId>commons-configuration</groupId><artifactId>commons-configuration</artifactId><version>1.10</version>
</dependency>

然后,我们添加一个方法来创建PropertiesConfiguration bean,稍后我们将使用它:

@Bean
@ConditionalOnProperty(name = "spring.config.location", matchIfMissing = false)
public PropertiesConfiguration propertiesConfiguration(@Value("${spring.config.location}") String path) throws Exception {String filePath = new File(path.substring("file:".length())).getCanonicalPath();PropertiesConfiguration configuration = new PropertiesConfiguration(new File(filePath));configuration.setReloadingStrategy(new FileChangedReloadingStrategy());return configuration;
}

检查配置文件中是否spring.config.location属性
@Value("${spring.config.location}" 注入配置文件地址
我们将FileChangedReloadingStrategy设置为具有默认刷新延迟的重新加载策略。这意味着PropertiesConfiguration检查文件修改日期,每5000ms检查一次。

从Environment环境变量中重载属性

如果我们想要重新加载通过环境实例加载的属性,我们必须扩展PropertySource,然后使用PropertiesConfiguration从外部属性文件返回新值。

让我们从扩展PropertySource开始:

public class ReloadablePropertySource extends PropertySource {PropertiesConfiguration propertiesConfiguration;public ReloadablePropertySource(String name, PropertiesConfiguration propertiesConfiguration) {super(name);this.propertiesConfiguration = propertiesConfiguration;}public ReloadablePropertySource(String name, String path) {super(StringUtils.isEmpty(name) ? path : name);try {this.propertiesConfiguration = new PropertiesConfiguration(path);this.propertiesConfiguration.setReloadingStrategy(new FileChangedReloadingStrategy());} catch (Exception e) {throw new PropertiesException(e);}}@Overridepublic Object getProperty(String s) {return propertiesConfiguration.getProperty(s);}
}

覆盖了PropertySource#getProperty方法,将其委托给PropertiesConfiguration#getProperty。因此,它会根据刷新延迟间隔检查更新的值。

现在,我们将把ReloadablePropertySource添加到环境的属性源:

@Configuration
public class ReloadablePropertySourceConfig {private ConfigurableEnvironment env;public ReloadablePropertySourceConfig(@Autowired ConfigurableEnvironment env) {this.env = env;}@Bean@ConditionalOnProperty(name = "spring.config.location", matchIfMissing = false)public ReloadablePropertySource reloadablePropertySource(PropertiesConfiguration properties) {ReloadablePropertySource ret = new ReloadablePropertySource("dynamic", properties);MutablePropertySources sources = env.getPropertySources();sources.addFirst(ret);return ret;}
}

上面添加了新的属性源作为第一项,因为我们希望它用相同的键覆盖任何现有的属性。

让我们创建一个bean来从环境中读取属性:

@Component
public class EnvironmentConfigBean {private Environment environment;public EnvironmentConfigBean(@Autowired Environment environment) {this.environment = environment;}public String getColor() {return environment.getProperty("application.theme.color");}
}

如果我们需要添加其他可重新加载的外部属性源,首先我们必须实现自定义的PropertySourceFactory:

public class ReloadablePropertySourceFactory extends DefaultPropertySourceFactory {@Overridepublic PropertySource<?> createPropertySource(String s, EncodedResource encodedResource)throws IOException {Resource internal = encodedResource.getResource();if (internal instanceof FileSystemResource)return new ReloadablePropertySource(s, ((FileSystemResource) internal).getPath());if (internal instanceof FileUrlResource)return new ReloadablePropertySource(s, ((FileUrlResource) internal).getURL().getPath());return super.createPropertySource(s, encodedResource);}
}

然后我们可以用@PropertySource注释组件的类:

@PropertySource(value = "file:path-to-config", factory = ReloadablePropertySourceFactory.class)

@ConfigurationProperties @Value 重新加载属性的限制

要使用@ConfigurationProperties获得同样的效果,我们需要重新构造实例。
但是,Spring将只创建具有原型或请求范围的组件的新实例。

因此,我们重新加载环境的技术也适用于上述情况,但是对于单例,我们别无选择,只能实现一个端点来销毁和重新创建bean,或者在bean本身内部处理属性的重新加载。

使用Actuator 和 Cloud重新加载配置

Spring Actuator 为服务运行状况、指标和配置提供了不同的接口,但没有为刷新bean提供任何接口。因此,我们需要Spring Cloud向其添加/refresh接口。此接口将重新加载Environment的所有属性源,然后发布一个EnvironmentChangeEvent。

Spring Cloud还引入了@RefreshScope,我们可以将其用于配置类或bean。因此,默认范围将是refresh而不是singleton。
使用refresh作用域,Spring将在一个EnvironmentChangeEvent上清除这些组件的内部缓存。然后,在下一次访问bean时,创建一个新的实例。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

引入Spring Cloud依赖

<dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement><properties><spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter</artifactId>
</dependency>

最后,让我们启用refresh端点:

management.endpoints.web.exposure.include=refresh

当我们使用Spring Cloud时,我们可以设置配置服务器来管理属性,但是我们也可以继续使用外部文件。下面,我们可以演示两种读取属性的方法:@Value和@ConfigurationProperties。

@ConfigurationProperties 刷新

@Component
@ConfigurationProperties(prefix = "application.theme")
@RefreshScope
public class ConfigurationPropertiesRefreshConfigBean {private String color;public void setColor(String color) {this.color = color;}//getter and other stuffs
}

ConfigurationPropertiesRefreshConfigBean,从application.theme读取“color”属性。注意,根据Spring 约定属性color需要setColor方法

在更改了application.theme的值之后。在我们的外部配置文件中,我们可以调用/refresh,这样,我们就可以在下一次访问时从bean中获得新的值。

@Value 刷新

@Component
@RefreshScope
public class ValueRefreshConfigBean {private String color;public ValueRefreshConfigBean(@Value("${application.theme.color}") String color) {this.color = color;} //put getter here
}

但是,有必要注意,/refresh不适用于具有显式单例作用域的bean

Spring Boot中的配置文件使用以及重新加载相关推荐

  1. SpringBoot中各配置文件的优先级及加载顺序

    我们在写程序的时候会碰到各种环境(开发.测试.生产),因而,在我们切换环境的时候,我们需要手工切换配置文件的内容.这大大的加大了运维人员的负担,同时会带来一定的安全隐患. 为此,为了能更合理地重写各属 ...

  2. Spring Boot||html页面的css等资源加载失败

    背景 这几天在用spring boot 框架来实现一个网站,静态页面已经写好,创建完成spring项目后将html文件.css等资源文件分别扔到了/项目名称/src/main/resources/te ...

  3. spring boot错误: 找不到或无法加载主类

    eclipse启动spring boot项目时出现问题:springboot错误: 找不到或无法加载主类 解决办法: 通过cmd命令行,进入项目目录进行,mvn clean install 进行编译, ...

  4. SpringBoot - Spring Boot 中的配置体系Profile全面解读

    文章目录 Pre Spring Boot 中的配置体系 配置文件与 Profile 主 application.properties 中指定激活的Profile Profile 配置信息只保存在一个文 ...

  5. 解决Spring boot中读取属性配置文件出现中文乱码的问题

    解决Spring boot中读取属性配置文件出现中文乱码的问题 参考文章: (1)解决Spring boot中读取属性配置文件出现中文乱码的问题 (2)https://www.cnblogs.com/ ...

  6. springboot 读取配置文件_使用 @ConfigurationProperties 在 Spring Boot 中加载配置

    本文地址: 使用 @ConfigurationProperties 在 Spring Boot 中加载配置 使用 Spring Boot 加载配置文件的配置非常便利,我们只需要使用一些注解配置一下就能 ...

  7. Java工作笔记-Spring boot中配置文件加密(Jasypt的使用)

    Jasypt Spring Boot提供了Spring Boot应用程序配置文件的加密.有下面3种方式在项目中集成jasypt-spring-boot: 1. 在classpath中添加jasypt- ...

  8. Spring Boot 中使用 MongoDB 增删改查

    本文快速入门,MongoDB 结合SpringBoot starter-data-mongodb 进行增删改查 1.什么是MongoDB ? MongoDB 是由C++语言编写的,是一个基于分布式文件 ...

  9. 徒手解密 Spring Boot 中的 Starter自动化配置黑魔法

    我们使用 Spring Boot,基本上都是沉醉在它 Stater 的方便之中.Starter 为我们带来了众多的自动化配置,有了这些自动化配置,我们可以不费吹灰之力就能搭建一个生产级开发环境,有的小 ...

最新文章

  1. python网盘提取码怎么用_Python 一键获取百度网盘提取码
  2. VMware Fusion:Windows程序和Mac无缝结合
  3. 使用PDB调试Python程序的完整实践
  4. 视频问答PPT大放送丨中信银行邓琼-GoldenDB分布式数据库研发与应用实践
  5. 【Python】表格文件处理
  6. 利用处理程序错误***(下)
  7. 【Silverlight】Bing Maps学习系列(二):通过Bing Maps Silverlight Control如何显示地图...
  8. web xml配置oracle数据源_SpringBoot2 整合JTA组件,多数据源事务管理
  9. Flash游戏开发性能优化
  10. 查看DDR的频率【学习笔记】
  11. c++ vector 一部分_要去|原创TheShy要去SKT、小C会给阿水打辅助!各大战队面临重新洗牌...
  12. 基于python/scipy学习概率统计(3):正态分布
  13. STM32F4的基础介绍
  14. sigmoid函数温习【函数曲线可视化与导函数曲线可视化】
  15. CTFHUB-SQL注入
  16. 数据分析 知识体系 Python篇
  17. JS切割截取字符串方法总结
  18. 蚁群算法解决多峰函数优化问题
  19. 超融合市场火爆,新华三因何蝉联第一?
  20. 游客屈指可数的人间天堂――四千美岛

热门文章

  1. html div 自动滚动到底部,javascript让DIV的滚动自动滚动到最底部-4种方法
  2. 父组件访问子组件的方法或参数 (子组件暴漏出方法defineExpose)
  3. Spring源码系列(十二)Spring创建Bean的过程(二)
  4. HDFS启动报错Expected to be able to read up until at least txid but unable to find any edit logs
  5. linux环境安装部署RF+Jenkins+Git(非完整版)
  6. linksys 打印软件_Linksys固件DD-WRT BitTorrent优化
  7. 张益唐被曝已证明黎曼猜想相关问题,震动数学界(文末送书)
  8. hadoop Error: JAVA_HOME is incorrectly set.Please update C:\hadoop-3.0.0\etc\hadoop\hadoop-env.cmd
  9. Ubuntu12.04 Skype4.2 提示Skype can't connect,安装Skype4.3
  10. 名帖181 黄庭坚 行书《苦笋赋》