所有文章

https://www.cnblogs.com/lay2017/p/11478237.html

触发监听器加载配置文件

上一篇文章中,我们看到了Environment对象的创建方法。同时也稍微提及了一下ConfigFileApplicationListener这个监听器,这个监听器主要工作是为了加载application.properties/yml配置文件的。

回顾一下prepareEnvironment方法的代码

private ConfigurableEnvironment prepareEnvironment(
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments
) {
// 创建一个Environment对象
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 配置Environment对象
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 触发监听器(主要是触发ConfigFileApplicationListener,这个监听器将会加载如application.properties/yml这样的配置文件)
listeners.environmentPrepared(environment);
// 省略
}

我们看到Environment对象在初始创建并配置之后会发布出一个事件给监听器,注意!这里的监听器并不是ConfigFileApplicationListener而是一个负责分发事件的监听器EventPublishingRunListener。

我们跟进EventPublishingRunListener监听器的environmentPrepared方法

private final SimpleApplicationEventMulticaster initialMulticaster;

@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster
.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}

这里包装了一个ApplicationEnvironmentPreparedEvent事件,并通过广播的方式广播给监听该事件的监听器,到这个时候才触发了ConfigFileApplicationListener

我们跟进ConfigFileApplicationListener的onApplicationEvent方法

@Override
public void onApplicationEvent(ApplicationEvent event) {
// 只触发Environment相关的事件
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}

Event将会触发onApplicationEnvironmentPreparedEvent

继续跟进

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
// 执行后置处理器
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}

我们看到,首先加载了Environment的后置处理器,然后经过排序以后遍历触发每个处理器。这里注意,ConfigFileApplicationListener本身也实现了EnvironmentPostProcessor接口,所以这里将会触发ConfigFileApplicationListener内部方法执行

我们跟进ConfigFileApplicationListener的postProcessEnvironment方法

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}

再跟进addPropertySources方法

protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
new Loader(environment, resourceLoader).load();
}

我们看到,这里实例化了一个Loader用来加载application配置文件,而核心逻辑就在load方法当中。

加载器加载application配置文件

跟进Loader加载器的构造方法中

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
// 文件application配置文件的资源加载器,包括propertis/xml/yml/yaml扩展名
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}

我们注意到,再构造方法中将会从spring.factories中加载PropertySourceLoader接口的具体实现类,具体请参阅:辅助阅读

我们打开spring.factories可以看到

这里包括两个实现

1)PropertiesPropertySourceLoader:用于加载property/xml格式的配置文件

2) YamlPropertySourceLoader:用于加载yml/yaml格式的配置文件

到这里,我们可以知道springboot支持的不同配置文件是通过选择不同的加载器来实现

下面,我们回到Loader加载器的load方法中,跟进加载的主要逻辑

public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化profiles
initializeProfiles();
while (!this.profiles.isEmpty()) {
// 消费一个profile
Profile profile = this.profiles.poll();
// active的profile添加到Environment
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
// 加载
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 重置Environment中的profiles
resetEnvironmentProfiles(this.processedProfiles);
// 加载
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 添加所有properties到Environment中
addLoadedPropertySources();
}

代码有点小长,我们根据如何加载默认的application.properties/yml配置文件的流程来了解一下

先跟进initializeProfiles方法看看如果初始化profiles

private void initializeProfiles() {
// 第一个profile为null,这样能保证首个加载application.properties/yml
this.profiles.add(null);
Set<Profile> activatedViaProperty = getProfilesActivatedViaProperty();
this.profiles.addAll(getOtherActiveProfiles(activatedViaProperty));
addActiveProfiles(activatedViaProperty);
// 没有额外配置profile的时候,将使用默认的
if (this.profiles.size() == 1) {
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}

这里注意两点

1)将会首先添加一个null,保证第一次加载的是application配置

2) 其次,如果没有配置profile,那么使用default。注意,我们的application配置文件还未加载,所以这里的"没有配置"并不是指你的application配置文件中有没有配置,而是如命令行、获取main方法传入等其它方法配置

我们并未配置任何active的profile,所以这里最终将产生一个这样的数据

profiles=[null, "default"]

回到load方法中,我们继续往下看

