MyBatis源码分析(三):MyBatis初始化(配置文件读取和解析)
一、 介绍MyBatis初始化过程
项目是简单的Mybatis应用,编写SQL Mapper,还有编写的SqlSessionFactoryUtil里面用了Mybatis的IO包里面的Resources获取配置文件的输入流,利用SqlSessionFactoryBuilder获取创建Session的工厂。
首先构建的是承载mybatis-config配置的Configuration类,它是由SqlSessionFactoryBuilder的build开始的,时序图如下:

二、相关代码
自己编写的SqlSessionFactoryUtil.java
1 public class SqlSessionFactoryUtil {
2 //SQLSessionFactory对象
3 private static SqlSessionFactory sqlSessionFactory = null;
4 //类线程锁
5 private static final Class CLASS_LOCK = SqlSessionFactoryUtil.class;
6
7 private SqlSessionFactoryUtil() {}
8
9 /**
10 * 构建SqlSessionFactory
11 */
12 public static SqlSessionFactory init() {
13 String resource = "mybatis-config.xml";
14 InputStream inputStream = null;
15 try {
16 inputStream = Resources.getResourceAsStream(resource);
17 } catch (IOException ex) {
18 Logger.getLogger(SqlSessionFactoryUtil.class.getName()).log(Level.SEVERE, null, ex);
19 }
20 synchronized(CLASS_LOCK) {
21 if(sqlSessionFactory == null) {
22 sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
23 }
24 }
25 return sqlSessionFactory;
26 }
27
28 /**
29 * 打开SqlSession
30 */
31 public static SqlSession openSqlSession() {
32 if (sqlSessionFactory == null) {
33 init();
34 }
35 return sqlSessionFactory.openSession();
36 }
37 }
源码 SqlSessionFactoryBuilder.java,首先是读取配置到Configuration类,再利用读取出来的config构建DefaultSqlSessionFactory
1 public class SqlSessionFactoryBuilder {
2
3 public SqlSessionFactory build(Reader reader) {
4 return build(reader, null, null);
5 }
6
7 public SqlSessionFactory build(Reader reader, String environment) {
8 return build(reader, environment, null);
9 }
10
11 public SqlSessionFactory build(Reader reader, Properties properties) {
12 return build(reader, null, properties);
13 }
14
15 public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
16 try {
17 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
18 return build(parser.parse());
19 } catch (Exception e) {
20 throw ExceptionFactory.wrapException("Error building SqlSession.", e);
21 } finally {
22 ErrorContext.instance().reset();
23 try {
24 reader.close();
25 } catch (IOException e) {
26 // Intentionally ignore. Prefer previous error.
27 }
28 }
29 }
30 //使用的是这个方法构建SqlSessionFactory
31 public SqlSessionFactory build(InputStream inputStream) {
32 return build(inputStream, null, null);
33 }
34
35 public SqlSessionFactory build(InputStream inputStream, String environment) {
36 return build(inputStream, environment, null);
37 }
38
39 public SqlSessionFactory build(InputStream inputStream, Properties properties) {
40 return build(inputStream, null, properties);
41 }
42 //构建build
43 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
44 try {
45 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //XMLConfigBuilder解析mybatis-config.xml配置
46 return build(parser.parse());
47 } catch (Exception e) {
48 throw ExceptionFactory.wrapException("Error building SqlSession.", e);
49 } finally {
50 ErrorContext.instance().reset();
51 try {
52 inputStream.close();
53 } catch (IOException e) {
54 // Intentionally ignore. Prefer previous error.
55 }
56 }
57 }
58
59 public SqlSessionFactory build(Configuration config) {
60 return new DefaultSqlSessionFactory(config);
61 }
62
63 }
源码 XMLConfigBuilder.java,读取并保存mybatis-config配置文件中大部分节点属性
1 public class XMLConfigBuilder extends BaseBuilder {
2
3 private boolean parsed;
4 private final XPathParser parser;
5 private String environment;
6 private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
7
8 public XMLConfigBuilder(Reader reader) {
9 this(reader, null, null);
10 }
11
12 public XMLConfigBuilder(Reader reader, String environment) {
13 this(reader, environment, null);
14 }
15
16 public XMLConfigBuilder(Reader reader, String environment, Properties props) {
17 this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
18 }
19
20 public XMLConfigBuilder(InputStream inputStream) {
21 this(inputStream, null, null);
22 }
23
24 public XMLConfigBuilder(InputStream inputStream, String environment) {
25 this(inputStream, environment, null);
26 }
27
28 public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
29 this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
30 }
31
32 private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
33 super(new Configuration());
34 ErrorContext.instance().resource("SQL Mapper Configuration");
35 this.configuration.setVariables(props);
36 this.parsed = false;
37 this.environment = environment;
38 this.parser = parser;
39 }
40
41 public Configuration parse() {
42 if (parsed) {
43 throw new BuilderException("Each XMLConfigBuilder can only be used once.");
44 }
45 parsed = true;
46 parseConfiguration(parser.evalNode("/configuration"));
47 return configuration;
48 }
49
50 private void parseConfiguration(XNode root) {
51 try {
52 //issue #117 read properties first
53 propertiesElement(root.evalNode("properties"));
54 Properties settings = settingsAsProperties(root.evalNode("settings"));
55 loadCustomVfs(settings);
56 loadCustomLogImpl(settings);
57 typeAliasesElement(root.evalNode("typeAliases"));
58 pluginElement(root.evalNode("plugins"));
59 objectFactoryElement(root.evalNode("objectFactory"));
60 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
61 reflectorFactoryElement(root.evalNode("reflectorFactory"));
62 settingsElement(settings);
63 // read it after objectFactory and objectWrapperFactory issue #631
64 environmentsElement(root.evalNode("environments"));
65 databaseIdProviderElement(root.evalNode("databaseIdProvider"));
66 typeHandlerElement(root.evalNode("typeHandlers"));
67 mapperElement(root.evalNode("mappers"));
68 } catch (Exception e) {
69 throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
70 }
71 }
72
73 private Properties settingsAsProperties(XNode context) {
74 if (context == null) {
75 return new Properties();
76 }
77 Properties props = context.getChildrenAsProperties();
78 // Check that all settings are known to the configuration class
79 MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
80 for (Object key : props.keySet()) {
81 if (!metaConfig.hasSetter(String.valueOf(key))) {
82 throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
83 }
84 }
85 return props;
86 }
87
88 private void loadCustomVfs(Properties props) throws ClassNotFoundException {
89 String value = props.getProperty("vfsImpl");
90 if (value != null) {
91 String[] clazzes = value.split(",");
92 for (String clazz : clazzes) {
93 if (!clazz.isEmpty()) {
94 @SuppressWarnings("unchecked")
95 Class<? extends VFS> vfsImpl = (Class<? extends VFS>)Resources.classForName(clazz);
96 configuration.setVfsImpl(vfsImpl);
97 }
98 }
99 }
100 }
101
102 private void loadCustomLogImpl(Properties props) {
103 Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
104 configuration.setLogImpl(logImpl);
105 }
106
107 private void typeAliasesElement(XNode parent) {
108 if (parent != null) {
109 for (XNode child : parent.getChildren()) {
110 if ("package".equals(child.getName())) {
111 String typeAliasPackage = child.getStringAttribute("name");
112 configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
113 } else {
114 String alias = child.getStringAttribute("alias");
115 String type = child.getStringAttribute("type");
116 try {
117 Class<?> clazz = Resources.classForName(type);
118 if (alias == null) {
119 typeAliasRegistry.registerAlias(clazz);
120 } else {
121 typeAliasRegistry.registerAlias(alias, clazz);
122 }
123 } catch (ClassNotFoundException e) {
124 throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
125 }
126 }
127 }
128 }
129 }
130
131 private void pluginElement(XNode parent) throws Exception {
132 if (parent != null) {
133 for (XNode child : parent.getChildren()) {
134 String interceptor = child.getStringAttribute("interceptor");
135 Properties properties = child.getChildrenAsProperties();
136 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
137 interceptorInstance.setProperties(properties);
138 configuration.addInterceptor(interceptorInstance);
139 }
140 }
141 }
142
143 private void objectFactoryElement(XNode context) throws Exception {
144 if (context != null) {
145 String type = context.getStringAttribute("type");
146 Properties properties = context.getChildrenAsProperties();
147 ObjectFactory factory = (ObjectFactory) resolveClass(type).newInstance();
148 factory.setProperties(properties);
149 configuration.setObjectFactory(factory);
150 }
151 }
152
153 private void objectWrapperFactoryElement(XNode context) throws Exception {
154 if (context != null) {
155 String type = context.getStringAttribute("type");
156 ObjectWrapperFactory factory = (ObjectWrapperFactory) resolveClass(type).newInstance();
157 configuration.setObjectWrapperFactory(factory);
158 }
159 }
160
161 private void reflectorFactoryElement(XNode context) throws Exception {
162 if (context != null) {
163 String type = context.getStringAttribute("type");
164 ReflectorFactory factory = (ReflectorFactory) resolveClass(type).newInstance();
165 configuration.setReflectorFactory(factory);
166 }
167 }
168
169 private void propertiesElement(XNode context) throws Exception {
170 if (context != null) {
171 Properties defaults = context.getChildrenAsProperties();
172 String resource = context.getStringAttribute("resource");
173 String url = context.getStringAttribute("url");
174 if (resource != null && url != null) {
175 throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
176 }
177 if (resource != null) {
178 defaults.putAll(Resources.getResourceAsProperties(resource));
179 } else if (url != null) {
180 defaults.putAll(Resources.getUrlAsProperties(url));
181 }
182 Properties vars = configuration.getVariables();
183 if (vars != null) {
184 defaults.putAll(vars);
185 }
186 parser.setVariables(defaults);
187 configuration.setVariables(defaults);
188 }
189 }
190
191 private void settingsElement(Properties props) {
192 configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
193 configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
194 configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
195 configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
196 configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
197 configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
198 configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
199 configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
200 configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
201 configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
202 configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
203 configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
204 configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
205 configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
206 configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
207 configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
208 configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
209 configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
210 configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
211 configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
212 configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
213 configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
214 configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
215 configuration.setLogPrefix(props.getProperty("logPrefix"));
216 configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
217 }
218
219 private void environmentsElement(XNode context) throws Exception {
220 if (context != null) {
221 if (environment == null) {
222 environment = context.getStringAttribute("default");
223 }
224 for (XNode child : context.getChildren()) {
225 String id = child.getStringAttribute("id");
226 if (isSpecifiedEnvironment(id)) {
227 TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
228 DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
229 DataSource dataSource = dsFactory.getDataSource();
230 Environment.Builder environmentBuilder = new Environment.Builder(id)
231 .transactionFactory(txFactory)
232 .dataSource(dataSource);
233 configuration.setEnvironment(environmentBuilder.build());
234 }
235 }
236 }
237 }
238
239 private void databaseIdProviderElement(XNode context) throws Exception {
240 DatabaseIdProvider databaseIdProvider = null;
241 if (context != null) {
242 String type = context.getStringAttribute("type");
243 // awful patch to keep backward compatibility
244 if ("VENDOR".equals(type)) {
245 type = "DB_VENDOR";
246 }
247 Properties properties = context.getChildrenAsProperties();
248 databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
249 databaseIdProvider.setProperties(properties);
250 }
251 Environment environment = configuration.getEnvironment();
252 if (environment != null && databaseIdProvider != null) {
253 String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
254 configuration.setDatabaseId(databaseId);
255 }
256 }
257
258 private TransactionFactory transactionManagerElement(XNode context) throws Exception {
259 if (context != null) {
260 String type = context.getStringAttribute("type");
261 Properties props = context.getChildrenAsProperties();
262 TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
263 factory.setProperties(props);
264 return factory;
265 }
266 throw new BuilderException("Environment declaration requires a TransactionFactory.");
267 }
268
269 private DataSourceFactory dataSourceElement(XNode context) throws Exception {
270 if (context != null) {
271 String type = context.getStringAttribute("type");
272 Properties props = context.getChildrenAsProperties();
273 DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
274 factory.setProperties(props);
275 return factory;
276 }
277 throw new BuilderException("Environment declaration requires a DataSourceFactory.");
278 }
279
280 private void typeHandlerElement(XNode parent) {
281 if (parent != null) {
282 for (XNode child : parent.getChildren()) {
283 if ("package".equals(child.getName())) {
284 String typeHandlerPackage = child.getStringAttribute("name");
285 typeHandlerRegistry.register(typeHandlerPackage);
286 } else {
287 String javaTypeName = child.getStringAttribute("javaType");
288 String jdbcTypeName = child.getStringAttribute("jdbcType");
289 String handlerTypeName = child.getStringAttribute("handler");
290 Class<?> javaTypeClass = resolveClass(javaTypeName);
291 JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
292 Class<?> typeHandlerClass = resolveClass(handlerTypeName);
293 if (javaTypeClass != null) {
294 if (jdbcType == null) {
295 typeHandlerRegistry.register(javaTypeClass, typeHandlerClass);
296 } else {
297 typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass);
298 }
299 } else {
300 typeHandlerRegistry.register(typeHandlerClass);
301 }
302 }
303 }
304 }
305 }
306
307 private void mapperElement(XNode parent) throws Exception {
308 if (parent != null) {
309 for (XNode child : parent.getChildren()) {
310 if ("package".equals(child.getName())) {
311 String mapperPackage = child.getStringAttribute("name");
312 configuration.addMappers(mapperPackage);
313 } else {
314 String resource = child.getStringAttribute("resource");
315 String url = child.getStringAttribute("url");
316 String mapperClass = child.getStringAttribute("class");
317 if (resource != null && url == null && mapperClass == null) {
318 ErrorContext.instance().resource(resource);
319 InputStream inputStream = Resources.getResourceAsStream(resource);
320 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
321 mapperParser.parse();
322 } else if (resource == null && url != null && mapperClass == null) {
323 ErrorContext.instance().resource(url);
324 InputStream inputStream = Resources.getUrlAsStream(url);
325 XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
326 mapperParser.parse();
327 } else if (resource == null && url == null && mapperClass != null) {
328 Class<?> mapperInterface = Resources.classForName(mapperClass);
329 configuration.addMapper(mapperInterface);
330 } else {
331 throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
332 }
333 }
334 }
335 }
336 }
337
338 private boolean isSpecifiedEnvironment(String id) {
339 if (environment == null) {
340 throw new BuilderException("No environment specified.");
341 } else if (id == null) {
342 throw new BuilderException("Environment requires an id attribute.");
343 } else if (environment.equals(id)) {
344 return true;
345 }
346 return false;
347 }
348
349 }
最后由SqlSessionFactoryBuilder返回的DefaultSqlSessionFactory的openSession()方法获取session,这里Mybatis的初始化就完成了,剩下的是mapper接口的映射工作了。
MyBatis源码分析(三):MyBatis初始化(配置文件读取和解析)的更多相关文章
- mybatis底层源码分析之--配置文件读取和解析
现在企业级开发中ssm是很常见的技术标配,mybatis比hibernate轻量了很多,而且学习成本相对较低,简单易上手. 那么,问题来了,简单好用的mybatis底层到底是如何实现的呢?都使用了什么 ...
- MyBatis源码分析(1)-MapConfig文件的解析
1.简述 MyBatis是一个优秀的轻ORM框架,由最初的iBatis演化而来,可以方便的完成sql语句的输入输出到java对象之间的相互映射,典型的MyBatis使用的方式如下: String re ...
- mybatis源码分析(二)------------配置文件的解析
这篇文章中,我们将讲解配置文件中 properties,typeAliases,settings和environments这些节点的解析过程. 一 properties的解析 private void ...
- 精尽 MyBatis 源码分析 - MyBatis 初始化(三)之 SQL 初始化(上)
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- MyBatis 源码分析 - 配置文件解析过程
* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...
- 精尽 MyBatis 源码分析 - MyBatis 初始化(一)之加载 mybatis-config.xml
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- 精尽MyBatis源码分析 - MyBatis初始化(二)之加载Mapper接口与XML映射文件
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- 精尽MyBatis源码分析 - MyBatis初始化(四)之 SQL 初始化(下)
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- MyBatis源码分析-MyBatis初始化流程
MyBatis 是支持定制化 SQL.存储过程以及高级映射的优秀的持久层框架.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以对配置和原生Map使用简 ...
- 精尽MyBatis源码分析 - SQL执行过程(三)之 ResultSetHandler
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
随机推荐
- 最长公共前缀 js 实现代码
编写一个函数来查找字符串数组中的最长公共前缀: 输入 : ["abca","abc","abca","abc",&quo ...
- 如何使用SQL的备份文件(.bak)恢复数据库
出于很多情况,数据库只剩下.bak文件,想要恢复数据库,找了很多资料才知道可以这样!!!!! 个人觉得图片教程更有意义,请看步骤: 1.选中"数据库" 右击 选择"还原数 ...
- Jmeter扩展组件开发(2) - 扩展开发第一个demo的实现
maven工程src目录介绍 main:写代码 main/java:写Java代码 main/resources:写配置文件 test:写测试代码 test/java demo实现 创建Package ...
- 怎么通俗的理解Netty呢?
目录 Netty(3.X) 简单体验 Netty的事件驱动机制 Netty的源码阅读 Netty(3.X) 有了Netty,你可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,W ...
- 低差异序列 (low-discrepancy sequences)之Halton序列均匀产生多维随机数的介绍与实现
Halton序列 在统计学中,Halton序列是用于生成空间中的点的序列,如Monte Carlo模拟的数值方法,虽然这些序列是确定性的,但它们的差异性很低,也就是说,在许多方面看起来是随机的.它们在 ...
- nginx 常用x代码
1.nginx 禁止ip直接访问,只允许域名访问,直接在.conf文件里 server上面再添加一个server 代码,不可以写同一个server里: server { listen 80 defau ...
- P7046-「MCOI-03」诗韵【SAM,倍增,树状数组】
正题 题目链接:https://www.luogu.com.cn/problem/P7046 题目大意 给出一个长度为 \(n\) 的字符串,然后 \(m\) 次把它的一个子串加入集合.如果一个字符串 ...
- P5110-块速递推【特征方程,分块】
正题 题目链接:https://www.luogu.com.cn/problem/P5110 题目大意 数列\(a\)满足 \[a_n=233a_{n-1}+666a_{n-2},a_0=0,a_1= ...
- 国庆出游神器:魔幻黑科技换天造物,让vlog秒变科幻大片!
摘要:国庆旅游景点人太多,拍出来的照片全是人人人.车车车,该怎么办?不妨试试这个黑科技,让你的出游vlog秒变科幻大片. 本文分享自华为云社区<国庆出游神器,魔幻黑科技换天造物,让vlog秒变科 ...
- 详解package-lock.json的作用
目录 详解package-lock.json package-lock.json的作用 版本号的定义规则与前缀对安装的影响 改动package.json后依旧能改变项目依赖的版本 当前项目的真实版本号 ...