基于zookeeper实现分布式配置中心(二)

  上一篇(基于zookeeper实现分布式配置中心(一))讲述了zookeeper相关概念和工作原理。接下来根据zookeeper的特性,简单实现一个分布式配置中心。

配置中心的优势

1、各环境配置集中管理。

2、配置更改,实时推送,jvm环境变量及时生效。

3、依靠配置变更,动态扩展功能,减少二次上线带来的成本。

4、减少开发人员、运维人员修改配置带来的额外开销。

 配置中心架构图

  

配置中心功能

1、配置管理平台中,操作人员可以创建项目所属系统、应用名称、实例名称、配置分组等信息。

2、配置管理平台中,操作人员可以上传配置文件,对属性有增、删、改、查的操作。

3、配置内容通过配置管理平台后台服务进行持久化(保存到数据库中)。

4、操作人员通过配置平台进行推送操作,将配置推送到zk集群相应结点(/cfgcenter/系统名称/应用名称/实例名称/分组名称)。

5、配置中心客户端监听zk集群中对应结点数据发生变化,读取变更后的内容,解析内容,刷新本地备份(分布式容灾)和Spring环境变量。

6、配置中心客户端如果和zk集群丢失连接,将加载本地本分配置到Spring环境变量。

7、配置中心客户端重新和zk集群建立连接,从zk集群中拉取最新配置内容,解析配置,刷新本地备份(分布式容灾)和Spring环境变量。

8、配置中心客户端将Spring环境变量刷新之后,动态刷新依赖配置中心配置的bean。

配置中心代码视图  

  

配置中心客户端设计解析

配置中心客户端初始化

@Component
public class CfgcenterInit implements ApplicationContextInitializer<ConfigurableWebApplicationContext>, ApplicationListener<ApplicationEvent> {private static Logger LOGGER = LoggerFactory.getLogger(CfgcenterInit.class);@Overridepublic void onApplicationEvent(ApplicationEvent event) {if (event instanceof ContextRefreshedEvent) {LOGGER.info("初始化配置中心客户端监听器...");ZKClient.getInstance().init();} else if (event instanceof RefreshEvent) {ZKClient.getInstance().getAeb().post(event);} else if (event instanceof ContextClosedEvent) {if (null != ZKClient.getInstance().getCw()) {ZKClient.getInstance().getCw().close();}}}@Overridepublic void initialize(ConfigurableWebApplicationContext cac) {try {ZookeeperProperties zookeeperProperties = ConfigurationBinder.withPropertySources(cac.getEnvironment()).bind(ZookeeperProperties.class);if (!zookeeperProperties.isEnabled()) {LOGGER.info("未开启配置中心客戶端...");return;}ZKClient.getInstance().binding(zookeeperProperties, new ZookeeperConfigProperties(), cac);} catch (Exception e) {LOGGER.error("配置中心客户端初始化异常...", e);}}
}

1、ApplicationContextInitializer#initialize方法中,获取zk连接信息配置,如果开启配置中心客户端,将ZookeeperProperties(zk集群连接信息)、ZookeeperConfigProperties(客户端监听zk集群结点信息)、ConfigurableWebApplicationContext (应用上下文)绑定到ZKClient实例中去。

2、ApplicationListener#onApplicationEvent方法中监听ContextRefreshedEvent(初始化配置中心客户端监听器)、RefreshEvent(配置刷新事件,通过guava的事件总线进行推送)、ContextClosedEvent(关闭配置中心客户端资源)。

配置中心客户端监听器

