在 http://flume.apache.org 上下载flume-1.6.0版本,将源码导入到Idea开发工具后如下图所示:

一、主要模块说明

  • flume-ng-channels 里面包含了filechannel,jdbcchannel,kafkachannel,memorychannel通道的实现。

  • flume-ng-clients 实现了log4j相关的几个Appender,使得log4j的日志输出可以直接发送给flume-agent;其中有一个LoadBalancingLog4jAppender的实现,提供了多个flume-agent的load balance和ha功能,采用flume作为日志收集的可以考虑将这个appender引入内部的log4j中。

  • flume-ng-configuration 这个主要就是Flume配置信息相关的类,包括载入flume-config.properties配置文件并解析。其中包括了Source的配置,Sink的配置,Channel的配置,在阅读源码前推荐先梳理这部分关系再看其他部分的。

  • flume-ng-core flume整个核心框架,包括了各个模块的接口以及逻辑关系实现。其中instrumentation是flume内部实现的一套metric机制,metric的变化和维护,其核心也就是在MonitoredCounterGroup中通过一个Map<key, AtomicLong>来实现metric的计量。ng-core下几乎大部分代码任然几种在channel、sink、source几个子目录下,其他目录基本完成一个util和辅助的功能。

  • flume-ng-node 实现启动flume的一些基本类,包括main函数的入口(Application.java中)。在理解configuration之后,从application的main函数入手,可以较快的了解整个flume的代码。

二、Flume逻辑结构图

三、flume-ng启动文件介绍

  1. ################################
  2. # constants
  3. ################################
  4. #设置常量值,主要是针对不同的参数执行相应的类,以启动Flume环境
  5. FLUME_AGENT_CLASS="org.apache.flume.node.Application"
  6. FLUME_AVRO_CLIENT_CLASS="org.apache.flume.client.avro.AvroCLIClient"
  7. FLUME_VERSION_CLASS="org.apache.flume.tools.VersionInfo"
  8. FLUME_TOOLS_CLASS="org.apache.flume.tools.FlumeToolsMain"
  9. #真正启动Flume环境的方法
  10. run_flume() {
  11. local FLUME_APPLICATION_CLASS
  12. if [ "$#" -gt 0 ]; then
  13. FLUME_APPLICATION_CLASS=$1
  14. shift
  15. else
  16. error "Must specify flume application class" 1
  17. fi
  18. if [ ${CLEAN_FLAG} -ne 0 ]; then
  19. set -x
  20. fi
  21. #执行这一行命令,执行相应的启动类,比如org.apache.flume.node.Application
  22. $EXEC $JAVA_HOME/bin/java $JAVA_OPTS $FLUME_JAVA_OPTS "${arr_java_props[@]}" -cp "$FLUME_CLASSPATH" \
  23. -Djava.library.path=$FLUME_JAVA_LIBRARY_PATH "$FLUME_APPLICATION_CLASS" $*
  24. }
  25. ################################
  26. # main
  27. ################################
  28. # set default params
  29. # 在启动的过程中使用到的参数
  30. FLUME_CLASSPATH=""
  31. FLUME_JAVA_LIBRARY_PATH=""
  32. #默认占用堆空间大小,这一块都可以根据JVM进行重新设置
  33. JAVA_OPTS="-Xmx20m"
  34. LD_LIBRARY_PATH=""
  35. opt_conf=""
  36. opt_classpath=""
  37. opt_plugins_dirs=""
  38. arr_java_props=()
  39. arr_java_props_ct=0
  40. opt_dryrun=""
  41. # 根据不同的参数,执行不同的启动类,每个常量所对应的类路径在代码前面有过介绍。
  42. if [ -n "$opt_agent" ] ; then
  43. run_flume $FLUME_AGENT_CLASS $args
  44. elif [ -n "$opt_avro_client" ] ; then
  45. run_flume $FLUME_AVRO_CLIENT_CLASS $args
  46. elif [ -n "${opt_version}" ] ; then
  47. run_flume $FLUME_VERSION_CLASS $args
  48. elif [ -n "${opt_tool}" ] ; then
  49. run_flume $FLUME_TOOLS_CLASS $args
  50. else
  51. error "This message should never appear" 1
  52. fi

这是其中最主要的一部分flume-ng命令行,根据重要性摘取了一段,感兴趣的读者可以自己到bin目录下查看全部。

