由于本文较长,需要耐住性子阅读,另外本文中涉及到的知识点较多,想要深入学习某知识点可以参考其他博客或官网资料。本文也非源码分析文章,示例中的源码大多是伪代码和剪辑过的代码示例,由于该轮子为公司内部使用所以源码不便公开,敬请谅解。造轮子不重要,重要的是掌握轮子原理,取其精华,去其糟粕。欢迎大家拍砖。

背景

目前部门内部接口调用基本都是基于Http的,并且部门内部也有封装好的HttpClient。即便如此,每次有新的项目也要写一些繁琐的请求代码,即使不写也是复制粘贴,体验不算太好。于是乎想造一个轮子使用,为什么说是造轮子呢?因为它的功能和SpringCloud的OpenFeign差不多,不过由于是自己项目使用,自然是没有OpenFeign功能强大。

原理

用过MyBatis的同学应该都知道Mapper,所以此次造轮子我借鉴(抄袭)了Spring-Mybatis的部分代码,而且也是先把它的代码大致过了一遍才开始动工,大概要掌握的知识有如下几点:

  • 动态代理
  • Spring的FactoryBean
  • Spring的自定义Bean注册机制,包括扫描,自定义BeanDefinition等
  • 自定义注解的使用
  • 反射机制

轮子目标

实现一个基于动态代理的HttpClient,看一下代码基本就明白了。

造轮子之前

  1. //日常编码方案(伪代码)
  2. public class HttpUtil {
  3. public Object post(String url){
  4. HttpClient client = new HttpClient(url);
  5. client.addHeader("Content-Type","application/json");
  6. return client.send();
  7. }
  8. }

造轮子之后

  1. //轮子方案
  2. @HttpApi("http://localhost:8080/")
  3. public interface UserService{
  4. @HttpGet("user/{id}")
  5. User getUserById(@Path("id") Long id);
  6. @HttpPost("user/register")
  7. boolean register(@Json User user);
  8. }
  9. //使用方法示例(伪代码)
  10. //本地Controller或者其他服务类
  11. public class UserController{
  12. //注入
  13. @Autowired
  14. private UserService userService;
  15. @GetMapping("/")
  16. public User getUser(){
  17. //发送Http请求,调用远程接口
  18. return userService.getUserById(1L);
  19. }
  20. }

OK,那么到这里也就基本介绍了这个轮子的用途和大体实现方向了。如果看上述示例代码还是不太明白的话,没关系,继续往下看。

轮子雏形

理解FactoryBean

想要实现动态获取Bean那么这个接口至关重要,为什么呢?试想一下,当你定义了一个接口例如:

  1. public interface UserService{
  2. User getUserById(Long id);
  3. }

那么我们势必要将该接口作为一个Bean注册到BeanFactory中,在《原理》那一段我们都知道使用动态代理创建实现类,那么如何优雅的将实现类作为Bean注册到BeanFactory中呢?此时FactoryBean接口就派上用场了。

  1. /**
  2. * If a bean implements this
  3. * interface, it is used as a factory for an object to expose, not directly as a
  4. * bean instance that will be exposed itself
  5. */
  6. public interface FactoryBean<T> {
  7. //获取真正的 bean 实例
  8. T getObject() throws Exception;
  9. // bean 类型
  10. Class<?> getObjectType();
  11. //是否单例
  12. boolean isSingleton();
  13. }

看英文注释就可以知道,当注册到BeanFactory中的类是FactoryBean的实现类时,暴露出来的真实的Bean其实是getObject()方法返回的bean实例,而不是FactoryBean本身。那么结合上文中的接口,我们简单定义一个UserServiceFactoryBean作为示范:

  1. @Component
  2. public class UserServiceFactoryBean implements FactoryBean<UserService> {
  3. @Override
  4. public UserService getObject() throws Exception {
  5. //使用动态代理创建UserService的实现类
  6. UserService serviceByProxy = createUserServiceByProxy();
  7. return serviceByProxy;
  8. }
  9. @Override
  10. public Class<?> getObjectType() {
  11. return UserService.class;
  12. }
  13. @Override
  14. public boolean isSingleton() {
  15. return true;
  16. }
  17. }

是不是很简单,虽然是继承自FactoryBean,但是注入到服务类中的对象其实是由动态代理生成的UserService的实现类。当然作为示例这么实现自然很简单,但是作为一个轮子提供给开发者使用的话,上边这段代码其实并不是开发者手动去写的,因为开发者只负责定义接口即可,那么如何来自动生成FactoryBean的实现类呢?这个就涉及到自定义BeanDefinition了。

