微服务使用logback+打成tar包方式

hello,all

为推行微服务统一部署管理,需要将所有微服务项目打成tar包的形式。

同时,承祝强力推荐logback,因此也同时去掉了log4j,集成logback(性能好呀)。

下面简要介绍一下要点,注意摆正姿势:

参考项目:revolver,分支:assembly_new

1.打tar包

  • 在pom.xml里引入maven-assembly-plugin插件
<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.0.0</version>
    <configuration>
        <descriptors>
            <descriptor>src/main/assembly.xml</descriptor>
        </descriptors>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  • 在src/main目录里添加assembly.xml(assembly的配置文件),内容如下:
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd">
   <id>assembly</id>
   <formats>
      <format>tar.gz</format>
   </formats>
   <includeBaseDirectory>true</includeBaseDirectory>

   <fileSets>
      <fileSet>
         <directory>bin</directory>
         <outputDirectory>bin</outputDirectory>
         <fileMode>0755</fileMode>
         <filtered>false</filtered>
      </fileSet>
      <fileSet>
         <directory>src/main/resources</directory>
         <outputDirectory>conf</outputDirectory>
         <fileMode>0644</fileMode>
         <filtered>false</filtered>
      </fileSet>
      <fileSet>
         <directory>${resource.path}</directory>
         <outputDirectory>conf</outputDirectory>
         <fileMode>0644</fileMode>
         <filtered>false</filtered>
      </fileSet>
   </fileSets>
   <dependencySets>
      <dependencySet>
         <outputDirectory>lib</outputDirectory>
      </dependencySet>
   </dependencySets>
</assembly>
  • 在项目的根目录(与src平级)添加bin目录,里面放四个脚本:dump.shserver.shstart.shstop.sh。脚本太多,这里就不粘贴出来了,可以到参考项目里取。拷贝脚本到自己项目之后需要修改start.sh里的
START_CLASS=启动类全限定名

启动类如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Main {
    private static Logger logger = LoggerFactory.getLogger(Main.class);

    private static final String CONFIG_FILE_SPRING = "spring.xml";
    private static volatile boolean running = true;
    private static ClassPathXmlApplicationContext context = null;

    public static void main(String[] args) {
        try {
            long before = System.currentTimeMillis();
            start();
            long time=System.currentTimeMillis()-before;
            String logLine = new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss]").format(new Date()) + " dubbox server started! it takes "+time+" ms.";
            System.out.println(logLine);
            logger.info(logLine);
        } catch (RuntimeException e) {
            e.printStackTrace();
            logger.error(e.getMessage(), e);
            System.exit(1);
        }

        if(!"false".equals(System.getProperty("dubbo.shutdown.hook"))) {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    context.stop();
                    Class t = Main.class;
                    synchronized(Main.class) {
                        running = false;
                        Main.class.notify();
                    }
                }
            });
        }

        synchronized (Main.class) {
            while (true) {
                try {
                    Main.class.wait();
                } catch (Throwable e) {
                }
            }
        }
    }

    private static void start() {
        context = new ClassPathXmlApplicationContext(new String[] { CONFIG_FILE_SPRING });
        context.start();
    }
}

注:脚本取自中间件组新项目里的bin目录下的脚本,然后加以修改。并且摒弃了原来deploy目录里的脚本。注意不要混合使用

  • 打了tar包之后,在线下jenkins部署的脚本如下
#!/bin/bash
echo "kill PID!!"
BUILD_ID=DONTKILLME
cd $WORKSPACE/target
echo "kill PID!"
ps -ef|grep $JOB_NAME|awk {'print $2'}|xargs kill -9
sleep 2
tar -zxvf $JOB_NAME-assembly.tar.gz
cd $JOB_NAME/bin
sh ./start.sh
sleep 2

2.引入logback

  • 在pom文件里引入logback依赖,1.2.3为logback目前最新版本。
<logback.version>1.2.3</logback.version>

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>${logback.version}</version>
</dependency>
  • 引入log4j-over-slf4j,并且在dubbo依赖里排除log4j。原因是dubbo默认使用lo4fj打印日志,而使用logback之后还想dubbo打印日志,需要如此配置。(SLF4J ship with a module called log4j-over-slf4j. It allows log4j users to migrate existing applications to SLF4J without changing a single line of code but simply by replacing the log4j.jar file with log4j-over-slf4j.jar, as described below.)
<slf4j.version>1.7.25</slf4j.version>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>${slf4j.version}</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.8.3</version>
    <exclusions>
        <exclusion>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </exclusion>
    </exclusions>
