java 日志查看_Java日志-程序员宅基地

技术标签: java 日志查看  

日志对于一个系统来说非常重要,查找异常信息、分析系统运行情况等都需要用到日志。所以无论是JDK还是第三方都提供了关于日志的相关工具,本文分别介绍以下几种工具,以及各种工具间的整合、原理。

JDK的java.util.logging包

第三方日志工具(commons-logging/slf4j,log4j/logback)

JDK的java.util.logging包

JDK1.4引入了java.util.logging包,包中主要的类包括:Logger、LogManager、Handler、Formatter。首先看一段比较简单的示例代码:

packageme.likeyao.jdk.logger;importjava.util.logging.Formatter;importjava.util.logging.Handler;importjava.util.logging.Level;importjava.util.logging.LogRecord;importjava.util.logging.Logger;public classJDKLoggerTest {public static voidmain(String[] args) {

Logger logger= Logger.getLogger("logger");

logger.info("hello world");

Handler handler= newHandler() {

@Overridepublic voidpublish(LogRecord record) {

}

@Overridepublic voidflush() {

}

@Overridepublic void close() throwsSecurityException {

}

};

handler.setFormatter(newFormatter() {

@OverridepublicString format(LogRecord record) {return null;

}

});

logger.setLevel(Level.INFO);

logger.log(Level.FINEST,"hello world");

}

}

通过Logger.getLogger(name)方法可以获取logger对象,logger对象有三个比较重要的概念:level、handler、formatter。level称为日志级别,在java.util.logging包中定义了java.util.logging.Level类,里面包含SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST(从高到低)7种日志级别。设置日志级别会过滤掉一部分日志,例如当日志级别设置为INFO级别时,CONFIG/FINE/FINER/FINEST级别的日志就会被忽略。handler解决的问题是日志输出到哪里,是到控制台(java.util.logging.ConsoleHandler),还是到文件(java.util.logging.FileHandler),或者是写到Socket中(java.util.logging.SocketHandler)。formatter定义了日志输出的格式,可以是XML(java.util.logging.XMLFormatter),也可以自己实现JSON格式的Fomatter。

logger对象是如何生成的

生成logger对象涉及到java.util.logging.LogManager类,LogManager中用到了单例模式,在static块中初始化了LoggerManager实例对象。生成logger对象的过程:

2dd358b3b9c3c0de0a84129ff770c900.png

从图中可以看到,logger对象是在LoggerManager中创建的。LoggerManager中有一个叫userContext的LoggerContext对象,userContext缓存了所有的logger对象(缓存在namedLoggers中),并维护了一套logger对象的父子结构。namedLoggers的定义:

private final Hashtable namedLoggers = new Hashtable<>();

LoggerWeakRef继承自java.lang.ref.WeakReference,namedLoggers并不直接持有logger对象,当持有logger的对象被垃圾回收之后,只有一个weekreference指向logger,方便垃圾回收logger对象。

JDK logger其他一些有意思的东西

有一个Logger.getLogger方法是两参数的,public static Logger getLogger(String name, String resourceBundleName),第二个参数最终会变成java.util.ResourceBundle对象,可以用来做国际化。

java.util.logging.Handler可以设置java.util.logging.Filter更灵活的过滤日志。

第三方日志工具(commons-logging/slf4j,log4j/logback)

首先把四个工具分成了两组,commons-logging/slf4j和log4j/logback。log4j/logback功能与java.util.logging包类似,提供实际的日志功能。commons-logging/slf4j是门面,作用是统一日志操作,屏蔽底层不同日志组件的差异。

commons-logging

commons-logging是apache的项目,使用commons-logging的代码:

packageme.likeyao.java.logger;importorg.apache.commons.logging.Log;importorg.apache.commons.logging.LogFactory;public classJCLTest {private static Log logger = LogFactory.getLog(JCLTest.class);public static void main(String[] args) throwsException {

logger.info("hello world");

}

}

代码中用到了commons-logging日志对象(org.apache.commons.logging.Log),日志对象工厂(org.apache.commons.logging.LogFactory)。前面有说过commons-logging是一个统一操作的门面,不涉及具体的日志功能,那日志对象是怎么产生的?查看Log对象的继承关系:

0ba3d22d50e8fff723d61b1a3f0fef81.png