四、从Flume-NG启动过程开始说起

从bin/flume-ng这个shell脚本可以看到Flume的起始于org.apache.flume.node.Application类,这是flume的main函数所在。

main方法首先会先解析shell命令,如果指定的配置文件不存在就抛出异常。

代码如下所示:

  1. Options options = new Options();
  2. Option option = new Option("n", "name", true, "the name of this agent");
  3. option.setRequired(true);
  4. options.addOption(option);
  5. option = new Option("f", "conf-file", true,
  6. "specify a config file (required if -z missing)");
  7. option.setRequired(false);
  8. options.addOption(option);
  9. option = new Option(null, "no-reload-conf", false,
  10. "do not reload config file if changed");
  11. options.addOption(option);
  12. // Options for Zookeeper
  13. option = new Option("z", "zkConnString", true,
  14. "specify the ZooKeeper connection to use (required if -f missing)");
  15. option.setRequired(false);
  16. options.addOption(option);
  17. option = new Option("p", "zkBasePath", true,
  18. "specify the base path in ZooKeeper for agent configs");
  19. option.setRequired(false);
  20. options.addOption(option);
  21. option = new Option("h", "help", false, "display help text");
  22. options.addOption(option);
  23. #命令行解析类
  24. CommandLineParser parser = new GnuParser();
  25. CommandLine commandLine = parser.parse(options, args);
  26. if (commandLine.hasOption('h')) {
  27. new HelpFormatter().printHelp("flume-ng agent", options, true);
  28. return;
  29. }
  30. String agentName = commandLine.getOptionValue('n');
  31. boolean reload = !commandLine.hasOption("no-reload-conf");
  32. if (commandLine.hasOption('z') || commandLine.hasOption("zkConnString")) {
  33. isZkConfigured = true;
  34. }

以上代码是Application类中校验shell命令行的代码,举个例子在启动flume的时候,使用如下命令行:

  1. ./bin/flume-ng agent -n agent -c conf -f conf/hw.conf -Dflume.root.logger=INFO,console

里面的-n -f等参数都是在上面代码中校验的。

再往下看main方法里的代码:

  1. File configurationFile = new File(commandLine.getOptionValue('f'));
  2. /*
  3. * The following is to ensure that by default the agent will fail on
  4. * startup if the file does not exist.
  5. */
  6. if (!configurationFile.exists()) {
  7. // If command line invocation, then need to fail fast
  8. if (System.getProperty(Constants.SYSPROP_CALLED_FROM_SERVICE) ==
  9. null) {
  10. String path = configurationFile.getPath();
  11. try {
  12. path = configurationFile.getCanonicalPath();
  13. } catch (IOException ex) {
  14. logger.error("Failed to read canonical path for file: " + path,
  15. ex);
  16. }
  17. throw new ParseException(
  18. "The specified configuration file does not exist: " + path);
  19. }
  20. }
  21. List<LifecycleAware> components = Lists.newArrayList();
  22. if (reload) {
  23. EventBus eventBus = new EventBus(agentName + "-event-bus");
  24. PollingPropertiesFileConfigurationProvider configurationProvider =
  25. new PollingPropertiesFileConfigurationProvider(
  26. agentName, configurationFile, eventBus, 30);
  27. components.add(configurationProvider);
  28. application = new Application(components);
  29. eventBus.register(application);
  30. } else {
  31. PropertiesFileConfigurationProvider configurationProvider =
  32. new PropertiesFileConfigurationProvider(
  33. agentName, configurationFile);
  34. application = new Application();
  35. application.handleConfigurationEvent(configurationProvider
  36. .getConfiguration());
  37. }
  38. }
  39. application.start();

说明:

根据命令中含有”no-reload-conf”参数,决定采用那种加载配置文件方式:

一、没有此参数,会动态加载配置文件,默认每30秒加载一次配置文件,因此可以动态修改配置文件;

二、有此参数,则只在启动时加载一次配置文件。实现动态加载功能采用了发布订阅模式,使用guava中的EventBus实现。

三、PropertiesFileConfigurationProvider这个类是配置文件加载类。

类图如下:

从图中可以看出在整个PollingPropertiesFileConfigurationProvider类中,它实现了LifecycleAware接口,而这个接口是掌管整个Flume生命周期的一个核心接口,LifecycleSupervisor实现了这个接口,通过上面代码中application.start方法触发LifecyleAware的start方法,下面是这个接口的方法定义及相关类代码:

  1. public interface LifecycleAware {
  2. /**
  3. * <p>
  4. * Starts a service or component.
  5. * </p>
  6. * @throws LifecycleException
  7. * @throws InterruptedException
  8. */
  9. public void start();
  10. /**
  11. * <p>
  12. * Stops a service or component.
  13. * </p>
  14. * @throws LifecycleException
  15. * @throws InterruptedException
  16. */
  17. public void stop();
  18. /**
  19. * <p>
  20. * Return the current state of the service or component.
  21. * </p>
  22. */
  23. public LifecycleState getLifecycleState();
  24. }

Application.start()方法内容:

  1. public synchronized void start() {
  2. for(LifecycleAware component : components) {
  3. supervisor.supervise(component,
  4. new SupervisorPolicy.AlwaysRestartPolicy(), LifecycleState.START);
  5. }
  6. }

LifecycleSupervisor.supervise方法内容如下:

  1. public synchronized void supervise(LifecycleAware lifecycleAware,
  2. SupervisorPolicy policy, LifecycleState desiredState) {
  3. if(this.monitorService.isShutdown()
  4. || this.monitorService.isTerminated()
  5. || this.monitorService.isTerminating()){
  6. throw new FlumeException("Supervise called on " + lifecycleAware + " " +
  7. "after shutdown has been initiated. " + lifecycleAware + " will not" +
  8. " be started");
  9. }
  10. Preconditions.checkState(!supervisedProcesses.containsKey(lifecycleAware),
  11. "Refusing to supervise " + lifecycleAware + " more than once");
  12. if (logger.isDebugEnabled()) {
  13. logger.debug("Supervising service:{} policy:{} desiredState:{}",
  14. new Object[] { lifecycleAware, policy, desiredState });
  15. }
  16. Supervisoree process = new Supervisoree();
  17. process.status = new Status();
  18. process.policy = policy;
  19. process.status.desiredState = desiredState;
  20. process.status.error = false;
  21. MonitorRunnable monitorRunnable = new MonitorRunnable();
  22. monitorRunnable.lifecycleAware = lifecycleAware;
  23. monitorRunnable.supervisoree = process;
  24. monitorRunnable.monitorService = monitorService;
  25. supervisedProcesses.put(lifecycleAware, process);
  26. ScheduledFuture<?> future = monitorService.scheduleWithFixedDelay(
  27. monitorRunnable, 0, 3, TimeUnit.SECONDS);
  28. monitorFutures.put(lifecycleAware, future);
  29. }

在上面的代码中,会创建MonitorRunnable对象,这个对象是个定时对象,里面的run方法主要是根据supervisoree.status.desiredState的值执行对应的操作。

包括:START,STOP等状态, 大家注意scheduleWithFixedDelay这个方法,这是java线程池自带的,要求每次任务执行完以后再延迟3秒,而不是每隔3秒执行一次,大家注意这一点。

又有同学会问循环调用会不会有问题,这里回应大家其实也没问题,这么做是为了重试机制,看下面代码:

  1. if (!lifecycleAware.getLifecycleState().equals( supervisoree.status.desiredState))

在MonitorRunnable内部有这样一个判断,当getLifecycleState与supervisoree.status.desiredState状态不相等的时候才会执行,而ifecycleAware.getLifecycleState()初始状态是IDLE。

时序调用图如下所示

注:

PollingPropertiesFileConfigurationProvider.start()方法会启动一个单线程FileWatcherRunnable每隔30s去加载一次配置文件:

  1. eventBus.post(getConfiguration())。

getConfiguration()解析了配置文件并且获取所有组件及配置属性

五、配置文件加载详细分析