public class ConfigWatcher implements Closeable, TreeCacheListener {private static Logger LOGGER = LoggerFactory.getLogger(ConfigWatcher.class);private AtomicBoolean running = new AtomicBoolean(false);private String context;private CuratorFramework source;private HashMap<String, TreeCache> caches;public ConfigWatcher(String context, CuratorFramework source) {this.context = context;this.source = source;}public void start() {if (this.running.compareAndSet(false, true)) {this.caches = new HashMap<>();if (!context.startsWith("/")) {context = "/" + context;}try {TreeCache cache = TreeCache.newBuilder(this.source, context).build();cache.getListenable().addListener(this);cache.start();this.caches.put(context, cache);// no race condition since ZookeeperAutoConfiguration.curatorFramework// calls curator.blockUntilConnected} catch (KeeperException.NoNodeException e) {// no node, ignore} catch (Exception e) {LOGGER.error("Error initializing listener for context " + context, e);}}}@Overridepublic void close() {if (this.running.compareAndSet(true, false)) {for (TreeCache cache : this.caches.values()) {cache.close();}this.caches = null;}}@Overridepublic void childEvent(CuratorFramework client, TreeCacheEvent event) {TreeCacheEvent.Type eventType = event.getType();switch (eventType) {case INITIALIZED:LOGGER.info("配置中心客户端同步服务端状态完成...");refreshEnvAndBeans(event);break;case NODE_REMOVED:case NODE_UPDATED:refreshEnvAndBeans(event);break;case CONNECTION_SUSPENDED:case CONNECTION_LOST:LOGGER.info("配置中心客户端与服务端连接异常...");break;case CONNECTION_RECONNECTED:LOGGER.info("配置中心客户端与服务端重新建立连接...");break;}}private void refreshEnvAndBeans(TreeCacheEvent event) {//刷新环境变量
        ZKClient.getInstance().refreshEnvironment();//刷新Bean
        ZKClient.getInstance().getAep().publishEvent(new RefreshEvent(this, event, getEventDesc(event)));}private String getEventDesc(TreeCacheEvent event) {StringBuilder out = new StringBuilder();out.append("type=").append(event.getType());TreeCacheEvent.Type eventType = event.getType();if (eventType == NODE_UPDATED|| eventType == NODE_REMOVED) {out.append(", path=").append(event.getData().getPath());byte[] data = event.getData().getData();if (data != null) {out.append(", data=").append(new String(data, StandardCharsets.UTF_8));}}return out.toString();}
}

1、通过TreeCache监听路径/cfgcenter/系统名称/应用名称/实例名称/分组名称(该路径下可能会存在多个子节点,每个子节点对应一份配置,每一份配置大小不能超过64k)。

2、TreeCache监听事件类型如下

  • INITIALIZED(完成同步服务端状态,同步状态【NODE_REMOVED、 NODE_UPDATED、CONNECTION_RECONNECTED】之后触发)
  • NODE_REMOVED(结点移除触发)
  • NODE_UPDATED(结点数据更新触发)
  • CONNECTION_SUSPENDED(连接丢失触发)
  • CONNECTION_LOST(完全丢失连接触发)
  • CONNECTION_RECONNECTED(重新连接触发)

3、监听到INITIALIZED、NODE_UPDATED、NODE_REMOVED事件之后,执行refreshEnvAndBeans方法,刷新spring环境变量,同时刷新spring容器相关的Bean。