</dependency>
  • 在resources_xxx里引入logback.xml,并且修改logDir的值为日志根目录,程序启动后会在日志根目录下创建四个文件夹debug、info、warn、error。不同级别的日志打到不同的文件夹里,并且按天滚动。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 定义日志文件 输出位置 -->
    <property name="logDir" value="/data/var/log/application/Revolver" />

    <property name="maxHistory" value="30" />

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p [USER_ID:%X{USER_ID}][%t][%L][%c.%M]%m%n</pattern>
        </encoder>
    </appender>

    <!-- ERROR级别日志 -->
    <appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logDir}/error/error.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}/error/error.log.%d{yyyy-MM-dd}</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p [USER_ID:%X{USER_ID}][%t][%L][%c.%M]%m%n</pattern>
        </encoder>
    </appender>

    <!-- WARN级别日志 appender -->
    <appender name="warnFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logDir}/warn/warn.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}/warn/warn.log.%d{yyyy-MM-dd}</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p [USER_ID:%X{USER_ID}][%t][%L][%c.%M]%m%n</pattern>
        </encoder>
    </appender>

    <!-- INFO级别日志 appender -->
    <appender name="infoFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logDir}/info/info.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}/info/info.log.%d{yyyy-MM-dd}</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p [USER_ID:%X{USER_ID}][%t][%L][%c.%M]%m%n</pattern>
        </encoder>
    </appender>

    <!-- DEBUG级别日志 appender -->
    <appender name="debugFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logDir}/debug/debug.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${logDir}/debug/debug.log.%d{yyyy-MM-dd}</fileNamePattern>
            <maxHistory>${maxHistory}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %p [USER_ID:%X{USER_ID}][%t][%L][%c.%M]%m%n</pattern>
        </encoder>
    </appender>

    <root>
        <level value="info"/>
        <!-- 控制台输出 -->
        <appender-ref ref="console"/>
        <!-- 文件输出 -->
        <appender-ref ref="errorFile"/>
        <appender-ref ref="warnFile"/>
        <appender-ref ref="infoFile"/>
        <appender-ref ref="debugFile"/>
    </root>
</configuration>

以上

by the way:markdown写起来真的很舒服

Java的Fork/Join任务,你写对了吗?

当我们需要执行大量的小任务时,有经验的Java开发人员都会采用线程池来高效执行这些小任务。然而,有一种任务,例如,对超过1000万个元素的数组进行排序,这种任务本身可以并发执行,但如何拆解成小任务需要在任务执行的过程中动态拆分。这样,大任务可以拆成小任务,小任务还可以继续拆成更小的任务,最后把任务的结果汇总合并,得到最终结果,这种模型就是Fork/Join模型。

Java7引入了Fork/Join框架,我们通过RecursiveTask这个类就可以方便地实现Fork/Join模式。

例如,对一个大数组进行并行求和的RecursiveTask,就可以这样编写:

class SumTask extends RecursiveTask<Long> {

    static final int THRESHOLD = 100;
    long[] array;
    int start;
    int end;

    SumTask(long[] array, int start, int end) {
    this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= THRESHOLD) {
            // 如果任务足够小,直接计算:
            long sum = 0;
            for (int i = start; i < end; i++) {
                sum += array[i];
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println(String.format("compute %d~%d = %d", start, end, sum));
            return sum;
        }
        // 任务太大,一分为二:
        int middle = (end + start) / 2;
        System.out.println(String.format("split %d~%d ==> %d~%d, %d~%d", start, end, start, middle, middle, end));
        SumTask subtask1 = new SumTask(this.array, start, middle);
        SumTask subtask2 = new SumTask(this.array, middle, end);
        invokeAll(subtask1, subtask2);
        Long subresult1 = subtask1.join();
        Long subresult2 = subtask2.join();
        Long result = subresult1 + subresult2;
        System.out.println("result = " + subresult1 + " + " + subresult2 + " ==> " + result);
        return result;
    }
}

编写这个Fork/Join任务的关键在于,在执行任务的compute()方法内部,先判断任务是不是足够小,如果足够小,就直接计算并返回结果(注意模拟了1秒延时),否则,把自身任务一拆为二,分别计算两个子任务,再返回两个子任务的结果之和。

最后写一个main()方法测试:

public static void main(String[] args) throws Exception {
    // 创建随机数组成的数组:
    long[] array = new long[400];
    fillRandom(array);
    // fork/join task:
    ForkJoinPool fjp = new ForkJoinPool(4); // 最大并发数4
    ForkJoinTask<Long> task = new SumTask(array, 0, array.length);
    long startTime = System.currentTimeMillis();
    Long result = fjp.invoke(task);
    long endTime = System.currentTimeMillis();
    System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}

关键代码是fjp.invoke(task)来提交一个Fork/Join任务并发执行,然后获得异步执行的结果。

我们设置任务的最小阀值是100,当提交一个400大小的任务时,在4核CPU上执行,会一分为二,再二分为四,每个最小子任务的执行时间是1秒,由于是并发4个子任务执行,整个任务最终执行时间大约为1秒。

新手在编写Fork/Join任务时,往往用搜索引擎搜到一个例子,然后就照着例子写出了下面的代码:

protected Long compute() {
    if (任务足够小?) {
        return computeDirect();
    }
    // 任务太大,一分为二:
    SumTask subtask1 = new SumTask(...);
    SumTask subtask2 = new SumTask(...);
    // 分别对子任务调用fork():
    subtask1.fork();
    subtask2.fork();
    // 合并结果:
    Long subresult1 = subtask1.join();
    Long subresult2 = subtask2.join();
    return subresult1 + subresult2;
}

