网络拾遗

Kafka相关:

kafka性能调优:https://blog.csdn.net/vegetable_bird_001/article/details/51858915

kafka高性能揭秘:https://www.cnblogs.com/qcloud1001/p/8984590.html

SpringBoot:

程序员DD的教程:http://blog.didispace.com/categories/Spring-Boot/

SpringCloud:

程序员DD的教程:http://blog.didispace.com/categories/Spring-Cloud/

ElasticSearch:

与springboot集成: https://blog.csdn.net/li521wang/article/details/83792552

blog精华推荐

  1. 张强-纯洁的微笑
  2. 方志明-博客专栏
  3. 程序猿DD的博客
  4. Spring Cloud Zuul 的 route 运行机制分析-黑洞之谜
  5. 简书 二月_春风
  6. 芋道源码解析
  7. Aoho的博客(security,权限服务整合)
  8. 知秋大佬的视频

Java线程停止的正确方式

Java线程停止以前的做法是通过Thread.stop() 的方式来停止具体的线程,但是这个方法目前是被废弃掉的,不推荐使用. 具体原因:

1、该方式是通过立即抛出ThreadDeath异常来达到停止线程的目的,而且此异常抛出可能发生在程序的任何一个地方,包括catch、finally等语句块中。

2、由于抛出ThreadDeatch异常,会导致该线程释放所持有的所有的锁,而且这种释放的时间点是不可控制的,可能会导致出现线程安全问题和数据不一致情况,比如在同步代码块中在执行数据更新操作时线程被突然停止。

因此,为了避免Thread.stop()带来的问题,推荐使用被称作为Interrupt(中断)的协作机制来停止一个正在运行的线程。Interrupt 方法,只是改变中断状态, 没有实际终止线程.

在JVM中,每个线程都有一个与之关联的Boolean属性,被称之为中断状态,可以通过Thread.currentThread().isInterrupted()来获取当前线程的中断状态,初始值为false。中断状态仅仅是线程的一个属性,用以表明该线程是否被中断。因为中断是线程之间的一种协作机制,因此对于被中断的线程而言,可以对中断做出响应,也可以不做任何响应.

在Java中,有些方法已经实现了对中断的响应处理,比如Thread.sleep()、Object.wait()、BlockingQueue.put()、BlockingQueue.take()等等。当线程执行正在这些方法时,被其他线程中断掉,该线程会首先清除掉中断状态(设置中断属性为false),然后抛出InterruptedException异常。