包扫描

还是以MyBatis为例,在Spring-MyBatis中,我们会使用@MapperScan注解来使应用程序启动的时候扫描指定包然后加载相应的Mapper。

  1. @MapperScan(basePackages = {"com.lunzi.demo.mapper"})

这里要注意的是,在MapperScan注解的定义中有这么一行@Import({MapperScannerRegistrar.class}),这个类是何方神圣?它做了什么事情?其实从它的命名我们大概能猜出来,它是负责扫描包并且注册Mapper的一个工具类。

  1. @Import({MapperScannerRegistrar.class})
  2. public @interface MapperScan {
  3. }

下面看一下这个类的定义:

  1. public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {}

到这里大概明白了,它继承了ImportBeanDefinitionRegistrar接口,并实现了registerBeanDefinitions方法。具体实现细节主要关注对被扫描之后的接口类做了什么处理。负责扫描的类是由SpringFramework提供的ClassPathBeanDefinitionScanner,有兴趣的同学可以去看看源码。扫描到了Mapper接口之后,我们看一下后续对这些接口做了什么处理。

主要查看:ClassPathMapperScanner.processBeanDefinitions方法

  1. private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
  2. GenericBeanDefinition definition;
  3. for (BeanDefinitionHolder holder : beanDefinitions) {
  4. definition = (GenericBeanDefinition) holder.getBeanDefinition();
  5. // 注:mapper接口是我们实际要用的bean,但是注册到BeanFactory的是MapperFactoryBean
  6. // the mapper interface is the original class of the bean
  7. // but, the actual class of the bean is MapperFactoryBean
  8. definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59
  9. //这里将beanClass设置为MapperFactoryBean
  10. definition.setBeanClass(this.mapperFactoryBean.getClass());
  11. //...中间一些无关代码忽略
  12. //然后设置注入模式为 AUTOWIRE_BY_TYPE
  13. definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
  14. }
  15. }

那么Spring将BeanDefinition添加到Bean列表中,注册Bean的任务就完成了,为什么拿Spring-MyBatis中的代码做讲解呢?原理都是相通的,那么我们回归到正题,下面我们要做的事情就是仿照其实现。

  • 定义扫描注册类

    1. public class HttpApiScannerRegistrar implements ImportBeanDefinitionRegistrar{
    2. }
  • 定义扫描注解

    1. @Import(HttpApiScannerRegistrar.class)
    2. public @interface HttpApiScan {
    3. }
  • 定义FactoryBean

    这里要注意这个httpApiInterface,这玩意是生成代理类的接口,应用大量反射方法解析该接口类,下文详细分析,这里我们只要关注FactoryBean即可。

    1. public class HttpApiFactoryBean<T> implements FactoryBean<T>,InitializingBean {
    2. private Class<T> httpApiInterface;
    3. @Override
    4. public T getObject() throws Exception {
    5. //下文讲述生成代理类的方法
    6. return ...;
    7. }
    8. }

    写到这里我们就可以初步验证一下了,要不然会枯燥乏味,给你们点正向反馈。

    1. @SpringBootApplication
    2. //添加扫描注解
    3. @HttpApiScan(basePackages = "com.lunzi.demo.api")
    4. public class HttpClientApiApplication {
    5. public static void main(String[] args) {
    6. SpringApplication.run(HttpClientApiApplication.class,args);
    7. }
    8. }

    随便定义一个接口,里面的方法名无所谓的,毕竟暂时是个空壳子,用不上。不过这个接口要放在com.lunzi.demo.api包下,保证被扫描到。

    1. public interface UserApiService {
    2. Object test();
    3. }

    在随便写个controller

    1. @RestController
    2. @RequestMapping("/")
    3. public class TestController {
    4. @Autowired(required = false)
    5. private UserApiService userApiService;
    6. @GetMapping("test")
    7. public Object[] getTestResult() {
    8. return userApiService.test();
    9. }
    10. }

    别着急,这里还不能运行,毕竟FactoryBean的getObject方法还没有实现。下面该轮到动态代理上场了。

动态代理

java中的动态代理并不复杂,按照套路走就完事了,首先要定义一个实现InvocationHandler接口的类。

  1. public class HttpApiProxy<T> implements InvocationHandler {
  2. @Override
  3. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  4. //先写个默认实现
  5. return "this is a proxy test";
  6. }
  7. }