 配置中心客户端刷新spring环境变量

public class ZookeeperPropertySourceLocator {public static final String ZOOKEEPER_PREPERTY_SOURCE_NAME = "cfg-zookeeper";private ZookeeperConfigProperties properties;private CuratorFramework curator;private static Logger LOGGER = LoggerFactory.getLogger(ZookeeperPropertySourceLocator.class);public ZookeeperPropertySourceLocator(CuratorFramework curator, ZookeeperConfigProperties properties) {this.curator = curator;this.properties = properties;}public String getContext() {return this.properties.getContext();}public PropertySource getCfgcenterPropertySource(Environment environment) {ConfigurableEnvironment env = (ConfigurableEnvironment) environment;return env.getPropertySources().get(ZOOKEEPER_PREPERTY_SOURCE_NAME);}public void locate(Environment environment) {if (environment instanceof ConfigurableEnvironment) {ConfigurableEnvironment env = (ConfigurableEnvironment) environment;String context = properties.getContext();CompositePropertySource composite = new CompositePropertySource(ZOOKEEPER_PREPERTY_SOURCE_NAME);try {PropertySource propertySource = create(context);composite.addPropertySource(propertySource);if (null != env.getPropertySources().get(ZOOKEEPER_PREPERTY_SOURCE_NAME)) {LOGGER.info("替换PropertySource: " + ZOOKEEPER_PREPERTY_SOURCE_NAME);env.getPropertySources().replace(ZOOKEEPER_PREPERTY_SOURCE_NAME, composite);} else {LOGGER.info("添加PropertySource: " + ZOOKEEPER_PREPERTY_SOURCE_NAME);env.getPropertySources().addFirst(composite);}} catch (Exception e) {if (this.properties.isFailFast()) {ReflectionUtils.rethrowRuntimeException(e);} else {LOGGER.error("Unable to load zookeeper config from " + context, e);}}}}@PreDestroypublic void destroy() {}private void backupZookeeperPropertySource(ZookeeperPropertySource zps) {String backupDir = BASE_BACKUP_DIR + this.properties.getContext();String backupFile = String.format("%s/%s", backupDir, APP_NAME + ".properties");File bakFile = new File(backupFile);StringBuilder data = new StringBuilder();for (String propertyName : zps.getPropertyNames()) {data.append(propertyName).append("=").append(zps.getProperty(propertyName)).append(System.lineSeparator());}try {FileUtils.writeStringToFile(bakFile, data.toString(), Charsets.UTF_8);LOGGER.info("配置中心客户端刷新本地备份完成, path: " + backupDir);} catch (IOException e) {LOGGER.error("配置中心客户端刷新本地备份异常..., path: " + backupDir, e);}}private PropertySource<CuratorFramework> create(String context) {ZookeeperPropertySource zps;if (ZKClient.getInstance().isConnected()) {zps = new ZookeeperPropertySource(context, this.curator, false);this.backupZookeeperPropertySource(zps);} else {zps = new ZookeeperPropertySource(context, this.curator, true);}return zps;}
}

ZookeeperPropertySourceLocator会创建ZookeeperPropertySource,然后放入Spring的Environment变量中。如果配置中心客户端和zk集群处于连接状态,加载完ZookeeperPropertySource之后,备份到本地。

public class ZookeeperPropertySource extends AbstractZookeeperPropertySource {private static Logger LOGGER = LoggerFactory.getLogger(ZookeeperPropertySource.class);private Map<String, String> properties = new LinkedHashMap<>();public ZookeeperPropertySource(String context, CuratorFramework source, boolean backup) {super(context, source);//加载本地配置if (backup) {String backupDir = BASE_BACKUP_DIR + this.getContext();String backupFile = String.format("%s/%s", backupDir, APP_NAME + ".properties");try {InputStream is = FileUtils.openInputStream(new File(backupFile));InputStreamReader isr = new InputStreamReader(is);Properties properties = new Properties();properties.load(isr);properties.forEach((k, v) -> this.properties.put((String) k, (String) v));} catch (Exception e) {LOGGER.error("配置中心客户端本地配置加载异常...", e);}}//加载远程配置else {findProperties(this.getContext(), null);}}@Overridepublic Object getProperty(String name) {return this.properties.get(name);}private byte[] getPropertyBytes(String fullPath) {try {byte[] bytes = null;try {bytes = this.getSource().getData().forPath(fullPath);} catch (KeeperException e) {if (e.code() != KeeperException.Code.NONODE) {throw e;}}return bytes;} catch (Exception exception) {ReflectionUtils.rethrowRuntimeException(exception);}return null;}@Overridepublic String[] getPropertyNames() {Set<String> strings = this.properties.keySet();return strings.toArray(new String[strings.size()]);}private void findProperties(String path, List<String> children) {try {LOGGER.info("entering findProperties for path: " + path);if (children == null) {children = getChildren(path);}if (children == null || children.isEmpty()) {return;}for (String child : children) {String childPath = path + "/" + child;List<String> childPathChildren = getChildren(childPath);byte[] bytes = getPropertyBytes(childPath);if (!ArrayUtils.isEmpty(bytes)) {registerKeyValue(childPath, new String(bytes, Charset.forName("UTF-8")));}// Check children even if we have found a value for the current znode
                findProperties(childPath, childPathChildren);}LOGGER.info("leaving findProperties for path: " + path);} catch (Exception exception) {ReflectionUtils.rethrowRuntimeException(exception);}}private void registerKeyValue(String path, String value) {String key = sanitizeKey(path);LOGGER.info(String.format("配置中心客户端解析配置节点(%s),数据:%s", key, value));try {Properties properties = new Properties();properties.load(new StringReader(value));properties.forEach((k, v) -> this.properties.put((String) k, (String) v));} catch (IOException e) {LOGGER.info(String.format("配置中心客户端解析配置节点(%s)异常...", key));}}private List<String> getChildren(String path) throws Exception {List<String> children = null;try {children = this.getSource().getChildren().forPath(path);} catch (KeeperException e) {if (e.code() != KeeperException.Code.NONODE) {throw e;}}return children;}}

