欢迎光临
我们一直在努力

IDEA+Springboot+JRebel热部署实现

mumupudding阅读(4)

步骤一:在IDEA中安装JRebel插件(File->settings->plugins->search in repositories),如下图

步骤二:安装完成之后,重启idea,破解JRebel插件(可以在help>JRebel>Activaction打开激活页面,也可在重启之后,直接点击右侧指导中进入激活页面)

选择License server方式(url可能失效,可以自行网上搜索)

Url:    http://139.199.89.239:1008/88414687-3b91-4286-89ba-2dc813b107ce

email:随便输入

 jrebel激活之后默认是联网使用的 , 在该模式下 , jrebel会一直联网监测激活信息 . 所以要调为离线使用的,步骤见下图

步骤三:设置IDEA为自动编译

步骤四:按住 Ctrl+Alt+Shift+/ 弹出,  选择Registry后勾选 

步骤五:选择springboot的入口类,右键选择debug with JRebel,等待启动完成即可。

java使用okhttp库实现Authorization认证请求

mumupudding阅读(4)

<!-- https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp -->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.9.1</version>
</dependency>
import okhttp3.*;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

public class HttpUtil {
    static public OkHttpClient client;

    static private Logger logger = Logger.getLogger(HttpUtil.class);

    static {

        client = new OkHttpClient.Builder()

                .connectTimeout(10000L, TimeUnit.MILLISECONDS)

                .readTimeout(10000L, TimeUnit.MILLISECONDS)

                .build();

    }

    /**
     47
     * 同步GET请求 带Authorization认证
     48
     */

    public static String okhttp_get(String get_url, HashMap<String, Object> get_data, String[] auth_base){


        final String credential = Credentials.basic(auth_base[0], auth_base[1]);

        String result = "";

        String data_params = generateParameters(get_data);

        String data_url = get_url + data_params;

        Request request = new Request.Builder()

                .url(data_url)

                .header("Authorization", credential)

                .get()

                .build();

        Call call = client.newCall(request);

        try {

            Response response = call.execute();

            //判断是否成功
            if (response.isSuccessful()){
                result = response.body().string();
            }else {
                return "请求失败";
            }
            logger.debug(result);


        } catch (Exception e) {

            logger.error("网络GET请求失败!提示信息:"+e.getMessage());

        }

        return result;

    }
    /**
     * Post请求 带Authorization认证
     * */
    public static String okhttp_post(String get_url, HashMap<String, Object> get_data, String[] auth_base){

        final String credential = Credentials.basic(auth_base[0], auth_base[1]);

        String result = "";

        RequestBody fromBody = generateParametersForPost(get_data).build();


        Request request = new Request.Builder()

                .url(get_url)

                .header("Authorization", credential)

                .post(fromBody)

                .build();

        Call call = client.newCall(request);

        try {

            Response response = call.execute();

            //判断是否成功
            if (response.isSuccessful()){
                result = response.body().string();
            }else {
                return "请求失败";
            }
            logger.debug(result);


        } catch (Exception e) {

            logger.error("网络GET请求失败!提示信息:"+e.getMessage());

        }

        return result;

    }

    //拼接参数

    private static String generateParameters(HashMap<String, Object> parameters) {

        String urlAttachment = "";

        if(parameters.size()>0){

            urlAttachment = "?";

            Object[] keys = parameters.keySet().toArray();

            for(Object key : keys)

            urlAttachment += key.toString() + "=" + parameters.get(key).toString() + "&";

            urlAttachment = urlAttachment.substring(0,urlAttachment.length()-1);
        }

        return urlAttachment;
    }

    //拼接参数用于POST请求

    private static FormBody.Builder generateParametersForPost(HashMap<String, Object> parameters) {

        FormBody.Builder builder = new FormBody.Builder();

        if(parameters.size()>0){
            Object[] keys = parameters.keySet().toArray();

            for(Object key : keys){
                Object ff = parameters.get(key);
                String aa = parameters.get(key).toString();

                builder.add(key.toString(),parameters.get(key).toString());
            }
        }

        return builder;
    }

    /**
     * 发送私密API请求
     *
     * @paramget_url请求地址
     * @paramget_data 请求参数列表
     * @return          返回JSON数据
     */
    private String auth_get(){
        String result = "";
        String api_key = "apikey";
        String api_secret = "apisecret";
        //认证信息
        String[] baseauth = {api_key,api_secret};
        //请求的URL的参数
        HashMap<String, Object> get_data = new HashMap<>();
        get_data.put("page","1");       //page参数
        get_data.put("name","test");  //name参数
        result = HttpUtil.okhttp_get("http://www.superl.org/page-about.html", get_data, baseauth);
        return result;
    }


}

Objc Block实现分析

mumupudding阅读(5)


Objc Block实现分析

Block在iOS开发中使用的频率是很高的,使用的场景包括接口异步数据的回调(AFN)、UI事件的回调(BlockKits)、链式调用(Masonry)、容器数据的遍历回调(NSArray、NSDictionary),具体的用法以及使用Block的一些坑这里就不一一赘述了,本文会从源代码的层面分析我们常用的Block的底层实现原理,做到知其然知其所以然。

本文会从如下几个主题切入,层层递进分析Block的底层原理,还原Block本来的面目

  • 无参数无返回值,不使用变量的Block分析
  • 使用自动变量的Block分析
  • 使用__block修饰的变量的Block分析
  • Block引用分析

无参数无返回值,不使用变量的Block分析

有如下的源代码,创建一个简单的block,在block做的处理是打印一个字符串,然后执行这个block。接下来会从源码入手对此进行分析:block是如何执行的

// 无参数无返回值的Block分析int main(int argc, const char * argv[]) {    void (^blk) (void) = ^{        printf("block invoke");    };    blk();    return 0;}// 输出:block invoke

使用clang -rewrite-objc main.m命令重写为C++语法的源文件。从重写后的代码中看到,一个简单block定义和调用的代码变成了大几十行,不过该源代码还算是相对简单的,我们从main函数入口逐步的进行分析:

main函数中创建__main_block_impl_0类型的实例

__main_block_impl_0结构体是什么鬼呢?我们看__main_block_impl_0结构体的实现,包含了__block_impl结构体和__main_block_desc_0结构体指针,为了方便,现在页先不用管__main_block_desc_0结构体,目前他还没有真正的使用到,后面讲到的__block修饰自动变量以及Block对对象的引用关系,设计到内存的拷贝和释放的时候会使用到该变量。__block_impl结构体包含了4个成员,为了简单分析,我们只关注其中的FuncPtr成员的FuncPtr成员,这个也是最重要的成员,其它的先忽略不计。

__main_block_impl_0实例的初始化参数

第一个是__main_block_func_0函数的地址,__main_block_func_0函数是什么呢,其实就是Block执行的方法,可以看到里面的实现就是一个简单的打印而已,和我们定义在block中的实现一样的,该参数用于初始化impl成员的。至于第二个参数先不管,在这个例子木有用到。

__main_block_impl_0实例的调用

创建了__main_block_impl_0结构体类型的实例之后,接下来就是获取到里面的FuncPtr指针指向的方法进行调用,参数是__main_block_impl_0结构体类型的实例本身,上一步创建步骤可知,FuncPtr指针是一个函数指针指向__main_block_func_0函数的地址,所以这里本质上就是__main_block_func_0函数的调用,结构是打印字符串"block invoke",一个简单的block执行孙然在代码量上增加了,其实也不算复杂。

注意点:(__block_impl *)blk这里做了一个强制转换,blk是__main_block_impl_0类型的实例的指针,根据结构体的内存布局规则,该结构体的第一个成员为__block_impl 类型,所以可以强制转换为__block_impl *类型。

由上面的分析,可以得出如下的结论:使用变量的Block调用本质上是使用函数指针调用函数

// 使用`clang -rewrite-objc main.m`命令重写为C++语法的源文件,对结果分析如下// __block_impl结构体struct __block_impl {  void *isa;  int Flags;  int Reserved;  void *FuncPtr;};// `__main_block_impl_0`是block的结构体,包含了两个成员__block_impl类型的impl成员以及__main_block_desc_0类型的Desc成员;一个构造方法__main_block_impl_0,构造方法中初始化impl成员和Desc成员struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};// Block执行的方法static void __main_block_func_0(struct __main_block_impl_0 *__cself) {        printf("block invoke");}static struct __main_block_desc_0 {  size_t reserved;  size_t Block_size;} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};// 重写后的入口函数mainint main(int argc, const char * argv[]) {    // 创建__main_block_impl_0类型的实例,名称为blk,这里把改实例强制转换为`void(funcPtr *)(void)`型的方法指针,不懂为何,因为在下一步会把该方法指针强制转换为对应的结构体,也就是说`void(funcPtr *)(void)`型的方法指针并没有真实的使用到。    void (*blk) (void) =    ((void (*)())&__main_block_impl_0(                                      (void *)__main_block_func_0,                                      &__main_block_desc_0_DATA));    // blk是`__main_block_impl_0`类型的实例的指针,根绝结构体的内存布局规则,该结构体的第一个成员为`__block_impl` 类型,可以强制转换为`__block_impl *`类型,获取FuncPtr函数指针,强制转换为`(void (*)(__block_impl *))`类型的函数指针,然后执行这个函数指针对应的函数,参数为blk    // 由上面的步骤可知,Block的调用本质上是使用函数指针调用函数    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);    return 0;}

使用自动变量的Block分析

有如下的源代码,创建一个简单的block,在block做的处理是打印一个字符串,使用到外部的自动变量,然后执行这个block。接下来会从源码入手对此进行分析:block是如何使用外部的自动变量的

// 使用变量的Block分析// 源代码int main(int argc, const char * argv[]) {    int age = 100;    const char *name = "zyt";    void (^blk) (void) = ^{        printf("block invoke age = %d age = %s", age, name);    };    age = 101;    blk();    return 0;}// 输出:block invoke age = 100 age = zyt

使用clang -rewrite-objc main.m命令重写为C++语法的源文件。对比上一个的数据结构方法和调用步骤大体上是一样的,只针对变化的地方进行分析

__main_block_impl_0结构体的变化

增加了两个成员:int类型的age成员、char*类型的name成员,用于保存外部变量的值,在初始化的时候就会使用自动变量的值初始化这两个成员的值

调用的变化

__main_block_func_0函数的参数struct __main_block_impl_0 *__cself在这个例子有使用到了,因为两个自动变量对应的值被保存在__main_block_impl_0结构体中了,方法中有使用到这两个变量,直接从__main_block_impl_0结构体中获取这两个值,但是这两个值是独立于自动变量的存在了

由上面的分析,可以得出如下的结论:使用变量的Block调用本质上是使用函数指针调用函数,参数是保存在block的结构体中的,并且保存的值而不是引用

// 使用`clang -rewrite-objc main.m`命令重写为C++语法的源文件,对结果分析如下// __block_impl结构体struct __block_impl {  void *isa;  int Flags;  int Reserved;  void *FuncPtr;};// __main_block_impl_0,包含了四个成员__block_impl类型的impl成员、__main_block_desc_0类型的Desc成员、int类型的age成员、char*类型的name成员;一个构造方法__main_block_impl_0,构造方法中初始化impl成员、Desc成员、age成员和name成员,比起上一个改结构体多了两个成员,用于保存外部变量的值struct __main_block_impl_0 {  struct __block_impl impl;  struct __main_block_desc_0* Desc;  int age;  const char *name;  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, const char *_name, int flags=0) : age(_age), name(_name) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};// Block执行的方法static void __main_block_func_0(struct __main_block_impl_0 *__cself) {  int age = __cself->age; // bound by copy  const char *name = __cself->name; // bound by copy        printf("block invoke age = %d age = %s", age, name);    }static struct __main_block_desc_0 {  size_t reserved;  size_t Block_size;} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};// 重写后的入口函数main// 由上面的步骤可知,使用变量的Block调用本质上是使用函数指针调用函数,参数是保存在block的结构体中的,并且保存的值而不是引用int main(int argc, const char * argv[]) {    int age = 100;    const char *name = "zyt";    void (*blk) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, name));    age = 101;    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);    return 0;}

使用__block修饰的变量的Block分析

有如下的源代码,有个__block修饰的int类型的自动变量age,在block和变量age的作用域中分别作了修改,从输入的结果看看是两次都生效了,接下来会从源码入手对此进行分析:block中是如何处理__block修饰的自动变量,该自动变量的内存是如何变化的

// 源代码int main(int argc, const char * argv[]) {    __block int age = 100;    const char *name = "zyt";    void (^blk) (void) = ^{        age += 2;        printf("block invoke age = %d age = %s", age, name);    };    age += 1;    blk();}// 输出 :// block invoke age = 103 age = zyt

使用clang -rewrite-objc main.m命令重写为C++语法的源文件,对结果分析如下的注释,该转换后的代码做了些许的调整,包括代码的缩进和添加了结构体声明,使得该代码可以直接运行

// clang改写后的代码如下,以下代码是经过调整可以直接运行的struct __main_block_desc_0;// __block_impl结构体struct __block_impl {    void *isa;    int Flags;    int Reserved;    void *FuncPtr;};// `__block`修饰的变量对应的结构体,里面包含了该变量的原始值也就是age成员,另外还有一个奇怪的`__forwarding`成员,稍后我们会分析它的用处  struct __Block_byref_age_0 {    void *__isa;    __Block_byref_age_0 *__forwarding;    int __flags;    int __size;    int age;};// `__main_block_impl_0`结构体包含了四个成员`__block_impl`类型的impl成员、`__main_block_desc_0`类型的Desc成员、`__Block_byref_age_0 *`类型的age成员、char*类型的name成员;一个构造方法`__main_block_impl_0`,构造方法中初始化impl成员、Desc成员、age成员和name成员,比起上一个结构体的变化是age的类型变为了包装自动变量的结构体了struct __main_block_impl_0 {    __block_impl impl;    __main_block_desc_0* Desc;    const char *name;    __Block_byref_age_0 *age; // by ref    __main_block_impl_0(void *fp,                        struct __main_block_desc_0 *desc,                        const char *_name,                        __Block_byref_age_0 *_age,                        int flags=0) :    name(_name),    age(_age->__forwarding) {        impl.isa = &_NSConcreteStackBlock;        impl.Flags = flags;        impl.FuncPtr = fp;        Desc = desc;    }};// Block执行的方法static void __main_block_func_0(struct __main_block_impl_0 *__cself) {    __Block_byref_age_0 *age = __cself->age; // bound by ref    const char *name = __cself->name; // bound by copy        (age->__forwarding->age) += 2;    printf("block invoke age = %d age = %s", (age->__forwarding->age), name);}// Block拷贝函数static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {    _Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}// Block销毁函数static void __main_block_dispose_0(struct __main_block_impl_0*src) {    _Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}static struct __main_block_desc_0 {    size_t reserved;    size_t Block_size;    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);    void (*dispose)(struct __main_block_impl_0*);} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};// 重写后的入口函数mainint main(int argc, const char * argv[]) {    __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {        (void*)0,        (__Block_byref_age_0 *)&age,        0,        sizeof(__Block_byref_age_0),        100};    const char *name = "zyt";    struct __main_block_impl_0 blockImpl =    __main_block_impl_0((void *)__main_block_func_0,                        &__main_block_desc_0_DATA,                        name,                        (__Block_byref_age_0 *)&age,                        570425344);    void (*blk) (void) = ((void (*)())&blockImpl);    (age.__forwarding->age) += 1;    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);}

__block修饰自动变量转换为C++代码的结构体关系图:
__block修饰自动变量转换为C++代码的结构体关系图

该重写的代码大部分和上面分析过的例子代码是类似,发现增加了两个处理方法Block拷贝函数__main_block_copy_0和Block销毁函数__main_block_dispose_0,这两个函数会保存在__main_block_desc_0结构体的copydispose成员中;另外添加了一个__Block_byref_age_0结构体类型用户处理__block修饰的自动变量。以下针对这两点从源代码的角度进行一个分析

