如何批量下载YouTube视频

需求

YouTube是世界最大的视频分享网站,从上边我们学到各种各样的知识,有时候我们可能会有批量从下边下载视频的需求。这就要用到我今天给大家推荐的工具youtube-dl,这个工具在github上star已经到了5w+,名字虽然叫youtube_dl,但是却可以抓取大多数网站的视频,包括B站、优酷、P站,甚至QQ音乐都可以下载。从suppportedsites可以看到所有的支持列表。下面我们以YouTube为例来介绍。

准备工作

下载视频,除了一个好的网速,我们还需要能够访问到视频(废话)。如果你浏览器输入youtube.com,提示无法访问此网站的话, 可以先去看另外一位同事的博客:科学上网。如果有什么问题可以在那篇博客底下留言。

第一步,安装

Mac

sudo -H pip install --upgrade youtube-dl

linux

sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
sudo chmod a+rx /usr/local/bin/youtube-dl

使用方式

我们以批量下载ted的视频为例,

youtube-dl --merge-output-format mp4 -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' --proxy socks5://127.0.0.1:6153 https://www.youtube.com/watch?v=qAC-5hTK-4c&list=UUAuUUnT6oDeKwE6v1NGQxug


-f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best'  // 以质量最好的视频和音频下载

--merge-output-format  mp4 // 将视频和音频合并成mp4格式,需要用的ffmpeg的音频处理库

--proxy socks5://127.0.0.1:6153  // 命令行可能需要设置代理

通过上边的命令,我们就可以下载列表中的所有视频啦。我们还可以添加 –playlist-start 参数,来指定从第几个视频下载,这样中途出错还可以继续下载。

其他

另外推荐几个比较好的youtube频道

  • Linus Tech Tips 非常火的科技博主
  • TED TED演讲,Ideas worth spreading
  • JSConf JS Conferences,全是前端干货
  • The Economist 经济学人,非常好的国际新闻来源,不止局限于经济。

使用Android Studio Lint静态分析(一)

主要内容

使用Android Studio提供的工具,配置Lint扫描范围和检查项。在使用 Lint 改进您的代码文档中,属于手动运行检查

程序静态分析

程序静态分析是指在不运行代码的方式下,通过词法分析、语法分析、控制流、数据流分析等技术对程序代码进行扫描,验证代码是否满足规范性、安全性、可靠性、可维护性等指标的一种代码分析技术。

Java-Android代码常用的分析工具

  • Checkstyle
  • FindBugs
  • Soot
  • Lint

使用 Lint 改进您的代码

Lint是Android Studio提供的 代码扫描工具,自动化地对代码进行扫描,帮助改善代码结构的规范性和可维护性,提高代码质量。

Lint静态扫描的工作原理如下图。

Android Studio中Lint的操作步骤

Android Stuido Lint提供了 Specify Inspection Scope 面板,在面板中对代码扫描任务做个性化定义。

打开Specify Inspection Scope面板:菜单栏->Analyze->Inspect Code

设定扫描范围Custom Scope

  • 在面板中可以配置Lint扫描的范围,其中File选项会根据面板弹出前光标所在位置显示三种状态:
    • Files:在打开的文件上调出面板,可以对当前打开的文件进行扫描;
    • Directory:在Project面板中选中名录调出面板,对选定的目录进行扫描;
    • 隐藏:不提供此选项
  • 个性化扫描范围设定
    • 标题右侧的下拉框可以选择已有的自定义扫描配置
    • 下拉框右侧的“…”按钮,可以调起自定义扫描配置面板,可以任意的为一项配置添加和删除待扫描的源文件。左侧一列是自定义的配置列表,他们将出现在上一层的下拉框内备选。
    • 设定完成之后,生成对应的配置文件放在.idea/scopes目录下

检查项设置

Specify Inspection Scope 面板上最后一项Inspection profile,指定在代码扫描过程中对哪些问题进行检查。系统默认只提供一个Default可选项,我们可以点击下拉框右边的“…”按钮添加个性化的可选项。

  • 如下图,我们需要先把Default配置项Copy to Project,改个可爱的名字后就可以在新产生的配置项上进行编辑了。

第一步可以点击橡皮擦(图中被弹出框盖住了),把从Default中复制下来的选项全部清除,然后根据需要选择合适的集合即可。

其中Android、General、XML和Spelling是必选项,如果工程中只有Java代码,就只需要勾选Java一项就可以了,否则可能还需要选上kotlin、flutter、dart等相关的选项。

另外,上面说的那些选项,下面还有许多的子选项,并不是都必须的,可以根据需要选择部分子选项进行检查。

设定完成之后,生成对应的配置文件存放在.idea/inspectionProfiles目录下

  • 问题警告级别

选中检查项设置某一个选项,右侧会给出对应的描述和安全提醒级别。Android Studio Lint提供了六级安全提示,每种级别对应着不同的外观,重要程度不同,醒目的程度也不一样。

设置了所需的选项之后,点击右下角的“OK”,就可以使用这个设置集合了。

执行Lint静态扫描&查看结果

扫描范围和检查项设置完成之后,就可以执行静态扫描了。点击Specify Inspection Scope面板右下角的“OK”按钮,就开始执行扫描操作了。扫描完成之后,Android Studio底部会弹出Inspection Results面板, 可以根据安全级别或者检查项进行归类,到这里我们就可以根据扫描的结果对代码进行相应的整理和修改了。

参考文章

自动静态方法的剖析

自动静态方法,主要有以下三个特点:
– 相比于编译器,可以做到对代码更加严格、个性化的检查
– 不真正检测代码的逻辑功能,只是站在代码本身的视角,基于规则,尽可能多地去发现代码错误
– 由于静态分析算法并不实际执行代码,完全是基于代码的词法分析、语法分析、控制流分析等技术,由于分析技术的局限性以及代码写法的多样性,所以会存在一定的误报率

基于这些特点,自动静态方法通常能够以极低的成本发现以下问题:
– 使用未初始化的变量
– 变量在使用前未定义
– 变量声明了但未使用
– 变量类型不匹配
– 部分的内存泄漏问题
– 空指针引用
– 缓冲区溢出
– 数组越界
– 不可达的僵尸代码
– 过高的代码复杂度
– 死循环
– 大量的重复代码块

