当前位置: 首页 > wzjs >正文

镇江方圆建设监理咨询有限公司网站快速网站排名提升

镇江方圆建设监理咨询有限公司网站,快速网站排名提升,石碣仿做网站,制作logo设计文章目录 一、入口二、源码解析LoggingApplicationListener 三、其它支持四、总结 本节以logback为背景介绍的 一、入口 gav: org.springframework.boot:spring-boot:3.3.4 spring.factories文件中有如下两个配置 org.springframework.boot.logging.LoggingSystemFactory\ …

文章目录

  • 一、入口
  • 二、源码解析
    • LoggingApplicationListener
  • 三、其它支持
  • 四、总结

本节以logback为背景介绍的

一、入口

gav: org.springframework.boot:spring-boot:3.3.4

spring.factories文件中有如下两个配置

org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factoryorg.springframework.context.ApplicationListener=\
org.springframework.boot.context.logging.LoggingApplicationListener,\
// 省略其它的...

这里定义了一个ApplicationListener的监听器, 以及三种不同日志实现的工厂

说明: 本节只分析使用logback作为slf4j实现的场景

二、源码解析

LoggingApplicationListener

继承链: GenericApplicationListener -> GenericApplicationListener -> SmartApplicationListener -> ApplicationListener

监听的事件为ApplicationEvent

public class LoggingApplicationListener implements GenericApplicationListener {// 触发事件public void onApplicationEvent(ApplicationEvent event) {// 启动初期触发if (event instanceof ApplicationStartingEvent startingEvent) {onApplicationStartingEvent(startingEvent);}// 环境准备之后触发else if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {onApplicationEnvironmentPreparedEvent(environmentPreparedEvent);}// 容器启动完成触发else if (event instanceof ApplicationPreparedEvent preparedEvent) {onApplicationPreparedEvent(preparedEvent);}// 容器关闭触发else if (event instanceof ContextClosedEvent contextClosedEvent) {onContextClosedEvent(contextClosedEvent);}// 容器启动失败触发else if (event instanceof ApplicationFailedEvent) {onApplicationFailedEvent();}}
}

容器启动事件

private void onApplicationStartingEvent(ApplicationStartingEvent event) {// 实例化LoggingSystem对象this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());// 初始化前置处理this.loggingSystem.beforeInitialize();
}// LoggingSystem.get
public static LoggingSystem get(ClassLoader classLoader) {// 系统配置的LoggingSystem; key:org.springframework.boot.logging.LoggingSystemString loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);if (StringUtils.hasLength(loggingSystemClassName)) {if (NONE.equals(loggingSystemClassName)) {return new NoOpLoggingSystem();}return get(classLoader, loggingSystemClassName);}// SPI获取LoggingSystem, 顺序是LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystemLoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);Assert.state(loggingSystem != null, "No suitable logging system located");return loggingSystem;
}// LogbackLoggingSystem#beforeInitialize
@Override
public void beforeInitialize() {// 获取logContext日志上下文LoggerContext loggerContext = getLoggerContext();if (isAlreadyInitialized(loggerContext)) {return;}super.beforeInitialize();configureJdkLoggingBridgeHandler();loggerContext.getTurboFilterList().add(FILTER);
}
// 获取logContext日志上下文
private LoggerContext getLoggerContext() {ILoggerFactory factory = getLoggerFactory();// ....return (LoggerContext) factory;
}
// 获取logContext日志上下文
private ILoggerFactory getLoggerFactory() {// slf4j获取LoggerContextILoggerFactory factory = LoggerFactory.getILoggerFactory();while (factory instanceof SubstituteLoggerFactory) {try {Thread.sleep(50);}catch (InterruptedException ex) {// 设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。Thread.currentThread().interrupt();throw new IllegalStateException("Interrupted while waiting for non-substitute logger factory", ex);}factory = LoggerFactory.getILoggerFactory();}return factory;
}