public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化profiles
initializeProfiles();
while (!this.profiles.isEmpty()) {
// 消费一个profile
Profile profile = this.profiles.poll();
// active的profile添加到Environment
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
// 加载
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 重置Environment中的profiles
resetEnvironmentProfiles(this.processedProfiles);
// 加载
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 添加所有properties到Environment中
addLoadedPropertySources();
}

while循环中,首先拿到的是profile=null,然后就直接进入第二个load加载方法加载配置文件

我们跟进第二个load加载方法(请注意区分load方法,后续还会出现load方法,我们以出现的顺序区分)

private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// 获取并遍历所有待搜索的位置
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
// 获取所有待加载的配置文件名
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
// 加载每个位置的每个文件
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}

该方法中的逻辑主要是搜索每个位置下的每个指定的配置文件名,并加载

跟进getSearchLocations看看要搜索哪些位置

private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";

private Set<String> getSearchLocations() {
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
locations.addAll(
asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
return locations;
}

很显然,由于我们没有自定义一些搜索位置,那么默认搜索classpath:/、classpath:/config/、file:./、file:./下

回到第二个load方法,我们再看看getSearchNames方法要加载哪些文件

private static final String DEFAULT_NAMES = "application";

private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
return asResolvedSet(property, null);
}
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}

相似的逻辑,最终返回默认的配置文件名application,也就是我们最熟悉的名字

接下来,再回到第二个load方法,我们可以跟进第三个load方法了,看看如何根据locations和names来加载配置文件

private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
// 省略
Set<String> processed = new HashSet<>();
// 遍历加载器
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 获取扩展名
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
// 加载对应扩展名的文件
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}

这个load方法主要逻辑表明将会加载每个加载器可以支持的配置文件,在Loader初始化的时候我们获得了两个加载器,同时每个加载器支持两种格式。所以这里的嵌套遍历中,我们将会尝试加载4种配置文件,如

1)application.properties

2) application.xml

3) application.yml

4) application.yaml