正是由于自动静态方法具有自动化程度高,检查发现问题的成本低以及能够发现的代码问题广等特点,所以该方法被很多企业和项目广泛应用于前期代码质量控制和代码质量度量。

在实际工程实践中,企业往往会结合自己的编码规范定制规程库,并与本地IDE开发环境和持续集成的流水线进行高度整合。

代码本地开发阶段,IDE环境就可以自动对代码实现自动静态检查;当代码递交到代码仓库后,CI/CD流水线也会自动触发代码静态检查,如果检测到潜在错误,就会自动邮件通知代码递交者。

GraphQL 浅析

官方定义:GraphQL 是一种可以与任何后端服务相关联的查询语言以及对应的执行引擎。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,这也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

GraphQL 能做什么?有什么优势

GraphQL 可以让服务调用者通过发送 GraphQL 查询语句(get 或 post 请求),来定制服务 API 接口的返回内容,而不会产生冗余数据。同时,多个数据查询请求可以被组合成一条 GraphQL 查询语句,最终只发送一次请求就可以获取页面需要的所有数据。

相比于 Restful 的优势:

  • 定制接口的返回内容,不产生冗余数据

  • 合并多个查询请求,减少请求次数

  • 通过上述两个特性,可以大幅减少接口访问所产生的流量

  • GraphQL 的接口支持嵌套(类似于数据库关系查询),可以方便的进行数据的聚合操作

GraphQL 基本概念

GraphQL type system 是在 GraphQL Schema 文件中定义的类型系统,它描述了返回的对象的类型与结构,是 GraphQL 实现的核心组成部分。

以博客系统为例,基础的 type 有 User 和 Post,分别代表用户和帖子。那么对于 User,一般会有一个字段叫 name,Post 可能会有两个字段 title 和 content。那么对于这样一个场景,使用 GraphQL Schema 的语法来描述它:

# 用户信息
type User {
    name: String
}

# 帖子信息
type Post {
    title: String
    content: String
}

通过这种方式我们可以描述类型系统的基本结构,同时它会在后台数据和 type system 之间建立映射关系。例如,我们调用了某个后台服务接口后返回了JSON数据 {“name”: “June”},我们指定了使用 Type User 来解析该数据,那么对应的数据和 Type 之间会建立映射关系,当数据不满足指定的格式时,如 {“title”: “Hello World!”},GraphQL 会提示报错信息。

上述 Schema 还可以利用相关特性继续扩展:

# 用户信息
type User {
    id: String!              # !表示该字段不能为空
    name: String
    posts: [Post]            # 可以通过 [Type] 来定义元素类型为某种 Type 的数组
    role: Role               # 可以使用枚举 enum
}

# 帖子信息
type Post {
    id: String!
    title: String
    content: String
    auther: User             # 可以通过 Type 来指定字段类型为某种 Type
}

# 枚举
enum Role { customer, master, admin }

至此,我们要完成这个 Schema type 定义文件的内容还差最后一步:定义进入 type system 的切入点。

我们需要定义一个 type,它是所有 query 查询的基础。该 type 的名称一般为 Query,它描述了我们公共的顶级 API。对应于上述场景,它的定义可能是这样的:

type Query {
    me: User
    user(id: String!): User
    post(id: String!): Post
}

上述代码中,我们描述了我们的三个顶级的 API 操作:

  • me:返回当前登录的用户的信息

  • user:根据传入的 id 参数,查询该 id 对应的用户的信息

  • post:根据传入的 id 参数,查询该 id 对应的帖子信息

上述代码也展示了 type system 的另一个功能,可以为字段配置参数来指定其行为。

当我们将整个 type system 打包到一起时,将 Query 上的 type 定义为查询的入口点,就创建了一个 GraphQL Schema。

更多

请查看相关文档

GraphQL Demo

此处,我们将按照上面的数据模型,来一步步构造一个 GraphQL Demo 项目,帮助开发者了解如何使用 GraphQL 技术来开发项目。

首先创建一个文件夹,并在其内部执行 npm init,初始化一个 node 项目:

mkdir graphql-demo
cd graphql-demo
npm init

安装 graphql-yoga 库,该库是一个 GraphQL server 库(基于 express-graphql),支持非常多的特性,此处重点讲述如何使用它起 demo 项目,就不作赘述。

npm install graphql-yoga -S

创建 src 文件夹,并在其内部新建我们的服务器代码入口 js 文件 index.js,将以下代码复制到 index.js 中:

const { GraphQLServer } = require('graphql-yoga');

const typeDefs = `
type Query {
    me: User
    user(id: String!): User
    post(id: String!): Post
}

type User {
    id: String!
    name: String
    posts: [Post]
    role: Role
}

type Post {
    id: String!
    title: String
    content: String
    auther: User
}

enum Role { customer, master, admin }
`;

const usersList = [
    {
        id: '1',
        name: 'June',
        role: 'admin'
    },
    {
        id: '2',
        name: 'Jim',
        role: 'customer'
    }
];

const postsList = [
    {
        id: "1001",
        title: "Hello World!",
        content: "Hello world, June~"
    },
    {
        id: "1002",
        title: "GraphQL 入门",
        content: "GraphQL 入门知识点"
    }
];

const resolvers = {
    Query: {
        me: (_, args) => Object.assign(usersList[0], {posts: postsList}),

        user: (_, { id }) => usersList.find(item => item.id === id) || null,

        post: (_, { id }) => postsList.find(item => item.id === id),

    },
};

const server = new GraphQLServer({
    typeDefs,
    resolvers
});

server.start(() => console.log('Server is running on localhost:4000'));

在这段代码中:

  • typeDefs 变量对应了上述内容中的 Schema 定义,内部定义了 type Query\User\Post,在 Query 中定义了 me、user、post 三个顶级接口

  • usersList 以及 postsList 变量保存了我们的应用数据,此处我们使用了写死的数据,实际开发的时候,这些数据往往是从数据库中读取的

  • resolvers 变量中,我们定义了 Query 中三个接口的解析器函数,用于处理这些接口调用的业务逻辑与返回值

  • 然后我们将 typeDefs 变量和 resolvers 变量传入到 GraphQLServer 实例中,并在 4000 端口启动对应的 Server 以及 GraphQL Playground 面板

