欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

使用Log4j2代碼方式配置實(shí)現(xiàn)線程級(jí)動(dòng)態(tài)控制

 更新時(shí)間:2021年12月22日 14:45:08   作者:檸檬睡客  
這篇文章主要介紹了使用Log4j2代碼方式配置實(shí)現(xiàn)線程級(jí)動(dòng)態(tài)控制,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

一 需求

最近平臺(tái)進(jìn)行升級(jí),要求日志工具從Log4j升級(jí)到Log4j2,以求性能上的提升。之前我寫(xiě)過(guò)以代碼方式的配置Log4j,來(lái)實(shí)現(xiàn)線程級(jí)日志對(duì)象的管理,今天把版本升級(jí)到Log4j2,依然采用原有思路來(lái)做,但是實(shí)現(xiàn)上有諸多區(qū)別,這是因?yàn)長(zhǎng)og4j2的實(shí)現(xiàn)較老版本改動(dòng)太多。

至于以配置文件方式配置的方法,我不做介紹,對(duì)于Log4j2的詳細(xì)實(shí)現(xiàn)亦然,這些部分有興趣的朋友可以自己網(wǎng)絡(luò)搜索,也可以自行跟蹤源碼查閱。

主要需求為每個(gè)線程單獨(dú)一個(gè)日志對(duì)象處理,可動(dòng)態(tài)控制日志輸出級(jí)別等參數(shù),可控制的同步及異步輸出模式,標(biāo)準(zhǔn)的日志頭格式等。

大體思路同我之前寫(xiě)的Log4j配置,只不過(guò)日志對(duì)象的創(chuàng)建管理等功能實(shí)現(xiàn)上略有區(qū)別。

二 對(duì)外暴露的接口

設(shè)計(jì)的重中之重,這部分一旦設(shè)計(jì)完成便不可輕易改動(dòng),后續(xù)的維護(hù)只能盡其可能的向前兼容。

首先我們需要提供對(duì)外可用的預(yù)設(shè)參數(shù)值,包括日志輸出等級(jí)、日志的輸出目標(biāo)(文件、控制臺(tái)、網(wǎng)絡(luò)及數(shù)據(jù)庫(kù)等)等,這些值設(shè)定我們均以static final來(lái)修飾。

第二部分是全局參數(shù)的設(shè)置,這些參數(shù)不隨日志對(duì)象的狀態(tài)而發(fā)生變動(dòng),反而是作為日志對(duì)象構(gòu)造的屬性值來(lái)源,比如說(shuō)日志文件的路徑、日志文件的最大容量、日志文件的備份數(shù)量,以及每個(gè)線程的日志對(duì)象初始的日志輸出等級(jí)等等。這部分參數(shù)可以從外部配置讀取,當(dāng)然也需要有默認(rèn)值設(shè)定,屬全局參數(shù)配置,以static修飾。

第三部分為日志輸出的接口定義,這部分可有可無(wú),但是對(duì)于大型項(xiàng)目來(lái)說(shuō)極為重要,以規(guī)范的接口進(jìn)行日志輸出,可以為日志的采集、分析帶來(lái)巨大便利,如通訊日志、異常日志等內(nèi)容的格式化輸出。

所以,按總體思路預(yù)先定義日志工具的外對(duì)暴露接口,如下:

public class LogUtil
{
	public static final int LevelTrace = 0;
	public static final int LevelDebug = 1;
	public static final int LevelInfo = 2;
	public static final int LevelWarn = 3;
	public static final int LevelError = 4;
	public static final String[] LevelName = new String[]
	{ "TRACE", "DEBUG", "INFO", "WARN", "ERROR" };
	public static final String TypeCommunication = "comm";
	public static final String TypeProcess = "proc";
	public static final String TypeException = "exce";
	public static final int AppenderConsole = 1;
	public static final int AppenderFile = 2;
	private static int DefaultLogLevel = LevelDebug;
	private static String FilePath = null;
	private static String FileSize = "100MB";
	private static int BackupIndex = -1;
	private static int BufferSize = 0;
	private static String LinkChar = "-";
	private static int LogAppender = AppenderFile;	
	public static void log(int logLevel, String logType, String logText)
	{
		getThreadLogger().log(logLevel, logType, logText);
	}
	public static void logCommunication()
	{
		// TODO
	}
	