方法小结

  1. 容器在启动时通过ApplicationStartingEvent事件创建日志上下文
  2. 可以通过系统属性配置LoggingSystem对象, key为org.springframework.boot.logging.LoggingSystem
  3. 如果没有指定使用的LoggingSystem, 那么通过SPI获取, 由于在spring.factories中配置的LoggingSystemFactory里面LogbackLoggingSystem.Factory在第一个, 所以默认使用的LogbackLoggingSystem.Factory(如果有logback相关包的话)
  4. 执行LogbackLoggingSystem的beforeInitialize进行前置初始化
  5. beforeInitialize中使用SLF4J创建日志上下文; 这里就是SL4FJ和logback的内容了, 通过前面文章的介绍, 大家应该很熟悉了

在容器启动时创建了LoggingSystem, 一般是LogbackLoggingSystem, 同时创建了日志上下文LogContext

环境准备事件

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {SpringApplication springApplication = event.getSpringApplication();// 容器启动事件中创建过了, 一般是LogbackLoggingSystemif (this.loggingSystem == null) {this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());}// 进行初始化initialize(event.getEnvironment(), springApplication.getClassLoader());
}protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {// 这里创建LogbackLoggingSystemProperties, 用于给日志上下文添加必要的属性getLoggingSystemProperties(environment).apply();// 从环境变量中获取logging.file.name和logging.file.path, 然后构建LogFilethis.logFile = LogFile.get(environment);if (this.logFile != null) {// 将logging.file.path的值添加到系统属性中, key为LOG_PATH// 将logging.file.path目录下spring.log文件的路径添加到系统属性中, key为LOG_FILEthis.logFile.applyToSystemProperties();}this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);// 设置spring启动时的日志等级, 如果环境变量中有debug, 那么是debug登记, // 如果环境变量中有trace, 那么是trace等级initializeEarlyLoggingLevel(environment);// 初始化LogbackLoggingSysteminitializeSystem(environment, this.loggingSystem, this.logFile);// 环境变量中获取logging.group的内容添加到loggerGroups中, 并设置spring启动时相关包的日志等级initializeFinalLoggingLevels(environment, this.loggingSystem);// 添加shutdown的回调registerShutdownHookIfNecessary(environment, this.loggingSystem);
}private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {// 环境变量中的logging.config, 指定的日志配置文件路径String logConfig = environment.getProperty(CONFIG_PROPERTY);if (StringUtils.hasLength(logConfig)) {// 去掉字符串两端的空格logConfig = logConfig.strip();}try {// 就封装了一个environment的getter方法LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);// 没有配置logging.config或者以-D开头if (ignoreLogConfig(logConfig)) {// LogbackLoggingSystem初始化system.initialize(initializationContext, null, logFile);}else {// LogbackLoggingSystem初始化system.initialize(initializationContext, logConfig, logFile);}}catch (Throwable ex) {// ...}
}