ZookeeperPropertySource通过构造参数backup来判断是加载zk集群中的配置还是本地备份配置。

配置中心客户端刷新Spring容器Bean

public abstract class BaseCfgcenterBean implements InitializingBean {private static Logger LOGGER = LoggerFactory.getLogger(BaseCfgcenterBean.class);@PostConstructpublic void init() {//注册到时间总线中
        ZKClient.getInstance().getAeb().register(this);}/*** z* 绑定自身目标**/protected void doBind() {Class<? extends BaseCfgcenterBean> clazz = this.getClass();if (org.springframework.util.ClassUtils.isCglibProxy(this)) {clazz = (Class<? extends BaseCfgcenterBean>) AopUtils.getTargetClass(this);}BaseCfgcenterBean target = binding(clazz, this.getDefaultResourcePath());this.copyProperties(target);}private void copyProperties(BaseCfgcenterBean target) {ReflectionUtils.doWithFields(this.getClass(), field -> {field.setAccessible(true);field.set(this, field.get(target));}, field -> AnnotatedElementUtils.isAnnotated(field, ConfigField.class));}/*** 绑定其他目标** @param clazz 目标类**/protected <T> T doBind(Class<T> clazz) {T target = binding(clazz, this.getDefaultResourcePath());if (target instanceof InitializingBean) {try {((InitializingBean) target).afterPropertiesSet();} catch (Exception e) {LOGGER.error(String.format("属性初始化失败[afterPropertiesSet], class=%s", ClassUtils.getSimpleName(clazz), e));}}return target;}private <T> T binding(Class<T> clazz, String defaultResourcePath) {Optional<PropertySource> propertySource = Optional.empty();//加载配置中心配置if (ZKClient.getInstance().isZkInit()) {propertySource = Optional.ofNullable(ZKClient.getInstance().resolvePropertySource());}//加载本地配置else {Optional<ResourcePropertySource> resourcePropertySource = ResourceUtils.getResourcePropertySource(defaultResourcePath);if (resourcePropertySource.isPresent()) {propertySource = Optional.ofNullable(resourcePropertySource.get());}}if (propertySource.isPresent()) {T target;try {target = ConfigurationBinder.withPropertySources(propertySource.get()).bind(clazz);} catch (Exception e) {LOGGER.error(String.format("属性绑定失败, class=%s", ClassUtils.getSimpleName(clazz)), e);return null;}return target;}return null;}@Overridepublic void afterPropertiesSet() {Class<?> target = this.getClass();if (AopUtils.isAopProxy(this)) {target = AopUtils.getTargetClass(this);}LOGGER.info(String.format("%s->%s模块引入配置中心%s...", this.getModuleName(), ClassUtils.getSimpleName(target), (ZKClient.getInstance().isConnected() ? "生效" : "无效")));}public String getModuleName() {return StringUtils.EMPTY;}@Subscribepublic void listenRefreshEvent(RefreshEvent refreshEvent) {this.afterPropertiesSet();LOGGER.info(refreshEvent.getEventDesc());this.refresh();}//通过事件进行刷新protected void refresh() {this.doBind();}//获取本地配置默认路径protected abstract String getDefaultResourcePath();
}

1、对象自身实现guava事件总线监听,监听RefreshEvent事件,触发对象属性刷新操作。

2、对象初始化时,注册自身目标到guava的事件总线对象中。

3、对象属性刷新,获取到PropertySource对象(配置中心配置或者项目自身静态配置),通过ConfigurationBinder工具类将配置重新绑定的对象属性。

配置管理平台接口

@RestController
@RequestMapping("cfg")
public class CfgController {private static Logger LOGGER = LoggerFactory.getLogger(CfgController.class);private static final String ZK_PATH_PATTERN0 = "/wmhcfg/projects/%s/%s";private static final String ZK_PATH_PATTERN1 = ZK_PATH_PATTERN0 + "/%s";private static final String ZK_PATH_PATTERN = ZK_PATH_PATTERN1 + "/%s";@Autowiredprivate CfgMapper mapper;@GetMapping(value = "/search", produces = MediaType.TEXT_PLAIN_VALUE)public String findCfgContents(@RequestBody @Validated SearchVO searchVO, @RequestParam(required = false) String cfgId) {List<CfgRecord> records = mapper.findRecords(searchVO);if (CollectionUtils.isEmpty(records)) {return StringUtils.EMPTY;}if (StringUtils.isNotBlank(cfgId)) {records = records.stream().filter(record -> cfgId.equals(record.getCfgId())).collect(Collectors.toList());}StringBuilder response = new StringBuilder();Properties properties = new Properties();records.forEach(record -> {try {properties.clear();properties.load(new StringReader(record.getCfgContent()));properties.forEach((key, value) -> response.append(key).append("=").append(value).append(System.lineSeparator()).append(System.lineSeparator()));} catch (IOException e) {LOGGER.error("配置解析异常...", e);}});return response.toString();}@PostMapping(value = "/send/{systemId}/{appId}/{groupId}/{cfgId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public BaseResponse sendCfgContent(@RequestBody String cfgContent, @PathVariable String systemId, @PathVariable String appId, @PathVariable String groupId, @PathVariable String cfgId) {BaseResponse baseResponse = new BaseResponse();baseResponse.setRestStatus(RestStatus.SUCCESS);SearchVO searchVO = new SearchVO();searchVO.setSystemId(systemId);searchVO.setAppId(appId);searchVO.setGroupId(groupId);List<CfgRecord> records = mapper.findRecords(searchVO);CfgRecord record = null;if (!CollectionUtils.isEmpty(records)) {for (CfgRecord cfgRecord : records) {if (cfgId.equals(cfgRecord.getCfgId())) {record = cfgRecord;record.setCfgContent(cfgContent);break;}}}if (null == record) {record = new CfgRecord();record.setSystemId(systemId);record.setAppId(appId);record.setGroupId(groupId);record.setCfgId(cfgId);record.setCfgContent(cfgContent);}StringBuilder cfgContentSB = new StringBuilder();Properties properties = new Properties();try {properties.load(new StringReader(record.getCfgContent()));} catch (IOException e) {LOGGER.error("配置解析异常...", e);baseResponse.setErrors(e.getMessage());baseResponse.setRestStatus(RestStatus.FAIL_50001);return baseResponse;}properties.forEach((key, value) -> cfgContentSB.append(key).append("=").append(value).append(System.lineSeparator()));record.setCfgContent(cfgContentSB.toString());if (null == record.getId()) {mapper.insertRecord(record);} else {mapper.updateRecord(record);}return baseResponse;}@PostMapping(value = "/push", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public BaseResponse pushCfgContent(@RequestBody @Validated PushVO pushVO) {BaseResponse baseResponse = new BaseResponse();baseResponse.setRestStatus(RestStatus.SUCCESS);String path = String.format(ZK_PATH_PATTERN, pushVO.getSystemId(), pushVO.getAppId(), pushVO.getGroupId(), pushVO.getCfgId());try {SearchVO searchVO = new SearchVO();searchVO.setSystemId(pushVO.getSystemId());searchVO.setAppId(pushVO.getAppId());searchVO.setGroupId(pushVO.getGroupId());List<CfgRecord> records = mapper.findRecords(searchVO);StringBuilder cfgContent = new StringBuilder();records.forEach(record -> cfgContent.append(record.getCfgContent()).append(System.lineSeparator()));if (!ZKHelper.setData(path, cfgContent.toString().getBytes())) {baseResponse.setRestStatus(RestStatus.FAIL_50001);}} catch (Exception e) {LOGGER.error("配置推送异常...", e);baseResponse.setRestStatus(RestStatus.FAIL_50001);baseResponse.setErrors(e.getMessage());}return baseResponse;}@PostMapping(value = "/create", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public BaseResponse createCfg(@RequestBody @Validated PushVO pushVO) {BaseResponse baseResponse = new BaseResponse();String path = String.format(ZK_PATH_PATTERN, pushVO.getSystemId(), pushVO.getAppId(), pushVO.getGroupId(), pushVO.getCfgId());if (ZKHelper.createPath(path)) {baseResponse.setRestStatus(RestStatus.SUCCESS);} else {baseResponse.setRestStatus(RestStatus.FAIL_50001);}return baseResponse;}@PostMapping(value = "/delete", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)public BaseResponse deleteCfg(@RequestBody @Validated DeleteVO deleteVO) {BaseResponse baseResponse = new BaseResponse();String path;if (StringUtils.isBlank(deleteVO.getGroupId())) {path = String.format(ZK_PATH_PATTERN0, deleteVO.getSystemId(), deleteVO.getAppId());} else if (StringUtils.isNotBlank(deleteVO.getGroupId()) && StringUtils.isBlank(deleteVO.getCfgId())) {path = String.format(ZK_PATH_PATTERN1, deleteVO.getSystemId(), deleteVO.getAppId(), deleteVO.getGroupId());} else {path = String.format(ZK_PATH_PATTERN, deleteVO.getSystemId(), deleteVO.getAppId(), deleteVO.getGroupId(), deleteVO.getCfgId());}if (ZKHelper.deletePath(path)) {baseResponse.setRestStatus(RestStatus.SUCCESS);} else {baseResponse.setRestStatus(RestStatus.FAIL_50001);}return baseResponse;}@GetMapping(value = "/getdata", produces = MediaType.TEXT_PLAIN_VALUE)public String getData(@RequestParam String path) {return ZKHelper.getData(path);}
}

