明年我18

明年我18 查看完整档案

广州编辑西安电子科技大学  |  软件工程 编辑  |  填写所在公司/组织填写个人主网站
编辑

明年我18

个人动态

明年我18 发布了文章 · 2019-11-23

ThreadLocal原理分析与代码验证

ThreadLocal提供了线程安全的数据存储和访问方式,利用不带key的get和set方法,居然能做到线程之间隔离,非常神奇。

比如

ThreadLocal<String> threadLocal = new ThreadLocal<>();

in thread 1

//in thread1
treadLocal.set("value1");
.....
//value的值是value1
String value = threadLocal.get();

in thread 2

//in thread2
treadLocal.set("value2");
.....
//value的值是value2
String value = threadLocal.get();

不论thread1和thread2是不是同时执行,都不会有线程安全问题,我们来测试一下。

线程安全测试

开10个线程,每个线程内都对同一个ThreadLocal对象set不同的值,会发现ThreadLocal在每个线程内部get出来的值,只会是自己线程内set进去的值,不会被别的线程影响。

static void testUsage() throws InterruptedException {
    Utils.println("-------------testUsage-------------------");
    ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    AtomicBoolean threadSafe = new AtomicBoolean(true);
    int count = 10;
    CountDownLatch countDownLatch = new CountDownLatch(count);
    Random random = new Random(736832);
    for (int i = 0; i < count; i ++){
        new Thread(() -> {
            try {
                //生成一个随机数
                Long value = System.nanoTime() + random.nextInt();
                threadLocal.set(value);
                Thread.sleep(1000);

                Long value2 = threadLocal.get();
                if (!value.equals(value2)) {
                    //get和set的value不一致,说明被别的线程修改了,但这是不可能出现的
                    threadSafe.set(false);
                    Utils.println("thread unsafe, this could not be happen!");
                }
            } catch (InterruptedException e) {

            }finally {
                countDownLatch.countDown();
            }

        }).start();
    }

    countDownLatch.await();

    Utils.println("all thread done, and threadSafe is " + threadSafe.get());
    Utils.println("------------------------------------------");
}

输出:

-------------testUsage------------------
all thread done, and threadSafe is true
-----------------------------------------

原理浅析

翻开ThreadLocal的源码,会发现ThreadLocal只是一个空壳子,它并不存储具体的value,而是利用当前线程(Thread.currentThread())的threadLocalMap来存储value,key就是这个threadLocal对象本身。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

Thread的threadLocals字段是ThreadLocalMap类型(你可以简单理解为一个key value的Map),key是ThreadLocal对象,value是我们在外层设置的值

  • 当我们调用threadLocal.set(value)方法的时候,会找到当前线程的threadLocals这个map,然后以this作为key去set key value
  • 当我们调用threadLocal.get()方法的时候,会找到当前线程的threadLocals这个map,然后以this作为key去get value
  • 当我们调用threadLocal.remove()方法的时候,会找到当前线程的threadLocals这个map,然后以this作为key去remove

这就相当于:

Thread.currentThread().threadLocals.set(threadLocal1, "value1");
.....
//value的值是value1
String value = Thread.currentThread().threadLocals.get(threadLocal1);

因为每个Thread都是不同的对象,所以他们的threadLocals也是不同的map,threadLocal在不同的线程里工作时,实际上是从不同的map里get/set,这也就是线程安全的原因了,了解到这一点就差不多了。

再深入一些,ThreadLocalMap的结构

如果继续翻ThreadLocalMap的源码,会发现它有个字段table,是Entry类型的数组。

我们不妨写段代码,把ThreadLocalMap的结构输出出来。

由于Thread.threadLocals和ThreadLocalMap类不是public的,我们只有通过反射来获取它的值。反射的代码如下(如果嫌长可以不看,直接看输出):

static Object getThreadLocalMap(Thread thread) throws NoSuchFieldException, IllegalAccessException {        
    //get thread.threadLocals
    Field threadLocals = Thread.class.getDeclaredField("threadLocals");
    threadLocals.setAccessible(true);
    return threadLocals.get(thread);
}

static void printThreadLocalMap(Object threadLocalMap) throws NoSuchFieldException, IllegalAccessException {
    String threadName = Thread.currentThread().getName();
    
    if(threadLocalMap == null){
        Utils.println("threadMap is null, threadName:" + threadName);
        return;
    }

    Utils.println(threadName);

    //get threadLocalMap.table
    Field tableField = threadLocalMap.getClass().getDeclaredField("table");
    tableField.setAccessible(true);
    Object[] table = (Object[])tableField.get(threadLocalMap);
    Utils.println("----threadLocals (ThreadLocalMap), table.length = " + table.length);

    for (int i = 0; i < table.length; i ++){
        WeakReference<ThreadLocal<?>> entry = (WeakReference<ThreadLocal<?>>)table[i];
        printEntry(entry, i);
    }
}
static void printEntry(WeakReference<ThreadLocal<?>> entry, int i) throws NoSuchFieldException, IllegalAccessException {
    if(entry == null){
        Utils.println("--------table[" + i + "] -> null");
        return;
    }
    ThreadLocal key = entry.get();
    //get entry.value
    Field valueField = entry.getClass().getDeclaredField("value");
    valueField.setAccessible(true);
    Object value = valueField.get(entry);

    Utils.println("--------table[" + i + "] -> entry key = " + key + ", value = " + value);
}

测试代码:

static void testStructure() throws InterruptedException {
    Utils.println("-------------testStructure----------------");
    ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

    Thread thread1 = new Thread(() -> {
        threadLocal1.set("threadLocal1-value");
        threadLocal2.set("threadLocal2-value");

        try {
            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            printThreadLocalMap(threadLocalMap);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread1");

    thread1.start();

    //wait thread1 done
    thread1.join();

    Thread thread2 = new Thread(() -> {
        threadLocal1.set("threadLocal1-value");
        try {
            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            printThreadLocalMap(threadLocalMap);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread2");

    thread2.start();
    thread2.join();
    Utils.println("------------------------------------------");
}

我们在创建了两个ThreadLocal的对象threadLocal1和threadLocal2,在线程1里为这两个对象设置值,在线程2里只为threadLocal1设置值。然后分别打印出这两个线程的threadLocalMap。

输出结果为:

-------------testStructure----------------
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = java.lang.ThreadLocal@33baa315, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@4d42db5c, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
thread2
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> null
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@4d42db5c, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
------------------------------------------

从结果上可以看出:

  • 线程1和线程2的threadLocalMap对象的table字段,是个数组,长度都是16
  • 由于线程1里给两个threadLocal对象设置了值,所以线程1的ThreadLocalMap里有两个entry,数组下标分别是1和10,其余的是null(如果你自己写代码验证,下标不一定是1和10,不需要纠结这个问题,只要前后对的上就行)
  • 由于线程2里只给一个threadLocal对象设置了值,所以线程1的ThreadLocalMap里只有一个entry,数组下标是10,其余的是null
  • threadLocal1这个对象在两个线程里都设置了值,所以当它作为key加入二者的threadLocalMap时,key是一样的,都是java.lang.ThreadLocal@4d42db5c;下标也是一样的,都是10。

为什么是WeakReference

查看Entry的源码,会发现Entry继承自WeakReference:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

构造函数里把key传给了super,也就是说,ThreadLocalMap中对key的引用,是WeakReference的。

Weak reference objects, which do not prevent their referents from being
made finalizable, finalized, and then reclaimed. Weak references are most
often used to implement canonicalizing mappings.

通俗点解释:

当一个对象仅仅被weak reference(弱引用), 而没有任何其他strong reference(强引用)的时候, 不论当前的内存空间是否足够,当GC运行的时候, 这个对象就会被回收。

看不明白没关系,还是写代码测试一下什么是WeakReference吧...

static void testWeakReference(){
    Object obj1 = new Object();
    Object obj2 = new Object();
    WeakReference<Object> obj1WeakRef = new WeakReference<>(obj1);
    WeakReference<Object> obj2WeakRf = new WeakReference<>(obj2);
    //obj32StrongRef是强引用
    Object obj2StrongRef = obj2;
    Utils.println("before gc: obj1WeakRef = " + obj1WeakRef.get() + ", obj2WeakRef = " + obj2WeakRf.get() + ", obj2StrongRef = " + obj2StrongRef);

    //把obj1和obj2设为null
    obj1 = null;
    obj2 = null;
    //强制gc
    forceGC();

    Utils.println("after gc: obj1WeakRef = " + obj1WeakRef.get() + ", obj2WeakRef = " + obj2WeakRf.get() + ", obj2StrongRef = " + obj2StrongRef);
}

结果输出:

before gc: obj1WeakRef = java.lang.Object@4554617c, obj2WeakRef = java.lang.Object@74a14482, obj2StrongRef = java.lang.Object@74a14482
after gc: obj1WeakRef = null, obj2WeakRef = java.lang.Object@74a14482, obj2StrongRef = java.lang.Object@74a14482

从结果上可以看出:

  • 我们先new了两个对象(为避免混淆,称他们为Object1和Object2),分别用变量obj1和obj2指向它们,同时定义了一个obj2StrongRef,也指向Object2,最后把obj1和obj2均指向null
  • 由于Object1没有变量强引用它了,所以在gc后,Object1被回收了,obj1WeakRef.get()返回了null
  • 由于Object2还有obj2StrongRef在引用它,所以gc后,Object2依然存在,没有被回收。

那么,ThreadLocalMap中对key的引用,为什么是WeakReference的呢?

因为大部分情况下,线程不死

大部分情况下,线程不会频繁的创建和销毁,一般都会用线程池。所以线程对象一般不会被清除,线程的threadLocalMap就一直存在。
如果key对ThreadLocal是强引用,那么key永远不会被回收,即使我们程序里再也不用它了。

但是key是弱引用的话,情况就会得到改善:只要没有指向threadLocal的强引用了,这个ThreadLocal对象就会被清理。

我们还是写代码测试一下吧。

/**
 * 测试ThreadLocal对象什么时候被回收
 * @throws InterruptedException
 */
static void testGC() throws InterruptedException {
    Utils.println("-----------------testGC-------------------");
    Thread thread1 = new Thread(() -> {
        ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
        ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

        threadLocal1.set("threadLocal1-value");
        threadLocal2.set("threadLocal2-value");

        try {
            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            Utils.println("print threadLocalMap before gc");
            printThreadLocalMap(threadLocalMap);

            //set threadLocal1 unreachable
            threadLocal1 = null;

            forceGC();

            Utils.println("print threadLocalMap after gc");
            printThreadLocalMap(threadLocalMap);


        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread1");

    thread1.start();
    thread1.join();
    Utils.println("------------------------------------------");
}

我们在一个线程里为两个ThreadLocal对象赋值,最后把其中一个对象的强引用移除,gc后打印当前线程的threadLocalMap。
输出结果如下:

-----------------testGC-------------------
print threadLocalMap before gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = java.lang.ThreadLocal@7bf9cebf, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@56342d38, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
print threadLocalMap after gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = java.lang.ThreadLocal@7bf9cebf, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = null, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
------------------------------------------

从输出结果可以看到,当我们把threadLocal1的强引用移除并gc之后,table[10]的key变成了null,说明threadLocal1这个对象被回收了;threadLocal2的强引用还在,所以table[1]的key不是null,没有被回收。

但是你发现没有,table[10]的key虽然是null了,但value还活着! table[10]这个entry对象,也活着!

是的,因为只有key是WeakReference....

无用的entry什么时候被回收?

通过查看ThreadLocal的源码,发现在ThreadLocal对象的get/set/remove方法执行时,都有机会清除掉map中已经无用的entry。

最容易验证清除无用entry的场景分别是:

  • remove:这个不用说了,这哥们本来就是做这个的
  • get:当一个新的threadLocal对象(没有set过value)发生get调用时,也会作为新的entry加入map,在加入的过程中,有机会清除掉无用的entry,逻辑和下面的set相同。
  • set: 当一个新的threadLocal对象(没有set过value)发生set调用时,会在map中加入新的entry,此时有机会清除掉无用的entry,清除的逻辑是:

    • 清除掉table数组中的那些无用entry中的一部分,记住是一部分,这个一部分可能全部,也可能是0,具体算法请看ThreadLocalMap.cleanSomeSlots,这里不解释了。
    • 如果上一步的"一部分"是0(即清除了0个),并且map的size(是真实size,不是table.length)大于等于threshold(table.length的2/3),会执行一次rehash,在rehash的过程中,清理掉所有无用的entry,并减小size,清理后的size如果还大于等于threshold - threshold/4,则把table扩容为原来的两倍大小。

还有其他场景,但不好验证,这里就不提了。

ThreadLocal源码就不贴了,贴了也讲不明白,相关逻辑在setInitialValue、cleanSomeSlots、expungeStaleEntries、rehash、resize等方法里。

在我们写代码验证entry回收逻辑之前,还需要简单的提一下ThreadLocalMap的hash算法。

entry数组的下标如何确定?

每个ThreadLocal对象,都有一个threadLocalHashCode变量,在加入ThreadLocalMap的时候,根据这个threadLocalHashCode的值,对entry数组的长度取余(hash & (len - 1)),余数作为下标。

那么threadLocalHashCode是怎么计算的呢?看源码:

public class ThreadLocal<T>{
    private final int threadLocalHashCode = nextHashCode();
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    ...
}

ThreadLocal类维护了一个全局静态字段nextHashCode,每new一个ThreadLocal对象,nextHashCode都会递增0x61c88647,作为下一个ThreadLocal对象的threadLocalHashCode。

这个0x61c88647,是个神奇的数字,只要以它为递增值,那么和2的N次方取余时,在有限的次数内不会发生重复。
比如和16取余,那么在16次递增内,不会发生重复。还是写代码验证一下吧。

int hashCode = 0;
int HASH_INCREMENT = 0x61c88647;
int length = 16;

for(int i = 0; i < length ; i ++){
    int h = hashCode & (length - 1);
    hashCode += HASH_INCREMENT;
    System.out.println("h = " + h + ", i = " + i);
}

输出结果为:

h = 0, i = 0
h = 7, i = 1
h = 14, i = 2
h = 5, i = 3
h = 12, i = 4
h = 3, i = 5
h = 10, i = 6
h = 1, i = 7
h = 8, i = 8
h = 15, i = 9
h = 6, i = 10
h = 13, i = 11
h = 4, i = 12
h = 11, i = 13
h = 2, i = 14
h = 9, i = 15

你看,h的值在16次递增内,没有发生重复。 但是要记住,2的N次方作为长度才会有这个效果,这也解释了为什么ThreadLocalMap的entry数组初始长度是16,每次都是2倍的扩容。

验证新threadLocal的get和set时回收部分无效的entry

为了验证出结果,我们需要先给ThreadLocal的nextHashCode重置一个初始值,这样在测试的时候,每个threadLocal的数组下标才会按照我们设计的思路走。

static void resetNextHashCode() throws NoSuchFieldException, IllegalAccessException {
    Field nextHashCodeField = ThreadLocal.class.getDeclaredField("nextHashCode");
    nextHashCodeField.setAccessible(true);
    nextHashCodeField.set(null, new AtomicInteger(1253254570));
}

然后在测试代码里,我们先调用resetNextHashCode方法,然后加两个ThreadLocal对象并set值,gc前把强引用去除,gc后再new两个新的theadLocal对象,分别调用他们的get和set方法。
在每个关键点打印出threadLocalMap做比较。

static void testExpungeSomeEntriesWhenGetOrSet() throws InterruptedException {
    Utils.println("----------testExpungeStaleEntries----------");
    Thread thread1 = new Thread(() -> {
        try {
            resetNextHashCode();

            //注意,这里必须有两个ThreadLocal,才能验证出threadLocal1被清理
            ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
            ThreadLocal<String> threadLocal2 = new ThreadLocal<>();

            threadLocal1.set("threadLocal1-value");
            threadLocal2.set("threadLocal2-value");


            Object threadLocalMap = getThreadLocalMap(Thread.currentThread());
            //set threadLocal1 unreachable
            threadLocal1 = null;
            threadLocal2 = null;
            forceGC();

            Utils.println("print threadLocalMap after gc");
            printThreadLocalMap(threadLocalMap);

            ThreadLocal<String> newThreadLocal1 = new ThreadLocal<>();
            newThreadLocal1.get();
            Utils.println("print threadLocalMap after call a new newThreadLocal1.get");
            printThreadLocalMap(threadLocalMap);

            ThreadLocal<String> newThreadLocal2 = new ThreadLocal<>();
            newThreadLocal2.set("newThreadLocal2-value");
            Utils.println("print threadLocalMap after call a new newThreadLocal2.set");
            printThreadLocalMap(threadLocalMap);


        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

    }, "thread1");

    thread1.start();
    thread1.join();
    Utils.println("------------------------------------------");
}

程序输出结果为:

----------testExpungeStaleEntries----------
print threadLocalMap after gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = null, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> null
--------table[9] -> null
--------table[10] -> entry key = null, value = threadLocal1-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
print threadLocalMap after call a new newThreadLocal1.get
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = null, value = threadLocal2-value
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> entry key = java.lang.ThreadLocal@2b63dc81, value = null
--------table[9] -> null
--------table[10] -> null
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> null
print threadLocalMap after call a new newThreadLocal2.set
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> null
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> null
--------table[7] -> null
--------table[8] -> entry key = java.lang.ThreadLocal@2b63dc81, value = null
--------table[9] -> null
--------table[10] -> null
--------table[11] -> null
--------table[12] -> null
--------table[13] -> null
--------table[14] -> null
--------table[15] -> entry key = java.lang.ThreadLocal@2e93c547, value = newThreadLocal2-value
------------------------------------------

从结果上来看,

  • gc后table[1]和table[10]的key变成了null
  • new newThreadLocal1.get后,新增了table[8],table[10]被清理了,但table[1]还在(这就是cleanSomeSlots中some的意思)
  • new newThreadLocal2.set后,新增了table[15],table[1]被清理了。

验证map的size大于等于table.length的2/3时回收所有无效的entry

    static void testExpungeAllEntries() throws InterruptedException {
        Utils.println("----------testExpungeStaleEntries----------");
        Thread thread1 = new Thread(() -> {
            try {
                resetNextHashCode();

                int threshold = 16 * 2 / 3;
                ThreadLocal[] threadLocals = new ThreadLocal[threshold - 1];
                for(int i = 0; i < threshold - 1; i ++){
                    threadLocals[i] = new ThreadLocal<String>();
                    threadLocals[i].set("threadLocal" + i + "-value");
                }

                Object threadLocalMap = getThreadLocalMap(Thread.currentThread());

                threadLocals[1] = null;
                threadLocals[8] = null;
                //threadLocals[6] = null;
                //threadLocals[4] = null;
                //threadLocals[2] = null;
                forceGC();

                Utils.println("print threadLocalMap after gc");
                printThreadLocalMap(threadLocalMap);

                ThreadLocal<String> newThreadLocal1 = new ThreadLocal<>();
                newThreadLocal1.set("newThreadLocal1-value");
                Utils.println("print threadLocalMap after call a new newThreadLocal1.get");
                printThreadLocalMap(threadLocalMap);

            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }

        }, "thread1");

        thread1.start();
        thread1.join();
        Utils.println("------------------------------------------");
    }

我们先创建了9个threadLocal对象并设置了值,然后去掉了其中2个的强引用(注意这2个可不是随意挑选的)。
gc后再添加一个新的threadLocal,最后打印出最新的map。输出为:

----------testExpungeStaleEntries----------
print threadLocalMap after gc
thread1
----threadLocals (ThreadLocalMap), table.length = 16
--------table[0] -> null
--------table[1] -> entry key = null, value = threadLocal1-value
--------table[2] -> entry key = null, value = threadLocal8-value
--------table[3] -> null
--------table[4] -> entry key = java.lang.ThreadLocal@60523912, value = threadLocal6-value
--------table[5] -> null
--------table[6] -> entry key = java.lang.ThreadLocal@48fccd7a, value = threadLocal4-value
--------table[7] -> null
--------table[8] -> entry key = java.lang.ThreadLocal@188bbe72, value = threadLocal2-value
--------table[9] -> null
--------table[10] -> entry key = java.lang.ThreadLocal@19e0ebe8, value = threadLocal0-value
--------table[11] -> entry key = java.lang.ThreadLocal@688bcb6f, value = threadLocal7-value
--------table[12] -> null
--------table[13] -> entry key = java.lang.ThreadLocal@46324c19, value = threadLocal5-value
--------table[14] -> null
--------table[15] -> entry key = java.lang.ThreadLocal@38f1283, value = threadLocal3-value
print threadLocalMap after call a new newThreadLocal1.get
thread1
----threadLocals (ThreadLocalMap), table.length = 32
--------table[0] -> null
--------table[1] -> null
--------table[2] -> null
--------table[3] -> null
--------table[4] -> null
--------table[5] -> null
--------table[6] -> entry key = java.lang.ThreadLocal@48fccd7a, value = threadLocal4-value
--------table[7] -> null
--------table[8] -> null
--------table[9] -> entry key = java.lang.ThreadLocal@1dae16b1, value = newThreadLocal1-value
--------table[10] -> entry key = java.lang.ThreadLocal@19e0ebe8, value = threadLocal0-value
--------table[11] -> null
--------table[12] -> null
--------table[13] -> entry key = java.lang.ThreadLocal@46324c19, value = threadLocal5-value
--------table[14] -> null
--------table[15] -> null
--------table[16] -> null
--------table[17] -> null
--------table[18] -> null
--------table[19] -> null
--------table[20] -> entry key = java.lang.ThreadLocal@60523912, value = threadLocal6-value
--------table[21] -> null
--------table[22] -> null
--------table[23] -> null
--------table[24] -> entry key = java.lang.ThreadLocal@188bbe72, value = threadLocal2-value
--------table[25] -> null
--------table[26] -> null
--------table[27] -> entry key = java.lang.ThreadLocal@688bcb6f, value = threadLocal7-value
--------table[28] -> null
--------table[29] -> null
--------table[30] -> null
--------table[31] -> entry key = java.lang.ThreadLocal@38f1283, value = threadLocal3-value
------------------------------------------

从结果上看:

  • gc后table[1]和table[2](即threadLocal1和threadLocal8)的key变成了null
  • 加入新的threadLocal后,table的长度从16变成了32(因为此时的size是8,正好等于10 - 10/4,所以扩容),并且threadLocal1和threadLocal8这两个entry不见了。

如果在gc前,我们把threadLocals[1、8、6、4、2]都去掉强引用,加入新threadLocal后会发现1、8、6、4、2被清除了,但没有扩容,因为此时size是5,小于10-10/4。这个逻辑就不贴测试结果了,你可以取消注释上面代码中相关的逻辑试试。

大部分场景下,ThreadLocal对象的生命周期是和app一致的,弱引用形同虚设

回到现实中。

我们用ThreadLocal的目的,无非是在跨方法调用时更方便的线程安全地存储和使用变量。这就意味着ThreadLocal的生命周期很长,甚至和app是一起存活的,强引用一直在。

既然强引用一直存在,那么弱引用就形同虚设了。

所以在确定不再需要ThreadLocal中的值的情况下,还是老老实实的调用remove方法吧!

代码地址

https://github.com/kongxiangxin/pine/tree/master/threadlocal

查看原文

赞 0 收藏 0 评论 0

明年我18 发布了文章 · 2019-11-22

'try(A a = new A())' VS 'try finally'

实现了AutoCloseable接口的类,可以在try的时候直接实例化对象。try代码块完成之后,自动调用close方法,相当于在finally里主动调用。但是出现异常后的流程和try finally有什么不同呢? 下面写代码测试一下。

首先定义一个类Cat,实现AutoCloseable接口

class Cat implements AutoCloseable{
    void sayHello() throws Exception {
        Utils.println("calling sayHello(), I will throw an exception");
        throw new Exception("Exception in sayHello() ");
    }

    @Override
    public void close() throws Exception {
        Utils.println("I'm closing, I will throw an exception");
        throw new Exception("Exception in close()");
    }
}

我们的这个Cat有以下特点:

  • sayHello方法会抛出异常
  • close方法也会抛出异常

test v1: 'try(Cat cat = new Cat())' VS 'try finally'

没有catch(不要被外层的catch迷惑,那只是为了打印异常)
static void testV1(){
    Utils.println("----try(Cat cat = new Cat())-----");
    try{
        try(Cat cat = new Cat()){
            cat.sayHello();
        }

    }catch (Exception e){
        Utils.println("cache error in main (" + e + "), let's see its stack trace");
        Utils.printStackTrace(e);
    }
    Utils.println("--------------");

    Utils.println("----try finally-----");
    try{
        Cat cat = null;
        try{
            cat = new Cat();
            cat.sayHello();
        }finally {
            if(cat != null){
                cat.close();
            }
        }
    }catch (Exception e){
        Utils.println("cache error in main (" + e + "), let's see its stack trace");
        Utils.printStackTrace(e);
    }
    Utils.println("--------------");
}

结果输出:

----test v1----------------------------------------
----try(Cat cat = new Cat())-----
calling sayHello(), I will throw an exception
I'm closing, I will throw an exception
cache error in main (java.lang.Exception: Exception in sayHello() ), let's see its stack trace
java.lang.Exception: Exception in sayHello() 
    at Cat.sayHello(Cat.java:4)
    at Test.testV1(Test.java:16)
    at Test.main(Test.java:4)
    Suppressed: java.lang.Exception: Exception in close()
        at Cat.close(Cat.java:10)
        at Test.testV1(Test.java:17)
        ... 1 more
--------------
----try finally-----
calling sayHello(), I will throw an exception
I'm closing, I will throw an exception
cache error in main (java.lang.Exception: Exception in close()), let's see its stack trace
java.lang.Exception: Exception in close()
    at Cat.close(Cat.java:10)
    at Test.testV1(Test.java:33)
    at Test.main(Test.java:4)
--------------

结论

  • try(Cat cat = new Cat())

    • try代码块完成之后会自动调用close
    • close抛出的异常,被Suppressed了,外层捕获的只有sayHello的异常,但通过堆栈可以找到这个Suppressed的异常
  • try finally

    • 外层捕获的是在finally执行close时抛出的异常,sayHello的异常完全不见了。

test v2: 'try(Cat cat = new Cat()) catch{}' VS 'try catch finally'

有catch,并且catch里再抛出异常
static void testV2(){
    Utils.println("----try(Cat cat = new Cat()) catch-----");
    try{
        try(Cat cat = new Cat()){
            cat.sayHello();
        } catch (Exception e) {
            Utils.println("cached err (" + e.getMessage() + "), I will throw an exception again");
            throw new Exception("Exception in catch", e);
        }

    }catch (Exception e){
        Utils.println("cache error in main (" + e + "), let's see its stack trace");
        Utils.printStackTrace(e);
    }
    Utils.println("-----------------------------------------");

    Utils.println("----try catch finally--------------------");
    try{
        Cat cat = null;
        try{
            cat = new Cat();
            cat.sayHello();
        } catch (Exception e) {
            Utils.println("cached err (" + e.getMessage() + "), I will throw an exception again");
            throw new Exception("Exception in catch", e);
        }finally {
            if(cat != null){
                cat.close();
            }
        }
    }catch (Exception e){
        Utils.println("cache error in main (" + e + "), let's see its stack trace");
        Utils.printStackTrace(e);
    }
    Utils.println("-------------------------------------------");
}

结果输出

----test v2------
----try(Cat cat = new Cat()){} catch{}-----
calling sayHello(), I will throw an exception
I'm closing, I will throw an exception
cached err (Exception in sayHello() ), I will throw an exception again
cache error in main (java.lang.Exception: Exception in catch), let's see its stack trace
java.lang.Exception: Exception in catch
    at Test.testV2(Test.java:50)
    at Test.main(Test.java:8)
-----------------------------------------
----try catch finally--------------------
calling sayHello(), I will throw an exception
cached err (Exception in sayHello() ), I will throw an exception again
I'm closing, I will throw an exception
cache error in main (java.lang.Exception: Exception in close()), let's see its stack trace
java.lang.Exception: Exception in close()
    at Cat.close(Cat.java:10)
    at Test.testV2(Test.java:70)
    at Test.main(Test.java:8)
-------------------------------------------
---------------------------------------------------------------------

结论

  • try(Cat cat = new Cat()) catch

    • catch之前就调用了close(符合try代码块完成之后会自动调用close这个结论)
    • catch到的是sayHello的异常,close抛出的异常依然被Suppressed了
    • catch中再次抛出的异常被外层捕获
  • try catch finally

    • 先走catch,再走finally,所以catch捕获的是sayHello的异常
    • catch中再次抛出的异常不见了,外层捕获的是在finally执行close时抛出的异常。

测试代码地址:https://github.com/kongxiangx...

查看原文

赞 0 收藏 0 评论 0

明年我18 发布了文章 · 2019-11-14

pom中更换阿里云仓库时不要忽略了pluginRepositories

用maven也大几年了,也一直在用阿里云的中央仓库。
不喜欢在maven的settings.xml里改,更喜欢直接在pom.xml里改,因为受git管理,小伙伴们拉下来即可。

然而网上的大部分技术文章都只会指导你这么配置:

<repositories>
    <repository>
        <id>aliyun</id>
        <url>https://maven.aliyun.com/repository/public</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

如果你只是配置了repositories,那么你会发现在mvn在下载依赖的时候,一部分从阿里云下载,一部分还是从默认的仓库(https://repo.maven.apache.org )下载。

# mvn clean install
[INFO] Scanning for projects...
Downloading from aliyun: http://maven.aliyun.com/nexus/content/groups/public/org/springframework/boot/spring-boot-starter-parent/2.0.2.RELEASE/spring-boot-s
tarter-parent-2.0.2.RELEASE.pom
Downloaded from aliyun: http://maven.aliyun.com/nexus/content/groups/public/org/springframework/boot/spring-boot-starter-parent/2.0.2.RELEASE/spring-boot-st
arter-parent-2.0.2.RELEASE.pom (12 kB at 3.1 kB/s)
...
...
...
[INFO]
[INFO] --------------------------< com.zy:zy-parent >--------------------------
[INFO] Building zy-parent 1.0.0-SNAPSHOT
[INFO] --------------------------------[ pom ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/3.0.0/maven-clean-plugin-3.0.0.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/3.0.0/maven-clean-plugin-3.0.0.pom (4.8 kB at 2.2 k
B/s)
...
...
...
[INFO]
[INFO] --- maven-clean-plugin:3.0.0:clean (default-clean) @ zy-parent ---
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-plugin-api/3.0/maven-plugin-api-3.0.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-plugin-api/3.0/maven-plugin-api-3.0.pom (2.3 kB at 1.5 kB/s)
...
...
...
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ zy-parent ---
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-plugin-api/2.2.1/maven-plugin-api-2.2.1.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-plugin-api/2.2.1/maven-plugin-api-2.2.1.pom (1.5 kB at 1.2 kB/s)
...
...
...
[INFO] Installing E:\work\scratch\lily\src\zy-parent\pom.xml to D:\tools\maven\repo\com\zy\zy-parent\1.0.0-SNAPSHOT\zy-parent-1.0.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 04:25 min
[INFO] Finished at: 2019-11-14T10:13:53+08:00
[INFO] ------------------------------------------------------------------------

原来,只有项目本身的依赖,走了aliyun这个repository,maven命令需要的插件(比如clean、install都是maven的插件),走的还是默认的repository。

查看maven的官方文档(http://maven.apache.org/pom.h... ),可以看到pom中除了repositories节点之外,还有一个关于仓库的节点是pluginRepositories:

Repositories are home to two major types of artifacts. The first are artifacts that are used as dependencies of other artifacts. These are the majority of plugins that reside within central. The other type of artifact is plugins. Maven plugins are themselves a special type of artifact. Because of this, plugin repositories may be separated from other repositories (although, I have yet to hear a convincing argument for doing so). In any case, the structure of the pluginRepositories element block is similar to the repositories element. The pluginRepository elements each specify a remote location of where Maven can find new plugins.

所以我们还需要再pom中增加pluginRepositories才可以。
这也是网上大部分文章里忽略掉的内容。。。。。

最终的pom文件如下:

<repositories>
    <repository>
        <id>aliyun</id>
        <url>https://maven.aliyun.com/repository/public</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
<pluginRepositories>
    <pluginRepository>
        <id>aliyun-plugin</id>
        <url>https://maven.aliyun.com/repository/public</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>

现在,你可以清空本地maven仓库中的包,然后再次执一下mvn clean install,看看是不是都走了阿里云的仓库了。

查看原文

赞 1 收藏 0 评论 1

明年我18 回答了问题 · 2019-11-01

关于mysql分库水平扩容,数据迁移的问题

5-8先同步1-4的数据过去,切换完之后,各自删掉不需要的数据(当然如果数据量不大不删也可以,留着不用而已)

关注 2 回答 1

明年我18 发布了文章 · 2019-10-07

dubbo-spring-boot的auto-configure示例找不到服务的解决办法

本示例基于dubbo-spring-boot-project 2.7.3版本,可能会根据新版的发布而过时,阅读时请注意。

关于dubbo在spring-boot中该如何使用,网上有很多例子,但因为时间跨度太久,很多例子已经过时了,一切还是要以官方的例子为准。

在github上搜索dubbo和spring-boot整合的项目的话,可能会找到下面两个,分别是

  1. alibaba / dubbo-spring-boot-starter
  2. apache / dubbo-spring-boot-project

第一个项目,已经归档了(archived),不再更新,所以我们要以第二个项目为准,千万别搞错了。

打开第二个项目的主页,就开始浏览README中的Getting Started章节。 这个章节给我们展示了一个无注册中心(dubbo.registry.address=N/A)的例子。

但是它却跑不起来,消费者启动后无法找到service provider,报Not found exported service的错误。

解决办法如下:
需要在消费者Reference服务提供者时,url里指明version。其实version已经指明了,但不知为何还要在url里再次指定。

//  @Reference(version = "1.0.0", url = "dubbo://127.0.0.1:12345")
    @Reference(version = "1.0.0", url = "dubbo://127.0.0.1:12345?version=1.0.0")
    private DemoService demoService;

另外,Getting Started中的pom依赖也比较简略,省略了spring-boot原本需要的依赖,您可以参考我这个修复版里pom中的依赖。

源码地址:https://github.com/kongxiangxin/dubbo-spring-boot-samples

查看原文

赞 0 收藏 0 评论 0

明年我18 回答了问题 · 2019-09-24

kafka 消费者组在分布式情况下消费者分配问题

每个分区只能被同一个消费组里面的一个消费者实例占用,所以你可以增加分区,然后用一致性hash的方式保证每个分区中的消息数量均衡

关注 3 回答 2

明年我18 发布了文章 · 2019-09-24

前后端分离部署时如何保护前端代码不被匿名访问

背景

现在很多项目早就采用前后端分离的方式开发和部署了。前端代码部署在nginx服务器上,由nginx直接对外提供静态文件的服务,后端接口则由nginx做反向代理。

这本来是极为合理的部署方式,但对于一些需要登录才能进行访问的系统,负责安全的同事就会提出如下的疑虑:

index.html允许匿名访问,别有用心之人岂不是可以根据index里的<script>标签,拿到你所有的前端代码了?

看来要解决这个问题。

思路

为了保护前端首页代码,一次请求的流程应该是下面这样:

用户发起首页的请求,服务端发现用户没有登录,跳转到登录页;
用户发起首页的请求,服务端发现用户已经登录了,正常输出首页的内容。

注意,这里是服务端判断,而不是客户端判断。

判断有没有登录,毫无疑问是是我们的java后端才能做到的事情,但是首页是html文件,在nginx下面,用户请求它的时候还没到后端这里呢,怎么判断?

当然,你可以把前端文件移到后端tomcat下,由tomcat提供服务,但这样又走回老路了,这不是一个好方法,不推荐。

其实,在不改变部署架构的前提下,我们简单的通过nginx的配置和后端接口的配合,就可以达到目的。

简单来说,利用nginx的rewrite + error_page指令来实现。

  1. 首先,利用nginx的rewrite指令,把对index的请求,rewrite到后端某个接口上
  2. 后端这个接口里判断当前用户是否已经登录,如果没有登录,返回302跳转,跳转到授权页去登录
  3. 如果后端接口判断当前用户已经登录,则返回一个错误码给nginx(例如404),nginx利用error_page,指定404时,输出index.html的文件内容。

nginx示例配置如下:

server {
       listen       80;
       server_name www.abc.com;
        recursive_error_pages on; #这个选项要开启
        location / {   
            root /path/to/front-end;
        }
        location /api #交由tomcat处理
        {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
            proxy_set_header   Cookie $http_cookie;
            proxy_pass http://localhost:9000;
        }

        location ~* ^(/|(/index\.html))$ {
            #未登录的情况下,不允许访问首页,注意这里rewrite到一个location,而不是直接proxy_pass到后端接口。因为我要在@fallback里利用queryString
            rewrite ^/(.*) /abcdefg?res=$request_uri; 
        }
        
        #
        location /abcdefg {
            proxy_pass http://localhost:9000/api/home/check-user?res=$request_uri;
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_intercept_errors on;
            error_page  404  = @fallback;
        }
        
        
        location @fallback {
            if ($query_string ~* ^res=([^&]*)) {
                    set $path $1;
                    rewrite ^ /local/scripts$path;
            }
        }
        location /local/scripts/ {
            internal; #nginx内部才有效的location,外部无法通过/local/scripts/这个路径访问
            alias /path/to/front-end/; #注意,最后有个/符号
            error_page  404  =200 /local/scripts/index.html;
        }
}

后端check-user接口示例如下:

@GetMapping("check-user")
public void checkUser(String res, HttpSession session, HttpServletRequest request, HttpServletResponse response) throws IOException {
    if(session.getAttribute("User") == null){
        response.sendRedirect("login?returnUrl=" + URLEncoder.encode(res, "UTF-8"));
        return;
    }
    response.setStatus(HttpStatus.SC_NOT_FOUND);
}
查看原文

赞 2 收藏 1 评论 0

明年我18 发布了文章 · 2019-09-15

拒绝一次性买卖:MyBatis的mapper和repository可重复生成工具

背景

MyBatis的历史可谓久远了,码农们也在用着各式各样的代码生成工具。然而这些工具大部分都有一个缺点,那就是只能一次性生成文件。如果我们期间在生成的文件里做了修改,再次生成时,很多工具会覆盖我们的修改。

为什么会在生成文件后进行修改呢? 因为工具只会帮我们生成通用的数据库访问方法(比如只生成基本的CURD操作),我们不可避免的要根据实际的业务需要,添加其他的操作方法。

同时,数据库也不是设计完之后就一成不变的了,我们也可能在开发的过程中,调整已经建好的表结构。这个时候问题就来了,利用工具再生成一次?那就要人肉合并修改;手动添加更改后的字段进去? 太多了怕遗漏。

所以,我们需要找到一个方法,解决这个痛点。

原理

熟悉.NET的同学可能知道,大名鼎鼎Visual Studio也会帮开发人员生成很多代码,比如asp.net中的aspx的后台代码,它是如何保证被工具所生成的代码片段和开发人员自己写的代码片段不冲突的呢?它实际上用到了C#的分部类(partial)特性。

简单来说,分部类,就是把一个类的代码,放到多个文件中去写,C#编译器负责把他们编译到一个类中。有了这个特性,代码生成器就只专注他负责的partial文件就可以了,开发人员的代码写到另外一个partial文件中,当年用partial + T4,写了很多代码生成模板,屡试不爽。

但我们的JAVA不支持这个神器啊(这里说句题外话,几年前我从C#转到JAVA的时候,就感觉C#在语言层面比JAVA好太多了,现在好几年没碰C#了,不知道它又先进到什么程度了),怎么办呢?

只有用不是办法的办法了,那就是继承。实体类、Repository接口,用继承的方式,把工具生成的代码和预留给开发人员人肉的代码,分割到两个文件中。

但mapper.xml怎么办?这个MyBatis帮我们想好了(赞一个),利用namespace即可做到。只要namespace指向同一个Repository接口,不论是不是在同一个xml文件里,MyBatis都可以正确找到。

例如我们有一个Repository是这么定义的:

public interface UserRepository{
    
    User selectByPrimaryKey(@Param("id") Long id);

    User selectByAccount(@Param("account") String account);
}

那么以下的两个mapper.xml结合起来是完全可用的

UserMapper1.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.abc.demo.repository.UserRepository">
    <sql id="TableName">
        jm_user
    </sql>

    <sql id="BaseColumnList">
     `id`, `account`, `email`, `is_active`
    </sql>

    <resultMap id="BaseResultMap" type="com.abc.demo.entity.User" autoMapping="false">
        <result column="id" property="id" jdbcType="BIGINT"/>
        <result column="account" property="account" jdbcType="VARCHAR"/>
        <result column="email" property="email" jdbcType="VARCHAR"/>
        <result column="is_active" property="isActive" jdbcType="BIT"/>
    </resultMap>
    <select id="selectByPrimaryKey" resultMap="BaseResultMap">
        select
        <include refid="BaseColumnList"/>
        from
        <include refid="TableName"/>
        where
            `id` = #{id}
    </select>
</mapper>

UserMapper2.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.abc.demo.repository.UserRepository">
    <select id="selectByAccount" resultMap="BaseResultMap">
        select
        <include refid="BaseColumnList"/>
        from
        <include refid="TableName"/>
        where
            `account` = #{account}
    </select>
</mapper>

你看,UserMapper2.xml中只定义了selectByAccount方法,BaseColumnList、TableName、BaseResultMap都没有重新定义,可以直接用UserMapper1.xml的。

jasmine —— 基于数据库模型和velocity模板的代码生成工具

利用以上原理,我写了一个代码生成工具,读取数据库模型,并基于velocity模板,生成代码。
可以命令行形式运行,也可以作为IDEA的插件运行。

项目地址:https://github.com/kongxiangx...
工具下载地址:https://github.com/kongxiangx...

源码里提供了一个示例DEMO,里面包含了MyBatis的代码生成模板,支持如下特性:

  1. 一键生成实体类、MyBatis Repository、MyBatis Mapper
  2. 实体类、MyBatis Repository和MyBatis Mapper均利用继承策略,划分出XXXX和XXXXBase两个文件,其中XXXX如果文件存在则不覆盖,XXXXBase每次生成都会覆盖。如果在生成后你需要做一些代码上的调整,请在XXXX文件中修改,而不要在XXXXBase中修改。这样做的好处是一旦我们的表结构发生变化需要重新生成时,不会覆盖您手动改过的代码。
  3. 如果表存在is_deleted字段,生成的delete方法是逻辑删除而不是物理删除。
  4. 如果表存在record_version字段,update语句带有乐观锁,即update .... set record_version=record_version + 1 where .... and record_version=#{record_version}
  5. 如果表存在create_time,insert语句这一列的值是now()
  6. 如果表存在update_time, insert和update语句这一列的值是now()

Quick Start

  1. 去releases页面,下载最新的jasmine-[version].zip,解压。
  2. clone源码至本地,根据实际情况,修改demo/jasmine.properties中jdbc相关的配置(主要是数据库连接配置)
  3. 执行以下命令:
/path/to/jasmine-[version]/bin/jasmine /path/to/jasmine-src/demo/jasmine.properties

如果一切正常,会在demo下看到生成出来的文件

查看原文

赞 1 收藏 1 评论 0

明年我18 回答了问题 · 2019-07-10

MySQL 在一个表中如何查询两个字段相同的行。

没看明白....

关注 5 回答 4

明年我18 关注了用户 · 2018-07-09

phodal @phodal

待我代码编成,娶你为妻可好 @花仲马

关注 1786

认证与成就

  • 获得 10 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-10-07
个人主页被 319 人浏览