小结

  1. springboot在环境准备完成后发出ApplicationEnvironmentPreparedEvent事件, 然后开始对LogbackLoggingSystemProperties进行初始化
  2. 创建LogbackLoggingSystemProperties对象, 并添加系统变量值, 下面是添加的内容
  • LOGGED_APPLICATION_NAME:spring.application.name的值
  • PID: pid的值
  • CONSOLE_LOG_CHARSET: 环境变量中logging.charset.console的值, 默认是U8
  • FILE_LOG_CHARSET: 环境变量中logging.charset.file的值, 默认是U8
  • CONSOLE_LOG_THRESHOLD: 环境变量中logging.threshold.console的值, 可选true/false
  • LOG_EXCEPTION_CONVERSION_WORD: 环境变量中logging.exception-conversion-word的值
  • CONSOLE_LOG_PATTERN: 环境变量中logging.pattern.console的值
  • FILE_LOG_PATTERN: 环境变量中logging.pattern.file的值
  • LOG_LEVEL_PATTERN:环境变量中logging.pattern.level的值
  • LOG_DATEFORMAT_PATTERN: 环境变量中logging.pattern.dateformat的值
  • LOG_CORRELATION_PATTERN: 环境变量中logging.pattern.correlation的值
  • 如果环境变量中logging.file.name存在, 添加LOG_FILE: file的值 到系统变量中
  • 如果环境变量中logging.file.path存在, 添加LOG_PATH: path的值 到系统变量中
  1. 设置springboot的日志等级, 如果系统变量中有debug值, 设置为debug等级, 如果有trace值, 设置为trace等级
  2. 可以在系统变量中使用logging.config指定日志文件路径, 也可以不指定使用默认的logback.xml, 然后进行LogbackLoggingSystem的初始化
  3. 设置一些包/类的日志等级, 该等级由第3步即系统变量中有debug值或者trace值来设置, 可以配置的内置模块日志等级的有如下几个
  • 如果环境变量中有debug, 那么设置包sql相关的包org.springframework.jdbc.core, org.hibernate.SQL,org.jooq.tools.LoggerListener和web相关的包org.springframework.core.codec, org.springframework.http,org.springframework.web,org.springframework.boot.actuate.endpoint.web,org.springframework.boot.web.servlet.ServletContextInitializerBeans的日志级别为debug
  • 如果环境变量中有trace, 那么设置包org.springframeworkorg.apache.tomcat,org.apache.catalina,org.eclipse.jetty,org.hibernate.tool.hbm2ddl的日志级别为trace
  • 如果环境变量中有logging.level, 那么设置指定web或者sql的日志等级为配置的日志等级, logging.level可以这么配置
logging.level.web=info
logging.level.org.springframework.boot=info
## 等等与上面环境变量中可配置的包相同

当然处理默认的web和sql两种类型的包之外, 还可以使用环境变量logging.group来自定义springboot中包或者类的日志级别

这里环境变量中logging.level的优先级要高于debug的配置

LogbackLoggingSystem的初始化

public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// 容器启动事件中创建的日志上下文LoggerContext loggerContext = getLoggerContext();if (isAlreadyInitialized(loggerContext)) {return;}// 非aot环境下直接返回false, 那么这里就是true, 这里对aot环境下不考虑if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) {// 初始化的核心super.initialize(initializationContext, configLocation, logFile);}// 环境上下文添加到日志上下文中loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment());loggerContext.getTurboFilterList().remove(FILTER);// 标记为初始化完成markAsInitialized(loggerContext);if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY+ "' system property. Please use 'logging.config' instead.");}
}

如果没有开启aot, 那么这个方法没有什么内容, 直接看AbstractLoggingSystem#initialize方法即可

AbstractLoggingSystem

public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// 环境变量中没有使用logging.config指定日志文件路径的话走这里if (StringUtils.hasLength(configLocation)) {initializeWithSpecificConfig(initializationContext, configLocation, logFile);return;}// 指定日志文件路径的话走这里initializeWithConventions(initializationContext, logFile);
}// 使用logging.config指定日志文件路径的场景
private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation,LogFile logFile) {// 使用系统属性中的值替换configLocation中的占位符configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);loadConfiguration(initializationContext, configLocation, logFile);
}// 没有指定日志文件路径的场景
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// logback支持的文件名,只取一个, 顺序为:"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" String config = getSelfInitializationConfig();// 存在上面这几种文件的话if (config != null && logFile == null) {// 重置容器状态, 并调用loadConfiguration方法开始解析配置reinitialize(initializationContext);return;}// 项目中没有配置默认的四个文件if (config == null) {// 这里获取spring扩展的四个文件名, 顺序为: "logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml" config = getSpringInitializationConfig();}// 存在配置文件的话if (config != null) {// 解析配置文件loadConfiguration(initializationContext, config, logFile);return;}// 使用一套默认的配置, appender仅为ConsoleAppender, 这里不做解释loadDefaults(initializationContext, logFile);
}