commons-logging分别为支持的日志工具提供了一个Log类的实现类,在列表中看到了Log4JLogger和Jdk14Logger等,意味着commons-logging可以log4j、java.util.logging组合使用。因为没有对应的logback实现,所以也就无法一起使用。下图解释commons-logging如何决定具体生成哪种Logger对象:

8c6884977f577c87d58ae0ea98510392.png

LogFactory在static块中初始化了HashTable对象factories,以ClassLoader为key,LogFactory为value缓存了所有LogFactory对象。主要看一下LogFactoryImpl的discoverLogImplementation方法是如何发现底层使用的日志工具(省略了方法一部分内容):

private static final String[] classesToDiscover ={

LOGGING_IMPL_LOG4J_LOGGER,//org.apache.commons.logging.impl.Log4JLogger

"org.apache.commons.logging.impl.Jdk14Logger","org.apache.commons.logging.impl.Jdk13LumberjackLogger","org.apache.commons.logging.impl.SimpleLog"};privateLog discoverLogImplementation(String logCategory)

...

Log result= null;//查看commons-logging.properties和System.getProperty是否配置了org.apache.commons.logging.log

String specifiedLogClassName =findUserSpecifiedLogClassName();if (specifiedLogClassName != null) {

...//如果有配置,直接使用配置的类创建Log

result =createLogFromClass(specifiedLogClassName,

logCategory,true);

...returnresult;

}

...//如果没有,遍历classesToDiscover数组,如果使用指定的ClassLoader Class.forName能加载到类,就创建Log对象

for(int i=0; i

result= createLogFromClass(classesToDiscover[i], logCategory, true);

}if (result == null) {throw newLogConfigurationException

("No suitable Log implementation");

}returnresult;

}

整个初始化Log对象的流程:

fcb2699c6f81af336458faaa37b7a84e.png

commons-logging中的classloader

网上搜commons-logging,可以看到不少文章都是在讲关于classloader的问题,贴出比较详细的一篇的链接:《Taxonomy of class loader problems encountered when using Jakarta Commons Logging》。文章中用的commons-logging本版较老,这里用commons-logging-1.2+log4k-1.2.17模拟一种场景,看看新commons-logging有什么改变。代码:

packageme.likeyao.java.logger;importjava.io.File;importjava.net.URL;importjava.net.URLClassLoader;importorg.apache.commons.logging.Log;importorg.apache.commons.logging.LogFactory;public classJCLTest3 {public static void main(String[] args) throwsException {//自定义classloader,模仿web容器classloader,自己加载优先

ChildClassLoader childClassLoader = newChildClassLoader(new URL[] { new File("c:/tmpclass/commons-logging-1.2.jar").toURL(),new File("C:/respository3/log4j/log4j/1.2.17/log4j-1.2.17.jar").toURL()});

Thread.currentThread().setContextClassLoader(childClassLoader);

Log log= LogFactory.getLog(JCLTest3.class);

log.error("hello world");

}

}class ChildClassLoader extendsURLClassLoader {publicChildClassLoader(URL[] urls) {super(urls, ClassLoader.getSystemClassLoader());

}

@Overrideprotected Class> loadClass(String name, booleanresolve)throwsClassNotFoundException {synchronized(getClassLoadingLock(name)) {

Class c=findLoadedClass(name);try{

c=findClass(name);if(resolve) {

resolveClass(c);

}

}catch(Exception e){

}if (c == null) {

c= super.loadClass(name, resolve);

}returnc;

}

}

}

命令行执行:java -cp .;commons-logging-1.2.jar me.likeyao.java.logger.JCLTest3。输出结果:执行没有抛异常,日志是通过JDK java.util.logging打出来了,而不是log4j。

c5e93e9a89e2386865dddec45a21e023.png

可以从代码中分析出为什么会产生这样的结果。这里用到了两种类加载器:系统类加载器(AppClassLoader)和自定义的ChildClassLoader,系统类加载器能加载到当前编译目录的class文件和commons-logging.jar,ChildClassLoader加载了commons-logging.jar和log4j.jar,线程上下文加载器被设置为ChildClassLoader。当运行java命令时,随着程序运行会使用系统类加载器加载Log类、LogFactory类,查看上面discoverLogImplementation方法源码,由于没有配置org.apache.commons.logging.log属性,具体使用哪个Log的实现类会通过遍历classesToDiscover数组决定。下面看一下classesToDiscover[0]时的情况:

private Log createLogFromClass(String logAdapterClassName, //org.apache.commons.logging.impl.Log4JLogger

String logCategory,booleanaffectState)throwsLogConfigurationException {

...//通过logAdapterClassName所制定的类创建的对象,方法返回的结果

Log logAdapter = null;

...//获取classloader,这里会返回ChildClassLoader,即main方法中设置的线程上下文加载器

ClassLoader currentCL =getBaseClassLoader();for(;;) {try{

...

Class c;try{//因为ChildClassLoader加载器可以获取到org.apache.commons.logging.impl.Log4JLogger类,正常得到c

c = Class.forName(logAdapterClassName, true, currentCL);

}catch(ClassNotFoundException originalClassNotFoundException) {

...

}//创建org.apache.commons.logging.impl.Log4JLogger对象,这里o的classloader是ChildClassLoader

constructor =c.getConstructor(logConstructorSignature);

Object o=constructor.newInstance(params);//注意下面的注释,因为Log和o的类加载器不一致,所以不进入if分支//Note that we do this test after trying to create an instance//[rather than testing Log.class.isAssignableFrom(c)] so that//we don't complain about Log hierarchy problems when the//adapter couldn't be instantiated anyway.

if (o instanceofLog) {

logAdapterClass=c;

logAdapter=(Log) o;break;

}//Oops, we have a potential problem here. An adapter class//has been found and its underlying lib is present too, but//there are multiple Log interface classes available making it//impossible to cast to the type the caller wanted. We//certainly can't use this logger, but we need to know whether//to keep on discovering or terminate now.//

//The handleFlawedHierarchy method will throw//LogConfigurationException if it regards this problem as//fatal, and just return if not.//根据allowFlawedHierarchy参数判断是否抛出LogConfigurationException,如果不抛异常,只是简单return,进入下一个循环

handleFlawedHierarchy(currentCL, c);

}catch(NoClassDefFoundError e) {

...break;

}catch(ExceptionInInitializerError e) {

...break;

}catch(LogConfigurationException e) {//call to handleFlawedHierarchy above must have thrown//a LogConfigurationException, so just throw it on

throwe;

}catch(Throwable t) {

...

}if (currentCL == null) {break;

}//try the parent classloader//currentCL = currentCL.getParent();

currentCL =getParentClassLoader(currentCL);

}

...returnlogAdapter;

}

classesToDiscover[0]的调用中,logAdapter并没有指向org.apache.commons.logging.impl.Log4JLogger对象,而是返回了null。所以进入下一次循环classesToDiscover[1](org.apache.commons.logging.impl.Jdk14Logger),这一次因为Log类和org.apache.commons.logging.impl.Jdk14Logger类都是由系统类加载器加载,所以最终执行的结果日志由java.util.logging打出。commons-logging使用classloader来加载Resource和发现底层具体日志工具,在web容器或者OSGI这些需要用类加载器做隔离的情况下的确会出现一些问题。

SLF4J/LOG4J

另一种日志门面SLF4J,它采用“静态绑定”的方式避免了commons-logging中有关类加载的一些问题。下面的把SLF4J和LOG4J放到一起,首先还是比较简单的使用代码:

packageme.likeyao.slf4j.logger;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;public classSLF4JTest {public static voidmain(String[] args) {

Logger logger= LoggerFactory.getLogger(SLF4JTest.class);

logger.error("hello world");

}

}

使用方式上看与commons-logging类似,除了类名上有一些区别。下图的是SLF4J整合LOG4J时Logger初始化时序图:

9507b6680b71411f8485ff0005b691b6.png

这里对几个类用了不同的颜色,蓝色代表类在slf4j-api包中,黄色代表类在slf4j-log4j12包中,红色代表类在log4j包中。相比commons-logging运行时通过classloader来发现底层日志工具,SLF4J是通过在不同的“桥梁包”里放置同名类org.slf4j.impl.StaticLoggerBinder来实现日志工具的绑定。例如,如果需要将SLF4J与java.util.logging整合,需要加入slf4j-jdk14-1.7.12,包中的StaticLoggerBinder类返回的loggerfactory是org.slf4j.impl.JDK14LoggerFactory;如果是SLF4J与log4j整合,需要加入slf4j-log4j12-1.7.12,包中的StaticLoggerBinder类返回的loggerfactory是org.slf4j.impl.Log4jLoggerFactory。这就是SLF4J的静态绑定。