很遗憾,这种写法是错!误!的!这样写没有正确理解Fork/Join模型的任务执行逻辑。

JDK用来执行Fork/Join任务的工作线程池大小等于CPU核心数。在一个4核CPU上,最多可以同时执行4个子任务。对400个元素的数组求和,执行时间应该为1秒。但是,换成上面的代码,执行时间却是两秒。

这是因为执行compute()方法的线程本身也是一个Worker线程,当对两个子任务调用fork()时,这个Worker线程就会把任务分配给另外两个Worker,但是它自己却停下来等待不干活了!这样就白白浪费了Fork/Join线程池中的一个Worker线程,导致了4个子任务至少需要7个线程才能并发执行。

打个比方,假设一个酒店有400个房间,一共有4名清洁工,每个工人每天可以打扫100个房间,这样,4个工人满负荷工作时,400个房间全部打扫完正好需要1天。

Fork/Join的工作模式就像这样:首先,工人甲被分配了400个房间的任务,他一看任务太多了自己一个人不行,所以先把400个房间拆成两个200,然后叫来乙,把其中一个200分给乙。

紧接着,甲和乙再发现200也是个大任务,于是甲继续把200分成两个100,并把其中一个100分给丙,类似的,乙会把其中一个100分给丁,这样,最终4个人每人分到100个房间,并发执行正好是1天。

如果换一种写法:

// 分别对子任务调用fork():
subtask1.fork();
subtask2.fork();

这个任务就分!错!了!

比如甲把400分成两个200后,这种写法相当于甲把一个200分给乙,把另一个200分给丙,然后,甲成了监工,不干活,等乙和丙干完了他直接汇报工作。乙和丙在把200分拆成两个100的过程中,他俩又成了监工,这样,本来只需要4个工人的活,现在需要7个工人才能1天内完成,其中有3个是不干活的。

其实,我们查看JDK的invokeAll()方法的源码就可以发现,invokeAll的N个任务中,其中N-1个任务会使用fork()交给其它线程执行,但是,它还会留一个任务自己执行,这样,就充分利用了线程池,保证没有空闲的不干活的线程。

Orika-Java Bean 映射框架

Orika

今天主要介绍一款Java Bean 映射框架—Orika

在此之前,先抛出几个问题,以便加深对orika的理解

  • Q1:什么是Java Bean 映射框架?
    • A:简单地说,就是从一个Bean A映射(转换)成另一个Bean B。举个粟子,实际开发场景中,很多时候都用到VO、BO、PO、DTO等POJO,我们需将其中一个POJO转换成另一个POJO。
  • Q2:如果要从一个Bean转换成另一个Bean,使用b.setXXX(a.getXXX()),手动转换不可以吗?
    • A:当然可以,但可以试想一下,当Bean的属性非常多(十几二十个)的时候,手动get set 不旦麻烦,还容易出错,浪费时间,纯体力劳动。
  • Q3:我听说过BeanUtils.copyProperties(a,b)也可以实现Bean属性的拷贝,它跟Orika有什么区别?
    • A:无论spring的BeanUtils.copyProperties还是apache.commons的BeanUtils.copyProperties,其底层实现机制都是运行时反射,其性能可想而知。在映射场景不频繁的场景或许还可以用一用,但是在一个频繁转换的场景,这样的性能损失,是不能接受的。

Orika是什么?

Orika是一个Java Bean映射框架,它将数据从一个对象递归拷贝到另一个对象。在开发多层应用程序时可能非常有用。其目的在于简化一个对象到另一个对象之间的映射过程。

Orika能干什么?

Orika能提供如下的功能:

  1. 映射复杂和深层结构的对象
  2. 无论是“Flatten”(扁平的)或“Expand”(深层次的)对象,都可以将嵌套属性映射到顶级属性,反之亦然。
  3. 随时快速地创建映射器,并使用自定义来控制部分或全部映射
  4. 处理代理或增强对象(如Hibernate或各种模拟框架)
  5. 通过一个配置,就可以实现双向映射
  6. 可以将任意POJO属性映射到List、Arrays、Map

Orika实现原理

Orika的设计思路就是预先通过javaassist把Java Bean之间的映射关系一次性生成目标拷贝方法代码。这样就可以避免在Bean映射环节一次次的读取映射规则。这就是Orika性能提升根本的原因。因此,理论上以生成的目标Java代码来运行映射是拷贝模式所能取到性能最大值的方法

注:Orika是我做组卷服务时候新学的一个框架,当时发现BeanUtils.copyProperties太慢了,根本不能忍。偶然的机会发现了这么一个东西,这真是刚想瞌睡就有人送上枕头。但是网上关于这个框架的资料非常少,没什么太实用的资料。可能原因有俩,一是知道的人少,二是它太简单易用了,根本就不需要详细地了解它,简单地配置后就能使用。

在此附上一个不知道几年前的参考资料:http://wangbt5191-hotmail-com.iteye.com/blog/1632444

时间有限,今天只做简单的理论介绍,下次将给出实战性的代码。

                                                                                                                           周雄

                                                                                                           搞技术,我们是认真的