先看一下FileWatcherRunnable内部的代码:

  1. public MaterializedConfiguration getConfiguration() {
  2. //初始化三大组件的配置Map,source,channel,sink
  3. MaterializedConfiguration conf = new SimpleMaterializedConfiguration();
  4. FlumeConfiguration fconfig = getFlumeConfiguration();
  5. AgentConfiguration agentConf = fconfig.getConfigurationFor(getAgentName());
  6. if (agentConf != null) {
  7. Map<String, ChannelComponent> channelComponentMap = Maps.newHashMap();
  8. Map<String, SourceRunner> sourceRunnerMap = Maps.newHashMap();
  9. Map<String, SinkRunner> sinkRunnerMap = Maps.newHashMap();
  10. try {
  11. loadChannels(agentConf, channelComponentMap);
  12. loadSources(agentConf, channelComponentMap, sourceRunnerMap);
  13. loadSinks(agentConf, channelComponentMap, sinkRunnerMap);
  14. Set<String> channelNames =
  15. new HashSet<String>(channelComponentMap.keySet());
  16. for(String channelName : channelNames) {
  17. ChannelComponent channelComponent = channelComponentMap.
  18. get(channelName);
  19. if(channelComponent.components.isEmpty()) {
  20. LOGGER.warn(String.format("Channel %s has no components connected" +
  21. " and has been removed.", channelName));
  22. channelComponentMap.remove(channelName);
  23. Map<String, Channel> nameChannelMap = channelCache.
  24. get(channelComponent.channel.getClass());
  25. if(nameChannelMap != null) {
  26. nameChannelMap.remove(channelName);
  27. }
  28. } else {
  29. LOGGER.info(String.format("Channel %s connected to %s",
  30. channelName, channelComponent.components.toString()));
  31. conf.addChannel(channelName, channelComponent.channel);
  32. }
  33. }
  34. for(Map.Entry<String, SourceRunner> entry : sourceRunnerMap.entrySet()) {
  35. conf.addSourceRunner(entry.getKey(), entry.getValue());
  36. }
  37. for(Map.Entry<String, SinkRunner> entry : sinkRunnerMap.entrySet()) {
  38. conf.addSinkRunner(entry.getKey(), entry.getValue());
  39. }
  40. } catch (InstantiationException ex) {
  41. LOGGER.error("Failed to instantiate component", ex);
  42. } finally {
  43. channelComponentMap.clear();
  44. sourceRunnerMap.clear();
  45. sinkRunnerMap.clear();
  46. }
  47. } else {
  48. LOGGER.warn("No configuration found for this host:{}", getAgentName());
  49. }
  50. return conf;
  51. }

说明:

一、在哪里加载的配置文件

其实是在这里,FlumeConfiguration fconfig = getFlumeConfiguration();

getFlumeConfiguration()这个方法是一个抽象方法,可以通过下图的方式查找加载方式。

我们选择PollingPropertiesFileConfigurationProvider这个,可以看到:

  1. @Override
  2. public FlumeConfiguration getFlumeConfiguration() {
  3. BufferedReader reader = null;
  4. try {
  5. reader = new BufferedReader(new FileReader(file));
  6. Properties properties = new Properties();
  7. properties.load(reader);
  8. return new FlumeConfiguration(toMap(properties));
  9. } catch (IOException ex) {
  10. LOGGER.error("Unable to load file:" + file
  11. + " (I/O failure) - Exception follows.", ex);
  12. } finally {
  13. if (reader != null) {
  14. try {
  15. reader.close();
  16. } catch (IOException ex) {
  17. LOGGER.warn(
  18. "Unable to close file reader for file: " + file, ex);
  19. }
  20. }
  21. }
  22. return new FlumeConfiguration(new HashMap<String, String>());
  23. }

就是上面这个方法通过JAVA最基本的流的方式加载的配置文件,也就是图上面我配置的flume的hw.conf配置文件。方法读取配置文件,然后解析成name(输姓名全称,即等号左侧的全部)、value(等号的右侧)对,存入一个Map当中,返回一个封装了这个Map的FlumeConfiguration对象。

FlumeConfiguration类的构造函数会遍历这个Map的所有<name,value>对,调用addRawProperty(String name, String value)处理<name,value>对,addRawProperty方法会先做一些合法性检查,启动Flume的时候会构造一个AgentConfiguration对象aconf,然后agentConfigMap.put(agentName, aconf),以后动态加载配置文件时只需要AgentConfiguration aconf = agentConfigMap.get(agentName)就可以得到,然后调用aconf.addProperty(configKey, value)处理。

二、我们重点看一下addProperty方法内部的parseConfigKey方法,这里会深入解析每一行配置文件内容。