LOG4J是常用的日志工具,与java.util.logging(jul)非常像,一些概念也是共通的。例如logger都是父子结构的、jul的handler对应log4j的appender、formater对应layout、也都存在filter。为了理清log4j中各个类的关系,整理一份类图:

bd9488fb6a377d967cbe0233e4586eaa.png

整个类图的核心是org.apache.log4j.Category,但从1.2版本后log4j不会直接产生Category对象,而是Logger对象。Logger类继承自Category,扩展了一个日志级别:trace。当调用Logger对象的info/warn等方法时会生成一个LoggerEvent对象,AppenderAttachableImpl会遍历所有的appender调用doAppend方法。如果event没有被Filter过滤掉,那最终会经过Layout格式化,输出到appender指定的地方。LOG4J提供了很多appender供使用,这一点比JDK的hander强大很多。

Appender的初始化在LogManager的static块中进行,最终解析发生在org.apache.log4j.xml.DOMConfigurator类中,logger、root、appender都在这里解析。

if (tagName.equals(CATEGORY) ||tagName.equals(LOGGER)) {

parseCategory(currentElement);

}else if(tagName.equals(ROOT_TAG)) {

parseRoot(currentElement);

}else if(tagName.equals(RENDERER_TAG)) {

parseRenderer(currentElement);

}else if(tagName.equals(THROWABLE_RENDERER_TAG)) {if (repository instanceofThrowableRendererSupport) {

ThrowableRenderer tr=parseThrowableRenderer(currentElement);if (tr != null) {

((ThrowableRendererSupport) repository).setThrowableRenderer(tr);

}

}

}else if (!(tagName.equals(APPENDER_TAG)||tagName.equals(CATEGORY_FACTORY_TAG)||tagName.equals(LOGGER_FACTORY_TAG))) {

quietParseUnrecognizedElement(repository, currentElement, props);

}

LOGBACK

logback是log4j的一个替代品,初始化和打日志代码流程相似。为什么要从log4j切换到logback,logback网站上已经给出了(Reasons to prefer logback over log4j)。要使用SLF4J+logback需要引入三个包:slf4j-api、logback-core、logback-classic。这里和log4j对比,介绍一下两种日志工具的父子结构。无论是java.util.logging还是log4j/logback都为logger对象提供了父子结构,这样做有什么好处?我觉得主要是这样logger对象会有一个树形的层次结构,底层的logger可以复用父logger中的一些配置,比如日志级别,appender等。从log4j和logback的配置文件中,也可以看出这一点,通过为某个包名的logger指定appender和日志级别,可以作用所有这个包下的logger。例如:

log4j和logback在处理父子结构时有一些差别,先看log4j的代码:

synchronized(ht) {

Object o=ht.get(key);if(o == null) {

logger=factory.makeNewLoggerInstance(name);

logger.setHierarchy(this);

ht.put(key, logger);

updateParents(logger);returnlogger;

}else if(o instanceofLogger) {return(Logger) o;

}else if (o instanceofProvisionNode) {//System.out.println("("+name+") ht.get(this) returned ProvisionNode");

logger =factory.makeNewLoggerInstance(name);

logger.setHierarchy(this);

ht.put(key, logger);

updateChildren((ProvisionNode) o, logger);

updateParents(logger);returnlogger;

}else{//It should be impossible to arrive here

return null; //but let's keep the compiler happy.

}

}

log4j在创建logger时,如果父节点是一个logger,那只维护一个子logger对父logger的引用。如果父节点不存在,就创建一些ProvisionNode对象(这是一个Vector的子类),保存所有下面子的logger。举个例子:当获取名称为x.y.z的logger时,首先会创建logger(x.y.z);然后查找父logger x.y,如果x.y不存在,就创建一个ProvisionNode对象,把logger(x.y.z)放到ProvisionNode对象中;然后继续向上搜索名称为x的logger,就将logger(x.y.z)的parent设置为logger(x)。当调用logger(x.y.z)对象的info/warn等方法时,如果logger(x.y.z)没有设置日志级别和appender,就是沿着parent向上搜索,直到rootLogger为止。