__main_block_copy_0方法中调用到的_Block_object_assign可以在 runtime.c 这里找到 ,主要看下_Block_object_assign方法里面的处理逻辑

  • flags参数值为8,是BLOCK_FIELD_IS_BYREF枚举对应的值,会走到_Block_byref_assign_copy方法的调用步骤
  • _Block_byref_assign_copy方法会在在堆上创建Block_byref对象,也就是Block对象,并且把栈上和堆上的Block对象的forwarding属性值都修改为指向堆上的Block对象,这样使用两个对象的修改值都会修改为同一个地方

栈上的__block自动变量__forwarding指向关系以及拷贝到堆上之后__forwarding指向关系如下图所示
栈上的自动变量指向关系以及拷贝到堆上之后指向关系如下图所示

具体使用到的代码和对应的注释如下:

/* * When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point * to do the assignment. */void _Block_object_assign(void *destAddr, const void *object, const int flags) {    //printf("_Block_object_assign(*%p, %p, %x)\n", destAddr, object, flags);    if ((flags & BLOCK_BYREF_CALLER) == BLOCK_BYREF_CALLER) {        if ((flags & BLOCK_FIELD_IS_WEAK) == BLOCK_FIELD_IS_WEAK) {            _Block_assign_weak(object, destAddr);        }        else {            // do *not* retain or *copy* __block variables whatever they are            _Block_assign((void *)object, destAddr);        }    }    // 代码会走到这个分支中,调用方法`_Block_byref_assign_copy`    else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {        // copying a __block reference from the stack Block to the heap        // flags will indicate if it holds a __weak reference and needs a special isa        _Block_byref_assign_copy(destAddr, object, flags);    }    // (this test must be before next one)    else if ((flags & BLOCK_FIELD_IS_BLOCK) == BLOCK_FIELD_IS_BLOCK) {        // copying a Block declared variable from the stack Block to the heap        _Block_assign(_Block_copy_internal(object, flags), destAddr);    }    // (this test must be after previous one)    else if ((flags & BLOCK_FIELD_IS_OBJECT) == BLOCK_FIELD_IS_OBJECT) {        //printf("retaining object at %p\n", object);        _Block_retain_object(object);        //printf("done retaining object at %p\n", object);        _Block_assign((void *)object, destAddr);    }}/* * Runtime entry points for maintaining the sharing knowledge of byref data blocks. * * A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data * Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr. * We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment it. * Otherwise we need to copy it and update the stack forwarding pointer * XXX We need to account for weak/nonretained read-write barriers. */static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {    struct Block_byref **destp = (struct Block_byref **)dest;    struct Block_byref *src = (struct Block_byref *)arg;            //printf("_Block_byref_assign_copy called, byref destp %p, src %p, flags %x\n", destp, src, flags);    //printf("src dump: %s\n", _Block_byref_dump(src));    if (src->forwarding->flags & BLOCK_IS_GC) {        ;   // don't need to do any more work    }    else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {        //printf("making copy\n");        // src points to stack        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));        // if its weak ask for an object (only matters under GC)        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);        copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack        // copy是拷贝到堆上的Block_byref类型对象,scr是原来的Block_byref类型对象,两者的forwarding成员都指向到堆上的Block_byref类型对象也就是copy,这样不管是在栈上修改__block修饰的变量(age.age = 102调用)还是在堆上修改__block修饰的变量()        copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)        src->forwarding = copy;  // patch stack to point to heap copy        copy->size = src->size;        if (isWeak) {            copy->isa = &_NSConcreteWeakBlockVariable;  // mark isa field so it gets weak scanning        }        if (src->flags & BLOCK_HAS_COPY_DISPOSE) {            // Trust copy helper to copy everything of interest            // If more than one field shows up in a byref block this is wrong XXX            copy->byref_keep = src->byref_keep;            copy->byref_destroy = src->byref_destroy;            (*src->byref_keep)(copy, src);        }        else {            // just bits.  Blast 'em using _Block_memmove in case they're __strong            _Block_memmove(                (void *)&copy->byref_keep,                (void *)&src->byref_keep,                src->size - sizeof(struct Block_byref_header));        }    }    // already copied to heap    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {        latching_incr_int(&src->forwarding->flags);    }    // assign byref data block pointer into new Block    _Block_assign(src->forwarding, (void **)destp);}

Block引用分析

Block强引用分析

// 定义类YTObject@interface YTObject : NSObject@property (nonatomic, strong) NSString *name;@property (nonatomic, copy) void (^blk)(void);- (void)testReferenceSelf;@end@implementation YTObject- (void)testReferenceSelf {    self.blk = ^ {        // 这里不管是使用self.name还是_name,从clang重写的代码上看,处理方式是一样的        printf("self.name = %s", self.name.UTF8String);    };    self.blk();}- (void)dealloc {    NSLog(@"==dealloc==");}@end// 使用YTObjectint main(int argc, const char * argv[]) {    YTObject *obj = [YTObject new];    obj.name = @"hello";    [obj testReferenceSelf];        return 0;}// 输出 :// self.name = hello

使用clang -rewrite-objc main.m命令重写为C++语法的源文件如下

static struct /*_method_list_t*/ { unsigned int entsize;  // sizeof(struct _objc_method) unsigned int method_count; struct _objc_method method_list[6];} _OBJC_$_INSTANCE_METHODS_YTObject __attribute__ ((used, section ("__DATA,__objc_const"))) = { sizeof(_objc_method), 6, {{(struct objc_selector *)"testReferenceSelf", "v16@0:8", (void *)_I_YTObject_testReferenceSelf}, {(struct objc_selector *)"dealloc", "v16@0:8", (void *)_I_YTObject_dealloc}, {(struct objc_selector *)"name", "@16@0:8", (void *)_I_YTObject_name}, {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_YTObject_setName_}, {(struct objc_selector *)"blk", "@?16@0:8", (void *)_I_YTObject_blk}, {(struct objc_selector *)"setBlk:", "v24@0:8@?16", (void *)_I_YTObject_setBlk_}}};struct __YTObject__testReferenceSelf_block_impl_0 {  struct __block_impl impl;  struct __YTObject__testReferenceSelf_block_desc_0* Desc;  YTObject *self;  __YTObject__testReferenceSelf_block_impl_0(void *fp, struct __YTObject__testReferenceSelf_block_desc_0 *desc, YTObject *_self, int flags=0) : self(_self) {    impl.isa = &_NSConcreteStackBlock;    impl.Flags = flags;    impl.FuncPtr = fp;    Desc = desc;  }};static void __YTObject__testReferenceSelf_block_func_0(struct __YTObject__testReferenceSelf_block_impl_0 *__cself) {    YTObject *self = __cself->self; // bound by copy    printf("self.name = %s", ((const char * _Nullable (*)(id, SEL))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")), sel_registerName("UTF8String")));}static void __YTObject__testReferenceSelf_block_copy_0(struct __YTObject__testReferenceSelf_block_impl_0*dst, struct __YTObject__testReferenceSelf_block_impl_0*src) {    _Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}static void __YTObject__testReferenceSelf_block_dispose_0(struct __YTObject__testReferenceSelf_block_impl_0*src) {    _Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}static struct __YTObject__testReferenceSelf_block_desc_0 {  size_t reserved;  size_t Block_size;  void (*copy)(struct __YTObject__testReferenceSelf_block_impl_0*, struct __YTObject__testReferenceSelf_block_impl_0*);  void (*dispose)(struct __YTObject__testReferenceSelf_block_impl_0*);} __YTObject__testReferenceSelf_block_desc_0_DATA = { 0, sizeof(struct __YTObject__testReferenceSelf_block_impl_0), __YTObject__testReferenceSelf_block_copy_0, __YTObject__testReferenceSelf_block_dispose_0};static void _I_YTObject_testReferenceSelf(YTObject * self, SEL _cmd) {    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setBlk:"), ((void (*)())&__YTObject__testReferenceSelf_block_impl_0((void *)__YTObject__testReferenceSelf_block_func_0, &__YTObject__testReferenceSelf_block_desc_0_DATA, self, 570425344)));    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("blk"))();}int main(int argc, const char * argv[]) {    YTObject *obj = ((YTObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YTObject"), sel_registerName("new"));    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)obj, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_fk_19cr58zj0f7f19001k_mxzlm0000gp_T_main_21b52d_mii_1);    ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("testReferenceSelf"));    return 0;}

Block引用对象转换为C++代码的结构体关系图:
Block引用对象转换为C++代码的结构体关系图

从类图上可以明显的看到__YTObject__testReferenceSelf_block_impl_0YTObject之间有循环依赖的关系,这样NSLog(@"==dealloc==");这段代码最终是没有调用到,也就是这里会出现Block循环引用导致内存泄漏问题

Block弱引用分析

Block的循环引用问题其中一种解决方案是可以使用weakself来解除这种强引用关系,防止内存的泄漏,代码的改造如下

@interface YTObject : NSObject@property (nonatomic, strong) NSString *name;@property (nonatomic, copy) void (^blk)(void);- (void)testReferenceSelf;@end@implementation YTObject- (void)testReferenceSelf {    __weak typeof(self) weakself = self;    self.blk = ^ {        __strong typeof(self) strongself = weakself;        // 这里不管是使用self.name还是_name,从clang重写的代码上看,处理方式是一样的        printf("self.name = %s\n", strongself.name.UTF8String);    };    self.blk();}- (void)dealloc {    printf("==dealloc==");}@end// 使用YTObjectint main(int argc, const char * argv[]) {    YTObject *obj = [YTObject new];    obj.name = @"hello";    [obj testReferenceSelf];        return 0;}

添加了weak之后需要使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.14 main.mm这个命令才能够重写为C++语言对应的代码,重写后的代码如下

static struct /*_method_list_t*/ {    unsigned int entsize;  // sizeof(struct _objc_method)    unsigned int method_count;    struct _objc_method method_list[6];} _OBJC_$_INSTANCE_METHODS_YTObject __attribute__ ((used, section ("__DATA,__objc_const"))) = {    sizeof(_objc_method),    6,    {{(struct objc_selector *)"testReferenceSelf", "v16@0:8", (void *)_I_YTObject_testReferenceSelf},        {(struct objc_selector *)"dealloc", "v16@0:8", (void *)_I_YTObject_dealloc},        {(struct objc_selector *)"name", "@16@0:8", (void *)_I_YTObject_name},        {(struct objc_selector *)"setName:", "v24@0:8@16", (void *)_I_YTObject_setName_},        {(struct objc_selector *)"blk", "@?16@0:8", (void *)_I_YTObject_blk},        {(struct objc_selector *)"setBlk:", "v24@0:8@?16", (void *)_I_YTObject_setBlk_}}};struct __YTObject__testReferenceSelf_block_impl_0 {    struct __block_impl impl;    struct __YTObject__testReferenceSelf_block_desc_0* Desc;    YTObject *const __weak weakself;    __YTObject__testReferenceSelf_block_impl_0(void *fp, struct __YTObject__testReferenceSelf_block_desc_0 *desc, YTObject *const __weak _weakself, int flags=0) : weakself(_weakself) {        impl.isa = &_NSConcreteStackBlock;        impl.Flags = flags;        impl.FuncPtr = fp;        Desc = desc;    }};static void __YTObject__testReferenceSelf_block_func_0(struct __YTObject__testReferenceSelf_block_impl_0 *__cself) {    YTObject *const __weak weakself = __cself->weakself; // bound by copy    __attribute__((objc_ownership(strong))) typeof(self) strongself = weakself;    printf("self.name = %s\n", ((const char * _Nullable (*)(id, SEL))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)strongself, sel_registerName("name")), sel_registerName("UTF8String")));}static void __YTObject__testReferenceSelf_block_copy_0(struct __YTObject__testReferenceSelf_block_impl_0*dst, struct __YTObject__testReferenceSelf_block_impl_0*src) {    _Block_object_assign((void*)&dst->weakself, (void*)src->weakself, 3/*BLOCK_FIELD_IS_OBJECT*/);}static void __YTObject__testReferenceSelf_block_dispose_0(struct __YTObject__testReferenceSelf_block_impl_0*src) {    _Block_object_dispose((void*)src->weakself, 3/*BLOCK_FIELD_IS_OBJECT*/);}static struct __YTObject__testReferenceSelf_block_desc_0 {    size_t reserved;    size_t Block_size;    void (*copy)(struct __YTObject__testReferenceSelf_block_impl_0*, struct __YTObject__testReferenceSelf_block_impl_0*);    void (*dispose)(struct __YTObject__testReferenceSelf_block_impl_0*);} __YTObject__testReferenceSelf_block_desc_0_DATA = { 0, sizeof(struct __YTObject__testReferenceSelf_block_impl_0), __YTObject__testReferenceSelf_block_copy_0, __YTObject__testReferenceSelf_block_dispose_0};static void _I_YTObject_testReferenceSelf(YTObject * self, SEL _cmd) {    __attribute__((objc_ownership(weak))) typeof(self) weakself = self;    __YTObject__testReferenceSelf_block_impl_0 blockImpl =    __YTObject__testReferenceSelf_block_impl_0((void *)__YTObject__testReferenceSelf_block_func_0,                                               &__YTObject__testReferenceSelf_block_desc_0_DATA,                                               weakself,                                               570425344)    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, s                                                          el_registerName("setBlk:"),                                                          ((void (*)())&blockImpl));    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("blk"))();}int main(int argc, const char * argv[]) {    YTObject *obj = ((YTObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YTObject"), sel_registerName("new"));    ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)obj, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_fk_19cr58zj0f7f19001k_mxzlm0000gp_T_main_6f39dd_mii_0);    ((void (*)(id, SEL))(void *)objc_msgSend)((id)obj, sel_registerName("testReferenceSelf"));        return 0;}

从上面的代码中可以看到__YTObject__testReferenceSelf_block_impl_0结构体中weakself成员是一个__weak修饰的YTObject类型对象,也就是说__YTObject__testReferenceSelf_block_impl_0YTObject的依赖是弱依赖。weak修饰变量是在runtime中进行处理的,在YTObject对象的Dealloc方法中会调用weak引用的处理方法,从weak_table中寻找弱引用的依赖对象,进行清除处理,可以查看Runtime源码中objc_object::clearDeallocating该方法的处理逻辑,另外关于__weak修饰的变量的详细处理可以查看Runtime相关的知识

Block弱引用对象转换为C++代码的结构体关系图:
Block弱引用对象转换为C++代码的结构体关系图

关于具体的weakSelf和strongSelf可以参考这篇文章深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用中的描述

weakSelf 是为了block不持有self,避免Retain Circle循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。

结束

以上就是关于Block底层实现的一些分析,不妥之处敬请指教

参考

深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用

探索Java日志的奥秘:底层日志系统-log4j2

mumupudding阅读(5)

前言

log4j2是apache在log4j的基础上,参考logback架构实现的一套新的日志系统(我感觉是apache害怕logback了)。
log4j2的官方文档上写着一些它的优点:

  • 在拥有全部logback特性的情况下,还修复了一些隐藏问题
  • API 分离:现在log4j2也是门面模式使用日志,默认的日志实现是log4j2,当然你也可以用logback(应该没有人会这么做)
  • 性能提升:log4j2包含下一代基于LMAX Disruptor library的异步logger,在多线程场景下,拥有18倍于log4j和logback的性能
  • 多API支持:log4j2提供Log4j 1.2, SLF4J, Commons Logging and java.util.logging (JUL) 的API支持
  • 避免锁定:使用Log4j2 API的应用程序始终可以选择使用任何符合SLF4J的库作为log4j-to-slf4j适配器的记录器实现
  • 自动重新加载配置:与Logback一样,Log4j 2可以在修改时自动重新加载其配置。与Logback不同,它会在重新配置发生时不会丢失日志事件。
  • 高级过滤: 与Logback一样,Log4j 2支持基于Log事件中的上下文数据,标记,正则表达式和其他组件进行过滤。
  • 插件架构: Log4j使用插件模式配置组件。因此,您无需编写代码来创建和配置Appender,Layout,Pattern Converter等。Log4j自动识别插件并在配置引用它们时使用它们。
  • 属性支持:您可以在配置中引用属性,Log4j将直接替换它们,或者Log4j将它们传递给将动态解析它们的底层组件。
  • Java 8 Lambda支持
  • 自定义日志级别
  • 产生垃圾少:在稳态日志记录期间,Log4j 2 在独立应用程序中是无垃圾的,在Web应用程序中是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。
  • 和应用server集成:版本2.10.0引入了一个模块log4j-appserver,以改进与Apache Tomcat和Eclipse Jetty的集成。

Log4j2类图:

这次从四个地方去探索源码:启动,配置,异步,插件化

源码探索

启动

log4j2的关键组件

  • LogManager

根据配置指定LogContexFactory,初始化对应的LoggerContext

  • LoggerContext

1、解析配置文件,解析为对应的java对象。
2、通过LoggerRegisty缓存Logger配置
3、Configuration配置信息
4、start方法解析配置文件,转化为对应的java对象
5、通过getLogger获取logger对象

  • Logger

LogManaer

该组件是Log4J启动的入口,后续的LoggerContext以及Logger都是通过调用LogManager的静态方法获得。我们可以使用下面的代码获取Logger

Logger logger = LogManager.getLogger();

可以看出LogManager是十分关键的组件,因此在这个小节中我们详细分析LogManager的启动流程。
LogManager启动的入口是下面的static代码块:

/**
     * Scans the classpath to find all logging implementation. Currently, only one will be used but this could be
     * extended to allow multiple implementations to be used.
     */
    static {
        // Shortcut binding to force a specific logging implementation.
        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
        final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME);
        if (factoryClassName != null) {
            try {
                factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
            } catch (final ClassNotFoundException cnfe) {
                LOGGER.error("Unable to locate configured LoggerContextFactory {}", factoryClassName);
            } catch (final Exception ex) {
                LOGGER.error("Unable to create configured LoggerContextFactory {}", factoryClassName, ex);
            }
        }

        if (factory == null) {
            final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();
            // note that the following initial call to ProviderUtil may block until a Provider has been installed when
            // running in an OSGi environment
            if (ProviderUtil.hasProviders()) {
                for (final Provider provider : ProviderUtil.getProviders()) {
                    final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
                    if (factoryClass != null) {
                        try {
                            factories.put(provider.getPriority(), factoryClass.newInstance());
                        } catch (final Exception e) {
                            LOGGER.error("Unable to create class {} specified in provider URL {}", factoryClass.getName(), provider
                                    .getUrl(), e);
                        }
                    }
                }

                if (factories.isEmpty()) {
                    LOGGER.error("Log4j2 could not find a logging implementation. "
                            + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                    factory = new SimpleLoggerContextFactory();
                } else if (factories.size() == 1) {
                    factory = factories.get(factories.lastKey());
                } else {
                    final StringBuilder sb = new StringBuilder("Multiple logging implementations found: \n");
                    for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) {
                        sb.append("Factory: ").append(entry.getValue().getClass().getName());
                        sb.append(", Weighting: ").append(entry.getKey()).append('\n');
                    }
                    factory = factories.get(factories.lastKey());
                    sb.append("Using factory: ").append(factory.getClass().getName());
                    LOGGER.warn(sb.toString());

                }
            } else {
                LOGGER.error("Log4j2 could not find a logging implementation. "
                        + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                factory = new SimpleLoggerContextFactory();
            }
        }
    }

这段静态代码段主要分为下面的几个步骤:

  1. 首先根据特定配置文件的配置信息获取loggerContextFactory
  2. 如果没有找到对应的Factory的实现类则通过ProviderUtil中的getProviders()方法载入providers,随后通过provider的loadLoggerContextFactory方法载入LoggerContextFactory的实现类
  3. 如果provider中没有获取到LoggerContextFactory的实现类或provider为空,则使用SimpleLoggerContextFactory作为LoggerContextFactory。

根据配置文件载入LoggerContextFactory

// Shortcut binding to force a specific logging implementation.
        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
        final String factoryClassName = managerProps.getStringProperty(FACTORY_PROPERTY_NAME);
        if (factoryClassName != null) {
            try {
                factory = LoaderUtil.newCheckedInstanceOf(factoryClassName, LoggerContextFactory.class);
            } catch (final ClassNotFoundException cnfe) {
                LOGGER.error("Unable to locate configured LoggerContextFactory {}", factoryClassName);
            } catch (final Exception ex) {
                LOGGER.error("Unable to create configured LoggerContextFactory {}", factoryClassName, ex);
            }
        }

在这段逻辑中,LogManager优先通过配置文件”log4j2.component.properties”通过配置项”log4j2.loggerContextFactory”来获取LoggerContextFactory,如果用户做了对应的配置,通过newCheckedInstanceOf方法实例化LoggerContextFactory的对象,最终的实现方式为:

public static <T> T newInstanceOf(final Class<T> clazz)
            throws InstantiationException, IllegalAccessException, InvocationTargetException {
        try {
            return clazz.getConstructor().newInstance();
        } catch (final NoSuchMethodException ignored) {
            // FIXME: looking at the code for Class.newInstance(), this seems to do the same thing as above
            return clazz.newInstance();
        }
    }

在默认情况下,不存在初始的默认配置文件log4j2.component.properties,因此需要从其他途径获取LoggerContextFactory。

通过Provider实例化LoggerContextFactory对象

代码:

if (factory == null) {
            final SortedMap<Integer, LoggerContextFactory> factories = new TreeMap<>();
            // note that the following initial call to ProviderUtil may block until a Provider has been installed when
            // running in an OSGi environment
            if (ProviderUtil.hasProviders()) {
                for (final Provider provider : ProviderUtil.getProviders()) {
                    final Class<? extends LoggerContextFactory> factoryClass = provider.loadLoggerContextFactory();
                    if (factoryClass != null) {
                        try {
                            factories.put(provider.getPriority(), factoryClass.newInstance());
                        } catch (final Exception e) {
                            LOGGER.error("Unable to create class {} specified in provider URL {}", factoryClass.getName(), provider
                                    .getUrl(), e);
                        }
                    }
                }

                if (factories.isEmpty()) {
                    LOGGER.error("Log4j2 could not find a logging implementation. "
                            + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                    factory = new SimpleLoggerContextFactory();
                } else if (factories.size() == 1) {
                    factory = factories.get(factories.lastKey());
                } else {
                    final StringBuilder sb = new StringBuilder("Multiple logging implementations found: \n");
                    for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) {
                        sb.append("Factory: ").append(entry.getValue().getClass().getName());
                        sb.append(", Weighting: ").append(entry.getKey()).append('\n');
                    }
                    factory = factories.get(factories.lastKey());
                    sb.append("Using factory: ").append(factory.getClass().getName());
                    LOGGER.warn(sb.toString());

                }
            } else {
                LOGGER.error("Log4j2 could not find a logging implementation. "
                        + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                factory = new SimpleLoggerContextFactory();
            }
        }

这里比较有意思的是hasProviders和getProviders都会通过线程安全的方式去懒加载ProviderUtil这个对象。跟进lazyInit方法:

protected static void lazyInit() {
        //noinspection DoubleCheckedLocking
        if (INSTANCE == null) {
            try {
                STARTUP_LOCK.lockInterruptibly();
                if (INSTANCE == null) {
                    INSTANCE = new ProviderUtil();
                }
            } catch (final InterruptedException e) {
                LOGGER.fatal("Interrupted before Log4j Providers could be loaded.", e);
                Thread.currentThread().interrupt();
            } finally {
                STARTUP_LOCK.unlock();
            }
        }
    }

再看构造方法:

private ProviderUtil() {
        for (final LoaderUtil.UrlResource resource : LoaderUtil.findUrlResources(PROVIDER_RESOURCE)) {
            loadProvider(resource.getUrl(), resource.getClassLoader());
        }
    }

这里的懒加载其实就是懒加载Provider对象。在创建新的providerUtil实例的过程中就会直接实例化provider对象,其过程是先通过getClassLoaders方法获取provider的类加载器,然后通过loadProviders(classLoader);加载类。在providerUtil实例化的最后,会统一查找”META-INF/log4j-provider.properties”文件中对应的provider的url,会考虑从远程加载provider。而loadProviders方法就是在ProviderUtil的PROVIDERS列表中添加对一个的provider。可以看到默认的provider是org.apache.logging.log4j.core.impl.Log4jContextFactory

LoggerContextFactory = org.apache.logging.log4j.core.impl.Log4jContextFactory
Log4jAPIVersion = 2.1.0
FactoryPriority= 10

很有意思的是这里懒加载加上了锁,而且使用的是
lockInterruptibly这个方法。lockInterruptibly和lock的区别如下:

lock 与 lockInterruptibly比较区别在于:

lock 优先考虑获取锁,待获取锁成功后,才响应中断。
lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的
Thread.interrupt 方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。 ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。

上面有一句注释值得注意:

/**
     * Guards the ProviderUtil singleton instance from lazy initialization. This is primarily used for OSGi support.
     *
     * @since 2.1
     */
    protected static final Lock STARTUP_LOCK = new ReentrantLock();
    // STARTUP_LOCK guards INSTANCE for lazy initialization; this allows the OSGi Activator to pause the startup and
    // wait for a Provider to be installed. See LOG4J2-373
    private static volatile ProviderUtil INSTANCE;

原来这里是为了让osgi可以阻止启动。
再回到logManager:
可以看到在加载完Provider之后,会做factory的绑定:

if (factories.isEmpty()) {
                    LOGGER.error("Log4j2 could not find a logging implementation. "
                            + "Please add log4j-core to the classpath. Using SimpleLogger to log to the console...");
                    factory = new SimpleLoggerContextFactory();
                } else if (factories.size() == 1) {
                    factory = factories.get(factories.lastKey());
                } else {
                    final StringBuilder sb = new StringBuilder("Multiple logging implementations found: \n");
                    for (final Map.Entry<Integer, LoggerContextFactory> entry : factories.entrySet()) {
                        sb.append("Factory: ").append(entry.getValue().getClass().getName());
                        sb.append(", Weighting: ").append(entry.getKey()).append('\n');
                    }
                    factory = factories.get(factories.lastKey());
                    sb.append("Using factory: ").append(factory.getClass().getName());
                    LOGGER.warn(sb.toString());

                }

到这里,logmanager的启动流程就结束了。

配置

在不使用slf4j的情况下,我们获取logger的方式是这样的:

Logger logger = logManager.getLogger(xx.class)

跟进getLogger方法:

    public static Logger getLogger(final Class<?> clazz) {
        final Class<?> cls = callerClass(clazz);
        return getContext(cls.getClassLoader(), false).getLogger(toLoggerName(cls));
    }

这里有一个getContext方法,跟进,

public static LoggerContext getContext(final ClassLoader loader, final boolean currentContext) {
        try {
            return factory.getContext(FQCN, loader, null, currentContext);
        } catch (final IllegalStateException ex) {
            LOGGER.warn(ex.getMessage() + " Using SimpleLogger");
            return new SimpleLoggerContextFactory().getContext(FQCN, loader, null, currentContext);
        }
    }

上文提到factory的具体实现是Log4jContextFactory,跟进getContext
方法:

public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
                                    final boolean currentContext) {
        final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext);
        if (externalContext != null && ctx.getExternalContext() == null) {
            ctx.setExternalContext(externalContext);
        }
        if (ctx.getState() == LifeCycle.State.INITIALIZED) {
            ctx.start();
        }
        return ctx;
    }

直接看start:

public void start() {
        LOGGER.debug("Starting LoggerContext[name={}, {}]...", getName(), this);
        if (PropertiesUtil.getProperties().getBooleanProperty("log4j.LoggerContext.stacktrace.on.start", false)) {
            LOGGER.debug("Stack trace to locate invoker",
                    new Exception("Not a real error, showing stack trace to locate invoker"));
        }
        if (configLock.tryLock()) {
            try {
                if (this.isInitialized() || this.isStopped()) {
                    this.setStarting();
                    reconfigure();
                    if (this.configuration.isShutdownHookEnabled()) {
                        setUpShutdownHook();
                    }
                    this.setStarted();
                }
            } finally {
                configLock.unlock();
            }
        }
        LOGGER.debug("LoggerContext[name={}, {}] started OK.", getName(), this);
    }

发现其中的核心方法是reconfigure方法,继续跟进:

private void reconfigure(final URI configURI) {
        final ClassLoader cl = ClassLoader.class.isInstance(externalContext) ? (ClassLoader) externalContext : null;
        LOGGER.debug("Reconfiguration started for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
                contextName, configURI, this, cl);
        final Configuration instance = ConfigurationFactory.getInstance().getConfiguration(this, contextName, configURI, cl);
        if (instance == null) {
            LOGGER.error("Reconfiguration failed: No configuration found for '{}' at '{}' in '{}'", contextName, configURI, cl);
        } else {
            setConfiguration(instance);
            /*
             * instance.start(); Configuration old = setConfiguration(instance); updateLoggers(); if (old != null) {
             * old.stop(); }
             */
            final String location = configuration == null ? "?" : String.valueOf(configuration.getConfigurationSource());
            LOGGER.debug("Reconfiguration complete for context[name={}] at URI {} ({}) with optional ClassLoader: {}",
                    contextName, location, this, cl);
        }
    }

可以看到每一个configuration都是从ConfigurationFactory拿出来的,我们先看看这个类的getInstance看看:

public static ConfigurationFactory getInstance() {
        // volatile works in Java 1.6+, so double-checked locking also works properly
        //noinspection DoubleCheckedLocking
        if (factories == null) {
            LOCK.lock();
            try {
                if (factories == null) {
                    final List<ConfigurationFactory> list = new ArrayList<ConfigurationFactory>();
                    final String factoryClass = PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FACTORY_PROPERTY);
                    if (factoryClass != null) {
                        addFactory(list, factoryClass);
                    }
                    final PluginManager manager = new PluginManager(CATEGORY);
                    manager.collectPlugins();
                    final Map<String, PluginType<?>> plugins = manager.getPlugins();
                    final List<Class<? extends ConfigurationFactory>> ordered =
                        new ArrayList<Class<? extends ConfigurationFactory>>(plugins.size());
                    for (final PluginType<?> type : plugins.values()) {
                        try {
                            ordered.add(type.getPluginClass().asSubclass(ConfigurationFactory.class));
                        } catch (final Exception ex) {
                            LOGGER.warn("Unable to add class {}", type.getPluginClass(), ex);
                        }
                    }
                    Collections.sort(ordered, OrderComparator.getInstance());
                    for (final Class<? extends ConfigurationFactory> clazz : ordered) {
                        addFactory(list, clazz);
                    }
                    // see above comments about double-checked locking
                    //noinspection NonThreadSafeLazyInitialization
                    factories = Collections.unmodifiableList(list);
                }
            } finally {
                LOCK.unlock();
            }
        }

        LOGGER.debug("Using configurationFactory {}", configFactory);
        return configFactory;
    }

这里可以看到ConfigurationFactory中利用了PluginManager来进行初始化,PluginManager会将ConfigurationFactory的子类加载进来,默认使用的XmlConfigurationFactory,JsonConfigurationFactory,YamlConfigurationFactory这三个子类,这里插件化加载暂时按下不表。
回到reconfigure这个方法,我们看到获取ConfigurationFactory实例之后会去调用getConfiguration方法:

public Configuration getConfiguration(final String name, final URI configLocation, final ClassLoader loader) {
        if (!isActive()) {
            return null;
        }
        if (loader == null) {
            return getConfiguration(name, configLocation);
        }
        if (isClassLoaderUri(configLocation)) {
            final String path = extractClassLoaderUriPath(configLocation);
            final ConfigurationSource source = getInputFromResource(path, loader);
            if (source != null) {
                final Configuration configuration = getConfiguration(source);
                if (configuration != null) {
                    return configuration;
                }
            }
        }
        return getConfiguration(name, configLocation);
    }

跟进getConfiguration,这里值得注意的是有很多个getConfiguration,注意甄别,如果不确定的话可以通过debug的方式来确定。

public Configuration getConfiguration(final String name, final URI configLocation) {

            if (configLocation == null) {
                final String config = this.substitutor.replace(
                    PropertiesUtil.getProperties().getStringProperty(CONFIGURATION_FILE_PROPERTY));
                if (config != null) {
                    ConfigurationSource source = null;
                    try {
                        source = getInputFromUri(FileUtils.getCorrectedFilePathUri(config));
                    } catch (final Exception ex) {
                        // Ignore the error and try as a String.
                        LOGGER.catching(Level.DEBUG, ex);
                    }
                    if (source == null) {
                        final ClassLoader loader = LoaderUtil.getThreadContextClassLoader();
                        source = getInputFromString(config, loader);
                    }
                    if (source != null) {
                        for (final ConfigurationFactory factory : factories) {
                            final String[] types = factory.getSupportedTypes();
                            if (types != null) {
                                for (final String type : types) {
                                    if (type.equals("*") || config.endsWith(type)) {
                                        final Configuration c = factory.getConfiguration(source);
                                        if (c != null) {
                                            return c;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } else {
                for (final ConfigurationFactory factory : factories) {
                    final String[] types = factory.getSupportedTypes();
                    if (types != null) {
                        for (final String type : types) {
                            if (type.equals("*") || configLocation.toString().endsWith(type)) {
                                final Configuration config = factory.getConfiguration(name, configLocation);
                                if (config != null) {
                                    return config;
                                }
                            }
                        }
                    }
                }
            }

            Configuration config = getConfiguration(true, name);
            if (config == null) {
                config = getConfiguration(true, null);
                if (config == null) {
                    config = getConfiguration(false, name);
                    if (config == null) {
                        config = getConfiguration(false, null);
                    }
                }
            }
            if (config != null) {
                return config;
            }
            LOGGER.error("No log4j2 configuration file found. Using default configuration: logging only errors to the console.");
            return new DefaultConfiguration();
        }

这里就会根据之前加载进来的factory进行配置的获取,具体的不再解析。
回到reconfigure,之后的步骤就是setConfiguration,入参就是刚才获取的config

private synchronized Configuration setConfiguration(final Configuration config) {
        Assert.requireNonNull(config, "No Configuration was provided");
        final Configuration prev = this.config;
        config.addListener(this);
        final ConcurrentMap<String, String> map = config.getComponent(Configuration.CONTEXT_PROPERTIES);

        try { // LOG4J2-719 network access may throw android.os.NetworkOnMainThreadException
            map.putIfAbsent("hostName", NetUtils.getLocalHostname());
        } catch (final Exception ex) {
            LOGGER.debug("Ignoring {}, setting hostName to 'unknown'", ex.toString());
            map.putIfAbsent("hostName", "unknown");
        }
        map.putIfAbsent("contextName", name);
        config.start();
        this.config = config;
        updateLoggers();
        if (prev != null) {
            prev.removeListener(this);
            prev.stop();
        }

        firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, prev, config));

        try {
            Server.reregisterMBeansAfterReconfigure();
        } catch (final Throwable t) {
            // LOG4J2-716: Android has no java.lang.management
            LOGGER.error("Could not reconfigure JMX", t);
        }
        return prev;
    }

这个方法最重要的步骤就是config.start,这才是真正做配置解析的

public void start() {
        LOGGER.debug("Starting configuration {}", this);
        this.setStarting();
        pluginManager.collectPlugins(pluginPackages);
        final PluginManager levelPlugins = new PluginManager(Level.CATEGORY);
        levelPlugins.collectPlugins(pluginPackages);
        final Map<String, PluginType<?>> plugins = levelPlugins.getPlugins();
        if (plugins != null) {
            for (final PluginType<?> type : plugins.values()) {
                try {
                    // Cause the class to be initialized if it isn't already.
                    Loader.initializeClass(type.getPluginClass().getName(), type.getPluginClass().getClassLoader());
                } catch (final Exception e) {
                    LOGGER.error("Unable to initialize {} due to {}", type.getPluginClass().getName(), e.getClass()
                            .getSimpleName(), e);
                }
            }
        }
        setup();
        setupAdvertisement();
        doConfigure();
        final Set<LoggerConfig> alreadyStarted = new HashSet<LoggerConfig>();
        for (final LoggerConfig logger : loggers.values()) {
            logger.start();
            alreadyStarted.add(logger);
        }
        for (final Appender appender : appenders.values()) {
            appender.start();
        }
        if (!alreadyStarted.contains(root)) { // LOG4J2-392
            root.start(); // LOG4J2-336
        }
        super.start();
        LOGGER.debug("Started configuration {} OK.", this);
    }

这里面有如下步骤:

  1. 获取日志等级的插件
  2. 初始化
  3. 初始化Advertiser
  4. 配置

先看一下初始化,也就是setup这个方法,setup是一个需要被复写的方法,我们以XMLConfiguration作为例子,

@Override
    public void setup() {
        if (rootElement == null) {
            LOGGER.error("No logging configuration");
            return;
        }
        constructHierarchy(rootNode, rootElement);
        if (status.size() > 0) {
            for (final Status s : status) {
                LOGGER.error("Error processing element {}: {}", s.name, s.errorType);
            }
            return;
        }
        rootElement = null;
    }

发现这里面有一个比较重要的方法constructHierarchy,跟进:

private void constructHierarchy(final Node node, final Element element) {
        processAttributes(node, element);
        final StringBuilder buffer = new StringBuilder();
        final NodeList list = element.getChildNodes();
        final List<Node> children = node.getChildren();
        for (int i = 0; i < list.getLength(); i++) {
            final org.w3c.dom.Node w3cNode = list.item(i);
            if (w3cNode instanceof Element) {
                final Element child = (Element) w3cNode;
                final String name = getType(child);
                final PluginType<?> type = pluginManager.getPluginType(name);
                final Node childNode = new Node(node, name, type);
                constructHierarchy(childNode, child);
                if (type == null) {
                    final String value = childNode.getValue();
                    if (!childNode.hasChildren() && value != null) {
                        node.getAttributes().put(name, value);
                    } else {
                        status.add(new Status(name, element, ErrorType.CLASS_NOT_FOUND));
                    }
                } else {
                    children.add(childNode);
                }
            } else if (w3cNode instanceof Text) {
                final Text data = (Text) w3cNode;
                buffer.append(data.getData());
            }
        }

        final String text = buffer.toString().trim();
        if (text.length() > 0 || (!node.hasChildren() && !node.isRoot())) {
            node.setValue(text);
        }
    }

发现这个就是一个树遍历的过程。诚然,配置文件是以xml的形式给出的,xml的结构就是一个树形结构。回到start方法,跟进doConfiguration:

protected void doConfigure() {
        if (rootNode.hasChildren() && rootNode.getChildren().get(0).getName().equalsIgnoreCase("Properties")) {
            final Node first = rootNode.getChildren().get(0);
            createConfiguration(first, null);
            if (first.getObject() != null) {
                subst.setVariableResolver((StrLookup) first.getObject());
            }
        } else {
            final Map<String, String> map = this.getComponent(CONTEXT_PROPERTIES);
            final StrLookup lookup = map == null ? null : new MapLookup(map);
            subst.setVariableResolver(new Interpolator(lookup, pluginPackages));
        }

        boolean setLoggers = false;
        boolean setRoot = false;
        for (final Node child : rootNode.getChildren()) {
            if (child.getName().equalsIgnoreCase("Properties")) {
                if (tempLookup == subst.getVariableResolver()) {
                    LOGGER.error("Properties declaration must be the first element in the configuration");
                }
                continue;
            }
            createConfiguration(child, null);
            if (child.getObject() == null) {
                continue;
            }
            if (child.getName().equalsIgnoreCase("Appenders")) {
                appenders = child.getObject();
            } else if (child.isInstanceOf(Filter.class)) {
                addFilter(child.getObject(Filter.class));
            } else if (child.getName().equalsIgnoreCase("Loggers")) {
                final Loggers l = child.getObject();
                loggers = l.getMap();
                setLoggers = true;
                if (l.getRoot() != null) {
                    root = l.getRoot();
                    setRoot = true;
                }
            } else if (child.getName().equalsIgnoreCase("CustomLevels")) {
                customLevels = child.getObject(CustomLevels.class).getCustomLevels();
            } else if (child.isInstanceOf(CustomLevelConfig.class)) {
                final List<CustomLevelConfig> copy = new ArrayList<CustomLevelConfig>(customLevels);
                copy.add(child.getObject(CustomLevelConfig.class));
                customLevels = copy;
            } else {
                LOGGER.error("Unknown object \"{}\" of type {} is ignored.", child.getName(),
                        child.getObject().getClass().getName());
            }
        }

        if (!setLoggers) {
            LOGGER.warn("No Loggers were configured, using default. Is the Loggers element missing?");
            setToDefault();
            return;
        } else if (!setRoot) {
            LOGGER.warn("No Root logger was configured, creating default ERROR-level Root logger with Console appender");
            setToDefault();
            // return; // LOG4J2-219: creating default root=ok, but don't exclude configured Loggers
        }

        for (final Map.Entry<String, LoggerConfig> entry : loggers.entrySet()) {
            final LoggerConfig l = entry.getValue();
            for (final AppenderRef ref : l.getAppenderRefs()) {
                final Appender app = appenders.get(ref.getRef());
                if (app != null) {
                    l.addAppender(app, ref.getLevel(), ref.getFilter());
                } else {
                    LOGGER.error("Unable to locate appender {} for logger {}", ref.getRef(), l.getName());
                }
            }

        }

        setParents();
    }

发现就是对刚刚获取的configuration进行解析,然后塞进正确的地方。回到start方法,可以看到昨晚配置之后就是开启logger和appender了。

异步

AsyncAppender

log4j2突出于其他日志的优势,异步日志实现。我们先从日志打印看进去。找到Logger,随便找一个log日志的方法。

public void debug(final Marker marker, final Message msg) {
        logIfEnabled(FQCN, Level.DEBUG, marker, msg, msg != null ? msg.getThrowable() : null);
    }

一路跟进

@PerformanceSensitive
    // NOTE: This is a hot method. Current implementation compiles to 29 bytes of byte code.
    // This is within the 35 byte MaxInlineSize threshold. Modify with care!
    private void logMessageTrackRecursion(final String fqcn,
                                          final Level level,
                                          final Marker marker,
                                          final Message msg,
                                          final Throwable throwable) {
        try {
            incrementRecursionDepth(); // LOG4J2-1518, LOG4J2-2031
            tryLogMessage(fqcn, level, marker, msg, throwable);
        } finally {
            decrementRecursionDepth();
        }
    }

可以看出这个在打日志之前做了调用次数的记录。跟进tryLogMessage,

@PerformanceSensitive
    // NOTE: This is a hot method. Current implementation compiles to 26 bytes of byte code.
    // This is within the 35 byte MaxInlineSize threshold. Modify with care!
    private void tryLogMessage(final String fqcn,
                               final Level level,
                               final Marker marker,
                               final Message msg,
                               final Throwable throwable) {
        try {
            logMessage(fqcn, level, marker, msg, throwable);
        } catch (final Exception e) {
            // LOG4J2-1990 Log4j2 suppresses all exceptions that occur once application called the logger
            handleLogMessageException(e, fqcn, msg);
        }
    }

继续跟进:

@Override
    public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message,
            final Throwable t) {
        final Message msg = message == null ? new SimpleMessage(Strings.EMPTY) : message;
        final ReliabilityStrategy strategy = privateConfig.loggerConfig.getReliabilityStrategy();
        strategy.log(this, getName(), fqcn, marker, level, msg, t);
    }

这里可以看到在实际打日志的时候,会从config中获取打日志的策略,跟踪ReliabilityStrategy的创建,发现默认的实现类为DefaultReliabilityStrategy,跟进看实际打日志的方法

@Override
    public void log(final Supplier<LoggerConfig> reconfigured, final String loggerName, final String fqcn, final Marker marker, final Level level,
            final Message data, final Throwable t) {
        loggerConfig.log(loggerName, fqcn, marker, level, data, t);
    }

这里实际打日志的方法居然是交给一个config去实现的。。。感觉有点奇怪。。跟进看看

@PerformanceSensitive("allocation")
    public void log(final String loggerName, final String fqcn, final Marker marker, final Level level,
            final Message data, final Throwable t) {
        List<Property> props = null;
        if (!propertiesRequireLookup) {
            props = properties;
        } else {
            if (properties != null) {
                props = new ArrayList<>(properties.size());
                final LogEvent event = Log4jLogEvent.newBuilder()
                        .setMessage(data)
                        .setMarker(marker)
                        .setLevel(level)
                        .setLoggerName(loggerName)
                        .setLoggerFqcn(fqcn)
                        .setThrown(t)
                        .build();
                for (int i = 0; i < properties.size(); i++) {
                    final Property prop = properties.get(i);
                    final String value = prop.isValueNeedsLookup() // since LOG4J2-1575
                            ? config.getStrSubstitutor().replace(event, prop.getValue()) //
                            : prop.getValue();
                    props.add(Property.createProperty(prop.getName(), value));
                }
            }
        }
        final LogEvent logEvent = logEventFactory.createEvent(loggerName, marker, fqcn, level, data, props, t);
        try {
            log(logEvent, LoggerConfigPredicate.ALL);
        } finally {
            // LOG4J2-1583 prevent scrambled logs when logging calls are nested (logging in toString())
            ReusableLogEventFactory.release(logEvent);
        }
    }

可以清楚的看到try之前是在创建LogEvent,try里面做的才是真正的log(好tm累),一路跟进。

private void processLogEvent(final LogEvent event, LoggerConfigPredicate predicate) {
        event.setIncludeLocation(isIncludeLocation());
        if (predicate.allow(this)) {
            callAppenders(event);
        }
        logParent(event, predicate);
    }

接下来就是callAppender了,我们直接开始看AsyncAppender的append方法:

/**
     * Actual writing occurs here.
     *
     * @param logEvent The LogEvent.
     */
    @Override
    public void append(final LogEvent logEvent) {
        if (!isStarted()) {
            throw new IllegalStateException("AsyncAppender " + getName() + " is not active");
        }
        final Log4jLogEvent memento = Log4jLogEvent.createMemento(logEvent, includeLocation);
        InternalAsyncUtil.makeMessageImmutable(logEvent.getMessage());
        if (!transfer(memento)) {
            if (blocking) {
                if (AbstractLogger.getRecursionDepth() > 1) { // LOG4J2-1518, LOG4J2-2031
                    // If queue is full AND we are in a recursive call, call appender directly to prevent deadlock
                    AsyncQueueFullMessageUtil.logWarningToStatusLogger();
                    logMessageInCurrentThread(logEvent);
                } else {
                    // delegate to the event router (which may discard, enqueue and block, or log in current thread)
                    final EventRoute route = asyncQueueFullPolicy.getRoute(thread.getId(), memento.getLevel());
                    route.logMessage(this, memento);
                }
            } else {
                error("Appender " + getName() + " is unable to write primary appenders. queue is full");
                logToErrorAppenderIfNecessary(false, memento);
            }
        }
    }

这里主要的步骤就是:

  1. 生成logEvent
  2. 将logEvent放入BlockingQueue,就是transfer方法
  3. 如果BlockingQueue满了则启用相应的策略

同样的,这里也有一个线程用来做异步消费的事情

private class AsyncThread extends Log4jThread {

        private volatile boolean shutdown = false;
        private final List<AppenderControl> appenders;
        private final BlockingQueue<LogEvent> queue;

        public AsyncThread(final List<AppenderControl> appenders, final BlockingQueue<LogEvent> queue) {
            super("AsyncAppender-" + THREAD_SEQUENCE.getAndIncrement());
            this.appenders = appenders;
            this.queue = queue;
            setDaemon(true);
        }

        @Override
        public void run() {
            while (!shutdown) {
                LogEvent event;
                try {
                    event = queue.take();
                    if (event == SHUTDOWN_LOG_EVENT) {
                        shutdown = true;
                        continue;
                    }
                } catch (final InterruptedException ex) {
                    break; // LOG4J2-830
                }
                event.setEndOfBatch(queue.isEmpty());
                final boolean success = callAppenders(event);
                if (!success && errorAppender != null) {
                    try {
                        errorAppender.callAppender(event);
                    } catch (final Exception ex) {
                        // Silently accept the error.
                    }
                }
            }
            // Process any remaining items in the queue.
            LOGGER.trace("AsyncAppender.AsyncThread shutting down. Processing remaining {} queue events.",
                queue.size());
            int count = 0;
            int ignored = 0;
            while (!queue.isEmpty()) {
                try {
                    final LogEvent event = queue.take();
                    if (event instanceof Log4jLogEvent) {
                        final Log4jLogEvent logEvent = (Log4jLogEvent) event;
                        logEvent.setEndOfBatch(queue.isEmpty());
                        callAppenders(logEvent);
                        count++;
                    } else {
                        ignored++;
                        LOGGER.trace("Ignoring event of class {}", event.getClass().getName());
                    }
                } catch (final InterruptedException ex) {
                    // May have been interrupted to shut down.
                    // Here we ignore interrupts and try to process all remaining events.
                }
            }
            LOGGER.trace("AsyncAppender.AsyncThread stopped. Queue has {} events remaining. "
                + "Processed {} and ignored {} events since shutdown started.", queue.size(), count, ignored);
        }

        /**
         * Calls {@link AppenderControl#callAppender(LogEvent) callAppender} on all registered {@code AppenderControl}
         * objects, and returns {@code true} if at least one appender call was successful, {@code false} otherwise. Any
         * exceptions are silently ignored.
         *
         * @param event the event to forward to the registered appenders
         * @return {@code true} if at least one appender call succeeded, {@code false} otherwise
         */
        boolean callAppenders(final LogEvent event) {
            boolean success = false;
            for (final AppenderControl control : appenders) {
                try {
                    control.callAppender(event);
                    success = true;
                } catch (final Exception ex) {
                    // If no appender is successful the error appender will get it.
                }
            }
            return success;
        }

        public void shutdown() {
            shutdown = true;
            if (queue.isEmpty()) {
                queue.offer(SHUTDOWN_LOG_EVENT);
            }
            if (getState() == State.TIMED_WAITING || getState() == State.WAITING) {
                this.interrupt(); // LOG4J2-1422: if underlying appender is stuck in wait/sleep/join/park call
            }
        }
    }

直接看run方法:

  1. 阻塞获取logEvent
  2. 将logEvent分发出去
  3. 如果线程要退出了,将blockingQueue里面的event消费完在退出。

AsyncLogger

直接从AsyncLogger的logMessage看进去:

public void logMessage(final String fqcn, final Level level, final Marker marker, final Message message,
            final Throwable thrown) {

        if (loggerDisruptor.isUseThreadLocals()) {
            logWithThreadLocalTranslator(fqcn, level, marker, message, thrown);
        } else {
            // LOG4J2-1172: avoid storing non-JDK classes in ThreadLocals to avoid memory leaks in web apps
            logWithVarargTranslator(fqcn, level, marker, message, thrown);
        }
    }

跟进logWithThreadLocalTranslator,

private void logWithThreadLocalTranslator(final String fqcn, final Level level, final Marker marker,
            final Message message, final Throwable thrown) {
        // Implementation note: this method is tuned for performance. MODIFY WITH CARE!

        final RingBufferLogEventTranslator translator = getCachedTranslator();
        initTranslator(translator, fqcn, level, marker, message, thrown);
        initTranslatorThreadValues(translator);
        publish(translator);
    }

这里的逻辑很简单,就是将日志相关的信息转换成RingBufferLogEvent(RingBuffer是Disruptor的无所队列),然后将其发布到RingBuffer中。发布到RingBuffer中,那肯定也有消费逻辑。这时候有两种方式可以找到这个消费的逻辑。

  • 找disruptor被使用的地方,然后查看,但是这样做会很容易迷惑
  • 按照Log4j2的尿性,这种Logger都有对应的start方法,我们可以从start方法入手寻找

在start方法中,我们找到了一段代码:

final RingBufferLogEventHandler[] handlers = {new RingBufferLogEventHandler()};
        disruptor.handleEventsWith(handlers);

直接看看这个RingBufferLogEventHandler的实现:

public class RingBufferLogEventHandler implements
        SequenceReportingEventHandler<RingBufferLogEvent>, LifecycleAware {

    private static final int NOTIFY_PROGRESS_THRESHOLD = 50;
    private Sequence sequenceCallback;
    private int counter;
    private long threadId = -1;

    @Override
    public void setSequenceCallback(final Sequence sequenceCallback) {
        this.sequenceCallback = sequenceCallback;
    }

    @Override
    public void onEvent(final RingBufferLogEvent event, final long sequence,
            final boolean endOfBatch) throws Exception {
        event.execute(endOfBatch);
        event.clear();

        // notify the BatchEventProcessor that the sequence has progressed.
        // Without this callback the sequence would not be progressed
        // until the batch has completely finished.
        if (++counter > NOTIFY_PROGRESS_THRESHOLD) {
            sequenceCallback.set(sequence);
            counter = 0;
        }
    }

    /**
     * Returns the thread ID of the background consumer thread, or {@code -1} if the background thread has not started
     * yet.
     * @return the thread ID of the background consumer thread, or {@code -1}
     */
    public long getThreadId() {
        return threadId;
    }

    @Override
    public void onStart() {
        threadId = Thread.currentThread().getId();
    }

    @Override
    public void onShutdown() {
    }
}

顺着接口找上去,发现一个接口:

/**
 * Callback interface to be implemented for processing events as they become available in the {@link RingBuffer}
 *
 * @param <T> event implementation storing the data for sharing during exchange or parallel coordination of an event.
 * @see BatchEventProcessor#setExceptionHandler(ExceptionHandler) if you want to handle exceptions propagated out of the handler.
 */
public interface EventHandler<T>
{
    /**
     * Called when a publisher has published an event to the {@link RingBuffer}
     *
     * @param event      published to the {@link RingBuffer}
     * @param sequence   of the event being processed
     * @param endOfBatch flag to indicate if this is the last event in a batch from the {@link RingBuffer}
     * @throws Exception if the EventHandler would like the exception handled further up the chain.
     */
    void onEvent(T event, long sequence, boolean endOfBatch) throws Exception;
}

通过注释可以发现,这个onEvent就是处理逻辑,回到RingBufferLogEventHandler的onEvent方法,发现里面有一个execute方法,跟进:

public void execute(final boolean endOfBatch) {
        this.endOfBatch = endOfBatch;
        asyncLogger.actualAsyncLog(this);
    }

这个方法就是实际打日志了,AsyncLogger看起来还是比较简单的,只是使用了一个Disruptor。

插件化

之前在很多代码里面都可以看到

final PluginManager manager = new PluginManager(CATEGORY);
manager.collectPlugins(pluginPackages);

其实整个log4j2为了获得更好的扩展性,将自己的很多组件都做成了插件,然后在配置的时候去加载plugin。
跟进collectPlugins。

 public void collectPlugins(final List<String> packages) {
        final String categoryLowerCase = category.toLowerCase();
        final Map<String, PluginType<?>> newPlugins = new LinkedHashMap<>();

        // First, iterate the Log4j2Plugin.dat files found in the main CLASSPATH
        Map<String, List<PluginType<?>>> builtInPlugins = PluginRegistry.getInstance().loadFromMainClassLoader();
        if (builtInPlugins.isEmpty()) {
            // If we didn't find any plugins above, someone must have messed with the log4j-core.jar.
            // Search the standard package in the hopes we can find our core plugins.
            builtInPlugins = PluginRegistry.getInstance().loadFromPackage(LOG4J_PACKAGES);
        }
        mergeByName(newPlugins, builtInPlugins.get(categoryLowerCase));

        // Next, iterate any Log4j2Plugin.dat files from OSGi Bundles
        for (final Map<String, List<PluginType<?>>> pluginsByCategory : PluginRegistry.getInstance().getPluginsByCategoryByBundleId().values()) {
            mergeByName(newPlugins, pluginsByCategory.get(categoryLowerCase));
        }

        // Next iterate any packages passed to the static addPackage method.
        for (final String pkg : PACKAGES) {
            mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase));
        }
        // Finally iterate any packages provided in the configuration (note these can be changed at runtime).
        if (packages != null) {
            for (final String pkg : packages) {
                mergeByName(newPlugins, PluginRegistry.getInstance().loadFromPackage(pkg).get(categoryLowerCase));
            }
        }

        LOGGER.debug("PluginManager '{}' found {} plugins", category, newPlugins.size());

        plugins = newPlugins;
    }

处理逻辑如下:

  1. 从Log4j2Plugin.dat中加载所有的内置的plugin
  2. 然后将OSGi Bundles中的Log4j2Plugin.dat中的plugin加载进来
  3. 再加载传入的package路径中的plugin
  4. 最后加载配置中的plugin

逻辑还是比较简单的,但是我在看源码的时候发现了一个很有意思的东西,就是在加载log4j2 core插件的时候,也就是

PluginRegistry.getInstance().loadFromMainClassLoader()

这个方法,跟进到decodeCacheFiles:

private Map<String, List<PluginType<?>>> decodeCacheFiles(final ClassLoader loader) {
        final long startTime = System.nanoTime();
        final PluginCache cache = new PluginCache();
        try {
            final Enumeration<URL> resources = loader.getResources(PluginProcessor.PLUGIN_CACHE_FILE);
            if (resources == null) {
                LOGGER.info("Plugin preloads not available from class loader {}", loader);
            } else {
                cache.loadCacheFiles(resources);
            }
        } catch (final IOException ioe) {
            LOGGER.warn("Unable to preload plugins", ioe);
        }
        final Map<String, List<PluginType<?>>> newPluginsByCategory = new HashMap<>();
        int pluginCount = 0;
        for (final Map.Entry<String, Map<String, PluginEntry>> outer : cache.getAllCategories().entrySet()) {
            final String categoryLowerCase = outer.getKey();
            final List<PluginType<?>> types = new ArrayList<>(outer.getValue().size());
            newPluginsByCategory.put(categoryLowerCase, types);
            for (final Map.Entry<String, PluginEntry> inner : outer.getValue().entrySet()) {
                final PluginEntry entry = inner.getValue();
                final String className = entry.getClassName();
                try {
                    final Class<?> clazz = loader.loadClass(className);
                    final PluginType<?> type = new PluginType<>(entry, clazz, entry.getName());
                    types.add(type);
                    ++pluginCount;
                } catch (final ClassNotFoundException e) {
                    LOGGER.info("Plugin [{}] could not be loaded due to missing classes.", className, e);
                } catch (final LinkageError e) {
                    LOGGER.info("Plugin [{}] could not be loaded due to linkage error.", className, e);
                }
            }
        }

        final long endTime = System.nanoTime();
        final DecimalFormat numFormat = new DecimalFormat("#0.000000");
        final double seconds = (endTime - startTime) * 1e-9;
        LOGGER.debug("Took {} seconds to load {} plugins from {}",
            numFormat.format(seconds), pluginCount, loader);
        return newPluginsByCategory;
    }

可以发现加载时候是从一个文件(PLUGIN_CACHE_FILE)获取所有要获取的plugin。看到这里的时候我有一个疑惑就是,为什么不用反射的方式直接去扫描,而是要从文件中加载进来,而且文件是写死的,很不容易扩展啊。然后我找了一下PLUGIN_CACHE_FILE这个静态变量的用处,发现了PluginProcessor这个类,这里用到了注解处理器

/**
 * Annotation processor for pre-scanning Log4j 2 plugins.
 */
@SupportedAnnotationTypes("org.apache.logging.log4j.core.config.plugins.*")
public class PluginProcessor extends AbstractProcessor {

    // TODO: this could be made more abstract to allow for compile-time and run-time plugin processing

    /**
     * The location of the plugin cache data file. This file is written to by this processor, and read from by
     * {@link org.apache.logging.log4j.core.config.plugins.util.PluginManager}.
     */
    public static final String PLUGIN_CACHE_FILE =
            "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";

    private final PluginCache pluginCache = new PluginCache();

    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
        System.out.println("Processing annotations");
        try {
            final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Plugin.class);
            if (elements.isEmpty()) {
                System.out.println("No elements to process");
                return false;
            }
            collectPlugins(elements);
            writeCacheFile(elements.toArray(new Element[elements.size()]));
            System.out.println("Annotations processed");
            return true;
        } catch (final IOException e) {
            e.printStackTrace();
            error(e.getMessage());
            return false;
        } catch (final Exception ex) {
            ex.printStackTrace();
            error(ex.getMessage());
            return false;
        }
    }
}

(不太重要的方法省略)

我们可以看到在process方法中,PluginProcessor会先收集所有的Plugin,然后在写入文件。这样做的好处就是可以省去反射时候的开销。
然后我又看了一下Plugin这个注解,发现它的RetentionPolicy是RUNTIME,一般来说PluginProcessor是搭配RetentionPolicy.SOURCE,CLASS使用的,而且既然你把自己的Plugin扫描之后写在文件中了,RetentionPolicy就没有必要是RUNTIME了吧,这个是一个很奇怪的地方。

小结

总算是把Log4j2的代码看完了,发现它的设计理念很值得借鉴,为了灵活性,所有的东西都设计成插件式。互联网技术日益发展,各种中间件层出不穷,而作为工程师的我们更需要做的是去思考代码与代码之间的关系,毫无疑问的是,解耦是最具有美感的关系。

作者:netflix

原文链接

本文为云栖社区原创内容,未经允许不得转载。

15个最好用的JavaScript代码压缩工具

mumupudding阅读(4)

JavaScript 代码压缩是指去除源代码里的所有不必要的字符,而不改变其功能的过程。这些不必要的字符通常包括空格字符,换行字符,注释以及块分隔符等用来增加可读性的代码,但并不需要它来执行。

在这篇文章中,我们选择了15个最好用的 JavaScript 压缩工具,有简单的在线转换器,GUI工具和命令行界面等。

1. JavaScript Minifier

它是一个很好的工具,带有API来缩小js代码。

2. JSMIni

如果您想快速轻松地缩小JavaScript或jQuery文件,请使用jsMini。只需复制和粘贴源代码,选择要基本压缩还是完全压缩,然后缩小代码。

3. JSCompress

JSCompress.com是一个在线javascript压缩器,允许您压缩和缩小javascript文件。压缩的javascript文件是生产环境的理想选择,因为它们通常会将文件的大小减少30-90%。大多数文件大小的减少是通过删除Web浏览器或访问者不需要的注释和额外的空白字符来实现的。

4. Minifier

一个简化CSS/JS的简单工具,没有大的设置。它将CSS中的URL从原来的位置重新工作到输出位置。它会自动解析CSS中的@import语句。

5. Gulp.js

js是流构建系统。它使用流和代码对配置,使一个更简单和更直观的构建。通过更喜欢代码而不是配置,GUMP使简单的事情变得简单,并使复杂的任务易于管理。通过利用节点流的强大功能,您可以获得不将中间文件写入磁盘的快速构建。GUP的严格插件指南确保插件保持简单,并按您预期的方式工作。

6. Uglifyjs

这个包实现了一个通用的JavaScript解析器/压缩器/美化工具包。它是在NodeJS上开发的,但是它应该在任何支持CommonJS模块系统的JavaScript平台上工作(如果您选择的平台不支持CommonJS,那么您可以很容易地实现它,或者放弃导出。

7. Grunt

grunt是一个用于JavaScript项目的基于任务的命令行构建工具。它有以下可以在项目中使用的预定义任务:连接文件、使用JSHint验证文件、使用UGIFIFYJS执行minify文件、使用节点单元运行单元测试等等。

8. Koala

koala是一个GUI应用程序,用于Less、Sass、Compass和CoffeeScript编译,以帮助Web开发人员更有效地使用它们。考拉可以在Windows、Linux和Mac上运行。

9. Prepros

PreProfessional是一个用于编译更少的工具,Sass、Compass、Stylus、Jade以及更多的带有自动CSS前缀的工具,它带有内置的服务器,用于跨浏览器测试。它运行在Windows、Mac和Linux上。

10. Ajax Minifier

此工具是一个Windows应用程序,允许您在不使用命令行或VisualStudio的情况下运行MicrosoftAjaxMinifier。它缩小了文件夹和嵌套文件夹中的所有javascript文件,缩小了单个javascript文件,启用/禁用了小型程序的超压缩和分析选项等等。

11. Smaller

更小的是一个强大的HTML,CSS和JavaScript压缩器在OSX上,它也有能力将多个文件组合成一个。压缩您的文件,使您的网站加载更快。

12. Ultra Minifier

超迷你是最简单的YUI压缩机GUI,以缩小Javascript和CSS代码,而不使用终端。

13. Require JS

RequireJS是一个JavaScript文件和模块加载器。它是为浏览器内使用而优化的,但它可以用于其他JavaScript环境,如Rhino和Node。使用像RequireJS这样的模块化脚本加载程序将提高代码的速度和质量。它包括一个优化工具,可以作为部署代码的打包步骤的一部分运行。优化工具可以组合和缩小JavaScript文件,以实现更好的性能。

14. Online JavaScript/CSS Compressor

这是一个用于压缩JavaScript或CSS的Web接口。该工具使用UgulifyJS 2、Clean-CSS和HTML缩略符.

15. Minify

minify是一个PHP 5应用程序,它可以帮助你遵循雅虎的一些高性能网站规则,它结合了多个css或Javascript文件,删除了不必要的空白和注释,并为它们提供gzip编码和最佳客户端缓存头。

深度解析React服务端渲染

mumupudding阅读(7)

React 服务端渲染

服务端渲染的基本套路就是用户请求过来的时候,在服务端生成一个我们希望看到的网页内容的HTML字符串,返回给浏览器去展示。

浏览器拿到了这个HTML之后,渲染出页面,但是并没有事件交互,这时候浏览器发现HTML中加载了一些js文件(也就是浏览器端渲染的js),就直接去加载。

加载好并执行完以后,事件就会被绑定上了。这时候页面被浏览器端接管了。也就是到了我们熟悉的js渲染页面的过程。

需要实现的目标:

  • React组件服务端渲染
  • 路由的服务端渲染
  • 保证服务端和浏览器的数据唯一
  • css的服务端渲染(样式直出)

一般的渲染方式

  • 服务端渲染:服务端生成html字符串,发送给浏览器进行渲染。
  • 浏览器端渲染:服务端返回空的html文件,内部加载js完全由js与css,由js完成页面的渲染

优点与缺点

服务端渲染解决了首屏加载速度慢以及seo不友好的缺点(Google已经可以检索到浏览器渲染的网页,但不是所有搜索引擎都可以)但增加了项目的复杂程度,提高维护成本。如果非必须,尽量不要用服务端渲染//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力整体思路

需要两个端:服务端、浏览器端(浏览器渲染的部分)

第一: 打包浏览器端代码

第二: 打包服务端代码并启动服务

第三: 用户访问,服务端读取浏览器端打包好的index.html文件为字符串,将渲染好的组件、样式、数据塞入html字符串,返回给浏览器

第四: 浏览器直接渲染接收到的html内容,并且加载打包好的浏览器端js文件,进行事件绑定,初始化状态数据,完成同构

React组件的服务端渲染

让我们来看一个最简单的React服务端渲染的过程。要进行服务端渲染的话那必然得需要一个根组件,来负责生成HTML结构

import React from 'react';import ReactDOM from 'react-dom'; ReactDOM.hydrate(<Container />, document.getElementById('root'));

当然这里用ReactDOM.render也是可以的,只不过hydrate会尽量复用接收到的服务端返回的内容,来补充事件绑定和浏览器端其他特有的过程引入浏览器端需要渲染的根组件,利用react的 renderToString API进行渲染

import { renderToString } from 'react-dom/server'import Container from '../containers'// 产生htmlconst content = renderToString(<Container/>)const html = `  <html>   <body>${content}</body>  </html>res.send(html)

在这里,renderToString也可以替换成renderToNodeStream,区别在于前者是同步地产生HTML,也就是如果生成HTML用了1000毫秒,那么就会在1000毫秒之后才将内容返回给浏览器,显然耗时过长。而后者则是以流的形式,将渲染结果塞给response对象,就是出来多少就返回给浏览器多少,可以相对减少耗时//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力路由的服务端渲染

一般场景下,我们的应用不可能只有一个页面,肯定会有路由跳转。我们一般这么用:

import { BrowserRouter, Route } from 'react-router-dom'const App = () => (  <BrowserRouter>    {/*...Routes*/}  <BrowserRouter/>)

但这是浏览器端渲染时候的用法。在做服务端渲染时,需要使用将BrowserRouter 替换为 StaticRouter区别在于,BrowserRouter 会通过HTML5 提供的 history API来保持页面与URL的同步,而StaticRouter则不会改变URL

import { createServer } from 'http'import { StaticRouter } from 'react-router-dom'createServer((req, res) => {  const html = renderToString(    <StaticRouter      location={req.url}      context={{}}    >      <Container />    <StaticRouter/>) })

这里,StaticRouter要接收两个属性:location: StaticRouter 会根据这个属性,自动匹配对应的React组件,所以才会实现刷新页面,服务端返回的对应路由的组与浏览器端保持一致

context: 一般用来传递一些数据,相当于一个载体,之后讲到样式的服务端渲染的时候会用到

Redux同构

数据的预获取以及脱水与注水我认为是服务端渲染的难点。这是什么意思呢?也就是说首屏渲染的网页一般要去请求外部数据,我们希望在生成HTML之前,去获取到这个页面需要的所有数据,然后塞到页面中去,这个过程,叫做“脱水”(Dehydrate),生成HTML返回给浏览器。浏览器拿到带着数据的HTML,去请求浏览器端js,接管页面,用这个数据来初始化组件。这个过程叫“注水”(Hydrate)。完成服务端与浏览器端数据的统一。//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力为什么要这么做呢?试想一下,假设没有数据的预获取,直接返回一个没有数据,只有固定内容的HTML结构,会有什么结果呢?

第一:由于页面内没有有效信息,不利于SEO。

第二:由于返回的页面没有内容,但浏览器端JS接管页面后回去请求数据、渲染数据,页面会闪一下,用户体验不好。

我们使用Redux来管理状态,因为有服务端代码和浏览器端代码,那么就分别需要两个store来管理服务端和浏览器端的数据。

组件的配置

组件要在服务端渲染的时候去请求数据,可以在组件上挂载一个专门发异步请求的方法,这里叫做loadData,接收服务端的store作为参数,然后store.dispatch去扩充服务端的store。

class Home extends React.Component {  componentDidMount() {    this.props.callApi()  }  render() {    return <div>{this.props.state.name}</div>  }}Home.loadData = store => { return store.dispatch(callApi())}const mapState = state => stateconst mapDispatch = {callApi}export default connect(mapState, mapDispatch)(Home)

路由的改造

因为服务端要根据路由判断当前渲染哪个组件,可以在这个时候发送异步请求。所以路由也需要配置一下来支持loadData方法。服务端渲染的时候,路由的渲染可以使用react-router-config这个库,用法如下(重点关注在路由上挂载loadData方法):

import { BrowserRouter } from 'react-router-dom'import { renderRoutes } from 'react-router-config'import Home from './Home'export const routes = [ {  path: '/',  component: Home,  loadData: Home.loadData,  exact: true, }]const Routers = <BrowserRouter>  {renderRoutes(routes)}<BrowserRouter/>

服务端获取数据

到了服务端,需要判断匹配的路由内的所有组件各自都有没有loadData方法,有就去调用,传入服务端的store,去扩充服务端的store。同时还要注意到,一个页面可能是由多个组件组成的,会发各自的请求,也就意味着我们要等所有的请求都发完,再去返回HTML。

import express from 'express'import serverRender from './render'import { matchRoutes } from 'react-router-config'import { routes } from '../routes'import serverStore from "../store/serverStore" const app = express()app.get('*', (req, res) => { const context = {css: []} const store = serverStore() // 用matchRoutes方法获取匹配到的路由对应的组件数组 const matchedRoutes = matchRoutes(routes, req.path) const promises = [] for (const item of matchedRoutes) {  if (item.route.loadData) {   const promise = new Promise((resolve, reject) => {    item.route.loadData(store).then(resolve).catch(resolve)   })   promises.push(promise)  } } // 所有请求响应完毕,将被HTML内容发送给浏览器 Promise.all(promises).then(() => {  // 将生成html内容的逻辑封装成了一个函数,接收req, store, context  res.send(serverRender(req, store, context)) })})

细心的同学可能注意到了上边我把每个loadData都包了一个promise。

const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve) console.log(item.route.loadData(store));})//在此我向大家推荐一个前端全栈开发交流圈:619586920 突破技术瓶颈,提升思维能力promises.push(promise)

这是为了容错,一旦有一个请求出错,那么下边Promise.all方法则不会执行,所以包一层promise的目的是即使请求出错,也会resolve,不会影响到Promise.all方法,也就是说只有请求出错的组件会没数据,而其他组件不会受影响。

注入数据

我们请求已经发出去了,并且在组件的loadData方法中也扩充了服务端的store,那么可以从服务端的数据取出来注入到要返回给浏览器的HTML中了。来看 serverRender 方法

const serverRender = (req, store, context) => { // 读取客户端生成的HTML const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString(  <Provider store={store}>   <StaticRouter location={req.path} context={context}>    <Container/>   </StaticRouter>  </Provider> ) // 注入数据 const initialState = `<script>  window.context = {   INITIAL_STATE: ${JSON.stringify(store.getState())}  }</script>` return template.replace('<!--app-->', content)  .replace('<!--initial-state-->', initialState)}

浏览器端用服务端获取到的数据初始化store

经过上边的过程,我们已经可以从window.context中拿到服务端预获取的数据了,此时需要做的事就是用这份数据去初始化浏览器端的store。保证两端数据的统一。

import { createStore, applyMiddleware, compose } from 'redux'import thunk from 'redux-thunk'import rootReducer from '../reducers' const defaultStore = window.context && window.context.INITIAL_STATEconst clientStore = createStore( rootReducer, defaultStore,// 利用服务端的数据初始化浏览器端的store compose(  applyMiddleware(thunk),  window.devToolsExtension ? window.devToolsExtension() : f=>f ))

至此,服务端渲染的数据统一问题就解决了,再来回顾一下整个流程:

  • 用户访问路由,服务端根据路由匹配出对应路由内的组件数组
  • 循环数组,调用组件上挂载的loadData方法,发送请求,扩充服务端store
  • 所有请求完成后,通过store.getState,获取到服务端预获取的数据,注入到window.context中
  • 浏览器渲染返回的HTML,加载浏览器端js,从window.context中取数据来初始化浏览器端的store,渲染组件

这里还有个点,也就是当我们从路由进入到其他页面的时候,组件内的loadData方法并不会执行,它只会在刷新,服务端渲染路由的时候执行。

这时候会没有数据。所以我们还需要在componentDidMount中去发请求,来解决这个问题。因为componentDidMount不会在服务端渲染执行,所以不用担心请求重复发送。

样式的服务端渲染

以上我们所做的事情只是让网页的内容经过了服务端的渲染,但是样式要在浏览器加载css后才会加上,所以最开始返回的网页内容没有样式,页面依然会闪一下。为了解决这个问题,我们需要让样式也一并在服务端渲染的时候返回。

首先,服务端渲染的时候,解析css文件,不能使用style-loader了,要使用isomorphic-style-loader。

{  test: /\.css$/,  use: [    'isomorphic-style-loader',    'css-loader',    'postcss-loader'  ],}

但是,如何在服务端获取到当前路由内的组件样式呢?回想一下,我们在做路由的服务端渲染时,用到了StaticRouter,它会接收一个context对象,这个context对象可以作为一个载体来传递一些信息。我们就用它!思路就是在渲染组件的时候,在组件内接收context对象,获取组件样式,放到context中,服务端拿到样式,插入到返回的HTML中的style标签中。来看看组件是如何读取样式的吧:

import style from './style/index.css'class Index extends React.Component {  componentWillMount() {   if (this.props.staticContext) {    const css = styles._getCss()    this.props.staticContext.css.push(css)   }  }}

在路由内的组件可以在props里接收到staticContext,也就是通过StaticRouter传递过来的context,isomorphic-style-loader 提供了一个 _getCss() 方法,让我们能读取到css样式,然后放到staticContext里。不在路由之内的组件,可以通过父级组件,传递props的方法,或者用react-router的withRouter包裹一下其实这部分提取css的逻辑可以写成高阶组件,这样就可以做到复用了

import React, { Component } from 'react' export default (DecoratedComponent, styles) => { return class NewComponent extends Component {  componentWillMount() {   if (this.props.staticContext) {    const css = styles._getCss()    this.props.staticContext.css.push(css)   }  }  render() {   return <DecoratedComponent {...this.props}/>  } }}

在服务端,经过组件的渲染之后,context中已经有内容了,我们这时候把样式处理一下,返回给浏览器,就可以做到样式的服务端渲染了

const serverRender = (req, store) => { const context = {css: []} const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') const content = renderToString(  <Provider store={store}>   <StaticRouter location={req.path} context={context}>    <Container/>   </StaticRouter>  </Provider> ) // 经过渲染之后,context.css内已经有了样式 const cssStr = context.css.length ? context.css.join('\n') : '' const initialState = `<script>  window.context = {   INITIAL_STATE: ${JSON.stringify(store.getState())}  }</script>` return template.replace('<!--app-->', content)  .replace('server-render-css', cssStr)  .replace('<!--initial-state-->', initialState)}

至此,服务端渲染就全部完成了。

React的服务端渲染,最好的解决方案就是Next.js。如果你的应用没有SEO优化的需求,又或者不太注重首屏渲染的速度,那么尽量就不要用服务端渲染。

因为会让项目变得复杂。此外,除了服务端渲染,SEO优化的办法还有很多,比如预渲染(pre-render)

redis哨兵模式

mumupudding阅读(7)


Redis sentinel介绍

Redis Sentinel是Redis高可用的实现方案。Sentinel是一个管理多个Redis实例的工具,它可以实现对Redis的监控、通知、自动故障转移。

Redis Sentinel的主要功能

Sentinel的主要功能包括主节点存活检测、主从运行情况检测、自动故障转移(failover)、主从切换。Redis的Sentinel最小配置是一主一从。 Redis的Sentinel系统可以用来管理多个Redis服务器,该系统可以执行以下四个任务:

  • 监控

    Sentinel会不断的检查主服务器和从服务器是否正常运行。

  • 通知

    当被监控的某个Redis服务器出现问题,Sentinel通过API脚本向管理员或者其他的应用程序发送通知。

  • 自动故障转移

    当主节点不能正常工作时,Sentinel会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点, 并且将其他的从节点指向新的主节点。

  • 配置提供者

    在Redis Sentinel模式下,客户端应用在初始化时连接的是Sentinel节点集合,从中获取主节点的信息。

Redis Sentinel的工作流程

参考链接(含图):http://www.cnblogs.com/jifeng/p/5138961.html

Sentinel负责监控集群中的所有主、从Redis,当发现主故障时,Sentinel会在所有的从中选一个成为新的主。并且会把其余的从变为新主的从。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。在Redis高可用架构中,Sentinel往往不是只有一个,而是有3个或者以上。目的是为了让其更加可靠,毕竟主和从切换角色这个过程还是蛮复杂的。

相关概念

  • 主观失效

    SDOWN(subjectively down),直接翻译的为”主观”失效,即当前sentinel实例认为某个redis服务为”不可用”状态.

  • 客观失效

    ODOWN(objectively down),直接翻译为”客观”失效,即多个sentinel实例都认为master处于”SDOWN”状态,那么此时master将处于ODOWN,ODOWN可以简单理解为master已经被集群确定为”不可用”,将会开启failover

环境准备

准备3台机器,其中每台机器上都有两个角色,分配如下:

主机名 IP:Port 角色
wangzb01 192.168.153.133:6379 Redis Master
wangzb02 192.168.153.134:6379 Redis Slave1
wangzb03 192.168.153.135:6379 Redis Slave2
wangzb01 192.168.153.133:26379 Sentinel1
wangzb02 192.168.153.134:26379 Sentinel2
wangzb03 192.168.153.135:26379 Sentinel3

部署

安装Redis

步骤略

部署Redis主从

步骤略

部署Sentinel

三台Sentinel配置文件是一样的,编辑配置文件

vi /etc/sentinel.conf #内容如下

# 端口port 26379# 是否后台启动daemonize yes# pid文件路径pidfile /var/run/redis-sentinel.pid# 日志文件路径logfile "/var/log/sentinel.log"# 定义工作目录dir /tmp# 定义Redis主的别名, IP, 端口,这里的2指的是需要至少2个Sentinel认为主Redis挂了才最终会采取下一步行为sentinel monitor mymaster 127.0.0.1 6379 2# 如果mymaster 30秒内没有响应,则认为其主观失效sentinel down-after-milliseconds mymaster 30000# 如果master重新选出来后,其它slave节点能同时并行从新master同步数据的台数有多少个,显然该值越大,所有slave节##点完成同步切换的整体速度越快,但如果此时正好有人在访问这些slave,可能造成读取失败,影响面会更广。最保守的设置##为1,同一时间,只能有一台干这件事,这样其它slave还能继续服务,但是所有slave全部完成缓存更新同步的进程将变慢。sentinel parallel-syncs mymaster 1# 该参数指定一个时间段,在该时间段内没有实现故障转移成功,则会再一次发起故障转移的操作,单位毫秒sentinel failover-timeout mymaster 180000# 不允许使用SENTINEL SET设置notification-script和client-reconfig-script。sentinel deny-scripts-reconfig yes

启动服务

启动顺序:主Redis -> 从Redis -> Sentinel1/2/3

Sentinel 启动命令

redis-sentinel /etc/sentinel.conf 

Sentinel操作

  • sentinel master mymaster

    输出被监控的主节点的状态信息

  • sentinel slaves mymaster

    查看mymaster的从信息

  • sentinel sentinels mymaster

    查看其他Sentinel信息

测试

停止Redis从

停止Redis主

停止sentinel1

客户端连接问题

使用sentinel后,客户端(如,php)如何连Redis呢?

参考:https://blog.51cto.com/chenql/1958910

Mybatis中SqlNode的组合模式

mumupudding阅读(13)

组合( Composite )模式就是把对象组合成树形结构,以表示“部分-整体”的层次结构,用户可以像处理一个简单对象一样来处理一个复杂对象,从而使得调用者无需了解复杂元素的内部结构。

组合模式中的角色有:

  • 抽象组件(容器):定义了树形结构中所有类的公共行为,例如add(),remove()等方法。
  • 树叶:最终实现类,没有子类。
  • 树枝:有子类的管理类,并通过管理方法调用其管理的子类的相关操作。
  • 调用者:通过容器接口操作整个树形结构。

具体组合模式的例子可以参考 设计模式整理

现在我们来说一下SqlNode是什么,来看这么一段配置文件

<select id="findByGameTypeCount" resultType="java.lang.Long">
   select count(*)
   from betdetails a inner join UserBetOrder b on a.orderId = b.id
   <where>
      <if test="gameType != null and gameType > 0">
         a.gameType = #{gameType} and
      </if>
      <if test="currDrawno != null">
         b.currentDrawno = #{currDrawno} and
      </if>
      <if test="orderId != null and orderId > 0">
         a.orderId = #{orderId} and
      </if>
      <if test="status != null and status >= 0">
         a.status = #{status} and
      </if>
      <if test="userId != null and userId > 0">
         b.userId = #{userId} and
      </if>
      <if test="start != null">
         a.createTime &gt;= #{start} and
      </if>
      <if test="end != null">
         a.createTime &lt;= #{end} and
      </if>
      1 = 1
   </where>
</select>
<insert id="insertBetdetailsByBatch" parameterType="java.util.List">
   insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values
   <foreach collection="list" item="item" index="index" separator=",">
      (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName})
   </foreach>
</insert>

这其中的<if><where><foreach>节点就是SqlNode节点,SqlNode是一个接口,代表着组合模式中的容器。只要是有SqlNode,那就代表着一定是一个动态的SQL,里面就有可能会有参数#{}

public interface SqlNode {
  //SqlNode接口中定义的唯一方法,该方法会根据用户传入的实参,解析该SqlNode所记录的动态SQL节点,并调用DynamicContext.appendSql()方法将解析后的SQL片段追加到
  //DynamicContext.sqlBuilder中保存
  //当SQL节点下的所有SqlNode完成解析后,就可以从DynamicContext中获取一条动态生成的完整的SQL语句
  boolean apply(DynamicContext context);
}

我们先来看一下DynamicContext是什么,它的核心字段如下

private final ContextMap bindings; //参考上下文
//在SqlNode解析动态SQL时,会将解析后的SQL语句片段添加到该属性中保存,最终拼凑出一条完成的SQL语句
private final StringBuilder sqlBuilder = new StringBuilder();

ContextMap是一个内部类,继承于HashMap,重写了get方法

static class ContextMap extends HashMap<String, Object> {
  private static final long serialVersionUID = 2977601501966151582L;
  //将用户传入的参数封装成MetaObject对象(类实例中检查类的属性是否包含getter,setter方法)
  private MetaObject parameterMetaObject;
  public ContextMap(MetaObject parameterMetaObject) {
    this.parameterMetaObject = parameterMetaObject;
  }

  @Override
  public Object get(Object key) {
    String strKey = (String) key;
    //如果ContextMap中已经包含了该key,则直接返回
    if (super.containsKey(strKey)) {
      return super.get(strKey);
    }
    //如果不包含该key,从parameterMetaObject中查找对应属性
    if (parameterMetaObject != null) {
      // issue #61 do not modify the context when reading
      return parameterMetaObject.getValue(strKey);
    }

    return null;
  }
}
public void appendSql(String sql) {
  sqlBuilder.append(sql);
  sqlBuilder.append(" ");
}

SqlNode的实现类如下

其中MixedSqlNode是树枝,TextSqlNode是树叶….

我们先来看一下TextSqlNode,TextSqlNode表示的是包含${}占位符的动态SQL节点。它的接口实现方法如下

@Override
public boolean apply(DynamicContext context) {
  //将动态SQL(带${}占位符的SQL)解析成完成SQL语句的解析器,即将${}占位符替换成实际的变量值
  GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
  //将解析后的SQL片段添加到DynamicContext中
  context.appendSql(parser.parse(text));
  return true;
}

BindingTokenParser是TextNode中定义的内部类,继承了TokenHandler接口,它的主要作用是根据DynamicContext.bindings集合中的信息解析SQL语句节点中的${}占位符。

private DynamicContext context;
private Pattern injectionFilter; //需要匹配的正则表达式
@Override
public String handleToken(String content) {
  //获取用户提供的实参
  Object parameter = context.getBindings().get("_parameter");
  //如果实参为null
  if (parameter == null) {
    //将参考上下文的value key设为null
    context.getBindings().put("value", null);
    //如果实参是一个常用数据类型的类(Integer.class,String.class,Byte.class等等)
  } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
    //将参考上下文的value key设为该实参
    context.getBindings().put("value", parameter);
  }
  //通过OGNL解析参考上下文的值
  Object value = OgnlCache.getValue(content, context.getBindings());
  String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
  //检测合法性
  checkInjection(srtValue);
  return srtValue;
}
private void checkInjection(String value) {
  if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
    throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
  }
}

在OgnlCache中,对原生的OGNL进行了封装。OGNL表达式的解析过程是比较耗时的,为了提高效率,OgnlCache中使用了expressionCashe字段(ConcurrentHashMap<String,Object>类型)对解析后的OGNL表达式进行缓存。为了说明OGNL,我们先来看一个例子

@Data
@ToString
public class User {
    private int id;
    private String name;
}
public class OGNLDemo {
    public void testOgnl1() throws OgnlException {
        OgnlContext context = new OgnlContext();
        context.put("cn","China");
        String value = (String) context.get("cn");
        System.out.println(value);

        User user = new User();
        user.setId(100);
        user.setName("Jack");
        context.put("user",user);
        Object u = context.get("user");
        System.out.println(u);
        Object ognl = Ognl.parseExpression("#user.id");
        Object value1 = Ognl.getValue(ognl,context,context.getRoot());
        System.out.println(value1);

        User user1 = new User();
        user1.setId(200);
        user1.setName("Mark");
        context.setRoot(user1);
        Object ognl1 = Ognl.parseExpression("id");
        Object value2 = Ognl.getValue(ognl1,context,context.getRoot());
        System.out.println(value2);

        Object ognl2 = Ognl.parseExpression("@@floor(10.9)");
        Object value3 = Ognl.getValue(ognl2, context, context.getRoot());
        System.out.println(value3);
    }

    public static void main(String[] args) throws OgnlException {
        OGNLDemo demo = new OGNLDemo();
        demo.testOgnl1();
    }
}

运行结果:

China
User(id=100, name=Jack)
100
200
10.0

private static final Map<String, Object> expressionCache = new ConcurrentHashMap<String, Object>();
public static Object getValue(String expression, Object root) {
  try {
    //创建OgnlContext对象
    Map<Object, OgnlClassResolver> context = Ognl.createDefaultContext(root, new OgnlClassResolver());
    //使用OGNL执行expression表达式
    return Ognl.getValue(parseExpression(expression), context, root);
  } catch (OgnlException e) {
    throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);
  }
}
private static Object parseExpression(String expression) throws OgnlException {
  //查找缓存
  Object node = expressionCache.get(expression);
  if (node == null) {
    //解析表达式
    node = Ognl.parseExpression(expression);
    //将表达式的解析结果添加到缓存中
    expressionCache.put(expression, node);
  }
  return node;
}

StaticTextSqlNode很简单,就是直接返回SQL语句

public class StaticTextSqlNode implements SqlNode {
  private final String text;

  public StaticTextSqlNode(String text) {
    this.text = text;
  }

  @Override
  public boolean apply(DynamicContext context) {
    context.appendSql(text);
    return true;
  }

}

IfSqlNode是解析<if>节点,字段含义如下

//用于解析<if>节点的test表达式的值
private final ExpressionEvaluator evaluator;
//记录<if>节点中test表达式
private final String test;
//记录了<if>节点的子节点
private final SqlNode contents;

接口方法如下

@Override
public boolean apply(DynamicContext context) {
  //检测test属性中记录的表达式
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    //如果test表达式为true,则执行子节点的apply()方法
    contents.apply(context);
    return true; //返回test表达式的结果为true
  }
  return false; //返回test表达式的结果为false
}

在ExpressionEvaluator中

public boolean evaluateBoolean(String expression, Object parameterObject) {
  //用OGNL解析expression表达式
  Object value = OgnlCache.getValue(expression, parameterObject);
  //处理Boolean类型
  if (value instanceof Boolean) {
    return (Boolean) value;
  }
  //处理数字类型
  if (value instanceof Number) {
    return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0;
  }
  return value != null;
}

TrimSqlNode会根据子节点的解析结果,添加或删除响应的前缀或后缀,比如有这么一段配置

<insert id="insertNotNullBetdetails" parameterType="com.cloud.model.game.Betdetails">
   insert into betdetails
   <trim prefix="(" suffix=")" suffixOverrides=",">
      <if test="id != null">id,</if>
      <if test="orderId != null">orderId,</if>
      <if test="actorIndex != null">actorIndex,</if>
      <if test="ballIndex != null">ballIndex,</if>
      <if test="ballValue != null">ballValue,</if>
      <if test="betAmount != null">betAmount,</if>
      <if test="createTime != null">createTime,</if>
      <if test="rate1 != null">rate1,</if>
      <if test="rate2 != null">rate2,</if>
      <if test="rate3 != null">rate3,</if>
      <if test="gameType != null">gameType,</if>
      <if test="status != null">status,</if>
      <if test="betResult != null">betResult,</if>
      <if test="awardAmount != null">awardAmount,</if>
      <if test="ballName != null">ballName,</if>
   </trim>
   <trim prefix="values (" suffix=")" suffixOverrides=",">
      <if test="id != null">#{id},</if>
      <if test="orderId != null">#{orderId},</if>
      <if test="actorIndex != null">#{actorIndex},</if>
      <if test="createTime != null">#{createTime},</if>
      <if test="ballIndex != null">#{ballIndex},</if>
      <if test="ballValue != null">#{ballValue},</if>
      <if test="betAmount != null">#{betAmount},</if>
      <if test="rate1 != null">#{rate1},</if>
      <if test="rate2 != null">#{rate2},</if>
      <if test="rate3 != null">#{rate3},</if>
      <if test="gameType != null">#{gameType},</if>
      <if test="status != null">#{status},</if>
      <if test="betResult != null">#{betResult},</if>
      <if test="awardAmount != null">#{awardAmount},</if>
      <if test="ballName != null">#{ballName},</if>
   </trim>
</insert>

TrimSqlNode中字段含义如下

private final SqlNode contents; //该<trim>节点的子节点
private final String prefix; //记录了前缀字符串(为<trim>节点包裹的SQL语句添加的前缀)
private final String suffix; //记录了后缀字符串(为<trim>节点包裹的SQL语句添加的后缀)
//如果<trim>节点包裹的SQL语句是空语句,删除指定的前缀,如where
private final List<String> prefixesToOverride;
//如果<trim>节点包裹的SQL语句是空语句,删除指定的后缀,如逗号
private final List<String> suffixesToOverride;

它的接口方法如下

@Override
public boolean apply(DynamicContext context) {
  //创建FilteredDynamicContext对象,FilteredDynamicContext是TrimSqlNode的内部类,继承于DynamicContext
  FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);
  //调用子节点的apply()方法进行解析,注意收集SQL语句的是filteredDynamicContext
  boolean result = contents.apply(filteredDynamicContext);
  //处理前缀和后缀
  filteredDynamicContext.applyAll();
  return result;
}