在定义一个代理工厂类,用于创建代理类,大家还记得httpApiInterface吗?创建代理类方法如下:

  1. public T newInstance(HttpApiProxy<T> httpApiProxy) {
  2. return (T) Proxy.newProxyInstance(httpApiInterface.getClassLoader(), new Class[]{httpApiInterface}, httpApiProxy);
  3. }

所以说知道FactoryBean的getObject方法怎么写了吧。

  1. @Override
  2. public T getObject() throws Exception {
  3. //由于成品轮子代码封装较多。此处为伪代码用于展示具体原理
  4. return new HttpProxyFactory().newInstance(new HttpApiProxy());
  5. }

到此为止,我们可以运行DMEO程序了,截图如下:

代理类成功生成了,毋庸置疑,方法调用时也会返回 "this is a proxy test";

组装配件

到此为止,我们实现了一个轮子外壳,它现在有什么作用呢?

  • 根据注解扫描包自动注册FactoryBean
  • FactoryBean的getObject返回bean对象使用动态代理创建
  • 在其他服务类中可注入
  • 调用接口方法能够正常返回

下一步就要一步步实现轮子配件了,我们先回到接口代码,假如有一个用户服务:

  1. //根据用户ID获取用户信息
  2. GET http://api.demo.com/v1/user/{id}
  3. //新注册一个用户
  4. POST http://api.demo.com/v1/user/register

对应客户端接口如下:

  1. public interface UserService{
  2. User getUserById(Long id);
  3. Boolean register(User user);
  4. }

所以结合上文中的Http服务信息,我们发现接口还缺少如下信息:

  • Host信息
  • URL信息
  • 参数类型信息

这里我先列举这三类,其实能够做的还有很多,后续我们升级轮子的时候在详细介绍。那么如何添加这些信息呢,那么就要用到注解功能了。首先添加Host信息:

  1. @HttpApi(host = "http://api.demo.com")
  2. public interface UserService{
  3. User getUserById(Long id);
  4. Boolean register(User user);
  5. }

是不是很简单呢?这里还要注意可扩展性,因为平时我们都会区分各种环境,开发,调试,测试,预发,生产环境,这里我们可以加上一个变量的功能,改造后如下:

  1. @HttpApi(host = "${api.user.host}")
  2. public interface UserService{
  3. User getUserById(Long id);
  4. Boolean register(User user);
  5. }

代码中的 api.user.host 只是一个示例,这里我们可以配置成任何变量,只要和配置文件中的对应即可。例如application-dev.yaml

  1. api:
  2. user:
  3. host: http://api.demo.dev.com/

解决了Host问题,是不是要添加具体的URL了,还要考虑HttpMethod,由于大部分都不是正规的RestfulApi所以在轮子中我们暂时只考虑GET,POST方法。

  1. @HttpApi(host = "${api.user.host}")
  2. public interface UserService{
  3. @HttpGet("/v1/user/{id}")
  4. User getUserById(Long id);
  5. @HttpPost("/v1/user/register")
  6. Boolean register(User user);
  7. }

到这里解决了Host和Url的问题,那么还有一个参数问题,比如上述代码中的Get方法。用过SpringBoot的同学都知道 @PathVariable 注解,那么这里也类似。而且方法也支持QueryString参数,所以要加一些参数注解来区分各个参数的不同位置。那么接口继续改造:

  1. @HttpApi(host = "${api.user.host}")
  2. public interface UserService{
  3. //http://host/v1/user/123
  4. @HttpGet("/v1/user/{id}")
  5. User getUserById(@Path("id")Long id); //增加 @Path 注解标明此id参数对应着路径中的{id}
  6. //http://host/v1/user/?id=123
  7. @HttpGet("/v1/user/")
  8. User getUserById(@Query("id")Long id); //增加 @Query 注解标明此id参数对应着路径中的?id=
  9. @HttpPost("/v1/user/register")
  10. Boolean register(User user);
  11. }

看完Get方法,是不是Post方法你们也有思路了呢?比如我们要支持以下几种类型的参数

  • Content-Type=application/json (@Json)
  • Content-Type=application/x-www-form-urlencoded (@Form)