执行 node ./src/index.js 命令,便可启动对应的 node GraphQL 服务器,然后在浏览器中打开 localhost:4000 页面,会显示 GraphQL Playground 面板,在此处便可以测试前文中叙述的各类 query 请求:

Playground 面板中可以做如下操作:

  • 在左侧的 Query 面板中执行查询语句,点击中间的运行按钮执行查询,在右侧的结果栏中获得得到的结果数据

  • 点击右侧的 SCHEMA 绿色按钮,可以查看对应的后台服务的接口以及 type 类型信息,调试非常方便

  • 下方的 QUERY VARIABLES,可以向 query 的传参,具体用法见上文

  • 下发的 HTTP HEADERS 可以向查询请求的 headers 中添加相应的属性,格式为 JSON 格式

  • 左侧的 Query 面板中输入查询语句时,会作格式校验,若不符合 GraphQL 规范,则会有红线提示

什么是PWA及实现

PWA:Progressive web application 渐进式web应用

PWA 是什么?

Progressive Web App (下文以“PWA”代指) 是一个令人兴奋的前端技术的革新。PWA综合了一系列技术使你的 web app表现得就像是 native mobile app。相比于纯 web 解决方案和纯 native 解决方案,PWA对于开发者和用户我觉得主要就是以下面个优点:

1.首先PWA依然是一个网页开发,所以开发速度依然很快,可以快速响应需求

2.PWA 可以离线访问,并且在网络恢复时可以同步最新数据(这个功能并不是PWA自带的,需要你在代码中实现相关逻辑,比如保存请求状态,检测网络状态等)。这点我觉得是最重要的,其他的都是扯淡,PWA 提供了一个控制缓存的新接口,让前端第一次真正有能力自由支配该缓存哪些静态内容,甚至可以缓存接口资源,这个比较屌,相当于在本地起了一个服务。在一些工具类的应用场景中可以在没有网络的情况下正常使用了,这就突破了传统网页的开发限制了。

PWA 技术目前被 Firefox,Chrome 和其他基于Blink内核的浏览器支持。微软正在努力在Edge浏览器上实现。https://www.caniuse.com/#search=servicework, 看caniuse支持情况还是可以的,已经有相当一部分浏览器已经支持PWA的新特性了

但即使有一部分浏览器不支持PWA, 但依然不影响我们将网站改造成PWA,因为它是渐进式的,就算离线访问不能实现,但其他功能都像原来一样没有影响。

如果实现一个简单的PWA

PWA的核心是service-worker(SW), 任何PWA都有一个service-worker.js(sw.js)文件, 目前大部分前端项目都深度集成了webpack, 在webpack打包项目中使用service-worker会有以下两个比较严重的问题
1.webpack生成的资源一般都会带有一串hash, sw的资源列表里面需要同步更新这些带hash的资源
2、每次更新代码,都需要通过更新sw文件版本号来通知客户端对所缓存的资源进行更新。(其实只要这一次的sw代码和上一次的sw代码不一样即可触发更新,但使用明确的版本号会更加合适)。

所幸现在webpack社区已经有人做了这件事,除了官方推荐的sw-precache-webpack-plugin, 还有一个就是我现在用的offline-plugin, 相比sw-precache-webpack-plugin,offline-plugin可能会有一下优点

1.更多的可选配置项,满足更加细致的配置要求;
2.更为详细的文档和例子;
3.更新频率相对更高,star数更多;
4.自动处理生命周期,用户无需纠结生命周期的坑;
5.支持AppCache;

基本使用

*** 安装 ***

npm install offline-plugin [--save-dev]

*** 使用步骤 ***
第一步:在webpack中使用
webpack-dev-server是将依赖的资源存入内存,不能跟SW搭配使用,只有在打完包之后使用,我是在本地打完包再在dist文件夹起一个server服务来测试SW有没有安装成功的

// webpack.prod.js
const OfflinePlugin = require('offline-plugin')
module.exports = {
  ...,
  plugins: [
    new OfflinePlugin({
      responseStrategy: 'cache-first',
      AppCache: false,
      safeToUseOptionalCaches: true,
      autoUpdate: true,
      caches: {
        main: [
          '**/*.js',
          '**/*.css',
          /\.(png|jpe?g|gif|svg)(\?.*)?$/,
          '/'
        ],
        additional: [
        ]
      },
      externals: [],
      excludes: ['**/.*', '**/*.map', '**/*.gz', '**/manifest-last.json'],
      ServiceWorker: {
        output: './sw.js',
        publicPath: `./sw.js`,
        scope: '/',
        minify: true,
        events: true
      }
    })
  ],
  ...
}

第二步:将runtime添加到项目入口文件中

require('offline-plugin/runtime').install()

// ES6/Babel/TypeScript
import * as OfflinePluginRuntime from 'offline-plugin/runtime'
OfflinePluginRuntime.install()

经过上面的步骤,offline-plugin已经集成到项目之中,直接使用webpack构建即可,但是这样还不可以,你肯定需要一定的自定义配置

配置

offline-plugin 本身提供了丰富的参数选项,下面我会说下我在项目中用到的
首先是在webpack中使用

new OfflinePlugin({
  responseStrategy: 'cache-first', // 这句话表示缓存优先
  AppCache: false, // 是否使用AppCache, 虽然AppCache已经被W3C废弃,但是依然有部分浏览器支持,所以offline-plugin默认支持AppCache
  safeToUseOptionalCaches: true, // Removes warning for about `additional` section usage
  autoUpdate: true, // 自动更新
  caches: { // webpack 打包正则匹配
    main: [
      '**/*.js',
      '**/*.css',
      /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      '/'
    ],
    additional: [],
    optional: []
  },
  externals: [], // 需要缓存的外部链接,例如配置http://hello.com/getuser,那么在请求这个接口的时候就会进行接口缓存
  excludes: ['**/.*', '**/*.map', '**/*.gz', '**/manifest-last.json'], // 需要过滤的文件
  ServiceWorker: {
    output: './sw.js', // 输出目录
    publicPath: './sw.js', // sw.js加载路劲
    scope: '/', // 作用域
    minify: true, // 开启压缩
    events: true // 当sw状态改变时候发射对应事件
  }
})