方法小结

  1. 如果使用logging.config指定了日志文件的路径(路径支持使用占位符, 将从系统变量中获取变量值), 使用loadConfiguration方法进行日志文件解析
  2. 如果没有指定日志文件的路径, 那么先获取默认配置文件(“logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml” ), 如果没有默认的配置文件, 取带有spring后缀的日志文件(“logback-test-spring.groovy”, “logback-test-spring.xml”, “logback-spring.groovy”, “logback-spring.xml” )
  3. 如果配置文件存在, 使用loadConfiguration方法进行日志文件解析
  4. 如果没有配置文件, 那么使用logback默认的容器, 以及一个ConsoleAppender

解析配置

protected void loadConfiguration(LoggingInitializationContext initializationContext, String location,LogFile logFile) {// 日志上下文LoggerContext loggerContext = getLoggerContext();// 停止并重启; 如果你的springBoot启动类中有静态属性Logger使用LoggerFactory.getLogger获取的话,它会在spring启动之前执行, 这里就会存在一个loggerContext, 需要关闭stopAndReset(loggerContext);withLoggingSuppressed(() -> {// initializationContext对象仅仅是环境上下文的载体, 提供getEnvironment方法if (initializationContext != null) {// 创建LogbackLoggingSystemProperties对象, 并将一堆环境变量添加到系统变量中, 上面的环境准备事件中有介绍applySystemProperties(initializationContext.getEnvironment(), logFile);}try {// 配置的日志文件资源Resource resource = new ApplicationResourceLoader().getResource(location);// 解析日志配置文件configureByResourceUrl(initializationContext, loggerContext, resource.getURL());}catch (Exception ex) {throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);}// 标识日志容器被启动loggerContext.start();});// 打印解析异常信息, 略过reportConfigurationErrorsIfNecessary(loggerContext);
}private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,URL url) throws JoranException {// 只允许xml为后缀的文件if (url.getPath().endsWith(".xml")) {// 使用springboot视线的SpringBootJoranConfigurator来解析配置JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);configurator.setContext(loggerContext);// 开始解析配置configurator.doConfigure(url);}else {throw new IllegalArgumentException("Unsupported file extension in '" + url + "'. Only .xml is supported");}
}

方法小结

  1. 添加一些环境变量参数到系统变量中
  2. 配置文件仅支持xml结尾的文件, 然后使用SpringBootJoranConfigurator来解析日志配置文件, 这里是对JoranConfigurator的扩展

SpringBootJoranConfigurator

class SpringBootJoranConfigurator extends JoranConfigurator {private final LoggingInitializationContext initializationContext;SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {this.initializationContext = initializationContext;}@Overrideprotected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) {// 添加处理configuration/springProperty的handlerdefaultProcessor.addHandler(SpringPropertyModel.class,(handlerContext, handlerMic) -> new SpringPropertyModelHandler(this.context,this.initializationContext.getEnvironment()));// 添加处理*/springProfile的handlerdefaultProcessor.addHandler(SpringProfileModel.class,(handlerContext, handlerMic) -> new SpringProfileModelHandler(this.context,this.initializationContext.getEnvironment()));super.addModelHandlerAssociations(defaultProcessor);}@Overridepublic void addElementSelectorAndActionAssociations(RuleStore ruleStore) {super.addElementSelectorAndActionAssociations(ruleStore);// 添加允许的标签configuration/springPropertyruleStore.addRule(new ElementSelector("configuration/springProperty"), SpringPropertyAction::new);// 添加允许的标签*/springProfileruleStore.addRule(new ElementSelector("*/springProfile"), SpringProfileAction::new);ruleStore.addTransparentPathPart("springProfile");}@Overridepublic void buildModelInterpretationContext() {super.buildModelInterpretationContext();// modelInterpretationContext中的JoranConfigurator替换成SpringBootJoranConfiguratorthis.modelInterpretationContext.setConfiguratorSupplier(() -> {SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(this.initializationContext);configurator.setContext(this.context);return configurator;});}// 省略一些代码...
}