我们举一个配置文件的例子:

  1. agent.sources=s1
  2. agent.channels=c1 c2
  3. agent.sinks=k1 k2
  4. agent.sources.s1.type=exec
  5. agent.sources.s1.command=tail -F /Users/it-od-m-2687/Downloads/abc.log
  6. agent.sources.s1.channels=c1
  7. agent.channels.c1.type=memory
  8. agent.channels.c1.capacity=10000
  9. agent.channels.c1.transactionCapacity=100
  10. agent.sinks.k1.type= org.apache.flume.sink.kafka.KafkaSink
  11. agent.sinks.k1.brokerList=127.0.0.1:9092
  12. agent.sinks.k1.topic=testKJ1
  13. agent.sinks.k1.serializer.class=kafka.serializer.StringEncoder
  14. agent.sinks.k1.channel=c1

解析上面的文件就是使用下面parseConfigKey这个方法:

  1. cnck = parseConfigKey(key, BasicConfigurationConstants.CONFIG_SINKGROUPS_PREFIX);
  1. public final class BasicConfigurationConstants {
  2. public static final String CONFIG_SOURCES = "sources";
  3. public static final String CONFIG_SOURCES_PREFIX = CONFIG_SOURCES + ".";
  4. public static final String CONFIG_SOURCE_CHANNELSELECTOR_PREFIX = "selector.";
  5. public static final String CONFIG_SINKS = "sinks";
  6. public static final String CONFIG_SINKS_PREFIX = CONFIG_SINKS + ".";
  7. public static final String CONFIG_SINK_PROCESSOR_PREFIX = "processor.";
  8. public static final String CONFIG_SINKGROUPS = "sinkgroups";
  9. public static final String CONFIG_SINKGROUPS_PREFIX = CONFIG_SINKGROUPS + ".";
  10. public static final String CONFIG_CHANNEL = "channel";
  11. public static final String CONFIG_CHANNELS = "channels";
  12. public static final String CONFIG_CHANNELS_PREFIX = CONFIG_CHANNELS + ".";
  13. public static final String CONFIG_CONFIG = "config";
  14. public static final String CONFIG_TYPE = "type";
  15. private BasicConfigurationConstants() {
  16. // disable explicit object creation
  17. }

1、我们用agent.sources.s1.command=s1来举例:

变量prefix指的是:sink,source,channel等关键字。

如下面代码:

  1. public final class BasicConfigurationConstants {
  2. public static final String CONFIG_SOURCES = "sources";
  3. public static final String CONFIG_SOURCES_PREFIX = CONFIG_SOURCES + ".";
  4. public static final String CONFIG_SOURCE_CHANNELSELECTOR_PREFIX = "selector.";
  5. public static final String CONFIG_SINKS = "sinks";
  6. public static final String CONFIG_SINKS_PREFIX = CONFIG_SINKS + ".";
  7. public static final String CONFIG_SINK_PROCESSOR_PREFIX = "processor.";
  8. public static final String CONFIG_SINKGROUPS = "sinkgroups";
  9. public static final String CONFIG_SINKGROUPS_PREFIX = CONFIG_SINKGROUPS + ".";
  10. public static final String CONFIG_CHANNEL = "channel";
  11. public static final String CONFIG_CHANNELS = "channels";
  12. public static final String CONFIG_CHANNELS_PREFIX = CONFIG_CHANNELS + ".";
  13. public static final String CONFIG_CONFIG = "config";
  14. public static final String CONFIG_TYPE = "type";
  15. private BasicConfigurationConstants() {
  16. // disable explicit object creation
  17. }

2、上面parseConfigKey方法,首先根据prefix判断prefix的后面,有少多字符。比如:sources.s1.command,在sources后面s1.command一共有10个字符。

3、解析出name变量,如s1,这个是自己定义的。

4、解析出configKey固定关键字,如command,这个是系统定义的。

5、封装new ComponentNameAndConfigKey(name, configKey)返回。

6、将sources、channel、sink配置信息,分别存放到sourceContextMap、channelConfigMap、sinkConfigMap三个HashMap,最后统一封装到AgentConfiguration对象中,然后再把AgentConfiguration存放到agentConfigMap中,key是agentName。说了这么多相信很多同学都已经晕了,agentConfigMap的结构如下图所示:

读源码是一个很痛苦的过程,不仅要分析整体框架的架构,还要理解作者的用意和设计思想,但只要坚持下来你会发现还是能学到很多东西的。

Flume-NG源码分析-整体结构及配置载入分析的更多相关文章

  1. WorldWind源码剖析系列:配置载入器类ConfigurationLoader

    配置载入器类ConfigurationLoader主要从指定的路径中加载保存星球相关参数的xml文件,从中读取数据来构造星球对象及其所关联的可渲染子对象列表并返回.该类的类图如下所示. 该类所包含的主 ...

  2. Mybatis 系列7-结合源码解析核心CRUD 配置及用法

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  3. 基于SpringBoot的Environment源码理解实现分散配置

    前提 org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性.Envi ...

  4. Asp.NetCore源码学习[1-2]:配置[Option]

    Asp.NetCore源码学习[1-2]:配置[Option] 在上一篇文章中,我们知道了可以通过IConfiguration访问到注入的ConfigurationRoot,但是这样只能通过索引器IC ...

  5. Web API 源码剖析之默认配置(HttpConfiguration)

    Web API 源码剖析之默认配置(HttpConfiguration) 我们在上一节讲述了全局配置和初始化.本节我们将就全局配置的Configuration只读属性进行展开,她是一个类型为HttpC ...

  6. Web API 源码剖析之全局配置

    Web API 源码剖析之全局配置 Web API  均指Asp.net Web API .本节讲述的是基于Web API 系统在寄宿于IIS. 本节主要讲述Web API全局配置.它是如何优雅的实现 ...

  7. CentOS 7上源码编译安装和配置LNMP Web+phpMyAdmin服务器环境

    CentOS 7上源码编译安装和配置LNMP Web+phpMyAdmin服务器环境 什么是LNMP? LNMP(别名LEMP)是指由Linux, Nginx, MySQL/MariaDB, PHP/ ...

  8. Asp.NetCore源码学习[2-1]:配置[Configuration]

    Asp.NetCore源码学习[2-1]:配置[Configuration] 在Asp. NetCore中,配置系统支持不同的配置源(文件.环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只 ...

  9. flume【源码分析】分析Flume的启动过程

    h2 { color: #fff; background-color: #7CCD7C; padding: 3px; margin: 10px 0px } h3 { color: #fff; back ...

随机推荐

  1. C#在VS2005开发环境中利用异步模式来对一个方法的执行时间进行超时控制

    using System.Threading; using System; namespace ConsoleApplication4 { public class Program { static ...

  2. bootstrapTable的数据后端分页排序

    数据后端分页排序,其实就是sql语句中oeder by做一些限制. 之前在写sql语句中的order by是写死,既然要写活,就要传参数到后台. 之前讲到bootstrapTable的queryPar ...

  3. 一张图掌握移动Web前端所有技术(大前端、工程化、预编译、自动化)

    你要的移动web前端都在这里! 大前端方向:移动Web前端.Native客户端.Node.js. 大前端框架:React.Vue.js.Koa  跨终端技术:HTML 5.CSS 3.JavaScri ...

  4. 前端js优化方案(一)

    最近在读<高性能javascript>,在这里记录一下读后的一些感受,顺便加上自己的一些理解,如果有兴趣的话可以关注的我的博客http://www.bloggeng.com/,我会不定期发 ...

  5. Thymeleaf的模板使用介绍

    参考网址: https://blog.csdn.net/hry2015/article/details/73476973 先定义一个html文件, 如下: 文件路径: templates/templa ...

  6. centos7.4 安装后的基本设置

    centos7.4 安装后的基本设置 设置主机名称 设置IP地址,网关 修改网卡名称 内核优化 系统安全设置 防火墙设置 ssh设置 同步系统时间 安装基础软件包 软件配置 设置主机名称 hostna ...

  7. C#中split的方法汇总

    字符串的处理往往离不开split方法,下面介绍几种split的用法: 1.对单个字符进行分割(注意这里是字符,不是字符串,故只能用单引号‘’) string s=abcdeabcdeabcde; st ...

  8. cms-友情链接实现静态化

    service: package com.open1111.service.impl; import java.util.List; import javax.servlet.ServletConte ...

  9. IOS 控件器的创建方式(ViewController)

    ● 控制器常见的创建方式有以下几种 ➢ 通过storyboard创建 ➢ 直接创建 NJViewController *nj = [[NJViewController alloc] init]; ➢ ...

  10. POJ 3666 Making the Grade(区间dp)

    修改序列变成非递减序列,使得目标函数最小.(这题数据有问题,只要求非递减 从左往右考虑,当前a[i]≥前一个数的取值,当固定前一个数的取值的时候我们希望前面操作的花费尽量小. 所以状态可以定义为dp[ ...