	public static void logException()
	{
		// TODO
	}	
	……
}

這里我暫定了一個(gè)公用的日志輸出接口名為log(),參數(shù)列表為日志的輸出等級(jí)、日志的類(lèi)型以及日志內(nèi)容。而通訊日志的輸出接口為logCommunication(),其參數(shù)列表暫時(shí)空著,按各位讀者的需求自行填寫(xiě),異常日志的輸出亦然。后文我會(huì)在測(cè)試部分對(duì)其填寫(xiě)。

為了描述方便,我僅定義了兩個(gè)日志輸出目標(biāo),一為控制臺(tái),二為文件。

三 代碼方式配置Log4j2日志對(duì)象

接下來(lái)是重頭戲,如果不采取配置文件的方式進(jìn)行Log4j2的配置,那么Log4j2會(huì)自行采用默認(rèn)的配置,這不是我們想要的。雖然這個(gè)過(guò)程我們無(wú)法選擇,也規(guī)避不了,但是我們最后都是使用Logger對(duì)象來(lái)進(jìn)行日志輸出的,所以我們只需要按需構(gòu)造Logger對(duì)象,并將其管理起來(lái)即可。

這里提供一個(gè)思路,首先LogUtil維護(hù)一組線程級(jí)日志對(duì)象,然后這一組線程級(jí)日志對(duì)象共同訪問(wèn)同一組Logger對(duì)象。

跟蹤源碼我發(fā)現(xiàn)Log4j2對(duì)Logger對(duì)象的構(gòu)造還是較為復(fù)雜的,使用了大量的Builder,其實(shí)較為早期的版本中也提供了構(gòu)造函數(shù)方式來(lái)初始化對(duì)象,但是后期的版本卻都被標(biāo)記了@depreciation。對(duì)于Builder模式大家自己查閱其他信息了解吧。

好消息是Log4j2的核心組件依然是Appender、Logger,只不過(guò)提供了更多的可配置內(nèi)容,包括日志同時(shí)按日期和文件大小進(jìn)行備份,這才是我想要的,之前寫(xiě)Log4j的時(shí)候我可是自己派生了一個(gè)Appender類(lèi)才實(shí)現(xiàn)的同時(shí)備份。

向控制臺(tái)輸出的Appender具體類(lèi)型為ConsoleAppender,我使用默認(rèn)的構(gòu)造函數(shù)來(lái)處理,這是唯一一個(gè)被公開(kāi)出來(lái),且沒(méi)有被@depreciation修飾的構(gòu)造函數(shù),想來(lái)只要是能通過(guò)默認(rèn)配置實(shí)現(xiàn)的都是被Log4j2認(rèn)可的吧,不然為啥要弄這么多Builder嘞。

向文件輸出的Appender我采用RollingFileAppender,無(wú)他,就是為了能夠?qū)崿F(xiàn)同時(shí)按日期及文件大小進(jìn)行備份,而且該Appender可適用全局異步模式,性能較AsyncAppender高了不少。它的創(chuàng)建方式要麻煩許多,因?yàn)槲倚枰O(shè)置觸發(fā)器來(lái)控制何時(shí)觸發(fā)備份,以及備份策略。

整體設(shè)計(jì)如下:

private Logger createLogger(String loggerName)
{
		Appender appender = null;
		PatternLayout.Builder layoutBuilder = PatternLayout.newBuilder();
		layoutBuilder.withCharset(Charset.forName(DefaultCharset));
		layoutBuilder.withConfiguration(loggerConfiguration);
		Layout<String> layout = layoutBuilder.build();
		if (LogUtil.AppenderConsole == LogUtil.getAppender())
		{
			appender = ConsoleAppender.createDefaultAppenderForLayout(layout);
		}
		if (LogUtil.AppenderFile == LogUtil.getAppender())
		{
			RollingFileAppender.Builder<?> loggerBuilder = RollingFileAppender.newBuilder();
			if (LogUtil.getBufferSize() > 0)
			{
				loggerBuilder.withImmediateFlush(false);
				loggerBuilder.withBufferedIo(true);
				loggerBuilder.withBufferSize(LogUtil.getBufferSize());
				System.setProperty(AsyncPropKey, AsyncPropVal);
			}
			else
			{
				loggerBuilder.withImmediateFlush(true);
				loggerBuilder.withBufferedIo(false);
				loggerBuilder.withBufferSize(0);
				System.getProperties().remove(AsyncPropKey);
			}
			loggerBuilder.withAppend(true);
			loggerBuilder.withFileName(getFilePath(loggerName));
			loggerBuilder.withFilePattern(spellBackupFileName(loggerName));
			loggerBuilder.withLayout(layout);
			loggerBuilder.withName(loggerName);
			loggerBuilder.withPolicy(CompositeTriggeringPolicy.createPolicy(
					SizeBasedTriggeringPolicy.createPolicy(LogUtil.getFileSize()),
					TimeBasedTriggeringPolicy.createPolicy("1", "true")));
			loggerBuilder.withStrategy(DefaultRolloverStrategy.createStrategy(
					LogUtil.getBackupIndex() > 0 ? String.valueOf(LogUtil.getBackupIndex()) : "-1", "1",
					LogUtil.getBackupIndex() > 0 ? null : "nomax", null, null, true, loggerConfiguration));
			appender = loggerBuilder.build();
		}
		appender.start();
		loggerConfiguration.addAppender(appender);
		AppenderRef appenderRef = AppenderRef.createAppenderRef(loggerName, Level.ALL, null);
		AppenderRef[] appenderRefs = new AppenderRef[]
		{ appenderRef };
		LoggerConfig loggerConfig = LoggerConfig.createLogger(false, Level.ALL, loggerName, "false", appenderRefs, null,
				loggerConfiguration, null);
		loggerConfig.addAppender(appender, null, null);
		loggerConfiguration.addLogger(loggerName, loggerConfig);
		loggerContext.updateLoggers();
		loggerConfiguration.start();
		Logger logger = LogManager.getLogger(loggerName);
		return logger;
}

注意?。∥以诔跏蓟疞ogger對(duì)象的時(shí)候,是根據(jù)LogUtil是否開(kāi)啟了異步輸出模式來(lái)判定是否需要開(kāi)啟全局異步模式的,這里簡(jiǎn)單說(shuō)一些Log4j2的異步模式。

Log4j2提供了兩種異步模式,第一種是使用AsyncAppender,這種方式跟我以前寫(xiě)的Log4j的異步輸出模式一樣,都是單獨(dú)開(kāi)啟一個(gè)線程來(lái)輸出日志。第二種方式是AsychLogger,這個(gè)就厲害了,官推,而且官方提供了兩種使用模式,一個(gè)是混合異步,一個(gè)是全局異步,全局異步時(shí)不需要對(duì)配置文件進(jìn)行任何改動(dòng),盡在應(yīng)用啟動(dòng)時(shí)添加一個(gè)jvm參數(shù)即可,并且據(jù)壓測(cè)數(shù)據(jù)顯示,全局異步模式下性能飆升。

更多的配置信息建議讀者朋友自行查閱官方文檔。

最后還有一點(diǎn)需要注意的是,Log4j2的全局異步是要依賴(lài)隊(duì)列緩存的,其實(shí)現(xiàn)采用的是disruptor,所以需要依賴(lài)這個(gè)外部jar,不然在初始化Logger對(duì)象的時(shí)候,你會(huì)看到相關(guān)異常。

貼一下依賴(lài)的Maven:

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-core</artifactId>
	<version>2.8.2</version>