当然还有例如文件上传等,这里先不做演示。在丰富一下Post接口方法:

  1. @HttpApi(host = "${api.user.host}")
  2. public interface UserService{
  3. @HttpGet("/v1/user/{id}")
  4. User getUserById(@Path("id")Long id);
  5. @HttpPost("/v1/user/register")
  6. Boolean register(@Json User user); //这里使用 @Json 和 @Form 区分参数类型
  7. }

OK,到了这里接口定义告一段落,一个很简单粗糙的版本就出来了。不过罗马也不是一天建成的,慢慢来。现在稍作总结,轮子新增了以下几个小组件:

  • HttpApi 类注解:定义通用配置,例如Host,timeout等
  • HttpGet 方法注解:定义HttpMethod,URL
  • HttpPost 方法注解:定义HttpMethod,URL
  • Path 参数注解:定义参数类型为路径参数
  • Query 参数注解:定义参数类型为QueryString参数
  • Json 参数注解:定义参数类型为application/json
  • Form 参数注解:定义参数类型为application/x-www-form-urlencoded

组件解析

现在客户端的接口已经定义好了,剩下我们要做的就是去解析它,并且将解析结果存起来供后续使用。什么时候取做解析呢?在前文中我们定义了HttpApiFactoryBean,下面我们也实现InitializingBean接口,然后在 afterPropertiesSet 方法中去解析。

在Mybatis中有一个贯穿全文的配置类:Configuration,这里我们也参照该模式,新建一个Configuration配置类。里面大概有哪些东东呢?

  • HttpConfig 当前接口服务的基础配置,存储解析后的host,超时时间,其他全局可用的配置信息等
  • Map<String,Object> 存放每个方法对应的接口定义细节,由于一个接口存在多个方法,这里就用Map存储
  • HttpApiRegistry 它负责注册接口和提供接口的动态代理实现

OK,那么下一步我们就是要看看afterPropertiesSet方法做了什么事情。

  1. @Override
  2. public void afterPropertiesSet() throws Exception {
  3. configuration.addHttpApi(this.httpApiInterface);
  4. }

在Configuration中,又调用了HttpApiRegistry的add方法:

  1. public final void addHttpApi(Class<?> type) {
  2. this.httpApiRegistry.add(type);
  3. }

这里可以看到关键参数是Class<?> type,对应我们的接口定义就是UserService.class。为什么要用Class呢?因为接下来我们要使用大量的反射方法去解析这个接口。

由于解析细节比较多,这里不再详细介绍,有兴趣的同学可以去看一下MyBatis解析Mapper的源码,我的灵感也是基于该源码做的实现。

这里我就跳过解析细节,给大家看一下解析的一个结果

  • knownHttpApis 保存了动态代理类缓存信息
  • httpApiStatements 对应着每个方法,从下图中可以看出包含HttpMethod,URL,参数,返回值等信息
  • methodParameters 是参数集合,每个参数包含参数名,参数类型,和一些其他Http的属性等

那么有了这些东西我们能干什么呢?我们回到HttpApiProxy 的 invoke 方法。

  1. @Override
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  3. //....其他代码
  4. //先获取到唯一ID 例如:com.demo.api.UserService.getUserById
  5. String id = this.mapperInterface.getName() + "." + method.getName();
  6. //执行HTTP请求
  7. return HttpExecutors.execute(id,configuration,args);
  8. }

这里要注意,如果接口定义的是重载方法,比如getUserById(Long id). getUserById(Long id1,Long id2);

很抱歉,直接扔给你一个异常,告诉你不允许这么定义,否则id就冲突了!就是这么简单粗暴。

HttpExecutors.execute(id,configuration,args) 方法流程图如下:

之所以后边的HttpClient实现没有详细介绍,因为这里的选择有很多,例如okhttp,httpClient,java原生的httpConnection等。

轮子跑起来

接口定义

  1. package com.demo.api;
  2. import com.xiaoju.manhattan.common.base.entity.BaseResponse;
  3. import com.xiaoju.manhattan.http.client.annotation.*;
  4. import java.util.List;
  5. @HttpApi(value = "${host}",
  6. connectTimeout = 2000,
  7. readTimeout = 2000,
  8. retryTime = 3,
  9. interceptor = "userApiServiceInterceptor",
  10. exceptionHandler = "userApiServiceErrorHandler")
  11. public interface UserApiService {
  12. /**
  13. * 根据用户ID获取用户信息
  14. */
  15. @HttpGet("/api/user/{id}")
  16. BaseResponse getByUserId(@Path("id") Long id);
  17. }