  为配置管理前端提供配置保存、配置推送、配置删除等操作。

配置中心测试

@Component
@ConfigurationProperties(prefix = "cfg.test")
public class TestCfgcenterBean extends BaseCfgcenterBean {@ConfigFieldprivate String text;@ConfigFieldprivate Map<String, List<String>> map;public String getText() {return text;}public void setText(String text) {this.text = text;}public Map<String, List<String>> getMap() {return map;}public void setMap(Map<String, List<String>> map) {this.map = map;}@Overrideprotected String getDefaultResourcePath() {return StringUtils.EMPTY;}@Overrideprotected void refresh() {super.refresh();System.out.println("text=" + this.text);System.out.println("map=" + JSON.toJSONString(map));}
}

TestCfgcenterBean继承BaseCfgcenterBean,配置中心配置变更后可以自动将新的配置绑定到对象上。

@SpringBootApplication(exclude = RedissonAutoConfiguration.class)
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableRetry
public class SpringbootApplication {public static void main(String[] args) {System.setProperty("xxx.system.id", "test_system");System.setProperty("xxx.app.id", "test_app");System.setProperty("groupenv", "x");SpringApplication.run(SpringbootApplication.class, args);}}

启动类设置配置中心客户端需要的环境变量:系统标识、项目标识、分组标识。