@Override
  public void run() {
    final Thread currentThread = Thread.currentThread();
    for (; ; ) {
      try {
        //空队列判断
        if (isEmpty()) {
          log.info("DelayedQueue Is Empty");
          synchronized (currentThread) {
            currentThread.wait();
          }
          continue;
        }
      } catch (InterruptedException e) {
        //上传中断状态
        currentThread.interrupt();
        log.error(e.getMessage(), e);
      }
    }

在平时代码中对中断的处理,不推荐生吞中,最好的做法就是上传中断状态,保留中断发生的证据,以便调用栈中更高层的代码能够知道发生了中断,并对中断做出响应。

然而上传中断状态, 并不能让for(;;)循环终止, 需要在开头添加如下的判断代码:

@Override
  public void run() {
    final Thread currentThread = Thread.currentThread();
    for (; ; ) {
      //判断线程状态: 如果是中断, 则抛出异常终止, 或者直接return
      if (currentThread.isInterrupted()) {
        throw new RuntimeException("Thread Has Been Stoped");
      }
      try {
        //空队列判断
        if (isEmpty()) {
          log.info("DelayedQueue Is Empty");
          synchronized (currentThread) {
            currentThread.wait();
          }
          continue;
        }
      } catch (InterruptedException e) {
        //上传中断状态
        currentThread.interrupt();
        log.error(e.getMessage(), e);
      }
    }

这样, 当其他地方调用thread.interrupt()方法时, 该线程才能被正常的终止.

参考链接:https://blog.csdn.net/trackle400/article/details/81775189

关于MySql的事务隔离原理

学习了林晓斌<<MySQL45讲>>中关于事务的文章后, 把一些点总结一下, 便于之后记忆:

Mysql在事务开始时,会创建一个”一致性读视图”. 它没有物理结构,作用是事务执行期间用来定义”我能看到什么数据”.

  1. 每个事务有全局唯一的ID, 并且向上递增(由MySQL的事务系统控制).

2. 表的每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

3. InnoDB 为每个事务构造了一个数组, 用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

这样在事务内的数据可见性, 就转化为: 当前事务构造的事务ID数组和行数据对应的row trx_id之间的映射关系. 一个数据版本,对于一个事务视图来说,除了当前事务的更新总是可见以外,有三种情况:

未提交,不可见;

已提交,但是是在视图创建后提交的,不可见;

已提交,而且是在视图创建前提交的,可见。

另外附加一个原则: 所有更新数据都是先读后写的,而这个读,只能读当前的值(即实时被更新的值),称为“当前读”(current read)。

同时, 更新数据前都会加排它锁, 所以如果前一个更新未提交, 当前更新就一直处于等待状态.

CentOS 安装 mongodb

转:https://www.cnblogs.com/saryli/p/9822819.html

1、下载安装包

curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.12.tgz

2、解压

tar -zxvf mongodb-linux-x86_64-4.0.12.tgz

3、移动到指定位置

mv mongodb-linux-x86_64-4.2.12/ /usr/local/mongodb

4、在/usr/local/mongodb下创建文件夹

mkdir -p /data/dbmkdir /logs

5、在/usr/local/mongodb/bin下新建配置

vi mongodb.conf
#数据文件存放目录
dbpath = /usr/local/mongodb/data/db 
#日志文件存放目录
logpath = /usr/local/mongodb/logs/mongodb.log
#端口
port = 27017
#以守护程序的方式启用,即在后台运行
fork = true
#http访问控制(centos下会此项设置无效:注释掉这个配置或者下载企业评估版)
nohttpinterface = true
auth= true
bind_ip=0.0.0.0

6、环境变量配置

vi /etc/profile
export MONGODB_HOME=/usr/local/mongodb export PATH=$PATH:$MONGODB_HOME/bin

保存后,重启系统配置

source /etc/profile

7、启动
在/usr/local/mongodb/bin下

3mongod -f mongodb.conf 
或
./mongod -f mongodb.conf

8、关闭

#进入控制台
# mongo
> use admin
> db.shutdownServer()
# 命令行
mongod --shutdown
或
./mongod --shutdown

9、开启端口

firewall-cmd --zone=public --add-port=27017/tcp --permanent

查看端口

firewall-cmd --permanent --query-port=27017/tcp

重启防火墙

firewall-cmd --reload

10、创建用户

进入客户端

./mongo

创建用户管理员:

#使用admin数据库
use admin
#创建用户,并分配权限
db.createUser({user:"root",pwd:"root123456",roles:["userAdminAnyDatabase"]})
#给root添加system表的权限
db.grantRolesToUser("root",[{role:"__system",db:"admin"}])
#验证并切换到root用户
db.auth('root','root123456')

以用户管理员身份登录,并切换数据库,创建数据库用户:
切换到test数据库

use test

创建用户名、密码、角色

db.createUser({user:"username",pwd:"@user123456*",roles:[{role:"readWrite",db:"securitydata"}]})

设置mongodb配置中的auth为true(/etc/mongod.conf):

security:authorization: enabled

验证mongodb数据库权限。

db.auth('user','@user123456*')

查看所有用户信息

db.system.users.find()

删除用户

use admin
#验证并切换到root用户
db.auth('root','root123456')
db.system.users.remove({user:"haha"})
db.system.users.find()

11. 非正常关闭修复

# no any other options,不保存损坏数据
$ mongod --repair
# 数据文件路径下包含修复的文件和一个空的mongo.lock文件
$ mongod --dbpath /data/db --repair
# 使用MMAPv1存储引擎还可以指定 --repairpath作为临时的

官方修复文档

Https双向认证,实现系统间通讯双向认证

转自: https://blog.csdn.net/u012977486/article/details/88815621

一、Http

HyperText Transfer Protocol,超文本传输协议,是互联网上使用最广泛的一种协议,所有WWW文件必须遵循的标准。HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全。

使用TCP端口为:80

二、Https

Hyper Text Transfer Protocol over Secure Socket Layer,安全的超文本传输协议,网景公式设计了SSL(Secure Sockets Layer)协议用于对Http协议传输的数据进行加密,保证会话过程中的安全性。

使用TCP端口默认为443

三、SSL协议加密方式

SSL协议即用到了对称加密也用到了非对称加密(公钥加密),在建立传输链路时,SSL首先对对称加密的密钥使用公钥进行非对称加密,链路建立好之后,SSL对传输内容使用对称加密。

对称加密

速度高,可加密内容较大,用来加密会话过程中的消息

公钥加密

加密速度较慢,但能提供更好的身份认证技术,用来加密对称加密的密钥

四、单向认证

Https在建立Socket连接之前,需要进行握手,具体过程如下:

1、客户端向服务端发送SSL协议版本号、加密算法种类、随机数等信息。

2、服务端给客户端返回SSL协议版本号、加密算法种类、随机数等信息,同时也返回服务器端的证书,即公钥证书

3、客户端使用服务端返回的信息验证服务器的合法性,包括:

证书是否过期

发型服务器证书的CA是否可靠

返回的公钥是否能正确解开返回证书中的数字签名

服务器证书上的域名是否和服务器的实际域名相匹配

验证通过后,将继续进行通信,否则,终止通信

4、客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择

5、服务器端在客户端提供的加密方案中选择加密程度最高的加密方式。

6、服务器将选择好的加密方案通过明文方式返回给客户端

7、客户端接收到服务端返回的加密方式后,使用该加密方式生成产生随机码,用作通信过程中对称加密的密钥,使用服务端返回的公钥进行加密,将加密后的随机码发送至服务器

8、服务器收到客户端返回的加密信息后,使用自己的私钥进行解密,获取对称加密密钥。 在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全。

五、双向认证

双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户端的认证,具体过程如下:

1、客户端向服务端发送SSL协议版本号、加密算法种类、随机数等信息。

2、服务端给客户端返回SSL协议版本号、加密算法种类、随机数等信息,同时也返回服务器端的证书,即公钥证书

3、客户端使用服务端返回的信息验证服务器的合法性,包括:

证书是否过期

发型服务器证书的CA是否可靠

返回的公钥是否能正确解开返回证书中的数字签名

服务器证书上的域名是否和服务器的实际域名相匹配

验证通过后,将继续进行通信,否则,终止通信

4、服务端要求客户端发送客户端的证书,客户端会将自己的证书发送至服务端

5、验证客户端的证书,通过验证后,会获得客户端的公钥

6、客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择

7、服务器端在客户端提供的加密方案中选择加密程度最高的加密方式

8、将加密方案通过使用之前获取到的公钥进行加密,返回给客户端

9、客户端收到服务端返回的加密方案密文后,使用自己的私钥进行解密,获取具体加密方式,而后,产生该加密方式的随机码,用作加密过程中的密钥,使用之前从服务端证书中获取到的公钥进行加密后,发送给服务端

10、服务端收到客户端发送的消息后,使用自己的私钥进行解密,获取对称加密的密钥,在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全。

六. 代码实现

 1. 多个springboot项目,以两个springboot项目为例,这里暂且叫Client项目,Server项目。

 2. Client项目所在的服务器,Client.p12(客户端证书库) ,Client.cer(客户端公钥);

     Server项目所在的服务器,Server.p12(服务端证书库),Server.cer(服务端公钥);

4.实现步骤

 1. 创建springboot项目,Client ,Server 此处,请自行百度创建如何创建springboot项目。

 2. 使用jdk自带的keytool工具,生成Client和Server端相应的证书,步骤如下:

      a. Client端证书生成步骤:

           1.生成客户端 Client.p12文件    

keytool -genkey -v -alias Client -keyalg RSA -storetype PKCS12 -keystore C:\D\jdk1.8.0_161\Client.p12

设置密码: lq123456
注意事项:生成证书,您的名字与姓氏一项,应该填写服务器的ip(此处应该是域名,但是没有域名,故此处填写服务器ip)
           2 . 导出客户端公钥Client.cer 文件

keytool -keystore C:\D\jdk1.8.0_161\Client.p12  -export -alias Client -file C:\D\jdk1.8.0_161\Client.cer

      b. Server端证书生成步骤

           1.  生成服务端Server.p12文件

keytool -genkey -v -alias Server  -keyalg RSA -storetype PKCS12 -keystore C:\D\jdk1.8.0_161\Server.p12

           设置密码: lq123456
   注意事项:生成证书,您的名字与姓氏一项,应该填写服务器的ip(此处应该是域名,但是没有域名,故此处填写服务器ip)
           2.    导出服务端公钥Server.cer 文件

keytool -keystore C:\D\jdk1.8.0_161\Server.p12 -export -alias Server -file C:\D\jdk1.8.0_161\Server.cer

      c. 将Client端和Server端的公钥文件(.cer文件)导入双方系统的jre运行环境的cacerts证书库

           1.    将客户端公钥导入的服务端jdk信任库

keytool -import -file Client.cer -keystore C:\D\jdk1.8.0_161\jre\lib\security\cacerts –v

           2.    将服务端公钥导入到客户端的jdk信任库

 keytool -import -file Server.cer -keystore  C:\D\jdk1.8.0_161\jre\lib\security\cacerts –v

           3.  将客户端公钥导入到服务端Server.p12证书库

keytool -import -v -file C:\D\jdk1.8.0_161\Client.cer  -keystore C:\D\jdk1.8.0_161\server.p12

      注意事项:此处导入的密码为changeit,默认密码

至此,证书生成完成,证书库导入完成!

       d. 代码实现

            1.Server端

            第一步:在application.properties中添加如下配置:包括本地证书库和受信任证书配置

server.port=8090
server.address=10.119.165.171
server.ssl.key-store=classpath:server.p12
server.ssl.key-store-password=lq123456
server.ssl.key-alias=server
server.ssl.keyStoreType=JKS
 
 
server.ssl.trust-store=classpath:server.p12
server.ssl.trust-store-password=lq123456
server.ssl.client-auth=need
server.ssl.trust-store-type=JKS
server.ssl.trust-store-provider=SUN


             第二步:服务端接口开放


package com.example.server1.controller;
 
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
/**
 * @author lucasliang
 * @date 01/03/2019 3:21 下午
 * @Description
 */
@RestController
@RequestMapping("/server")
public class ServerController {
 
  @RequestMapping("/hello")
  public String getUrlInfo() {
    return "************request https success************";
  }
 
}

     2. Client端

             第一步:在application.properties中添加如下配置:

 server.port=8091
 server.address=10.119.165.171
 server.ssl.key-store=classpath:client.p12
 server.ssl.key-store-password=lq123456
 server.ssl.key-alias=client
 server.ssl.keyStoreType=JKS

第二步:单元测试:

package com.example.client;
 
import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
/**
 * @author lucasliang
 * @date 04/03/2019 2:13 下午
 * @Description
 */
@SpringBootTest(classes = {Client1ApplicationTests.class})
@RunWith(SpringRunner.class)
public class P12CertTest {
 
  private final static String TEST_URL = "https://10.119.165.171:8090/server/hello";
 
  @Test
  public void getHKVesselTrip() throws Exception {
    KeyStore clientStore = KeyStore.getInstance("PKCS12");
    clientStore
        .load(new FileInputStream("C:\\D\\jdk1.8.0_161\\shuangxiang\\client_original.p12"),
            "lq123456".toCharArray());
 
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(clientStore, "lq123456".toCharArray());
    KeyManager[] kms = kmf.getKeyManagers();
    TrustManagerFactory tmf = TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
 
    KeyStore trustStore = KeyStore.getInstance("JKS");
    trustStore.load(new FileInputStream("C:\\D\\jdk1.8.0_161\\jre\\lib\\security\\cacerts"),
        "changeit".toCharArray());
    tmf.init(trustStore);
    TrustManager[] tms = tmf.getTrustManagers();
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kms, tms, new SecureRandom());
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,
        SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
    CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();
    try {
      HttpGet httpget = new HttpGet(TEST_URL);
      System.out.println("executing request" + httpget.getRequestLine());
      CloseableHttpResponse response = httpclient.execute(httpget);
      try {
        HttpEntity entity = response.getEntity();
        if (entity != null) {
          System.out.println(EntityUtils.toString(entity));
        }
      } finally {
        response.close();
      }
    } finally {
 
      httpclient.close();
    }
 
  }
 
}

此时,Client端发送请求到Server端,请求成功,https双向认证完成!

关于Hibernate更新后, 获取的值仍是旧的问题

最近在更新商品库存的时候遇到一个诡异的问题:

1.取出本地商品信息,

2.查询总库库存, 将库存更新到本地

3.再取出商品和库存, 更新库存预警信息

结果在第三步骤出了问题: 取出来的库存是旧的, 不是刚更新的库存???

JAVA类 SkuRepo:

@Modifying
@Query(value = "update Sku set inv=:inv where id=:id")
void updateInv(@Param("id") String id, @Param("inv") int inv);

JAVA类 SkuServie:

//获取sku
Sku slSkuDB = skuRepo.findById("123456").orElse(null);
//获取总库库存
int inv = 0;
........
//更新库存
skuRepo.updateInv(slSkuDB.getId(), inv);
//发送事件
Event.publish(slSkuDB.getId());

JAVA类 SkuInvListener:
//监听库存改变事件
public void onInvChange(String skuId){
Sku slSkuDB = skuRepo.findById("123456").orElse(null);
int inv = slSkuDB.getInv();
................这里获取的库存是旧的
}

问题分析:

经过调试, 发现库存肯定更新成功的.

那么事件监听中, Sku对象不是从数据库取的, 而是上次Hibernate中已有的对象.

而且updateInv并没有更新Hibernate的缓存.

验证:

无奈, 只能断点debug: 发现”SkuServie”获取的Sku和”SkuInvListener”获取的Sku, 指向的Hibernate对象是同一个, 而且inv还是旧值. HQL语句也不会触发缓存更新!!!

处理:

更新库存
  skuRepo.updateInv(slSkuDB.getId(), inv);
改成:
  slSkuDB.setInv(inv);
  skuRepo.save(slSkuDB);

总结:

使用Hibernate一定要实时关注它的缓存问题(一级缓存和二级缓存)

更新对象,最好用Hibernate自带的更新操作, 任何Native SQL和HQL都不会同步到缓存中.

Redis缓存清理

1.访问redis根目录    cd  /usr/local/redis-XXX

2.登录redis:redis-cli -h 127.0.0.1 -p 6379

3.查看所有key值:keys *

4.删除指定索引的值:del key

5.清空整个 Redis 服务器的数据:flushall 

6.清空当前库中的所有 key:flushdb 

Linux 保护进程不被OOM Killer

From: https://blog.csdn.net/flysqrlboy/article/details/89011635

最近在预发环境上有一个重要的进程隔三差五就被OOM Killer干掉(通过查看CentOS系统日志/var/log/messages揪出来是OOM Killer干的)。该机器上跑着各种进程,内存是有些吃紧。这当然可以通过加大机器内存或者迁走某些进程来解决。但一时又没有多余的机器和内存资源,只能自己动手丰衣足食了(资源短缺确实是更能激发人去思考更优更节省的方案)。现在我要解决的是如下两个问题:

为什么被OOM Killer干掉的是这个进程而不是其他的?
能保护某个进程不被OOM Killer 干掉吗?
Surviving the Linux OOM Killer》一文正是对上面两个问题的介绍。

基本概念:

Linux 内核有个机制叫OOM killer(Out Of Memory killer),该机制会监控那些占用内存过大,尤其是瞬间占用内存很快的进程,然后防止内存耗尽而自动把该进程杀掉。内核检测到系统内存不足、挑选并杀掉某个进程的过程可以参考内核源代码linux/mm/oom_kill.c,当系统内存不足的时候,out_of_memory()被触发,然后调用select_bad_process()选择一个”bad”进程杀掉。如何判断和选择一个”bad进程呢?linux选择”bad”进程是通过调用oom_badness(),挑选的算法和想法都很简单很朴实:最bad的那个进程就是那个最占用内存的进程。

如何查看:

grep "Out of memory" /var/log/messages

查看系统日志方法:

运行egrep -i -r ‘killed process’ /var/log命令, 也可运行dmesg命令

sudo dmesg -T | grep "(java)" 

How does OOM Killer choose which process to kill?

Linux 内核会给每个运行中的进程分配一个叫 oom_score 的分数,它表示当系统可用内存很低时,一个进程被kill掉的可能性有多大。分数越高,越有可能被kill掉。分数值很简单:等于进程的内存占用百分比乘以10。比如一个进程占50%的内存,它的oom_score值就是 50 X 10 = 500 .
一个进程的oom_score被记录在/proc/$pid/oom_score 文件中。

Can I ensure some important processes do not get killed by OOM Killer?

OOM Killer会检查 /proc/$pid/oom_score_adj文件来调整最终的分数 (oom_score)。所以我们可以通过在这个文件中给一个大的负数,以降低该进程被选中并终止的可能性。oom_score_adj可以在-1000到1000间变化。如果你给了-1000,进程即使使用了100%的内存也不会被OOM Killer干掉。可通过下面命令修改oom_score_adj(比如设为-200):

sudo echo -200 > /proc/$pid/oom_score_adj

或者,我们可以:

echo -17 > /proc/<pid>/oom_adj

-17表示禁用OOM我们也可以对把整个系统的OOM给禁用掉:

sysctl -w vm.panic_on_oom=1sysctl -p

参数/proc/sys/vm/overcommit_memory可以控制进程对内存过量使用的应对策略
当overcommit_memory=0 允许进程轻微过量使用内存,但对于大量过载请求则不允许
当overcommit_memory=1 永远允许进程overcommit

当overcommit_memory=2 永远禁止overcommit

版权声明:本文为CSDN博主「keke_xin」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/keke_Xin/article/details/84829816

Caveats of adjusting OOM scores

警告:解决内存不足的最好办法还是增加可用内存(例如更好的硬件)或者将某些进程移到别的地方去,又或者优化代码以减少内存消耗。

oom killer理解和日志分析:知识储备

oom killer理解和日志分析:日志分析

JavaScript AST 抽象语法树

转自: https://www.overtaking.top/2018/07/25/20180725130233/

AST 抽象语法树简介

AST(Abstract Syntax Tree)是源代码的抽象语法结构树状表现形式,Webpack、ESLint、JSX、TypeScript 的编译和模块化规则之间的转化都是通过 AST 来实现对代码的检查、分析以及编译等操作。

JavaScript 语法的 AST 语法树

JavaScript 中想要使用 AST 进行开发,要知道抽象成语法树之后的结构是什么,里面的字段名称都代表什么含义以及遍历的规则,可以通过 http://esprima.org/demo/parse.html 来实现 JavaScript 语法的在线转换。

通过在线编译工具,可以将 function fn(a, b) {} 编译为下面的结构。

{
  "type": "Program",
  "body": [
    {
      "type": "FunctionDeclaration",
      "id": {
        "type": "Identifier",
        "name": "fn"
      },
      "params": [
        {
          "type": "Identifier",
          "name": "a"
        },
        {
          "type": "Identifier",
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "body": []
      },
      "generator": false,
      "expression": false,
      "async": false
    }
  ],
  "sourceType": "script"
}

将 JavaScript 语法编译成抽象语法树后,需要对它进行遍历、修该并重新编译,遍历树结构的过程为 “先序深度优先”。

esprima、estraverse 和 escodegen

esprimaestraverse 和 escodegen 模块是操作 AST 的三个重要模块,也是实现 babel 的核心依赖,下面是分别介绍三个模块的作用。

esprima 将 JS 转换成 AST

esprima 模块的用法如下:文件:esprima-test.js

const esprima = require('esprima');

let code = 'function fn() {}';

// 生成语法树
let tree = esprima.parseScript(code);

console.log(tree);

// Script {
//   type: 'Program',
//   body:
//   [ FunctionDeclaration {
//     type: 'FunctionDeclaration',
//     id: [Identifier],
//     params: [],
//     body: [BlockStatement],
//     generator: false,
//     expression: false,
//     async: false } ],
//   sourceType: 'script' }

通过上面的案例可以看出,通过 esprima 模块的 parseScript 方法将 JS 代码块转换成语法树,代码块需要转换成字符串,也可以通过 parseModule 方法转换一个模块。

estraverse 遍历和修改 AST

查看遍历过程:文件:estraverse-test.js

const esprima = require('esprima');
const estraverse = require('estraverse');

let code = 'function fn() {}';

// 遍历语法树
estraverse.traverse(esprima.parseScript(code), {
  enter(node) {
    console.log('enter', node.type);
  },
  leave() {
    console.log('leave', node.type);
  }
});

// enter Program
// enter FunctionDeclaration
// enter Identifier
// leave Identifier
// enter BlockStatement
// leave BlockStatement
// leave FunctionDeclaration
// leave Program

上面代码通过 estraverse 模块的 traverse 方法将 esprima 模块转换的 AST 进行了遍历,并打印了所有的 type 属性并打印,每含有一个 type 属性的对象被叫做一个节点,修改是获取对应的类型并修改该节点中的属性即可。

其实深度遍历 AST 就是在遍历每一层的 type 属性,所以遍历会分为两个阶段,进入阶段和离开阶段,在 estraverse 的 traverse 方法中分别用参数指定的 entry 和 leave 两个函数监听,但是我们一般只使用 entry

escodegen 将 AST 转换成 JS

下面的案例是一个段 JS 代码块被转换成 AST,并将遍历、修改后的 AST 重新转换成 JS 的全过程。文件:escodegen-test.js

const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');

let code = 'function fn() {}';

// 生成语法树
let tree = esprima.parseScript(code);

// 遍历语法树
estraverse.traverse(tree, {
  enter(node) {
    // 修改函数名
    if (node.type === 'FunctionDeclaration') {
      node.id.name = 'ast';
    }
  }
});

// 编译语法树
let result = escodegen.generate(tree);

console.log(result);

// function ast() {
// }

在遍历 AST 的过程中 params 值为数组,没有 type 属性。

实现 Babel 语法转换插件

实现语法转换插件需要借助 babel-core 和 babel-types 两个模块,其实这两个模块就是依赖 esprimaestraverse 和 escodegen 的。

使用这两个模块需要安装,命令如下:

1
npm install babel-core babel-types

plugin-transform-arrow-functions

plugin-transform-arrow-functions 是 Babel 家族成员之一,用于将箭头函数转换 ES5 语法的函数表达式。

文件:plugin-transform-arrow-functions.js

const babel = require('babel-core');
const types = require('babel-types');

// 箭头函数代码块
let sumCode = `
const sum = (a, b) => {
  return a + b;
}`;
let minusCode = `const minus = (a, b) => a - b;`;

// 转化 ES5 插件
const ArrowPlugin = {
  // 访问者(访问者模式)
  visitor: {
    // path 是树的路径
    ArrowFunctionExpression(path) {
      // 获取树节点
      let node = path.node;

      // 获取参数和函数体
      let params = node.params;
      let body = node.body;

      // 判断函数体是否是代码块,不是代码块则添加 return 和 {}
      if (!types.isBlockStatement(body)) {
        let returnStatement = types.returnStatement(body);
        body = types.blockStatement([returnStatement]);
      }

      // 生成一个函数表达式树结构
      let func = types.functionExpression(null, params, body, false, false);

      // 用新的树结构替换掉旧的树结构
      path.replaceWith(func);
    }
  }
};

// 生成转换后的代码块
let sumResult = babel.transform(sumCode, {
  plugins: [ArrowPlugin]
});

let minusResult = babel.transform(minusCode, {
  plugins: [ArrowPlugin]
});

console.log(sumResult.code);
console.log(minusResult.code);

// let sum = function(a, b) {
//   return a + b;
// };
// let minus = function(a, b) {
//   return a - b;
// };

我们主要使用 babel-core 的 transform 方法将 AST 转化成代码块,第一个参数为转换前的代码块(字符串),第二个参数为配置项,其中 plugins 值为数组,存储修改 babal-core 转换的 AST 的插件(对象),使用 transform 方法将旧的 AST 处理成新的代码块后,返回值为一个对象,对象的 code 属性为转换后的代码块(字符串)。

内部修改通过 babel-types 模块提供的方法实现,API 可以到 https://github.com/babel/babel/tree/6.x/packages/babel-types 中查看。

ArrowPlugin 就是传入 transform 方法的插件,必须含有 visitor 属性(固定),值同为对象,用于存储修改语法树的方法,方法名要严格按照 API,对应的方法会修改 AST 对应的节点。

在 types.functionExpression 方法中参数分别代表,函数名(匿名函数为 null)、函数参数(必填)、函数体(必填)、是否为 generator 函数(默认 false)、是否为 async 函数(默认 false),返回值为修改后的 AST,path.replaceWith 方法用于替换 AST,参数为新的 AST。

plugin-transform-classes

plugin-transform-classes 也是 Babel 家族中的成员之一,用于将 ES6 的 class 类转换成 ES5 的构造函数。

文件:plugin-transform-classes.js

const babel = require('babel-core');
const types = require('babel-types');

// 类
let code = `
class Person {
  constructor(name) {
    this.name = name;
  }
  getName () {
    return this.name;
  }
}`;

// 将类转化 ES5 构造函数插件
const ClassPlugin = {
  visitor: {
    ClassDeclaration(path) {
      let node = path.node;
      let classList = node.body.body;

      // 将取到的类名转换成标识符 { type: 'Identifier', name: 'Person' }
      let className = types.identifier(node.id.name);
      let body = types.blockStatement([]);
      let func = types.functionDeclaration(
        className,
        [],
        body,
        false,
        false
      );
      path.replaceWith(func);

      // 用于存储多个原型方法
      let es5Func = [];

      // 获取 class 中的代码体
      classList.forEach((item, index) => {
        // 函数的代码体
        let body = classList[index].body;

        // 获取参数
        let params = item.params.length ?
          item.params.map(val => val.name) :
          [];

        // 转化参数为标识符
        params = types.identifier(params);

        // 判断是否是 constructor,如果构造函数那就生成新的函数替换
        if (item.kind === 'constructor') {
          // 生成一个构造函数树结构
          func = types.functionDeclaration(
            className,
            [params],
            body,
            false,
            false
          );
        } else {
          // 其他情况是原型方法
          let proto = types.memberExpression(
            className,
            types.identifier('prototype')
          );

          // 左侧层层定义标识符 Person.prototype.getName
          let left = types.memberExpression(
            proto,
            types.identifier(item.key.name)
          );

          // 右侧定义匿名函数
          let right = types.functionExpression(
            null,
            [params],
            body,
            false,
            false
          );

          // 将左侧和右侧进行合并并存入数组
          es5Func.push(types.assignmentExpression('=', left, right));
        }
      });

      // 如果没有原型方法,直接替换
      if (es5Func.length === 0) {
        path.replaceWith(func);
      } else {
        es5Func.push(func);
        // 替换 n 个节点
        path.replaceWithMultiple(es5Func);
      }
    }
  }
};

// 生成转换后的代码块
result = babel.transform(code, {
  plugins: [ClassPlugin]
});

console.log(result.code);

// Person.prototype.getName = function() {
//     return this.name;
// }
// function Person(name) {
//     this.name = name;
// }

上面这个插件的实现要比 plugin-transform-arrow-functions 复杂一些,归根结底还是将要互相转换的 ES6 和 ES5 语法树做对比,找到他们的不同,并使用 babel-types 提供的 API 对语法树对应的节点属性进行修改并替换语法树,值得注意的是 path.replaceWithMultiple 与 path.replaceWith 不同,参数为一个数组,数组支持多个语法树结构,可根据具体修改语法树的场景选择使用,也可根据不同情况使用不同的替换方法。

总结

通过本节我们了解了什么是 AST 抽象语法树、抽象语法树在 JavaScript 中的体现以及在 NodeJS 中用于生成、遍历和修改 AST 抽象语法树的核心依赖,并通过使用 babel-core 和 babel-types 两个模块简易模拟了 ES6 新特性转换为 ES5 语法的过程,希望可以为后面自己实现一些编译插件提供了思路。

AST – 抽象语法树

转自: http://blog.chinaunix.net/uid-26750235-id-3139100.html

抽象语法树简介

()简介

抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。

抽象语法树在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。

()抽象语法树实例

(1)四则运算表达式

表达式: 1+3*(4-1)+2

抽象语法树为:

(2)xml

代码2.1

  1. <letter>
  2.   <address>
  3.     <city>ShiChuang</city>
  4.   </address>
  5.   <people>
  6.     <id>12478</id>
  7.     <name>Nosic</name>
  8.   </people>
  9. </letter>

抽象语法树

(3)程序1

代码2.2

  1. while b != 0
  2. {
  3.     if a > b
  4.         a = a-b
  5.     else
  6.         b = b-a
  7. }
  8. return a

抽象语法树

(4)程序2

代码2.3

  1. sum=0
  2. for i in range(0,100)
  3.     sum=sum+i
  4. end

抽象语法树

()为什么需要抽象语法树

当在源程序语法分析工作时,是在相应程序设计语言的语法规则指导下进行的。语法规则描述了该语言的各种语法成分的组成结构,通常可以用所谓的前后文无关文法或与之等价的Backus-Naur范式(BNF)将一个程序设计语言的语法规则确切的描述出来。前后文无关文法有分为这么几类:LL(1),LR(0),LR(1), LR(k) ,LALR(1)等。每一种文法都有不同的要求,如LL(1)要求文法无二义性和不存在左递归。当把一个文法改为LL(1)文法时,需要引入一些隔外的文法符号与产生式。

例如,四则运算表达式的文法为:

文法1.1

  1. E->T|EAT
  2. T->F|TMF
  3. F->(E)|i
  4. A->+|-
  5. M->*|/

改为LL(1)后为:

文法1.2

  1. E->TE’
  2. E’->ATE’|e_symbol
  3. T->FT’
  4. T’->MFT’|e_symbol
  5. F->(E)|i
  6. A->+|-
  7. M->*|/

例如,当在开发语言时,可能在开始的时候,选择LL(1)文法来描述语言的语法规则,编译器前端生成LL(1)语法树,编译器后端对LL(1)语法树进行处理,生成字节码或者是汇编代码。但是随着工程的开发,在语言中加入了更多的特性,用LL(1)文法描述时,感觉限制很大,并且编写文法时很吃力,所以这个时候决定采用LR(1)文法来描述语言的语法规则,把编译器前端改生成LR(1)语法树,但在这个时候,你会发现很糟糕,因为以前编译器后端是对LL(1)语树进行处理,不得不同时也修改后端的代码。

抽象语法树的第一个特点为:不依赖于具体的文法。无论是LL(1)文法,还是LR(1),或者还是其它的方法,都要求在语法分析时候,构造出相同的语法树,这样可以给编译器后端提供了清晰,统一的接口。即使是前端采用了不同的文法,都只需要改变前端代码,而不用连累到后端。即减少了工作量,也提高的编译器的可维护性。

抽象语法树的第二个特点为:不依赖于语言的细节。在编译器家族中,大名鼎鼎的gcc算得上是一个老大哥了,它可以编译多种语言,例如c,c++,java,ADA,Object C, FORTRAN, PASCAL,COBOL等等。在前端gcc对不同的语言进行词法,语法分析和语义分析后,产生抽象语法树形成中间代码作为输出,供后端处理。要做到这一点,就必须在构造语法树时,不依赖于语言的细节,例如在不同的语言中,类似于if-condition-then这样的语句有不同的表示方法

在c中为:

  1. if(condition)
  2. {
  3.     do_something();
  4. }

     在fortran中为:

  1. If condition then
  2.     do_somthing()
  3. end if

在构造if-condition-then语句的抽象语法树时,只需要用两个分支节点来表于,一个为condition,一个为if_body。如下图:

在源程序中出现的括号,或者是关键字,都会被丢掉。

正则表达式 – 匹配XML标签

开发中经常用到html标签匹配, 这里做个整理:

1.匹配标签 <[^>]+>

private static final String REGEX = "<[^>]+>";
static Pattern pattern = Pattern.compile(REGEX, Pattern.CASE_INSENSITIVE);
 Matcher matcher = pattern.matcher(content);
    while (matcher.find()) {
      MatchResult result = matcher.toMatchResult();
      System.out.println(result.group(0));
    }

2. 获取标签里面的某个属性值 attr=\”([^\”]*)\”

private static final String REGEX = "name=\"([^\"]*)\""; //取name属性
static Pattern pattern = Pattern.compile(REGEX, Pattern.CASE_INSENSITIVE);
 Matcher matcher = pattern.matcher(tag);
    if(matcher.find()) {
      MatchResult result = matcher.toMatchResult();
      System.out.println(result.group(1));
    }

3. 结合标签, 直接获取所有input标签里面的name属性: <TAG[^>]*ATTR=\”([^\”]*)\”[^>]+>

private static final String REGEX = "<input[^>]*name=\"([^\"]*)\"[^>]+>";
static Pattern pattern = Pattern.compile(REGEX, Pattern.CASE_INSENSITIVE);
while (matcher.find()) {
      MatchResult result = matcher.toMatchResult();
      System.out.println(result.group(1));
    }