客户端

  1. @RestController
  2. @RequestMapping("/")
  3. public class TestController {
  4. @Autowired(required = false)
  5. private UserApiService userApiService;
  6. @GetMapping("user")
  7. public BaseResponse<User> getUserById() {
  8. Long id = System.currentTimeMillis();
  9. return userApiService.getByUserId(id);
  10. }
  11. }

模拟用户Http服务接口

  1. @RestController
  2. @RequestMapping("/api/user")
  3. public class DemoController {
  4. @GetMapping("{id}")
  5. public BaseResponse getUserById(@PathVariable("id") Long id) throws Exception{
  6. User user = new User();
  7. user.setName("轮子");
  8. user.setId(id);
  9. user.setAddress("博客模拟地址");
  10. return BaseResponse.build(user);
  11. }
  12. }

正常调用

  1. {
  2. "data": {
  3. "id": 1586752061978,
  4. "name": "轮子",
  5. "address": "博客模拟地址"
  6. },
  7. "errorCode": 0,
  8. "errorMsg": "ok",
  9. "success": true
  10. }

拦截器示例

  1. @Component(value = "userApiServiceInterceptor")
  2. public class UserApiServiceInterceptor implements HttpApiInterceptor {
  3. @Override
  4. public Object beforeExecute(RequestContext requestContext) {
  5. //添加通用签名请求头
  6. String signature = "1234567890";
  7. requestContext.addHeader("signature", signature);
  8. //添加通用参数
  9. requestContext.addParameter("from","blog");
  10. return null;
  11. }
  12. @Override
  13. public Object afterExecute(RequestContext requestContext) {
  14. return null;
  15. }
  16. }

服务端改造

  1. @GetMapping("{id}")
  2. public BaseResponse getUserById(HttpServletRequest request, @PathVariable("id") Long id) throws Exception {
  3. User user = new User();
  4. user.setName("轮子");
  5. user.setId(id);
  6. user.setAddress("博客模拟地址:" + request.getHeader("signature") + "|" + request.getParameter("from"));
  7. return BaseResponse.build(user);
  8. }

调用结果:

  1. {
  2. "data": {
  3. "id": 1586752450283,
  4. "name": "轮子",
  5. "address": "博客模拟地址:1234567890|blog"
  6. },
  7. "errorCode": 0,
  8. "errorMsg": "ok",
  9. "success": true
  10. }

错误处理器与拦截器原理相同,不在演示。

总结

从想法抛出到具体实现大概用了几天的时间,这个轮子到底能不能在项目中跑还是个未知数,不过我还是保持乐观态度,毕竟大量借鉴了MyBatis的源码实现,嘿嘿。

当然还有一些不足之处:

  • 类结构设计还需要改进,还有较大的优化空间,向大师们学习

  • 不支持文件上传(如何支持?你知道怎么做了吗?)

  • 不支持 HttpPut,HttpDelete (加一些扩展,很容易)

  • 不支持切换底层HttpClient实现逻辑,如果能根据当前引用包动态加载就好了,类似Slf4j的门面模式

可扩展点:

  • HttpGet可以加入缓存机制
  • 拦截器可以丰富功能
  • 异步请求支持

开发难点:

  • 由于高度的抽象和大量的泛型使用,需要对反射原理掌握的更加深入一些
  • 对Spring生态要深入理解和学习

开发领悟:

  • 不会不懂的地方就看开源框架源码,你会发现新世界

其实写一个轮子不是为了写而写,而是要有实际开发痛点,轮子造出来之后是否可以使用?是否哗众取宠华而不实?当然这些要经过实战的检验,好用不好用,开发说了算。现在已经接近尾声,希望能给大家带来一些收获!拜了个拜。