着重介绍main 跟 additional字段表示的意义
main: [] 这里配置的是serviceWorker在install阶段需要缓存的文件清单,如果其中有一个失败了,那么整个serviceWorder就会安装失败,所以必须谨慎配置
additional: [] 这里配置的文件清单会在serviceWorker activate的时候进行缓存,与main不一样,如果这里的文件缓存失败,不会影响serviceWorker的正常安装。而且,在接下来页面的ajax异步请求中,还能进行缓存尝试
optional: [] 这里配置的文件清单在serviceWorker安装激活阶段不会进行缓存,只有在监听到网络请求的时候才进行缓存。
刚开始写的时候将静态资源放在additional中进行缓存,总是不能离线访问,直到我把静态资源放到main中缓存之后,居然就可以实现离线访问了,具体原因我也是不太清楚
还要一个需要注意的坑是,有些网络教程是scope设置成/static/sw.js, 最后在网页中会提示,sw最大的作用域权限在/static下面,言外之意这么写是无法将sw的作用域设置在/根路径下面。那就不能缓存html文件了, 然后还整了一堆有的没有的解决办法,耽误挺长时间的,其实直接将sw.js打到根目录,然后设置scope为/就行了。

在入口文件中使用

OfflinePluginRuntime.install({
  // 监听sw事件,当更新ready的时候,调用applyUpdate以跳过等待,新的sw立即接替老的sw
  onUpdateReady: () => {
    console.log('SW Event:', 'onUpdateReady')
    OfflinePluginRuntime.applyUpdate()
  },
  onUpdated: () => {
    console.log('SW Event:', 'onUpdated')
    window.swUpdate = true
  }
})

降级处理

if ('serviceWorker' in navigator) {
  console.log('Start trying Registrating Scope')
  navigator.serviceWorker.register('sw.js', { scope: '/' })
    .then((reg) => {
      // registration worked
      console.log('Registration succeeded. Scope is ' + reg.scope)
    })
    .catch((error) => {
      // registration failed
      console.log('Registration failed with ' + error)
    })
}

PWA其实跟manifest.json没有关系,有它还是没他都不影响service-worker的工作,它的作用告诉浏览器当把页面发送到主屏幕上时该怎么显示: 应用名称 应用图标 启动页面….
还有manifest.json在苹果浏览器中并不能正常工作,但是可以用下面这个设置模拟实现

<!-- 应用图标: -->

<!-- 启动画面: -->

<!-- 应用名称: -->

<!-- 全屏效果: -->

<!-- 设置状态栏颜色: -->

这里推荐一个基于 Vue.js 的 PWA 解决方案LAVAS, 但是这个我没用过,看是百度出品应该不会太差,用过上面那个插件,demo

如果各位还想继续深入了解PWA内部实现原理可以参考下借助Service Worker和cacheStorage缓存及离线开发CacheStorage Api

最后说点题外话,因为PWA是渐进性的,就算有一部分浏览器不支持,也不影响其正常使用,综合考虑其开发成本和实现之后节省的流量费用,个人觉得还是值得一试的。

Find Enjoy On the web

The real key to finding the right person for marriage can be through an online dating services. For that girl who have not discovered the appropriate http://www.bridesbouquet.net lover, a web dating service will help your ex to have that specialized man or woman. There are several online dating sites solutions which you can give consideration to from a various internet dating businesses, that may let you find out in addition to listen to a multitude of women each day.

Online dating services provides you with a chance to experiment with some women create judgements about your new your life along. Once meeting with women of all ages, first thing you need to carry out is normally make a personality account that you will present to potential goes. A great idea is usually to communicate gradually together with evidently while you are producing little discuss and witness the tendencies, so that you realize several stuff that will help you decide the proper fit.

Friends and family can be quite a method to obtain assist in figuring out which in turn girls you may want to night out. You are able to engage in a game referred to as “Who Daters” and see when you have fulfilled any kind of women that you would like to meet or perhaps get married to. You can participate in an identical online game online and select the women that you’ll be the majority of interested in. You may also try to enjoy a new dating game to begin simply by requesting his or her telephone number and then function to you way up following that.

Online dating services lets you spend some time alongside one another without needing to be seen in the street. Because you usually are not literally reaching your partner, you are able to spend time understanding each other far better via e-mail or perhaps instant messaging. This really is one of the most effective ways to discover a lover you desire to invest your daily life with.

If you are searching for lonely hearts you need to match, you might wonder exactly how you are able to say to the good ones from the poor kinds. It is possible to use the Internet to master about every one of the ladies who currently have responded to your personal ad. You can read the particular opinions the fact that the ladies have written about all of them. Study a number of the message board discussions and learn some other ladies’ discussions which you believe you may want to talk to.

Special occasions also are important. If you are looking for a woman to look at a person out on to start a date for your wedding anniversary, you can examine your current calendar to have an future day or maybe a school flow or even graduating party. If you are a little burned out and want to require a minor crack coming from a very long day at work, a web based online dating service can provide you with time aside that are needed.

Whatever you are looking for, keep in mind that you ought not get sketched in just too many females to meet the right one. Rather, you must really have a thing that you both love carrying out.

跨域的几种常规解决方案

基本上现在大部分互联网公司都采用前后端分离的方式进行开发,这样不可避免的会出现跨域问题,趁着今天不忙,写个blog,总结下自己对跨域的理解以及一些常规解决方案,欢迎补充

跨域出现原因

浏览器同源策略,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS(跨站脚本攻击) CSRF(跨站请求伪造)等攻击。所谓同源指的是”协议 + 域名 + 端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
同源策略限制的内容有:
1.cookie、localStorage、IndexDB等存储性内容
2.DOM节点
3.Ajax请求发送后,结果被浏览器拦截了
但是有三个标签是允许跨域加载资源的

<img src="" />
<link href="" />
<script src=""></script>

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。为了保证用户信息的安全,防止恶意的网站窃取数据。
协议 域名 端口 只要这三者有一个不同就会涉及跨域问题

有两点值得注意
*** 1.如果是协议和端口造成的跨域问题,前台是无能为力的(或者说仅凭前端是无能为力的) ***
*** 2.在跨域问题上,仅仅是通过URL的首部来识别而不会根据域名对应的IP地址是否相同来判断。URL首部可以理解为:协议、域名和端口必须相同 ***