 客户端与zk第一建立连接,同步完状态之后,触发INITIALIZED事件,刷新bean属性配置。

客户端与zk断开重连之后,同步完状态后触发INITIALIZED事件,刷新bean属性配置。

需要源码

请关注订阅号,回复:cfgcenter, 便可查看。

转载于:https://www.cnblogs.com/hujunzheng/p/10932153.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/531192.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis分布式锁实战

背景 目前开发过程中&#xff0c;按照公司规范&#xff0c;需要依赖框架中的缓存组件。不得不说&#xff0c;做组件的大牛对CRUD操作的封装&#xff0c;连接池、缓存路由、缓存安全性的管控都处理的无可挑剔。但是有一个小问题&#xff0c;该组件没有对分布式锁做实现&#xff…

基于RobotFramework实现自动化测试

Java robotframework seleniumlibrary 使用Robot Framework Maven Plugin&#xff08;http://robotframework.org/MavenPlugin/&#xff09;执行自动化测试chromedriver下载&#xff1a; http://chromedriver.storage.googleapis.com/index.htmlchromedriver和chrome版本对应…

Springboot国际化信息(i18n)解析

国际化信息理解 国际化信息也称为本地化信息 。 Java 通过 java.util.Locale 类来表示本地化对象&#xff0c;它通过 “语言类型” 和 “国家/地区” 来创建一个确定的本地化对象 。举个例子吧&#xff0c;比如在发送一个具体的请求的时候&#xff0c;在header中设置一个键值对…

C语言一看就能上手的干货!你确定你不来看吗?

本地环境设置 如果您想要设置 C 语言环境&#xff0c;您需要确保电脑上有以下两款可用的软件&#xff0c;文本编辑器和 C 编译器。 文本编辑器 这将用于输入您的程序。文本编辑器包括 Windows Notepad、OS Edit command、Brief、Epsilon、EMACS 和 vim/vi。文本编辑器的名称…

10万码农五年的C语言笔记!你现在知道别人为什么这么优秀了吗?

c语言对许多同学来说确实是一门比较难学的课程&#xff0c;不仅抽象&#xff0c;而且繁琐&#xff0c;但这又是一门不得不学的课程。前两节可能还有兴致听一听&#xff0c;然而&#xff0c;再过几节课就是一脸蒙比。凭空要想出一道题的算法和程序&#xff0c;根本无从下手。 所…

C语言/C++编程学习:C语言环境设置!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…

C语言指针原来也可以这么的通俗易懂!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…

C语言过时了?你在做梦?

为什么要使用C语言&#xff1f; 在过去的四十年里&#xff0c;C语言已经成为世界上最流行、最重要的一种编程语言。 C是一种融合了控制特性的现代语言&#xff0c;而我们已发现在计算机科学的理论和实践中&#xff0c;控制特性是很重要的。其设计使得用户可以自然地采用自顶向…

C语言深入理解!助你向大佬迈进!

Dennis Ritchie 过世了&#xff0c;他发明了C语言&#xff0c;一个影响深远并彻底改变世界的计算机语言。一门经历40多年的到今天还长盛不衰的语言&#xff0c;今天很多语言都受到C的影响&#xff0c;C&#xff0c;Java&#xff0c;C#&#xff0c;Perl&#xff0c; PHP&#xf…

【初涉C语言】程序员欢迎来到C语言的世界!

计算机发展史 机器语言所有的代码里面只有0和1优点&#xff1a;直接对硬件产生作用&#xff0c;程序的执行效率非常高缺点&#xff1a;指令又多又难记、可读性差、无可移植性汇编语言符号化的机器语言&#xff0c;用一个符号&#xff08;英文单词、数字&#xff09;来代表一条…

C语言和C++的区别整理详解!

c和c主要区别 根据书中的描述&#xff0c;进行了整理 推荐一个我自己的C/C交流裙815393895 1、 源代码文件的扩展名 摘自1.4.1 C实现源代码文件的扩展名UNIXC、cc、cxx、cGNU CC、cc、cxx、cpp、cDigital Marscpp、cxxBorland CcppWatcomcppMicrosoft Visual Ccpp、cxx、cc…

揭示C语言函数调用的本质解析

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得到…

C语言/C++编程学习:不找C/C++的工作也要学C/C++的原因

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…

【网络攻防】精通C语言的黑客才是真正的黑客!

精通C语言的黑客才是真正的黑客 黑客界&#xff0c;有两样重要的课程&#xff0c;一是计算机的本质&#xff0c;二是编译原理。相对于汇编等底层语言&#xff0c;它简单&#xff1b;相对于其它高级语言&#xff0c;它更为接近计算机&#xff1b;同样它对黑客的两大课程很有帮助…

我两小时学完指针,你学会数组/指针与函数需要多久?

数组与函数&#xff1a; 这段函数中 函数的参数是数组&#xff0c;注意数组作为函数参数时&#xff0c;数组名和数组元素个数时分别传递的。 指针与函数&#xff1a; 这段函数中的参数是指针变量&#xff0c;传入的是数组的数组名或者首元素的地址&#xff0c;然后用引领操作…

C语言发展历史,C语言特点,C语言利于弊,入门须知三招

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 这些是C/C能做的 服务器开发工程师、人工智能、云计算工程师、信息安全&#xff08;黑客反黑客&#xff09;、大数据 、数据平台、嵌入式工程师、流媒体服务器、数据控解、图像处理、音频视频开发工程…

程序员怎么看待C语言?最伟大?最落后?

一&#xff0c;前言 对我来说&#xff0c;C语言应该可以算得上是世界上最伟大的编程语言。全中国口气最大的程序员&#xff0c;业界称之为“垠神”&#xff0c;曾经发过文章吐槽过业界各种主流的编程语言&#xff08;对Java&#xff0c;的Python稍微宽容一些&#xff09;&…

如何学习C语言?就是这么简单粗暴!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的。 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理…

C/C++对编程的重要性!其他编程语言都是弟弟!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…

C语言其实不难,只是你没有找对方法!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…