</dependency>
<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-api</artifactId>
	<version>2.8.2</version>
</dependency>
<dependency>
	<groupId>com.lmax</groupId>
	<artifactId>disruptor</artifactId>
	<version>3.3.6</version>
</dependency>

嚴(yán)重強(qiáng)調(diào):AsyncAppender、AsyncLogger以及全局異步的System.property設(shè)置,不要同時(shí)出現(xiàn)?。?!

最最后,我特么還是要嘴賤啰嗦一下,不見(jiàn)得“異步”就是好的,你想想看異步模式無(wú)非是額外線程或者緩存來(lái)實(shí)現(xiàn),這些也是要吃資源的,日志量大的場(chǎng)景下其帶來(lái)的收益很高,但小量日志場(chǎng)景下其對(duì)性能資源的消耗很可能大于其帶來(lái)的性能收益,請(qǐng)酌情使用。

四 線程級(jí)日志對(duì)象的設(shè)計(jì)

按上文中的設(shè)計(jì)思路,LogUtil持有一組線程級(jí)日志對(duì)象,而這一組日志對(duì)象又共享一組Logger對(duì)象。延續(xù)Log4j版本的設(shè)計(jì),線程級(jí)日志對(duì)象類(lèi)型依然為T(mén)hreadLogger,其基礎(chǔ)屬性為線程ID、日志類(lèi)型以及日志的輸出級(jí)別等。

為了解決多并發(fā)問(wèn)題,使用ConcurrentHashMap來(lái)存儲(chǔ)Logger對(duì)象,其Key值為線程ID,這里需要注意的是ConcurrentHashMap的put操作盡管能保證可見(jiàn)性,但是不能保證操作的原子性,所以在其put操作上需要額外加鎖。

class ThreadLogger
{
	private static final String AsyncPropKey = "log4j2.contextSelector";
	private static final String AsyncPropVal = "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector";
	private static final String DefaultCharset = "UTF-8";
	private static final String DefaultDateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS";
	private static final Object LoggerMapLock = new Object();
	private static final ConcurrentHashMap<String, Logger> LoggerMap = new ConcurrentHashMap<>();
	private static LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
	private static Configuration loggerConfiguration = loggerContext.getConfiguration();
	public static void cleanLoggerMap()
	{
		synchronized (LoggerMapLock)
		{
			LoggerMap.clear();
		}
	}
	
	private Logger getLogger(String loggerName)
	{
		if (StringUtil.isEmpty(loggerName))
			return null;
		Logger logger = LoggerMap.get(loggerName);
		if (logger == null)
		{
			synchronized (LoggerMapLock)
			{
				logger = createLogger(loggerName);
				LoggerMap.put(loggerName, logger);
			}
		}
		return logger;
	}
}

五 標(biāo)準(zhǔn)日志頭

頭部?jī)?nèi)容應(yīng)包含當(dāng)前的日志輸出級(jí)別,日志打印所在的類(lèi)名、行號(hào),當(dāng)前的線程號(hào)、進(jìn)程號(hào)等信息。這里簡(jiǎn)單介紹下進(jìn)程號(hào)及類(lèi)名行號(hào)的獲取。

每個(gè)應(yīng)用進(jìn)程的進(jìn)程號(hào)唯一,所以?xún)H在第一次獲取該信息時(shí)獲取即可,類(lèi)名行號(hào)則通過(guò)方法棧的棧信息獲取,如下:

private static String ProcessID = null;
private String logLocation = null;
public static String getProcessID()
{
	if (ProcessID == null)
	{
		ProcessID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
	}
	return ProcessID;
}
private static String getLocation()
{
	StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
	StringBuilder builder = new StringBuilder(stackTraceElements[3].getClassName());
	builder.append(":");
	builder.append(stackTraceElements[3].getLineNumber());
	return builder.toString();
}