请求跨域了,那么请求到底发出去没有?
*** 跨域并不是请求发布出去,请求能发出去,服务端能收到请求且能正常返回结果,只是结果被浏览器拦截了 ***
那为什么通过表单的形式可以发起跨域请求,为什么ajax不行,因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,ajax可以获取响应,但是表单不会获取新的内容,所以可以发起跨域请求,同时也说明了跨域并不能完全阻止CSRF,因为请求是可以发出去的

常用的解决方法

1.jsonp

这应该是我接触到的第一个解决跨域的方法
原理是利用script标签的开放策略,实现JS的跨域请求,但是这需要服务端的配合
下面以$.ajax方法为例

$.ajax({
  url: 'http://****/test',
  type: 'GET',
  data: {
    'action': 'abc'
  },
  dataType: 'jsonp',
  jsonp: 'callback'
})

在jquery的内部,请求会被转化成http://****/test?callback=jQuery2030038573939353227615_1402643146875&action=abc 然后动态加载

<script type="text/javascript"src="http://****/test?callback=jQuery2030038573939353227615_1402643146875&action=abc"></script>

然后后端就会执行callback(传递参数 ),把数据通过实参的形式发送出去。

使用JSONP 模式来请求数据的整个流程:客户端发送一个请求,规定一个可执行的函数名(这里就是 jQuery做了封装的处理,自动帮你生成回调函数并把数据取出来供success属性方法来调用,而不是传递的一个回调句柄),服务器端接受了这个 callback函数名,然后把数据通过实参的形式发送出去

在jquery 源码中, jsonp的实现方式是动态添加*** script 标签来调用服务器提供的 js脚本。jquery 会在window对象中加载一个全局的函数,当 script 代码插入时函数执行,执行完毕后就 script ***会被移除。

这种解决跨域的方法现在基本没用了,前后耦合高且写法费劲,只能发送get请求,我可能更多的使用下面的几个方法

2.CORS

跨域资源共享(Cross-Origin Resource Sharing,简称 CORS),是 HTML5 提供的标准跨域解决方案。
CORS 需要浏览器跟后端同时支持,IE8/9可以通过XDomainRequest实现
不过使用CORS来解决跨域问题需要服务端支持,然后通过设置Access-Control-Allow-Origin来解决跨域问题。客户端可以不做处理,跟普通的请求一样就可以
下面以koa为例

const { NODE_ENV } = process.env
const Koa = require('koa')
const cors = require('kcors')
const logger = require('koa-logger')
const bodyParser = require('koa-bodyparser')
const jwt = require('koa-jwt')
const config = require('./config')[NODE_ENV]
const router = require('./routes')
const errorHandle = require('./middlewares/errorHandle')

const { secretKey } = config

const app = new Koa()
app.use(cors()) // 实现服务端CORS
app.use(logger())
app.use(bodyParser({
  onerror: function (err, ctx) {
    ctx.throw('body parse error', 422)
  }
}))

app.use(errorHandle)

app.use(jwt({ secret: secretKey })
  .unless({
    path: [/\/register/, /\/login/, /\/forgetPassWord/, /\/downExcel/]
  }))

app.use(router.routes())

const { port } = config

app.listen(port)

console.log('app started on port ' + port)

module.exports = app

使用了一个kcors的中间件来实现服务端跨域资源共享,

3.nginx 反向代理

这也需要服务端配合
下面还是用一段Ngxin配置来说明这个问题

server {
  listen 80;
  server_name www.domain1.com;
  location / {
    proxy_pass http://www.domain2.com:8080; #反向代理
    proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
    index index.html index.htm;
    # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
    add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
    add_header Access-Control-Allow-Credentials true;
  }
}

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

4.WebSocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

5.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
1)页面和其打开的新窗口的数据传递
2)多窗口之间消息传递
3)页面与嵌套的iframe消息传递
4)上面三个场景的跨域数据传递

*** postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。 ***
具体的实现方法可以查看官方文档

6.Node中间件代理(两次跨域)

实现原理:*** 同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。 ***
代理服务器,需要做以下几个步骤:
1) 接受客户端请求 。
2) 将请求 转发给服务器。
3) 拿到服务器 响应 数据。
4) 将 响应 转发给客户端。

// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
  $.ajax({
    url: 'http://localhost:3000',
    type: 'post',
    data: { name: 'xiamen', password: '123456' },
    contentType: 'application/json;charset=utf-8',
    success: function(result) {
      console.log(result) // {"title":"fontend","password":"123456"}
    },
    error: function(msg) {
      console.log(msg)
    }
  })
</script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
  // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:将请求转发给服务器
  const proxyRequest = http
    .request(
      {
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
      },
      serverResponse => {
        // 第三步:收到服务器的响应
        var body = ''
        serverResponse.on('data', chunk => {
          body += chunk
        })
        serverResponse.on('end', () => {
          console.log('The data is ' + body)
          // 第四步:将响应结果转发给浏览器
          response.end(body)
        })
      }
    )
    .end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})

7.window.name + iframe

这个方法比较讨巧
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