logback的结构稍微有一些区别,所有的节点都是Logger对象,对象中有指向parent和children的引用,并且创建节点时,会把parent的日志级别直接复制到自己对象中。

//if the desired logger does not exist, them create all the loggers//in between as well (if they don't already exist)

String childName;while (true) {int h =LoggerNameUtil.getSeparatorIndexOf(name, i);if (h == -1) {

childName=name;

}else{

childName= name.substring(0, h);

}//move i left of the last point

i = h + 1;synchronized(logger) {

childLogger=logger.getChildByName(childName);if (childLogger == null) {

childLogger=logger.createChildByName(childName);

loggerCache.put(childName, childLogger);

incSize();

}

}

logger=childLogger;if (h == -1) {returnchildLogger;

}

}

总结

SL4J/COMMONS-LOGGING、LOG4J、LOGBACK、JUL都是常用的Java日志工具,基本思想都是通过Factory生成logger对象,然后由LogManager管理/缓存logger对象,同时维护一个父子结构方便复用配置。logger对象包含三要素:日志级别、输出到哪里、格式化。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_30082367/article/details/114060708

智能推荐

hive使用适用场景_大数据入门:Hive应用场景-程序员宅基地

文章浏览阅读5.8k次。在大数据的发展当中,大数据技术生态的组件,也在不断地拓展开来,而其中的Hive组件,作为Hadoop的数据仓库工具,可以实现对Hadoop集群当中的大规模数据进行相应的数据处理。今天我们的大数据入门分享,就主要来讲讲,Hive应用场景。关于Hive,首先需要明确的一点就是,Hive并非数据库,Hive所提供的数据存储、查询和分析功能,本质上来说,并非传统数据库所提供的存储、查询、分析功能。Hive..._hive应用场景

zblog采集-织梦全自动采集插件-织梦免费采集插件_zblog 网页采集插件-程序员宅基地

文章浏览阅读496次。Zblog是由Zblog开发团队开发的一款小巧而强大的基于Asp和PHP平台的开源程序,但是插件市场上的Zblog采集插件,没有一款能打的,要么就是没有SEO文章内容处理,要么就是功能单一。很少有适合SEO站长的Zblog采集。人们都知道Zblog采集接口都是对Zblog采集不熟悉的人做的,很多人采取模拟登陆的方法进行发布文章,也有很多人直接操作数据库发布文章,然而这些都或多或少的产生各种问题,发布速度慢、文章内容未经严格过滤,导致安全性问题、不能发Tag、不能自动创建分类等。但是使用Zblog采._zblog 网页采集插件

Flink学习四:提交Flink运行job_flink定时运行job-程序员宅基地

文章浏览阅读2.4k次,点赞2次,收藏2次。restUI页面提交1.1 添加上传jar包1.2 提交任务job1.3 查看提交的任务2. 命令行提交./flink-1.9.3/bin/flink run -c com.qu.wc.StreamWordCount -p 2 FlinkTutorial-1.0-SNAPSHOT.jar3. 命令行查看正在运行的job./flink-1.9.3/bin/flink list4. 命令行查看所有job./flink-1.9.3/bin/flink list --all._flink定时运行job

STM32-LED闪烁项目总结_嵌入式stm32闪烁led实验总结-程序员宅基地

文章浏览阅读1k次,点赞2次,收藏6次。这个项目是基于STM32的LED闪烁项目,主要目的是让学习者熟悉STM32的基本操作和编程方法。在这个项目中,我们将使用STM32作为控制器,通过对GPIO口的控制实现LED灯的闪烁。这个STM32 LED闪烁的项目是一个非常简单的入门项目,但它可以帮助学习者熟悉STM32的编程方法和GPIO口的使用。在这个项目中,我们通过对GPIO口的控制实现了LED灯的闪烁。LED闪烁是STM32入门课程的基础操作之一,它旨在教学生如何使用STM32开发板控制LED灯的闪烁。_嵌入式stm32闪烁led实验总结

Debezium安装部署和将服务托管到systemctl-程序员宅基地