這里需要注意,類(lèi)名行號(hào)信息對(duì)于每一條日志都不盡相同,所以不能聲明為static,并且需要在LogUtil中獲取,并通過(guò)ThreadLogger的setLocation()方法傳入到ThreadLogger對(duì)象,最后由ThreadLogger的日志頭拼裝函數(shù)拼接到日志頭部信息中:

private String spellLogText(int logLevel, String logText)
{
	StringBuilder builder = new StringBuilder();
	SimpleDateFormat sdf = new SimpleDateFormat(DefaultDateFormat);
	builder.append(sdf.format(Calendar.getInstance().getTime()));
	builder.append("|");
	builder.append(LogUtil.LevelName[logLevel]);
	builder.append("|");
	builder.append(getProcessID());
	builder.append("|");
	builder.append(this.threadID);
	builder.append("|");
	builder.append(this.threadUUID);
	builder.append("|");
	builder.append(this.logLocation);
	builder.append("|");
	builder.append(logText);
	return builder.toString();
}

六 異常日志的堆棧信息打印

需要注意咯,異常的日志輸出略微復(fù)雜些,這也是我經(jīng)常被人問(wèn)起的一個(gè)問(wèn)題,很多從事400或大機(jī)開(kāi)發(fā)的同事轉(zhuǎn)入java開(kāi)發(fā)后,最常問(wèn)的就是異常堆棧的問(wèn)題,看不懂,不知道怎么來(lái)的。

這里我只提一個(gè)事情,Exception的構(gòu)造,其成員有兩個(gè),其一為cause,Throwable類(lèi)型,其二為message,String類(lèi)型,構(gòu)造函數(shù)提供較多,請(qǐng)大家自己做一個(gè)測(cè)試,看看不同構(gòu)造其輸出的內(nèi)容有何不同,cause和message成員又有何關(guān)系。

如果你弄明白了Exception的構(gòu)造,那么下面的邏輯不難理解:

private static String getExceptionStackTrace(Exception e)
{
	StringBuilder builder = new StringBuilder();
	builder.append(e.toString());
	StackTraceElement[] stackTraceElements = e.getStackTrace();
	for (int i = 0; i < stackTraceElements.length; i++)
	{
		builder.append("\r\nat ");
		builder.append(stackTraceElements[i].toString());
	}
	Throwable throwable = e.getCause();
	while (throwable != null)
	{
		builder.append("\r\nCaused by:");
		builder.append(throwable.toString());
		stackTraceElements = throwable.getStackTrace();
		for (int i = 0; i < stackTraceElements.length; i++)
		{
			builder.append("\r\nat ");
			builder.append(stackTraceElements[i].toString());
		}
		throwable = throwable.getCause();
	}
	return builder.toString();
}

七 測(cè)試

補(bǔ)充一個(gè)邏輯實(shí)現(xiàn),這里我以異常日志的打印作為測(cè)試接口,并在多線程并發(fā)場(chǎng)景下實(shí)現(xiàn)異步輸出。

異常日志輸出接口補(bǔ)充完整:

public static void logException(String desc, Exception exception)
{
	getThreadLogger().setLogLocation(getLocation());
	StringBuilder builder = new StringBuilder("Description=");
	builder.append(StringUtil.isEmpty(desc) ? "" : desc);
	builder.append(",Exception=");
	builder.append(getExceptionStackTrace(exception));
	log(LevelError, TypeException, builder.toString());
}

測(cè)試代碼:

public static void main(String[] args)
{
	LogUtil.setAppender(LogUtil.AppenderFile);
	LogUtil.setBufferSize(1024); // 1KB緩存
	LogUtil.setFileSize("1MB");
	LogUtil.setBackupIndex(10);
	LogUtil.setFilePath("C:\\log");
	for (int i = 0; i < 4; i++)
	{
		Thread thread = new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				LogUtil.setModule(Thread.currentThread().getId() + "");
				for (int j = 0; j < 100000; j++)
				{
					LogUtil.logException("test", new Exception("my test"));
				}
			}
		});
		thread.start();
	}
}