FilteredDynamicContext的字段属性含义如下

private DynamicContext delegate; //底层封装的DynamicContext对象
private boolean prefixApplied; //是否已经处理过前缀
private boolean suffixApplied; //是否已经处理过后缀
private StringBuilder sqlBuffer; //用于记录子节点解析后的结果

FilteredDynamicContext的applyAll()方法

public void applyAll() {
  //获取子节点解析后的结果,并全部转化为大写
  sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());
  String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);
  if (trimmedUppercaseSql.length() > 0) {
    //处理前缀
    applyPrefix(sqlBuffer, trimmedUppercaseSql);
    //处理后缀
    applySuffix(sqlBuffer, trimmedUppercaseSql);
  }
  //将解析后的结果SQL片段添加到DynamicContext的StringBuilder中
  delegate.appendSql(sqlBuffer.toString());
}
private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!prefixApplied) { //如果还没有处理过前缀
    prefixApplied = true; //更新为已处理
    if (prefixesToOverride != null) { //如果需要删除的前缀列表不为null
      //遍历该前缀列表
      for (String toRemove : prefixesToOverride) {
        //如果<trim>子节点收集上来的SQL语句以该前缀开头
        if (trimmedUppercaseSql.startsWith(toRemove)) {
          //从<trim>子节点收集上来的StringBuilder中删除该前端
          sql.delete(0, toRemove.trim().length());
          break;
        }
      }
    }
    //如果有前缀字符串(比如说"("),将前缀字符串插入StringBuilder最前端
    if (prefix != null) {
      sql.insert(0, " ");
      sql.insert(0, prefix);
    }
  }
}
private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) {
  if (!suffixApplied) { //如果还没有处理过后缀
    suffixApplied = true; //更新为已处理后缀
    if (suffixesToOverride != null) { //如果需要处理的后缀列表不为null
      //遍历该后缀列表
      for (String toRemove : suffixesToOverride) {
        //如果从<trim>子节点收集上来的SQL语句以该后缀结尾
        if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) {
          //获取该后缀的起始位置
          int start = sql.length() - toRemove.trim().length();
          //获取该后缀的终止位置
          int end = sql.length();
          //从<trim>子节点收集上来的StringBuilder中删除该后端
          sql.delete(start, end);
          break;
        }
      }
    }
    //如果有后缀字符串(比如说")"),将前缀字符串拼接上StringBuilder最后端
    if (suffix != null) {
      sql.append(" ");
      sql.append(suffix);
    }
  }
}

