为何一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?【享学Spring Cloud】
每篇一句
你应该思考:为什么往往完成比完美更重要?
前言
在Spring Cloud微服务应用体系中,远程调用都应负载均衡。我们在使用RestTemplate作为远程调用客户端的时候,开启负载均衡极其简单:一个@LoadBalanced注解就搞定了。
相信大家大都使用过Ribbon做Client端的负载均衡,也许你有和我一样的感受:Ribbon虽强大但不是特别的好用。我研究了一番,其实根源还是我们对它内部的原理不够了解,导致对一些现象无法给出合理解释,同时也影响了我们对它的定制和扩展。本文就针对此做出梳理,希望大家通过本文也能够对Ribbon有一个较为清晰的理解(本文只解释它@LoadBalanced这一小块内容)。
开启客户端负载均衡只需要一个注解即可,形如这样:
@LoadBalanced // 标注此注解后,RestTemplate就具有了客户端负载均衡能力
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
说Spring是Java界最优秀、最杰出的重复发明轮子作品一点都不为过。本文就代领你一探究竟,为何开启RestTemplate的负载均衡如此简单。
说明:本文建立在你已经熟练使用
RestTemplate,并且了解RestTemplate它相关组件的原理的基础上分析。若对这部分还比较模糊,强行推荐你先参看我前面这篇文章:RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
RibbonAutoConfiguration
这是Spring Boot/Cloud启动Ribbon的入口自动配置类,需要先有个大概的了解:
@Configuration
// 类路径存在com.netflix.client.IClient、RestTemplate等时生效
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
// // 允许在单个类中使用多个@RibbonClient
@RibbonClients
// 若有Eureka,那就在Eureka配置好后再配置它~~~(如果是别的注册中心呢,ribbon还能玩吗?)
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class })
// 加载配置:ribbon.eager-load --> true的话,那么项目启动的时候就会把Client初始化好,避免第一次惩罚
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {
@Autowired
private RibbonEagerLoadProperties ribbonEagerLoadProperties;
// Ribbon的配置文件们~~~~~~~(复杂且重要)
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
// 特征,FeaturesEndpoint这个端点(`/actuator/features`)会使用它org.springframework.cloud.client.actuator.HasFeatures
@Bean
public HasFeatures ribbonFeature() {
return HasFeatures.namedFeature("Ribbon", Ribbon.class);
}
// 它是最为重要的,是一个org.springframework.cloud.context.named.NamedContextFactory 此工厂用于创建命名的Spring容器
// 这里传入配置文件,每个不同命名空间就会创建一个新的容器(和Feign特别像) 设置当前容器为父容器
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
// 这个Bean是关键,若你没定义,就用系统默认提供的Client了~~~
// 内部使用和持有了SpringClientFactory。。。
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
...
}
这个配置类最重要的是完成了Ribbon相关组件的自动配置,有了LoadBalancerClient才能做负载均衡(这里使用的是它的唯一实现类RibbonLoadBalancerClient)
@LoadBalanced
注解本身及其简单(一个属性都木有):
// 所在包是org.springframework.cloud.client.loadbalancer
// 能标注在字段、方法参数、方法上
// JavaDoc上说得很清楚:它只能标注在RestTemplate上才有效
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
它最大的特点:头上标注有@Qualifier注解,这是它生效的最重要因素之一,本文后半啦我花了大篇幅介绍它的生效时机。
关于@LoadBalanced 自动生效的配置,我们需要来到这个自动配置类:LoadBalancerAutoConfiguration
LoadBalancerAutoConfiguration
// Auto-configuration for Ribbon (client-side load balancing).
// 它的负载均衡技术依赖于的是Ribbon组件~
// 它所在的包是:org.springframework.cloud.client.loadbalancer
@Configuration
@ConditionalOnClass(RestTemplate.class) //可见它只对RestTemplate生效
@ConditionalOnBean(LoadBalancerClient.class) // Spring容器内必须存在这个接口的Bean才会生效(参见:RibbonAutoConfiguration)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class) // retry的配置文件
public class LoadBalancerAutoConfiguration {
// 拿到容器内所有的标注有@LoadBalanced注解的Bean们
// 注意:必须标注有@LoadBalanced注解的才行
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
// LoadBalancerRequestTransformer接口:允许使用者把request + ServiceInstance --> 改造一下
// Spring内部默认是没有提供任何实现类的(匿名的都木有)
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
// 配置一个匿名的SmartInitializingSingleton 此接口我们应该是熟悉的
// 它的afterSingletonsInstantiated()方法会在所有的单例Bean初始化完成之后,再调用一个一个的处理BeanName~
// 本处:使用配置好的所有的RestTemplateCustomizer定制器们,对所有的`RestTemplate`定制处理
// RestTemplateCustomizer下面有个lambda的实现。若调用者有需要可以书写然后扔进容器里既生效
// 这种定制器:若你项目中有多个RestTempalte,需要统一处理的话。写一个定制器是个不错的选择
// (比如统一要放置一个请求拦截器:输出日志之类的)
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
});
}
// 这个工厂用于createRequest()创建出一个LoadBalancerRequest
// 这个请求里面是包含LoadBalancerClient以及HttpRequest request的
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
}
// =========到目前为止还和负载均衡没啥关系==========
// =========接下来的配置才和负载均衡有关(当然上面是基础项)==========
// 若有Retry的包,就是另外一份配置,和这差不多~~
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {、
// 这个Bean的名称叫`loadBalancerClient`,我个人觉得叫`loadBalancerInterceptor`更合适吧(虽然ribbon是唯一实现)
// 这里直接使用的是requestFactory和Client构建一个拦截器对象
// LoadBalancerInterceptor可是`ClientHttpRequestInterceptor`,它会介入到http.client里面去
// LoadBalancerInterceptor也是实现负载均衡的入口,下面详解
// Tips:这里可没有@ConditionalOnMissingBean哦~~~~
@Bean
public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
// 向容器内放入一个RestTemplateCustomizer 定制器
// 这个定制器的作用上面已经说了:在RestTemplate初始化完成后,应用此定制化器在**所有的实例上**
// 这个匿名实现的逻辑超级简单:向所有的RestTemplate都塞入一个loadBalancerInterceptor 让其具备有负载均衡的能力
// Tips:此处有注解@ConditionalOnMissingBean。也就是说如果调用者自己定义过RestTemplateCustomizer类型的Bean,此处是不会执行的
// 请务必注意这点:容易让你的负载均衡不生效哦~~~~
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
...
}
这段配置代码稍微有点长,我把流程总结为如下几步:
LoadBalancerAutoConfiguration要想生效类路径必须有RestTemplate,以及Spring容器内必须有LoadBalancerClient的实现Bean
1.LoadBalancerClient的唯一实现类是:org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClientLoadBalancerInterceptor是个ClientHttpRequestInterceptor客户端请求拦截器。它的作用是在客户端发起请求之前拦截,进而实现客户端的负载均衡restTemplateCustomizer()返回的匿名定制器RestTemplateCustomizer它用来给所有的RestTemplate加上负载均衡拦截器(需要注意它的@ConditionalOnMissingBean注解~)
不难发现,负载均衡实现的核心就是一个拦截器,就是这个拦截器让一个普通的RestTemplate逆袭成为了一个具有负载均衡功能的请求器
LoadBalancerInterceptor
该类唯一被使用的地方就是LoadBalancerAutoConfiguration里配置上去~
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
// 这个命名都不叫Client了,而叫loadBalancer~~~
private LoadBalancerClient loadBalancer;
// 用于构建出一个Request
private LoadBalancerRequestFactory requestFactory;
... // 省略构造函数(给这两个属性赋值)
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
}
此拦截器拦截请求后把它的serviceName委托给了LoadBalancerClient去执行,根据ServiceName可能对应N多个实际的Server,因此就可以从众多的Server中运用均衡算法,挑选出一个最为合适的Server做最终的请求(它持有真正的请求执行器ClientHttpRequestExecution)。
LoadBalancerClient
请求被拦截后,最终都是委托给了LoadBalancerClient处理。
// 由使用负载平衡器选择要向其发送请求的服务器的类实现
public interface ServiceInstanceChooser {
// 从负载平衡器中为指定的服务选择Service服务实例。
// 也就是根据调用者传入的serviceId,负载均衡的选择出一个具体的实例出来
ServiceInstance choose(String serviceId);
}
// 它自己定义了三个方法
public interface LoadBalancerClient extends ServiceInstanceChooser {
// 执行请求
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
// 重新构造url:把url中原来写的服务名 换掉 换成实际的
URI reconstructURI(ServiceInstance instance, URI original);
}
它只有一个实现类RibbonLoadBalancerClient (ServiceInstanceChooser是有多个实现类的~)。
RibbonLoadBalancerClient
首先我们应当关注它的choose()方法:
public class RibbonLoadBalancerClient implements LoadBalancerClient {
@Override
public ServiceInstance choose(String serviceId) {
return choose(serviceId, null);
}
// hint:你可以理解成分组。若指定了,只会在这个偏好的分组里面去均衡选择
// 得到一个Server后,使用RibbonServer把server适配起来~~~
// 这样一个实例就选好了~~~真正请求会落在这个实例上~
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
// 根据ServiceId去找到一个属于它的负载均衡器
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
}
choose方法:传入serviceId,然后通过SpringClientFactory获取负载均衡器com.netflix.loadbalancer.ILoadBalancer,最终委托给它的chooseServer()方法选取到一个com.netflix.loadbalancer.Server实例,也就是说真正完成Server选取的是ILoadBalancer。
ILoadBalancer以及它相关的类是一个较为庞大的体系,本文不做更多的展开,而是只聚焦在我们的流程上
LoadBalancerInterceptor执行的时候是直接委托执行的loadBalancer.execute()这个方法:
RibbonLoadBalancerClient:
// hint此处传值为null:一视同仁
// 说明:LoadBalancerRequest是通过LoadBalancerRequestFactory.createRequest(request, body, execution)创建出来的
// 它实现LoadBalancerRequest接口是用的一个匿名内部类,泛型类型是ClientHttpResponse
// 因为最终执行的显然还是执行器:ClientHttpRequestExecution.execute()
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
return execute(serviceId, request, null);
}
// public方法(非接口方法)
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
// 同上:拿到负载均衡器,然后拿到一个serverInstance实例
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
if (server == null) { // 若没找到就直接抛出异常。这里使用的是IllegalStateException这个异常
throw new IllegalStateException("No instances available for " + serviceId);
}
// 把Server适配为RibbonServer isSecure:客户端是否安全
// serverIntrospector内省 参考配置文件:ServerIntrospectorProperties
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server));
//调用本类的重载接口方法~~~~~
return execute(serviceId, ribbonServer, request);
}
// 接口方法:它的参数是ServiceInstance --> 已经确定了唯一的Server实例~~~
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
// 拿到Server)(说白了,RibbonServer是execute时的唯一实现)
Server server = null;
if (serviceInstance instanceof RibbonServer) {
server = ((RibbonServer) serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
// 说明:执行的上下文是和serviceId绑定的
RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
...
// 真正的向server发送请求,得到返回值
// 因为有拦截器,所以这里肯定说执行的是InterceptingRequestExecution.execute()方法
// so会调用ServiceRequestWrapper.getURI(),从而就会调用reconstructURI()方法
T returnVal = request.apply(serviceInstance);
return returnVal;
... // 异常处理
}
returnVal是一个ClientHttpResponse,最后交给handleResponse()方法来处理异常情况(若存在的话),若无异常就交给提取器提值:responseExtractor.extractData(response),这样整个请求就算全部完成了。
使用细节
针对@LoadBalanced下的RestTemplate的使用,我总结如下细节供以参考:
- 传入的
String类型的url必须是绝对路径(http://...),否则抛出异常:java.lang.IllegalArgumentException: URI is not absolute serviceId不区分大小写(http://user/...效果同http://USER/...)serviceId后请不要跟port端口号了~~~
最后,需要特别指出的是:标注有@LoadBalanced的RestTemplate只能书写serviceId而不能再写IP地址/域名去发送请求了。若你的项目中两种case都有需要,请定义多个RestTemplate分别应对不同的使用场景~
本地测试
了解了它的执行流程后,若需要本地测试(不依赖于注册中心),可以这么来做:
// 因为自动配置头上有@ConditionalOnMissingBean注解,所以自定义一个覆盖它的行为即可
// 此处复写它的getServer()方法,返回一个固定的(访问百度首页)即可,方便测试
@Bean
public LoadBalancerClient loadBalancerClient(SpringClientFactory factory) {
return new RibbonLoadBalancerClient(factory) {
@Override
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
return new Server("www.baidu.com", 80);
}
};
}
这么一来,下面这个访问结果就是百度首页的html内容喽。
@Test
public void contextLoads() {
String obj = restTemplate.getForObject("http://my-serviceId", String.class);
System.out.println(obj);
}
此处
my-serviceId肯定是不存在的,但得益于我上面自定义配置的LoadBalancerClient
什么,写死return一个Server实例不优雅?确实,总不能每次上线前还把这部分代码给注释掉吧,若有多个实例呢?还得自己写负载均衡算法吗?很显然Spring Cloud早早就为我们考虑到了这一点:脱离Eureka使用配置listOfServers进行客户端负载均衡调度(<clientName>.<nameSpace>.listOfServers=<comma delimited hostname:port strings>)
对于上例我只需要在主配置文件里这么配置一下:
# ribbon.eureka.enabled=false # 若没用euraka,此配置可省略。否则不可以
my-serviceId.ribbon.listOfServers=www.baidu.com # 若有多个实例请用逗号分隔
效果完全同上。
Tips:这种配置法不需要是完整的绝对路径,
http://是可以省略的(new Server()方式亦可)
自己添加一个记录请求日志的拦截器可行吗?
显然是可行的,我给出示例如下:
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> list = new ArrayList<>();
list.add((request, body, execution) -> {
System.out.println("当前请求的URL是:" + request.getURI().toString());
return execution.execute(request, body);
});
restTemplate.setInterceptors(list);
return restTemplate;
}
这样每次客户端的请求都会打印这句话:当前请求的URI是:http://my-serviceId,一般情况(缺省情况)自定义的拦截器都会在负载均衡拦截器前面执行(因为它要执行最终的请求)。若你有必要定义多个拦截器且要控制顺序,可通过Ordered系列接口来实现~
最后的最后,我抛出一个非常非常重要的问题:
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired + @LoadBalanced能把你配置的RestTemplate自动注入进来拿来定制呢???核心原理是什么?
提示:本原理内容属于
Spring Framwork核心技术,建议深入思考而不囫囵吞枣。有疑问的可以给我留言,我也将会在下篇文章给出详细解答(建议先思考)
推荐阅读
RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
@Qualifier高级应用---按类别批量依赖注入【享学Spring】
总结
本文以大家熟悉的@LoadBalanced和RestTemplate为切入点介绍了Ribbon实现负载均衡的执行流程,当然此部分对Ribbon整个的核心负载体系知识来说知识冰山一角,但它作为敲门砖还是很有意义的,希望本文能勾起你对Ribbon体系的兴趣,深入了解它~
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一起飞 ==
为何一个@LoadBalanced注解就能让RestTemplate拥有负载均衡的能力?【享学Spring Cloud】的更多相关文章
- 深入理解@LoadBalanced注解的实现原理与客户端负载均衡
前提 在阅读这篇博客之前,希望你对SpringCloud套件熟悉和理解,更希望关注下微服务开发平台 概述 在使用springcloud ribbon客户端负载均衡的时候,可以给RestTemplate ...
- RestTemplate相关组件:ClientHttpRequestInterceptor【享学Spring MVC】
每篇一句 做事的人和做梦的人最大的区别就是行动力 前言 本文为深入了解Spring提供的Rest调用客户端RestTemplate开山,对它相关的一些组件做讲解. Tips:请注意区分RestTemp ...
- RestTemplate的使用和原理你都烂熟于胸了吗?【享学Spring MVC】
每篇一句 人圆月圆心圆,人和家和国和---中秋节快乐 前言 在阅读本篇之前,建议先阅读开山篇效果更佳.RestTemplate是Spring提供的用于访问Rest服务的客户端工具,它提供了多种便捷访问 ...
- 一起来学Spring Cloud | 第一章 :如何搭建一个多模块的springcloud项目
在spring cloud系列章节中,本来已经写了几个章节了,但是自己看起来有些东西写得比较杂,所以重构了一下springcloud的章节内容,新写了本章节,先教大家在工作中如何搭建一个多模块的spr ...
- 【Spring Cloud 源码解读】之 【这也太神奇了,RestTemplate加上一个@LoadBalanced注解就能实现负载均衡!】
前提概要: 前天,有个前端大佬问了我两个问题:为啥不引入Ribbon依赖就能使用Ribbon?为啥RestTemplate加上@LoadBalanced注解就能负载均衡了?我也表示很疑惑,而我自己其实 ...
- Spring RestTemplate具备负载均衡功能
在创建RestTemplate的Bean时使用@LoadBalanced注解, 就可以自动配置为使用ribbon.如下面的示例所示: @Configuration public class MyCo ...
- 自定义实现一个loghub(或kafka)的动态分片消费者负载均衡?
一般地,像kafka之类的消息中间件,作为一个可以保持历史消息的组件,其消费模型一般是主动拉取方式.这是为了给消费者足够的自由,回滚或者前进. 然而,也正是由于将消费消息的权力交给了消费者,所以,消费 ...
- springcloud ribbon的 @LoadBalanced注解
在使用springcloud ribbon客户端负载均衡的时候,可以给RestTemplate bean 加一个@LoadBalanced注解,就能让这个RestTemplate在请求时拥有客户端负载 ...
- RestTemplate的逆袭之路,从发送请求到负载均衡
上篇文章我们详细的介绍了RestTemplate发送请求的问题,熟悉Spring的小伙伴可能会发现:RestTemplate不就是Spring提供的一个发送请求的工具吗?它什么时候具有了实现客户端负载 ...
随机推荐
- cc2530中单片机的通用I/O接口
cc2530中有21个输入/输出引脚. 这些引脚可以设置为通用I/O或者设置为外设I/O.(其实这里的外设还是不太懂到底指什么,网上说输入设备,但是通用I/O也可以输入啊,为什么要弄外设I/O?) 其 ...
- MYSQL中group_concat( )函数中参数的排序方法
使用mysql中的group_concat( )函数连接指定字段时,可以先对该字段进行排序. PS:是因为二刷mysql的51道题的第12题遇到的:查询和" 01 "号同学学习的课 ...
- 一些开源的dashboard 解决方案
简单收集了以下开源dashboard 的项目,记录下 plotly-dash 基于python 的dash 开发工具,很不错 项目地址 https://github.com/plotly/dash k ...
- 用于C#的极速序列化/反序列工具 MessagePack
MessagePack 比MsgPack-Cli快10倍,并且优于其他C#序列化器.MessagePack for C#还内置了对LZ4压缩的支持 - 一种极快的压缩算法.对于性能追求很重要,特别是在 ...
- pandas批量读取带有日期的文件夹简单操作
工作中碰到了这样一个数据处理的问题,想让你把某个文件夹下的子文件夹中的excel表级联成为1张表,用excel来做会很浪费时间并且很劳累,这时候我们就可以用pandas来加大工作效率,只需要半个小时就 ...
- python 整数转16进制数
def toHex(num): """ :type num: int :rtype: str """ chaDic = {: : : : : ...
- 使用 Python 和 Flask 设计 RESTful API
近些年来 REST (REpresentational State Transfer) 已经变成了 web services 和 web APIs 的标配. 在本文中我将向你展示如何简单地使用 Pyt ...
- python发送钉钉机器人脚本
#!/usr/bin/python# -*- coding: utf-8 -*-import requestsimport jsonimport sysimport os headers = {'Co ...
- 配置IDEA项目JDK环境
打开IDEA,然后点击[Configure]->[Project Defaults]->[Project Structure],如下图: 然后左侧点击树形菜单的[Project Sett ...
- 【NWJS】解析node-webkit(NWJS)的打包和发布
目录结构: contents structure [-] 下载和安装node-webkit 建立一个简单的WEB应用 生成EXE可执行文件 修改icon 封包 Enigma Virtual Box I ...