一个关于HttpClient的轮子的更多相关文章

  1. 记录一个使用HttpClient过程中的一个bug

    最近用HttpClient进行链接请求,开了多线程之后发现经常有线程hang住,查看线程dump java.lang.Thread.State: RUNNABLE at java.net.Socket ...

  2. ASP.NET Core中如何针对一个使用HttpClient对象的类编写单元测试

    原文地址: How to unit test a class that consumes an HttpClient with IHttpClientFactory in ASP.NET Core? ...

  3. 开源一个自己造的轮子:基于图的任务流引擎GraphScheduleEngine

    GraphScheduleEngine是什么: GraphScheduleEngine是一个基于DAG图的任务流引擎,不同语言编写.运行于不同机器上的模块.程序,均可以通过订阅GraphSchedul ...

  4. 记一个netcore HttpClient的坑

    异常信息 The SSL connection could not be established, see inner exception ---> AuthenticationExceptio ...

  5. 9102年了,汇总下HttpClient问题,封印一个

    如果找的是core的HttpClientFactory 出门右转. 官方写法,高并发下,TCP连接不能快速释放,导致端口占完,无法连接 Dispose 不是马上关闭tcp连接 主动关闭的一方为什么不能 ...

  6. 安装rpy2 报错<cdef source string>:23:5: before: blah1 解决办法就是直接下载一个rpy2的轮子

    win7上安装rpy2, python环境是3.6.1. 使用pip install rpy2直接安装rpy2,对应的版本时rpy2 3.0.5 报如下错误: ERROR: Complete outp ...

  7. Java的异步HttpClient

    上篇提到了高性能处理的关键是异步,而我们当中许多人依旧在使用同步模式的HttpClient访问第三方Web资源,我认为原因之一是:异步的HttpClient诞生较晚,许多人不知道:另外也可能是大多数W ...

  8. webmagic的设计机制及原理-如何开发一个Java爬虫

    之前就有网友在博客里留言,觉得webmagic的实现比较有意思,想要借此研究一下爬虫.最近终于集中精力,花了三天时间,终于写完了这篇文章.之前垂直爬虫写了一年多,webmagic框架写了一个多月,这方 ...

  9. httpclient瓶颈

    问题现象: 1.系统异常,应用拒绝访问. 2.web容器线程爆满 问题分析: 1.数据库正常 2.日志信息没有异常 问题思考: 1.应用访问量突破顶峰. 2.应用在某处存在瓶颈 发现问题: 需要了解线 ...

随机推荐

  1. 大龄IT人的新的一年

    一转眼,工作十几年了,之前由于有时要出差,孩子偶尔放回老家,有时到处找人看孩子,虽然不出差时都是有我来带,孩子还是和我很亲,但是一直没时间关注她的学习,只是睡前读读绘本,报了个英语培训班,偶尔玩玩识字 ...

  2. python code practice(二):KMP算法、二分搜索的实现、哈希表

    1.替换空格 题目描述:请实现一个函数,将一个字符串中的每个空格替换成“%20”.例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy. 分析: 将长度为 ...

  3. 五分钟了解Semaphore

    一.前言 多个线程之间的同步,我们会用到Semaphore,翻译成中文就是信号量.使用Semaphore可以限制多个线程对同一资源的访问.我们先看下C#中对Semaphore的定义,如下图: 翻译成中 ...

  4. 认识Oracle数据库系统--详细解说

    1.3 认识Oracle数据库系统 Oracle数据库是美国Oracle公司的一款关系型数据库管理系统,简称为Oracle RDBMS,是目前数据库市场上最为强大和流行的数据库系统之一.Oracle是 ...

  5. 【简说Python WEB】pyechart在flask中的应用

    个人笔记总结,可读性不高.只为自己总结用.怕日后忘记. 这里用到了tushare,pandas等python组件. pyechart的案例 c = ( Bar() .add_xaxis([" ...

  6. Java单例设计模式的实现

    1. 单例设计模式的定义 单例设计模式确保类只有一个实例对象,类本身负责创建自己的对象并向整个系统提供这个实例.在访问这个对象的时候,访问者可以直接获取到这个唯一对象而不必由访问者进行实例化. 单例设 ...

  7. F-NAScan:一款网络资产扫描工具

    此脚本的大概流程为: ICMP存活探测-->端口开放探测-->端口指纹服务识别-->提取快照(若为WEB)-->生成结果报表 用法 python NAScan.py -h 10 ...

  8. 为什么我推荐你用Ubuntu开发?

    前言: 鱼哥在做多媒体开发时,领导倒逼我们用Ubuntu开发,后来才发现它的牛逼和高效.所以对于还在用Windows上开发的朋友,鱼哥建议,趁周末,搞个双系统,切到Ubuntu上开发, Ubuntu最 ...

  9. MySQL学习(5)

    三 触发器 对某个表进行某种操作(如:增删改查),希望触发某个动作,可以使用触发器. 1.创建触发器 create trigger trigger1_before_insert_tb1 before ...

  10. [尊老爱幼] Queen

    You are given a rooted tree with vertices numerated from 1 to n . A tree is a connected graph withou ...