SpringBootJoranConfigurator类在不考虑aot的情况下, 添加了对configuration/springProperty*/springProfile标签的支持, 其中*/springProfile是一种后缀标签的形式, 也就是说它可以放在任意标签的后面; 下面看看这两个handler

SpringPropertyModelHandler

@Override
public void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {SpringPropertyModel propertyModel = (SpringPropertyModel) model;// 作用域, 支持LOCAL(model上下文), CONTEXT(日志上下文), SYSTEM(系统级别); 默认是LOCAL, 在解析配置文件时有效Scope scope = ActionUtil.stringToScope(propertyModel.getScope());// 默认值String defaultValue = propertyModel.getDefaultValue();// source就是属性的名称String source = propertyModel.getSource();// name和source都不能为空if (OptionHelper.isNullOrEmpty(propertyModel.getName()) || OptionHelper.isNullOrEmpty(source)) {addError("The \"name\" and \"source\" attributes of <springProperty> must be set");}// 将属性添加到指定的作用域中PropertyModelHandlerHelper.setProperty(intercon, propertyModel.getName(), getValue(source, defaultValue),scope);
}
// 从环境变量中获取source属性对应的值
private String getValue(String source, String defaultValue) {if (this.environment == null) {addWarn("No Spring Environment available to resolve " + source);return defaultValue;}return this.environment.getProperty(source, defaultValue);
}

方法小结

  1. configuration/springProperty标签支持name,source,scope, defaultValue四个属性
  • name: 标签名称
  • source: 属性名称; 从环境变量中获取值的那个key
  • scope: 属性存放的位置, LOCAL:logback配置文件解析期间, CONTEXT:日志上线文范文内, SYSTEM: 系统属性

总的来说就是: configuration/springProperty将从环境变量中获取的值添加到日志容器中, 供解析日志使用, 其中key为name属性的值, value为source属性在环境变量中对应的值

例如:

// application.properties
log.fileName=info.log// logback.xml
<configuration><springProperty name="fileName" source="log.fileName" scope="LOCAL" defaultValue="temp.log"/>
</configuration>

SpringProfileModelHandler

class SpringProfileModelHandler extends ModelHandlerBase {private final Environment environment;@Overridepublic void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {SpringProfileModel profileModel = (SpringProfileModel) model;// 如果当前spring的环境(spring.profiles.active)不是springProfile指定下的, 那么被springProfile标签包裹的子标签将不会生效if (!acceptsProfiles(intercon, profileModel)) {model.deepMarkAsSkipped();}}private boolean acceptsProfiles(ModelInterpretationContext ic, SpringProfileModel model) {if (this.environment == null) {return false;}// name根据逗号分割String[] profileNames = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(model.getName()));if (profileNames.length == 0) {return false;}for (int i = 0; i < profileNames.length; i++) {try {// 从LOCAL、CONTEXT、SYSTEM范围内获取值替换占位符; 没有占位符的话用原值profileNames[i] = OptionHelper.substVars(profileNames[i], ic, this.context);}catch (ScanException ex) {throw new RuntimeException(ex);}}// 判断是不是环境变量中配置的spring.profiles.active的值return this.environment.acceptsProfiles(Profiles.of(profileNames));}
}

方法小结

springProfile标签可以放在任意子标签下, 其中name属性用来指定当前的环境, 它可以指定什么环境下使用什么样的配置, 如果当前环境与springProfile配置的不同, 那么springProfile的子标签将不会生效; 例如

<root level="info"><springProfile name="dev,test"><appender-ref ref="CONSOLE" /></springProfile><springProfile name="prod"><appender-ref ref="ROLLER" /></springProfile>
</root>

这种配置下dev或者test环境 CONSOLE的appender将会生效, ROLLER的appender不会生效

三、其它支持

spring还提供了ColorConverter,ExtendedWhitespaceThrowableProxyConverter,WhitespaceThrowableProxyConverter转换器, 用来给控制台输出颜色日志的、异常等

四、总结