再跟进loadForFileExtension方法,看看具体每种的加载

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
// 当前没有profile
if (profile != null) {
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// 加载具体格式的文件
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

由于当前profile=null,所以我们直接进入第四个load方法

跟进第四个load方法,由于该方法有点长,我们省略次要的代码

private void load(
PropertySourceLoader loader,
String location,
Profile profile,
DocumentFilter filter,
DocumentConsumer consumer
) {
try {
// 获取资源
Resource resource = this.resourceLoader.getResource(location); // 加载为Document对象
List<Document> documents = loadDocuments(loader, name, resource); List<Document> loaded = new ArrayList<>();
// 遍历Document集合
for (Document document : documents) {
if (filter.match(document)) {
// 添加profile
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
// 回调处理每个document
loaded.forEach((document) -> consumer.accept(profile, document));
}
} catch (Exception ex) {}
}

首先配置文件会被加载为Document这样的内存对象,并最终回调处理。

这里我们看到回调是调用consumer这样一个接口,我们得回到第一个load方法,看看调用第二个load方法的时候传入的consumer是啥

public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化profiles
initializeProfiles();
while (!this.profiles.isEmpty()) {
// 消费一个profile
Profile profile = this.profiles.poll();
// active的profile添加到Environment
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
// 加载
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 重置Environment中的profiles
resetEnvironmentProfiles(this.processedProfiles);
// 加载
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 添加所有properties到Environment中
addLoadedPropertySources();
}

我们看到,在调用第二个load方法的时候就通过addToLoaded这个方法的执行来获取一个consumer,用来回调处理配置文件的Document对象。

注意!在调用addToLoaded的时候通过方法引用指定了一个method,这个method将在consumer回调的内部被使用。

method=MutablePropertySources:addLast

跟进addToLoaded方法

private Map<Profile, MutablePropertySources> loaded;

private DocumentConsumer addToLoaded(
BiConsumer<MutablePropertySources,
PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
// 省略
MutablePropertySources merged = this.loaded.computeIfAbsent(profile, (k) -> new MutablePropertySources());
// 回调method
addMethod.accept(merged, document.getPropertySource());
};
}

我们看到,loaded是一个profile和MutableProperySources的键值组合。方法逻辑中将会先获取loaded里面的MutablePropertySources,然后调用addLast方法将Document中的PropertySource给添加到MutablePropertySources中。

到这里,一个application配置文件被加载到内存了。但是还没完,前面的文章中我们说过Environment对象是应用程序环境的抽象,包含了properties。那么,我们还得将这些内存中的PropertySource给添加到Environment中。

回到第一个load方法中

public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化profiles
initializeProfiles();
while (!this.profiles.isEmpty()) {
// 消费一个profile
Profile profile = this.profiles.poll();
// active的profile添加到Environment
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
// 加载
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 重置Environment中的profiles
resetEnvironmentProfiles(this.processedProfiles);
// 加载
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 添加所有properties到Environment中
addLoadedPropertySources();
}

我们看到最后一行,addLoadedPropertySources方法的作用也就是将之前loaded里面的东西给添加到Environment中

跟进addLoadedPropertySources方法

private void addLoadedPropertySources() {
MutablePropertySources destination = this.environment.getPropertySources();
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
// 反向排序
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
// 遍历loaded
for (MutablePropertySources sources : loaded) {
// 遍历配置
for (PropertySource<?> source : sources) {
// 排重
if (added.add(source.getName())) {
// 添加每个到Environment中
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}

这个方法很清晰地表明,将loaded中地PropertySource给追加到Environment中。

到这里,默认的application.properties/yml这样地配置文件就被加载到了Environment当中了。不过还没有结束,这里还有个比较重要的问题,多环境的时候application.properties/yml配置文件中指定了profile,就会加载application-{profile}.properties/yml是怎么实现的呢?

加载多环境的application-{profile}配置文件

回到我们之前的第四个load方法

private void load(
PropertySourceLoader loader,
String location,
Profile profile,
DocumentFilter filter,
DocumentConsumer consumer
) {
try {
// 获取资源
Resource resource = this.resourceLoader.getResource(location); // 加载为Document对象
List<Document> documents = loadDocuments(loader, name, resource); List<Document> loaded = new ArrayList<>();
// 遍历Document集合
for (Document document : documents) {
if (filter.match(document)) {
// 添加profile
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
// 回调处理每个document
loaded.forEach((document) -> consumer.accept(profile, document));
}
} catch (Exception ex) {}
}

这里,已经把默认的application.properties/yml给加载成为了Document。然后在遍历documents的时候,会把Document中的profiles做一次添加

我们跟进addActiveProfiles看看

private Deque<Profile> profiles;

void addActiveProfiles(Set<Profile> profiles) {
if (profiles.isEmpty()) {
return;
}
// 省略
// 添加到队列
this.profiles.addAll(profiles);
// 省略
this.activatedProfiles = true;
// 移除掉default
removeUnprocessedDefaultProfiles();
}

我们看到,新的profiles首先会被添加到现有队列中。最初的profiles=[null, "default"]。而后,我们消费了null,profiles=["default"]。现在,我们添加一个profile="test"。那么,profiles=["default", "test"]。

再看最后一行removeUnprocessedDefaultProfiles,将会移除default。所以,最终profiles=["test"]。

再回到第一个load方法中

public void load() {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// 初始化profiles
initializeProfiles();
while (!this.profiles.isEmpty()) {
// 消费一个profile
Profile profile = this.profiles.poll();
// active的profile添加到Environment
if (profile != null && !profile.isDefaultProfile()) {
addProfileToEnvironment(profile.getName());
}
// 加载
load(profile, this::getPositiveProfileFilter, addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
// 重置Environment中的profiles
resetEnvironmentProfiles(this.processedProfiles);
// 加载
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
// 添加所有properties到Environment中
addLoadedPropertySources();
}

这时候while循环里面将会拿到profile="test",跟之前一样一路下去,直到loadForFileExtension方法

跟进loadForFileExtension

private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// 拼接如:application-test.properties/yml
String profileSpecificFile = prefix + "-" + profile + fileExtension;
// 加载文件
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// 加载具体格式的文件
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}

我们看到,当有profile的时候文件名就不再是application.properties/yml了。它会把profile给拼接上去,所以就变成了application-test.properties/yml,并加载文件。后续也一样得最终添加到Environment当中。

总结

application配置文件的加载过程逻辑并不复杂,只是具体细节比较多,所以代码中包含了不少附加的逻辑。那么抛开细节,我们可以看到其实就是到相应的目录下搜索相应的文件是否存在,加载到内存以后再添加到Environment当中。

至于具体的细节如:加载文件的时候编码相关、多个文件相同配置是否覆盖、加载器如何解析各种配置文件的内容有时间也可以仔细阅读。

springboot启动流程(四)application配置文件加载过程的更多相关文章

  1. asp.netcore 深入了解配置文件加载过程

    前言     配置文件中程序运行中,担当着不可或缺的角色:通常情况下,使用 visual studio 进行创建项目过程中,项目配置文件会自动生成在项目根目录下,如 appsettings.json, ...

  2. 登陆获取shell时的配置文件加载过程

    最近遇到一台ubuntu服务器登陆时默认语言环境变量变成posix问题, 导致中文显示乱码,影响程序的正常运行 # locale LANG= LANGUAGE= LC_CTYPE="POSI ...

  3. .net core 深入了解配置文件加载过程

    前言     配置文件中程序运行中,担当着不可或缺的角色:通常情况下,使用 visual studio 进行创建项目过程中,项目配置文件会自动生成在项目根目录下,如 appsettings.json, ...

  4. web应用启动的时候SpringMVC容器加载过程

    <!-- 配置DispatcherServlet --> <servlet> <servlet-name>springmvc</servlet-name> ...

  5. Java web 项目 web.xml 配置文件加载过程

    转载自:http://blog.csdn.net/luoliehe/article/details/46884757#comments WEB加载web.xml初始化过程: 在启动Web项目时,容器( ...

  6. springboot启动流程(目录)

    springboot出现有段时间了,不过却一直没有怎么去更多地了解它.一方面是工作的原因,另一方面是原来觉得是否有这个必要,但要持续做java似乎最终逃不开要去了解它的命运.于是考虑花一段时间去学习一 ...

  7. 你所不知道的SQL Server数据库启动过程(用户数据库加载过程的疑难杂症)

    前言 本篇主要是上一篇文章的补充篇,上一篇我们介绍了SQL Server服务启动过程所遇到的一些问题和解决方法,可点击查看,我们此篇主要介绍的是SQL Server启动过程中关于用户数据库加载的流程, ...

  8. (4.21)SQL Server数据库启动过程(用户数据库加载过程的疑难杂症)

    转自:指尖流淌 http://www.cnblogs.com/zhijianliutang/p/4100103.html SQL Server数据库启动过程(用户数据库加载过程的疑难杂症) 前言 本篇 ...

  9. SQL Server 数据库启动过程(用户数据库加载过程的疑难杂症)

    前言 本篇主要是上一篇文章的补充篇,上一篇我们介绍了SQL Server服务启动过程所遇到的一些问题和解决方法,可点击查看,我们此篇主要介绍的是SQL Server启动过程中关于用户数据库加载的流程, ...

随机推荐

  1. xss绕过姿势

    #未完待续... 00x1.绕过 magic_quotes_gpc magic_quotes_gpc=ON 是php中的安全设置,开启后会把一些特殊字符进行轮换, 比如: ' 会被转换为 \' 再比如 ...

  2. @Deprecated注解

    它的作用是对不应该再使用的方法添加注解,当编程人员使用这些方法时,将会在编译时显示提示信息,它与javadoc里的@deprecated标记有相同的功能,准确的说,它还不如javadoc @depre ...

  3. 【转载】 AutoML技术现状与未来展望

    原文地址: https://www.cnblogs.com/marsggbo/p/9309520.html ---------------------------------------------- ...

  4. react起步——从零开始编写react项目

    # index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> ...

  5. PAT 甲级 1054 The Dominant Color (20 分)(简单题)

    1054 The Dominant Color (20 分)   Behind the scenes in the computer's memory, color is always talked ...

  6. MySQL复制表结构

    示例SQL: create table testdb.test_table_back like testdb.test_table

  7. 【Leetcode_easy】747. Largest Number At Least Twice of Others

    problem 747. Largest Number At Least Twice of Others 题意: solution1: class Solution { public: int dom ...

  8. 【leetcode_easy_$】577. Employee Bonus

    problem 577. Employee Bonus 参考 1. Leetcode_easy_$_577. Employee Bonus; 2. https://www.cnblogs.com/li ...

  9. xcode出现the file couldn't be opened怎么解决

    右键——show In finder——显示xcode包内容——将有数字的删除——把有用的xcode双击

  10. iOS-条形码扫描技术SDK:ZBar(转)

    ios条形码扫描技术 iOS 应用里实现条形码扫描功能,有个免费开源的 SDK:ZBar(http://zbar.sourceforge.net/iphone/sdkdoc/install.html) ...