<!-- a.html(http://localhost:3000/b.html) -->
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
  let first = true
  // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
  function load() {
    if(first){
      // 第1次onload(跨域页)成功后,切换到同域代理页面
      let iframe = document.getElementById('iframe');
      iframe.src = 'http://localhost:3000/b.html';
      first = false;
    }else{
      // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
      console.log(iframe.contentWindow.name);
    }
  }
</script>

b.html为中间代理页,与a.html同域,内容为空。

 <!-- c.html(http://localhost:4000/c.html) -->
<script>
  window.name = '我不爱你'  
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

location.hash + iframe

实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。
同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

<!-- a.html -->
<iframe src="http://localhost:4000/c.html#iloveyou"></iframe>
<script>
  window.onhashchange = function () { //检测hash的变化
    console.log(location.hash);
  }
</script>
<!-- b.html -->
<script>
  window.parent.parent.location.hash = location.hash 
  //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
</script>
// c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#idontloveyou';
document.body.appendChild(iframe);

document.domain + iframe

*** 该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 ***
只需要给页面添加 document.domain =’test.com’ 表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值

<!-- a.html -->
<body>
  hello
  <iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'zf1.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>
<!-- b.html -->
<body>
   hello
   <script>
     document.domain = 'zf1.cn'
     var a = 100;
   </script>
</body>

SVG学习总结

SVG(Scalable Vector Graphics)可缩放矢量图 是一种使用 XML 描述 2D 图形的语言。
SVG 基于 XML,这意味着 SVG DOM 中的每个元素都是可用的。您可以为某个元素附加 JavaScript 事件处理器。
在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。

svg的一些优势:

  • SVG 可被非常多的工具读取和修改(比如记事本)
  • SVG 与 JPEG 和 GIF 图像比起来,尺寸更小,且可压缩性更强。
  • SVG 是可伸缩的
  • SVG 图像可在任何的分辨率下被高质量地打印
  • SVG 可在图像质量不下降的情况下被放大
  • SVG 图像中的文本是可选的,同时也是可搜索的(很适合制作地图)
  • SVG 可以与 Java 技术一起运行
  • SVG 是开放的标准
  • SVG 文件是纯粹的 XML

svg可通过,,标签引入html中。

图形绘制中的一些tips

  • 显示层次由标签自上至下顺序指定
  • stroke-dasharray: 虚线定义 显示隐藏像素数循环
  • rect的弧度 由rx,ry指定 如果存在一个值未定义 则默和已存在的值相等。
  • X1,y1 x2,y2作为点位置的书写方式

SVG通过视窗、视野控制svg的显示

  • 视窗由svg的width,height控制 视野有viewBox属性控制
  • 视窗是html引入svg的显示大小。视野是svg上会被显示的的区域。先通过视野获取显示内容,再缩放适配到视窗。
  • 默认视窗的大小是300px、150px
  • viewBox 注意B大写。否则无效
  • 如果不指定视窗尺寸 但指定viewBox视野 则默认填充
  • 视窗,视野都指定并且成比例。则经过缩放直接适配。如果二者不等比例。则需要preserveAspectRatio属性来确定填充策略。
    preserveAspectRatio
第二个参数 描述
meet(默认) 保持长宽比,整个viewBox在viewport中可见; 图形往往缩小
slice 保持长宽比,viewBox覆盖viewport的全部区域; 图形往往放大
none 不在保持长宽比,直接拉伸缩放,但如果第一个参数存在则无效

当第二个参数选择meet/slice时 就会出现某个方向未被填满,或者溢出的情况需要设置x,y方向的对齐策略

| 第一个参数 | 描述 |
| :—-: | :——-:|
| xMin/xMid/xMax |x方向左对齐,居中对齐,右对齐 |
| YMin/YMid/YMax(Y大写) | y方向左对齐,居中对齐,右对齐 |
两行组合在一起x在前,y在后组成第一个参数
meet,slice,x,y对齐效果演示
黑色矩形为视野,虚线矩形为视窗

渐变

线性渐变
<defs>
    <linearGradient id="orange_red" x1="0%" y1="0%" x2="100%" y2="0%">
        <stop offset="0%" style="stop-color:rgb(255,255,0);stop-opacity:1"/>
        <stop offset="100%" style="stop-color:rgb(255,0,0);stop-opacity:1"/>
    </linearGradient>
</defs>
<ellipse cx="200" cy="190" rx="85" ry="55"
style="fill:url(#orange_red)"/>
</svg>
  • 渐变方式独立定义
  • x1,y1,x2,y2描述渐变方向
  • 如果offset如果出现逆序 例如:50% green,30% red 30%会变为50% 圆滑过度的绿色的另一半变成红色。并且有明显的红绿分隔线。
放射渐变

cx,cy 径向渐变的中心点X和Y坐标。它的值使用用填充的百分比值。如果没有定义则默认值为50%
fx, fy 径向渐变的焦点X和Y值。它的值使用用填充的百分比值。如果没有定义则默认值为50%。
注意:在Firefox 3.05中如果值低于5%可能会发生问题。
径向渐变的聚焦点是颜色辐射的角度。你可以将径向渐变想象为一盏灯,那么聚焦点决定灯光从什么方向“照射”在图形上。50%,50%表示在图形的正上方,
r 径向渐变的半径

变换

类似于css tranform语法 可以线性叠加

安卓自定义注解支持和示例实现

开头

编码时使用注解,可以提高编码效率、简化代码增强可读性等优点;使用注解还是代码静态扫描的一部分,促进代码规范。安卓注解使用介绍一文中介绍了JDK/SDK提供的注解和support/ButterKnife等第三方提供的注解库,还有其他的一些库,这些基本已经能够满足需求。

support/ButterKnife是应用很广的注解库,它们也是属于“自定义注解”的范畴,只是有因为使用的多了,实际上成为了一个“标准”。

本文从“造库”的角度介绍自定义注解的相关支持,并提供一个示例实现。但是,本文不提供自定义注解相关的静态检查,这需要lint的支持,本文不做介绍,希望后面的文章有机会介绍一下,这里先占个坑

第三方注解库

引入一个注解库,以ButterKnife为例:
– 添加注解库

implementation 'com.jakewharton:butterknife:8.4.0'
  • 添加注解处理器
annotationProcessor 'com.jakewharton:butterknife:8.4.0'

添加了这两个库之后,就可以使用这个注解库了。

如果是library项目】,还需要引入butterknife-gradle-plugin插件,在安卓注解使用介绍中有具体介绍。

定义注解

所有的注解都默认继承自java.lang.annotation.Annotation

定义注解时可以声明0..N个成员,例如下面的定义,可以用default为成员指定默认值;成员名称可以按照程序语言的变量命名规则任意给定,成员的类型也是有限制的。在使用时需要指定参数名:@StringAnnotation(value = “data”),当成员只有一个且命名为value时,可省略。

8中基本数据类型,String,Class,Annotation及子类,枚举;

上面列举类型的数组,例如:String[]

public @interface StringAnnotation /*extends Annotation*/{
    String value() default "";
}

动态注解和静态注解

注解要在解析后才能最终发挥作用,解析过程有上面提到的 注解处理器 完成。依据注解处理器解析过程执行的时机,注解可以分为动态注解和静态注解。

动态注解

动态注解又叫运行时注解,注解的解析过程在执行期间进行,使用反射机制完成解析过程,会影响性能;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicIntentKey {
    String value() default "";
}
public class DynamicUtil {
    public static void inject(Activity activity) {
        Intent intent = activity.getIntent();
        // 反射
        for (Field field : activity.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(DynamicIntentKey.class)) {
                // 获取注解
                DynamicIntentKey annotation = field.getAnnotation(DynamicIntentKey.class);
                String intentKey = annotation.value();
                // 读取实际的IntentExtra值
                Serializable serializable = intent.getSerializableExtra(intentKey);
                if (serializable == null) {
                    if (field.getType().isAssignableFrom(String.class)) {
                        serializable = "";
                    }
                }
                try {
                    // 插入值
                    boolean accessible = field.isAccessible();
                    field.setAccessible(true);
                    field.set(activity, serializable);
                    field.setAccessible(accessible);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

静态注解

静态注解出现在动态注解之后,并取代动态注解。静态注解相对于动态注解,把注解的解释过程放在编译阶段,在运行时不再需要解释,而是直接使用编译的结果。

因此,编译阶段需要使用相应的工具生成所需的代码。

  • 先定义一个注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface StaticIntentKey {
    String value();
}
  • 然后为这个注解定义一个处理器

注解解释器需要继承自AbstractProcessor基类,并使用@AutoService(Processor.class)声明这个类是一个注解处理器。


import com.google.auto.service.AutoService; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; @AutoService(Processor.class) public class StaticIntentProcessor extends AbstractProcessor { }

public abstract class AbstractProcessor implements Processor { }
  • 注解处理器基类AbstractProcessor实现自Processor接口,其中init()和getSupportedOptions()在抽象类AbstractProcessor给出了实现,StaticIntentProcessor的主体功能是实现process()方法,完成类生成。
public interface Processor {
    Set<String> getSupportedOptions();
    // 支持的注解类的类名集合
    Set<String> getSupportedAnnotationTypes();
    // 支持的Java版本
    SourceVersion getSupportedSourceVersion();

    void init(ProcessingEnvironment var1);

    boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);

    Iterable<? extends Completion> getCompletions(Element var1, AnnotationMirror var2, ExecutableElement var3, String var4);
}
  • 通过下面的注解处理器,为所有使用了这个注解的类生成处理代码,不再需要运行时通过反射获得。

因为这个实现没有专门实现一个对应的android-library类型的工程,所以在使用这个注解时,需要先编译完成,编译完成之后有了对应的注解处理器,才可以在Android工程中使用。

@AutoService(Processor.class)
public class StaticIntentProcessor extends AbstractProcessor {

    private TypeName activityClassName = ClassName.get("android.app", "Activity").withoutAnnotations();
    private TypeName intentClassName = ClassName.get("android.content", "Intent").withoutAnnotations();

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 支持java1.7
        return SourceVersion.RELEASE_7;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 只处理 StaticIntentKey 注解
        return Collections.singleton(StaticIntentKey.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
        // StaticMapper的bind方法
        MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .addParameter(activityClassName, "activity");

        // 查找所有的需要注入的类描述
        List<InjectDesc> injectDescs = findInjectDesc(set, re);

        for (int i1 = 0; i1 < injectDescs.size(); i1++) {
            InjectDesc injectDesc = injectDescs.get(i1);

            // 创建需要注解的类的Java文件,如上面所述的 IntentActivity$Binder
            TypeName injectedType = createInjectClassFile(injectDesc);
            TypeName activityName = typeName(injectDesc.activityName);

            // $T导入类型
            // 生成绑定分发的代码
            method.addCode((i1 == 0 ? "" : " else ") + "if (activity instanceof $T) {\n", activityName);
            method.addCode("\t$T binder = new $T();\n", injectedType, injectedType);
            method.addCode("\tbinder.bind((" + activityName + ") activity);\n", activityName, activityName);
            method.addCode("}");
        }
        // 创建StaticMapper类
        createJavaFile("com.campusboy.annotationtest", "StaticMapper", method.build());

        return false;
    }

    private List<InjectDesc> findInjectDesc(Set<? extends TypeElement> set, RoundEnvironment re) {

        Map<TypeElement, List<String[]>> targetClassMap = new HashMap<>();

        // 先获取所有被StaticIntentKey标示的元素
        Set<? extends Element> elements = re.getElementsAnnotatedWith(StaticIntentKey.class);
        for (Element element : elements) {
            // 只关心类别是属性的元素
            if (element.getKind() != ElementKind.FIELD) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support field");
                continue;
            }

            // 此处找到的是类的描述类型
            // 因为我们的StaticIntentKey的注解描述是field,所以closingElement元素是类
            TypeElement classType = (TypeElement) element.getEnclosingElement();

            System.out.println(classType);

            // 对类做缓存,避免重复
            List<String[]> nameList = targetClassMap.get(classType);
            if (nameList == null) {
                nameList = new ArrayList<>();
                targetClassMap.put(classType, nameList);
            }

            // 被注解的值,如staticName
            String fieldName = element.getSimpleName().toString();
            // 被注解的值的类型,如String,int
            String fieldTypeName = element.asType().toString();
            // 注解本身的值,如key_name
            String intentName = element.getAnnotation(StaticIntentKey.class).value();

            String[] names = new String[]{fieldName, fieldTypeName, intentName};
            nameList.add(names);
        }

        List<InjectDesc> injectDescList = new ArrayList<>(targetClassMap.size());
        for (Map.Entry<TypeElement, List<String[]>> entry : targetClassMap.entrySet()) {
            String className = entry.getKey().getQualifiedName().toString();
            System.out.println(className);

            // 封装成自定义的描述符
            InjectDesc injectDesc = new InjectDesc();
            injectDesc.activityName = className;
            List<String[]> value = entry.getValue();
            injectDesc.fieldNames = new String[value.size()];
            injectDesc.fieldTypeNames = new String[value.size()];
            injectDesc.intentNames = new String[value.size()];
            for (int i = 0; i < value.size(); i++) {
                String[] names = value.get(i);
                injectDesc.fieldNames[i] = names[0];
                injectDesc.fieldTypeNames[i] = names[1];
                injectDesc.intentNames[i] = names[2];
            }
            injectDescList.add(injectDesc);
        }

        return injectDescList;
    }

    private void createJavaFile(String pkg, String classShortName, MethodSpec... method) {
        TypeSpec.Builder builder = TypeSpec.classBuilder(classShortName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
        for (MethodSpec spec : method) {
            builder.addMethod(spec);
        }
        TypeSpec clazzType = builder.build();

        try {
            JavaFile javaFile = JavaFile.builder(pkg, clazzType)
                    .addFileComment(" This codes are generated automatically. Do not modify!")
                    .indent("    ")
                    .build();
            // write to file
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private TypeName createInjectClassFile(InjectDesc injectDesc) {

        ClassName activityName = className(injectDesc.activityName);
        ClassName injectedClass = ClassName.get(activityName.packageName(), activityName.simpleName() + "$Binder");

        MethodSpec.Builder method = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                .addParameter(activityName, "activity");

        // $T导入作为类,$N导入作为纯值,$S导入作为字符串
        method.addStatement("$T intent = activity.getIntent()", intentClassName);
        for (int i = 0; i < injectDesc.fieldNames.length; i++) {
            TypeName fieldTypeName = typeName(injectDesc.fieldTypeNames[i]);
            method.addCode("if (intent.hasExtra($S)) {\n", injectDesc.intentNames[i]);
            method.addCode("\tactivity.$N = ($T) intent.getSerializableExtra($S);\n", injectDesc.fieldNames[i], fieldTypeName, injectDesc.intentNames[i]);
            method.addCode("}\n");
        }

        // 生成最终的XXX$Binder文件
        createJavaFile(injectedClass.packageName(), injectedClass.simpleName(), method.build());

        return injectedClass;
    }

    private TypeName typeName(String className) {
        return className(className).withoutAnnotations();
    }

    private ClassName className(String className) {

        // 基础类型描述符
        if (className.indexOf(".") <= 0) {
            switch (className) {
                case "byte":
                    return ClassName.get("java.lang", "Byte");
                case "short":
                    return ClassName.get("java.lang", "Short");
                case "int":
                    return ClassName.get("java.lang", "Integer");
                case "long":
                    return ClassName.get("java.lang", "Long");
                case "float":
                    return ClassName.get("java.lang", "Float");
                case "double":
                    return ClassName.get("java.lang", "Double");
                case "boolean":
                    return ClassName.get("java.lang", "Boolean");
                case "char":
                    return ClassName.get("java.lang", "Character");
                default:
            }
        }

        // 手动解析 java.lang.String,分成java.lang的包名和String的类名
        String packageD = className.substring(0, className.lastIndexOf('.'));
        String name = className.substring(className.lastIndexOf('.') + 1);
        return ClassName.get(packageD, name);
    }

    private static class InjectDesc {
        private String activityName;
        private String[] fieldNames;
        private String[] fieldTypeNames;
        private String[] intentNames;

        @Override
        public String toString() {
            return "InjectDesc{" +
                    "activityName='" + activityName + '\'' +
                    ", fieldNames=" + Arrays.toString(fieldNames) +
                    ", intentNames=" + Arrays.toString(intentNames) +
                    '}';
        }
    }
}

示例工程

示例工程:customize-annotation

代码生成库:javaPoet 使用这个库可以更方便地生成代码。

参考文章

如何实时监控nginx

nginx 是什么?
nginx 是异步框架的 Web服务器,也可以用作反向代理,负载平衡器 和 HTTP缓存。

nginx 监控的主要指标?

  1. 包括每秒请求数

    Accepts(接受):客户端连接数
    Handled(已处理):成功的客户端连接数
Active(活跃):

Waiting(等待)
Reading(读)
Writing(写)

  1. 服务器错误率
    服务器错误率等于在单位时间内5xx错误状态的总数除以状态码(1XX,2XX,3XX,4XX,5XX)的总数。
    服务器错误率是衡量服务的重要的指标,是监控指标中有价值的一个。

  2. 请求处理时间
    请求处理时间指标记录了nginx处理每个请求的时间,从读到客户端的第一个请求字节到完成请求。较长的响应时间说明上游服务出现问题。

我们是如何收集的?
基于Openresty和Prometheus、Grafana设计的,实现了针对域名、接口级别的状态码、请求时间统计,Grafana做展示。

如何配置?
github: https://github.com/knyar/nginx-lua-prometheus.git

lua_shared_dict prometheus_metrics 10M;
lua_package_path "nginx-lua-prometheus/?.lua;;";


init_by_lua '
            prometheus = require("prometheus").init("prometheus_metrics")
            metric_requests = prometheus:counter("nginx_http_requests_total", "Number of HTTP requests", {"host", "status"})
            metric_api_requests = prometheus:counter("nginx_http_api_requests_total", "Number of HTTP requests", {"host", "api", "status"})
            metric_latency = prometheus:histogram("nginx_http_request_duration_seconds", "HTTP request latency", {"host"})
            metric_connections = prometheus:gauge("nginx_http_connections", "Number of HTTP connections", {"state"})
    ';


    log_by_lua '
            local server_name = ngx.var.server_name
            local status = ngx.var.status
            local request_time = ngx.var.request_time
            local uri = ngx.var.uri
            local regex = [[\.[a-z]+\.[a-z]+$]]
            local m = ngx.re.match(server_name, regex)
            if m then
                metric_requests:inc(1, {server_name, status})
                metric_latency:observe(tonumber(request_time), {server_name})
                code = {"400", "403", "404", "500", "502"}
                for _, v in ipairs(code) do
                    if status == v then
                        metric_api_requests:inc(1, {server_name, uri, status})
                    end
                end
            end
    ';


    server {
            listen 9145;
            server_name _;
            allow ip;
            deny all;
            location /metrics {
                    content_by_lua '
                            prometheus:collect()
                            ';
            }
            location /clean {
                content_by_lua '
                        ngx.shared.prometheus_metrics:flush_all()
                ';
            }
    }

prometheus配置pull获取监控数据
http://ip:9145/metrics

Grafana如何展示?
根据nginx收集指标配置

nginx共享内存分配是有大小限制的,如下配置是清理共享内存数据

location /clean {
                content_by_lua '
                        ngx.shared.prometheus_metrics:flush_all()
                ';
            }