文章浏览阅读63次。本文介绍了安装和部署Debezium的详细步骤,并演示了如何将Debezium服务托管到systemctl以进行方便的管理。本文将详细介绍如何安装和部署Debezium,并将其服务托管到systemctl。解压缩后,将得到一个名为"debezium"的目录,其中包含Debezium的二进制文件和其他必要的资源。注意替换"ExecStart"中的"/path/to/debezium"为实际的Debezium目录路径。接下来,需要下载Debezium的压缩包,并将其解压到所需的目录。

Android 控制屏幕唤醒常亮或熄灭_android实现拿起手机亮屏-程序员宅基地

文章浏览阅读4.4k次。需求:在诗词曲文项目中,诗词整篇朗读的时候,文章没有读完会因为屏幕熄灭停止朗读。要求:在文章没有朗读完毕之前屏幕常亮,读完以后屏幕常亮关闭;1.权限配置:设置电源管理的权限。

随便推点

目标检测简介-程序员宅基地

文章浏览阅读2.3k次。目标检测简介、评估标准、经典算法_目标检测

记SQL server安装后无法连接127.0.0.1解决方法_sqlserver 127 0 01 无法连接-程序员宅基地

文章浏览阅读6.3k次,点赞4次,收藏9次。实训时需要安装SQL server2008 R所以我上网上找了一个.exe 的安装包链接:https://pan.baidu.com/s/1_FkhB8XJy3Js_rFADhdtmA提取码:ztki注:解压后1.04G安装时Microsoft需下载.NET,更新安装后会自动安装如下:点击第一个傻瓜式安装,唯一注意的是在修改路径的时候如下不可修改:到安装实例的时候就可以修改啦数据..._sqlserver 127 0 01 无法连接

js 获取对象的所有key值,用来遍历_js 遍历对象的key-程序员宅基地

文章浏览阅读7.4k次。1. Object.keys(item); 获取到了key之后就可以遍历的时候直接使用这个进行遍历所有的key跟valuevar infoItem={ name:'xiaowu', age:'18',}//的出来的keys就是[name,age]var keys=Object.keys(infoItem);2. 通常用于以下实力中 <div *ngFor="let item of keys"> <div>{{item}}.._js 遍历对象的key

粒子群算法(PSO)求解路径规划_粒子群算法路径规划-程序员宅基地

文章浏览阅读2.2w次,点赞51次,收藏310次。粒子群算法求解路径规划路径规划问题描述    给定环境信息,如果该环境内有障碍物,寻求起始点到目标点的最短路径, 并且路径不能与障碍物相交,如图 1.1.1 所示。1.2 粒子群算法求解1.2.1 求解思路    粒子群优化算法(PSO),粒子群中的每一个粒子都代表一个问题的可能解, 通过粒子个体的简单行为,群体内的信息交互实现问题求解的智能性。    在路径规划中,我们将每一条路径规划为一个粒子,每个粒子群群有 n 个粒 子,即有 n 条路径,同时,每个粒子又有 m 个染色体,即中间过渡点的_粒子群算法路径规划

量化评价:稳健的业绩评价指标_rar 海龟-程序员宅基地

文章浏览阅读353次。所谓稳健的评估指标,是指在评估的过程中数据的轻微变化并不会显著的影响一个统计指标。而不稳健的评估指标则相反,在对交易系统进行回测时,参数值的轻微变化会带来不稳健指标的大幅变化。对于不稳健的评估指标,任何对数据有影响的因素都会对测试结果产生过大的影响,这很容易导致数据过拟合。_rar 海龟

IAP在ARM Cortex-M3微控制器实现原理_value line devices connectivity line devices-程序员宅基地

文章浏览阅读607次,点赞2次,收藏7次。–基于STM32F103ZET6的UART通讯实现一、什么是IAP,为什么要IAPIAP即为In Application Programming(在应用中编程),一般情况下,以STM32F10x系列芯片为主控制器的设备在出厂时就已经使用J-Link仿真器将应用代码烧录了,如果在设备使用过程中需要进行应用代码的更换、升级等操作的话,则可能需要将设备返回原厂并拆解出来再使用J-Link重新烧录代码,这就增加了很多不必要的麻烦。站在用户的角度来说,就是能让用户自己来更换设备里边的代码程序而厂家这边只需要提供给_value line devices connectivity line devices