  1. spring自动装配了LoggingApplicationListener监听器, 监听ApplicationEvent事件, 在springboot启动周期中对日志做了一些扩展
  • 在springboot启动初期(ApplicationStartingEvent事件), 实例化了LogbackLoggingSystem对象
  • 在环境准备完成后(ApplicationEnvironmentPreparedEvent事件), 对logback容器做了初始化并启动
  1. springboot对日志slf4j的实现默认顺序为LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystem, 确保其中有ch.qos.logback:logback-classic:版本号的包

  2. 关于环境变量中配置的logging.file.namelogging.file.path属性, 是用来给默认日志配置设置滚动文件的, 就像appender中的file属性一样, 但是如果你配置了日志文件(例如logback.xml), 它就没什么用了

  3. 可以在环境变量中配置 debug=true或者trace=true来设置springboot内置包的日志等级; 同样也可以在环境变量中设置指定包的日志级别, 就不限于debug或者trace了, 例如 logging.level.web=info; logging.level.org.springframework.boot=info 这种logging.level.web=info的方式优先级高于debug=true

  4. 可以使用环境变量logging.config配置日志文件的位置, 支持classpath的配置, 即放在项目的resources目录下即可;

  • 如过没有使用logging.config指定日志配置, 那么会默认读取"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"中的一个
  • 如果这一步也没有指定, 那么读取springboot扩展的配置文件"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml"
  • 如果直接没有配置文件, 那么默认构建ConsoleAppender和root的logger对象, 如果配置了logging.file.namelogging.file.path属性, 那么就会多创建一个info级别的RollingFileAppender
  1. springboot使用SpringBootJoranConfigurator扩展了JoranConfigurator, 添加了如下的相关支持
  • configuration/springProperty标签, 用来从环境变量中获取属性, 作用到解析日志文件中
  • */springProfile标签, 该标签是后缀匹配型, 可以放在任意位置, 它用于指定哪些配置在不同的环境下生效
  1. springboot还提供了额外的转换器, 例如ColorConverter, 大家配置ConsoleAppender的时候可以借用它
  2. 另外, 在遇到Thread.sleep的时候, 可以用Thread.currentThread().interrupt();设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。也曾看到一些地方什么都不处理, 这种应该是标准做法, 在几个源码中见过了

个人公众号
在这里插入图片描述

http://www.dtcms.com/wzjs/479921.html

相关文章:

  • 泉州网站建设推广2023年4 5月份疫情结束吗
  • 有没有代做模型的网站360推广
  • 大德通众包 做网站怎么样软文代写费用
  • 疫情爆发seo学校培训班
  • 网站如何做查询表单软文营销的技巧有哪些
  • 做网站那种布局好在线生成个人网站
  • 数据库网站建设多少钱沈阳seo关键词排名优化软件
  • 网站广告条怎么做百度营销大学
  • 张家界做旅游网站吸引客流的25个技巧
  • 网页制作模板的网站免费最近实时热点事件
  • 英文网站后台维护杭州网络整合营销公司
  • 网络营销的方法有哪些方式快速排名seo
  • 怎样在文章后做网站链接百度快照优化
  • 南通科技网站建设福州百度推广优化排名
  • 搜索引擎收录提交搜索引擎优化举例说明
  • 电子商城网站开发支持手机端网店运营推广平台
  • 网站通栏代码公司网站怎么建立
  • 有帮忙做网站的吗b2b免费发布信息平台
  • 小程序管理平台登陆市场seo是什么
  • 做网站如何给图片命名ip域名查询
  • 手机网站建设服务器深圳搜索排名优化
  • 淄博网站制作高端网络网站如何推广
  • 织梦如何做电商网站外链网站大全
  • 营销型网站建设需要有什么功能google关键词优化
  • 烟台城发建设集团网站如何快速推广一个app
  • 三明做网站的公司竞价推广代运营企业
  • 分析seo做的不好的网站百度搜索排名查询
  • 建个网站做外贸网站seo基础优化
  • 如何建设社交网站百度推广平台
  • 建立网站的过程网站建设策划方案