測(cè)試結(jié)果:

日志目錄

日志內(nèi)容

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java 中的 this 和 super 區(qū)別

    Java 中的 this 和 super 區(qū)別

    這篇文章主要介紹了Javathis與super本質(zhì)區(qū)別,this與super是類(lèi)實(shí)例化時(shí)通往Object類(lèi)通道的打通者;this和super在程序中由于其經(jīng)常被隱式的使用而被我們忽略,但是理解其作用和使用規(guī)范肯定是必須的。接下來(lái)將詳述this與super的作用和區(qū)別,需要的朋友可以參考一下
    2021-11-11
  • Mysql字段和java實(shí)體類(lèi)屬性類(lèi)型匹配方式

    Mysql字段和java實(shí)體類(lèi)屬性類(lèi)型匹配方式

    這篇文章主要介紹了Mysql字段和java實(shí)體類(lèi)屬性類(lèi)型匹配方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-07-07
  • Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼

    Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼

    這篇文章主要介紹了Java8如何將Array轉(zhuǎn)換為Stream的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • Java設(shè)計(jì)模式之java命令模式詳解

    Java設(shè)計(jì)模式之java命令模式詳解

    這篇文章主要介紹了Java設(shè)計(jì)模式編程中命令模式的使用,在一些處理請(qǐng)求響應(yīng)的場(chǎng)合經(jīng)??梢杂玫矫钅J降木幊趟悸?需要的朋友可以參考下
    2021-09-09
  • springboot整合通用Mapper簡(jiǎn)化單表操作詳解

    springboot整合通用Mapper簡(jiǎn)化單表操作詳解

    這篇文章主要介紹了springboot整合通用Mapper簡(jiǎn)化單表操作,通用Mapper是一個(gè)基于Mybatis,將單表的增刪改查通過(guò)通用方法實(shí)現(xiàn),來(lái)減少SQL編寫(xiě)的開(kāi)源框架,需要的朋友可以參考下
    2019-06-06
  • Java反射機(jī)制及Method.invoke詳解

    Java反射機(jī)制及Method.invoke詳解

    這篇文章主要介紹了Java反射機(jī)制及Method.invoke詳解,本文講解了JAVA反射機(jī)制、得到某個(gè)對(duì)象的屬性、得到某個(gè)類(lèi)的靜態(tài)屬性、執(zhí)行某對(duì)象的方法、執(zhí)行某個(gè)類(lèi)的靜態(tài)方法等內(nèi)容,需要的朋友可以參考下
    2015-03-03
  • 利用spring aop實(shí)現(xiàn)動(dòng)態(tài)代理

    利用spring aop實(shí)現(xiàn)動(dòng)態(tài)代理

    這篇文章主要為大家詳細(xì)介紹了利用spring aop實(shí)現(xiàn)動(dòng)態(tài)代理的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-03-03
  • 詳解Java的Enum的使用與分析

    詳解Java的Enum的使用與分析

    這篇文章主要介紹了詳解Java的Enum的使用與分析的相關(guān)資料,需要的朋友可以參考下
    2017-02-02
  • Java初學(xué)者問(wèn)題圖解(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)

    Java初學(xué)者問(wèn)題圖解(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)

    本文通過(guò)圖文并茂的形式給大家介紹了java初學(xué)者問(wèn)題,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下
    2017-04-04
  • 從0開(kāi)始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門(mén)與項(xiàng)目實(shí)踐

    從0開(kāi)始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門(mén)與項(xiàng)目實(shí)踐

    這篇文章主要介紹了從0開(kāi)始學(xué)習(xí)大數(shù)據(jù)之java spark編程入門(mén)與項(xiàng)目實(shí)踐,結(jié)合具體入門(mén)項(xiàng)目分析了大數(shù)據(jù)java spark編程項(xiàng)目建立、調(diào)試、輸出等相關(guān)步驟及操作技巧,需要的朋友可以參考下
    2019-11-11

最新評(píng)論