WhereSqlNode和SetSqlNode都继承于TrimSqlNode,他们只是在TrimSqlNode的属性中指定了固定的标记。

public class WhereSqlNode extends TrimSqlNode {

  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}
public class SetSqlNode extends TrimSqlNode {

  private static List<String> suffixList = Arrays.asList(",");

  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", null, null, suffixList);
  }

}

ForEachSqlNode,在动态SQL语句中,通常需要对一个集合进行迭代,Mybatis提供了<foreach>标签实现该功能。在使用<foreach>标签迭代集合时,不仅可以使用集合的元素和索引值,还可以在循环开始之前或结束之后添加指定的字符串,也允许在迭代过程中添加指定的分隔符。配置样例如下

<insert id="insertBetdetailsByBatch" parameterType="java.util.List">
   insert into betdetails(id,orderId,actorIndex,createTime,ballIndex,ballValue,betAmount,rate1,rate2,rate3,gameType,status,betResult,awardAmount,ballName) values
   <foreach collection="list" item="item" index="index" separator=",">
      (#{item.id},#{item.orderId},#{item.actorIndex},#{item.createTime},#{item.ballIndex},#{item.ballValue},#{item.betAmount},#{item.rate1},#{item.rate2},#{item.rate3},#{item.gameType},#{item.status},#{item.betResult},#{item.awardAmount},#{item.ballName})
   </foreach>
</insert>

ForEachSqlNode中各个字段含义如下:

private final ExpressionEvaluator evaluator;
private final String collectionExpression;
private final SqlNode contents;
private final String open;
private final String close;
private final String separator;
private final String item;
private final String index;
private final Configuration configuration;

MySQL数据库优化

mumupudding阅读(5)

前言

数据库优化一方面是找出系统的瓶颈,提高MySQL数据库的整体性能,而另一方面需要合理的结构设计和参数调整,以提高用户的相应速度,同时还要尽可能的节约系统资源,以便让系统提供更大的负荷.

1. 优化一览图

2. 优化

笔者将优化分为了两大类,软优化和硬优化,软优化一般是操作数据库即可,而硬优化则是操作服务器硬件及参数设置.

2.1 软优化

2.1.1 查询语句优化

1.首先我们可以用EXPLAIN或DESCRIBE(简写:DESC)命令分析一条查询语句的执行信息.
2.例:

DESC SELECT * FROM `user`

显示:

其中会显示索引和查询数据读取数据条数等信息.

2.1.2 优化子查询

在MySQL中,尽量使用JOIN来代替子查询.因为子查询需要嵌套查询,嵌套查询时会建立一张临时表,临时表的建立和删除都会有较大的系统开销,而连接查询不会创建临时表,因此效率比嵌套子查询高.

2.1.3 使用索引

索引是提高数据库查询速度最重要的方法之一,关于索引可以参高笔者<MySQL数据库索引>一文,介绍比较详细,此处记录使用索引的三大注意事项:

  1. LIKE关键字匹配’%’开头的字符串,不会使用索引.
  2. OR关键字的两个字段必须都是用了索引,该查询才会使用索引.
  3. 使用多列索引必须满足最左匹配.

2.1.4 分解表

对于字段较多的表,如果某些字段使用频率较低,此时应当,将其分离出来从而形成新的表,

2.1.5 中间表

对于将大量连接查询的表可以创建中间表,从而减少在查询时造成的连接耗时.

2.1.6 增加冗余字段

类似于创建中间表,增加冗余也是为了减少连接查询.

2.1.7 分析表,,检查表,优化表

分析表主要是分析表中关键字的分布,检查表主要是检查表中是否存在错误,优化表主要是消除删除或更新造成的表空间浪费.

1. 分析表: 使用 ANALYZE 关键字,如ANALYZE TABLE user;

  1. Op:表示执行的操作.
  2. Msg_type:信息类型,有status,info,note,warning,error.
  3. Msg_text:显示信息.

2. 检查表: 使用 CHECK关键字,如CHECK TABLE user [option]

option 只对MyISAM有效,共五个参数值:

  1. QUICK:不扫描行,不检查错误的连接.
  2. FAST:只检查没有正确关闭的表.
  3. CHANGED:只检查上次检查后被更改的表和没被正确关闭的表.
  4. MEDIUM:扫描行,以验证被删除的连接是有效的,也可以计算各行关键字校验和.
  5. EXTENDED:最全面的的检查,对每行关键字全面查找.

3. 优化表:使用OPTIMIZE关键字,如OPTIMIZE [LOCAL|NO_WRITE_TO_BINLOG] TABLE user;

LOCAL|NO_WRITE_TO_BINLOG都是表示不写入日志.,优化表只对VARCHAR,BLOB和TEXT有效,通过OPTIMIZE TABLE语句可以消除文件碎片,在执行过程中会加上只读锁.

2.2 硬优化

2.2.1 硬件三件套

1.配置多核心和频率高的cpu,多核心可以执行多个线程.
2.配置大内存,提高内存,即可提高缓存区容量,因此能减少磁盘I/O时间,从而提高响应速度.
3.配置高速磁盘或合理分布磁盘:高速磁盘提高I/O,分布磁盘能提高并行操作的能力.

2.2.2 优化数据库参数

优化数据库参数可以提高资源利用率,从而提高MySQL服务器性能.MySQL服务的配置参数都在my.cnf或my.ini,下面列出性能影响较大的几个参数.

  • key_buffer_size:索引缓冲区大小
  • table_cache:能同时打开表的个数
  • query_cache_size和query_cache_type:前者是查询缓冲区大小,后者是前面参数的开关,0表示不使用缓冲区,1表示使用缓冲区,但可以在查询中使用SQL_NO_CACHE表示不要使用缓冲区,2表示在查询中明确指出使用缓冲区才用缓冲区,即SQL_CACHE.
  • sort_buffer_size:排序缓冲区

传送门:更多参数

2.2.3 分库分表

因为数据库压力过大,首先一个问题就是高峰期系统性能可能会降低,因为数据库负载过高对性能会有影响。另外一个,压力过大把你的数据库给搞挂了怎么办?所以此时你必须得对系统做分库分表 + 读写分离,也就是把一个库拆分为多个库,部署在多个数据库服务上,这时作为主库承载写入请求。然后每个主库都挂载至少一个从库,由从库来承载读请求。

2.2.4 缓存集群

如果用户量越来越大,此时你可以不停的加机器,比如说系统层面不停加机器,就可以承载更高的并发请求。然后数据库层面如果写入并发越来越高,就扩容加数据库服务器,通过分库分表是可以支持扩容机器的,如果数据库层面的读并发越来越高,就扩容加更多的从库。但是这里有一个很大的问题:数据库其实本身不是用来承载高并发请求的,所以通常来说,数据库单机每秒承载的并发就在几千的数量级,而且数据库使用的机器都是比较高配置,比较昂贵的机器,成本很高。如果你就是简单的不停的加机器,其实是不对的。所以在高并发架构里通常都有缓存这个环节,缓存系统的设计就是为了承载高并发而生。所以单机承载的并发量都在每秒几万,甚至每秒数十万,对高并发的承载能力比数据库系统要高出一到两个数量级。所以你完全可以根据系统的业务特性,对那种写少读多的请求,引入缓存集群。具体来说,就是在写数据库的时候同时写一份数据到缓存集群里,然后用缓存集群来承载大部分的读请求。这样的话,通过缓存集群,就可以用更少的机器资源承载更高的并发。

结语

一个完整而复杂的高并发系统架构中,一定会包含:各种复杂的自研基础架构系统。各种精妙的架构设计.因此一篇小文顶多具有抛砖引玉的效果,但是数据库优化的思想差不多就这些了.

mongoDB进阶

mumupudding阅读(4)

通过配置文件启动mongo服务器

参数 含义
–dbpath 指定数据库文件存放的目录
–port 端口默认是27017
–fork 以后台守护的方式进行启动
–logpath 指定日志文件输出路径
–config 指定一个配置文件
–auth 以安全方式启动数据库,需要验证账号和密码

直接在命令行中通过mongod --dbpath ...这样启动服务器,如果参数太多的话,就比较麻烦

So,我们可以选择通过运行配置文件的方式启动服务器

首先,要在一个目录下创建一个mongo.config(后缀名无所谓)

注意: log文件会自动生成,但data目录必须优先创建好

//mongo.config文件

dbpath=E:\mongouse\data
#数据库日志存放目录
logpath=E:\mongouse\log
#以追加的方式记录日志
logappend = true
#端口号 默认为27017
port=27017 
#以后台方式运行进程
fork=true 
#开启用户认证
auth=false
#关闭http接口,默认关闭http端口访问
nohttpinterface=true
#mongodb所绑定的ip地址
bind_ip = 127.0.0.1 
#启用日志文件,默认启用
journal=true 
#这个选项可以过滤掉一些无用的日志信息,若需要调试使用请设置为false
quiet=true 

然后在命令行中输入

mongod --config mongo.config

导入导出数据

  • mongoimport 导入
  • mongoexport 导出
参数 含义
-h[–host] 链接的数据库
–port 端口号
-u 用户名
-p 密码
-d 指定哪个数据库
-c 指定导出的集合
-o 导出的路径
-q 进行过滤的

方法一

mongoexport与导出数据库

mongoexport -d school -c students -o ./stu.bak

导出的是一个文件

整个文件是一个json

mongoimport与导入数据库

mongoimport -h 127.0.0.1 --port 27017 -d school -c students --file stu.bak

默认-h-p--file都可以省略

方法二

mongodump与导出数据库

和上面的区别在于不会转换(上面的会转换成json),适用于数据库中存在二进制数据的情况(二进制是转换不成json的)

导出整个数据库

mongodump -o mdmp

导出其中一个数据库

mongodump -d school -o school.dmp

导出来的样子

(school.dmp文件夹下有一个school文件)

mongorestore与导入数据库

如果想要导入整个数据库

mongorestore mdmp

如果只想导入其中一个数据库

mongorestore -d school mdmp/school

方法三:直接拷贝数据

将整个文件夹拷贝到指定目录下,然后在启动数据库服务器时将其指定为--dbpath

锁定和解锁数据库

强制将缓存区中的数据真正写入后锁住数据库

必须在admin数据库中使用命令

db.runCommand({fsync:1,lock:1}); //类似于node中的fs.fsync

解锁

db.fsyncUnlock();

示例:

打开一个命令行,先锁住

再打开一个命令行,像数据库中写入,会发现

迟迟不返回,说明正在等待写入

但当我们解锁

会发现原本正在等待的数据已经写入

安全措施

  • 物理隔离:电都不插
  • 网络隔离:区域网
  • 防火墙(IP/IP段/白名单/黑名单)
  • 用户名和密码验证

用户管理

要使用户生效,需要在启动服务器时加上--auth

mongod ... --auth

这样我们就不能裸连数据库了,必须要使用账号登录。

查看角色

show roles;

内置角色

  • 数据库用户角色:read、readWrite;
  • 数据库管理角色:dbAdmin、dbOwner、userAdmin
  • 集群管理角色:clusterAdmin、clusterManager、clusterMonitor、hostManage;
  • 备份恢复角色:backup、restore
  • 所有数据库角色:readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase
  • 超级用户角色:root
  • 内部角色:_system

用户的操作都需要在admin下面进行操作

如果在某个数据库下面执行操作,那么只对当前数据库生效

addUser已经废弃,默认会创建root用户,不安全,不再建议使用

创建用户

针对school数据库可以读

db.createUser({user:'ahhh',pwd:'123',roles:[{db:'school',role:'read'}]});

roles中不加db表示对所有数据库都有权限

显示用户权限

use admin;

var r = db.runCommand({usersInfo:'ahhh',showPrivileges:true})

printjson(r);

修改密码

修改密码

db.changeUserPassword({'ahhh','123456'});

验证密码是否正确

db.auth('ahhh','123456')

添加个人信息

db.runCommand({updateUser:'ahhh',pwd:'123',customData:{name:'ahuang',age:111,telephone:'123123xxx'}});

高级命令

首先runCommand中的参数是一个文档(JSON),但在runCommand中它是具有特殊意义的一些字段。

load(”)

路径分隔符必须使用/而不是\

D:\WEB\database\data //-->错误的

D:/WEB/database/data //-->正确的

group:分组

有以下数据

var stus = [
  {province:'北京',home:'北京',age:1}
  ,{province:'北京',home:'北京',age:2}
  ,{province:'北京',home:'北京',age:3}
  ,{province:'广东',home:'广州',age:1}
  ,{province:'广东',home:'佛山',age:2}
  ,{province:'广东',home:'东莞',age:3}   
]

我们这样执行命令进行分组

db.runCommand({
  group:{
    ns:'students' //namespace
    ,key:{home:1} //按照哪个key分组 可以写很多个
    ,query:{age:{$gt:1}} //满足条件才参与分组
    ,initial:{total:0} //每一组的初始值
    ,$reduce:function(doc,initial){
      initial.total += doc.age; //每个文档累加一次 最终会得到该分组下所有年龄的总和
    }
  }
});

分组结果

//retval:返回值类型说明

{
    "retval" : [
        {
            "home" : "北京",
            "total" : 5
        },
        {
            "home" : "佛山",
            "total" : 2
        },
        {
            "home" : "东莞",
            "total" : 3
        }
    ],
    "count" : NumberLong(4),
    "keys" : NumberLong(3),
    "ok" : 1
}

distinct:查找不重复的key值

以下会在students集合下查找所有key为home的值

db.runCommand({distinct:'students',key:'home'});

返回是这样的

{ "values" : [ "北京", "广州", "佛山", "东莞" ], "ok" : 1 }

执行命令时也可以这样执行

db.runCommand({distinct:'students',key:'home'}).values;

这样能直接得到key的值组成的数组

 [ "北京", "广州", "佛山", "东莞" ]

drop

删除集合除了db.xxx.drop(),也可以

db.runCommand({drop:'students'});

其它

查看数据库信息

db.runCommand({buildInfo:1}); 

查看students集合下上一次的执行错误信息

db.runCommand({getLastError:'students'}); 

固定集合

有着固定大小的集合,满了以后会覆盖掉最先插入的。(先入先出)

特性

  • 没有索引
  • 插入和查询速度非常快,不需要重新分配空间
  • 特别适合存储日志

创建固定集合

  • size单位是kb
  • max单位是
  • capped:是否有上限封顶,必须为true
db.createCollection('logs',{size:5,max:5,capped:true})

非固定集合转换为固定集合

db.runCommand({convertToCapped:'logs',size:5})

gridfs:网格文件存储系统

gridfs是mongodb自带的文件系统,使用二进制存储文件。

mongodb可以以BSON格式保存二进制对象,但是BSON对象的体积不能超过4M。所以mongodb提供了mongofiles。它可以把一个大文件透明地分割成小文件(256k),从而保存大体积的数据

GridFS用于存储和恢复那些超过16M(BSON文件限制)的文件(如:图片、音频、视频等)

GridFS用两个集合来存储一个文件:fs.files(元信息)与fs.chunks(实际内容)

每个文件的实际内容被存在chunks(二进制数据)中,和文件有关的meta数据(filename,content_type,还有用户自定义的属性)将会被存在files集合中。

使用

存储

将1.txt放到myfiles数据库中

mongofiles -d myfiles put 1.txt

一个文件会在fs.files中对应一个id

假如我们有两份文件存储在gridfs中

可以发现files中有两个id

fs.chunks中则不是这样了,它会分成很多份

查看文件列表

查看myfiles数据库下的所有文件

mongofiles -d myfiles list

详细查看文件信息

db.fs.files.find();

db.fs.files.find(files_id:objectId(''));;

获取&&下载

mongofiles  -d myfile get 1.txt

删除文件

mongofiles -d myfiles delete 1.txt

eval

执行脚本

db.eval('1+1');
<<<
2
db.eval("return 'hello'");
<<<
hello
db.system.js.insert({_id:'xx',value:'111'});
//类似于声明了一个全局变量
db.eval("return xx");
<<<
111

会存储在当前数据库下的Functions文件夹下(和Collections文件夹同级)

db.system.js.insert({_id:'say',value:function(){return 'hello'}});
//类似于声明了一个全局变量
db.eval("say()");
<<<
hello

上一篇:mongoDB基础

打开看板娘