Albert's Blog

技术创造价值

环境:

springBoot 1.3.6.RELEASE
thrift 0.9.3
ubuntu IntelliJ IDEA 14.1.4

生成springBoot项目框架

本文采用了idea 自带的Spring Initializr作为生成项目框架的向导, 依赖管理采用Gradle

如果你的idea 新建项目中没有这个选项,那么需要添加这个 Spring Boot 这个idea插件

  • 打开idea设置选项框,搜索中搜索 Spring Boot
  • 在插件(Plugins)中,把 Spring Boot这个插件勾上
添加thrift依赖及gradle build插件

完整的build.gradle文件如下:

buildscript {
    ext {
        springBootVersion = '1.3.6.RELEASE'
    }
    repositories {
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'spring-boot'

buildscript {
    repositories {
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath "gradle.plugin.org.jruyi.gradle:thrift-gradle-plugin:0.3.1"
    }
}

apply plugin: "org.jruyi.thrift"

compileThrift {
    generator 'rb'
    outputDir = file('src/main/thrift/generated')
    sourceDir = file('src/main/thrift/src')
}

task copyTask(type: Copy) {
    from 'src/main/thrift/generated/gen-java'
    into 'src/main/java/com/albert/thrift'
}

jar {
    baseName = 'thriftSpringBoot'
    version = 'v0.0.1'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile("org.apache.thrift:libthrift:0.9.3")
    runtime('mysql:mysql-connector-java')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

其中:

  • apply plugin: "org.jruyi.thrift" 插件是方便把*.thrift编译成实际使用的各个开发语言的接口文件,你也可以采用thrift的命令行生成
  • compileThrift就是该插件提供的gradle task, 重写这个task是为了配置thrift编译的参数。generator是设置生成什么语言的接口,此文件中添加了ruby语言的接口, JAVA语言的接口是自动生成的,因为我们添加了 apply plugin: 'java' 这个task。outputDirsourceDir分别代表了thrift文件的源文件地址和编译后的目标文件地址
  • compile("org.apache.thrift:libthrift:0.9.3")用于添加thrift依赖
  • task copyTask(type: Copy)是用来把生成java接口文件复制到源代码目录的一个task,当然,你也可以手动把生成的java接口文件复制到制定的文件夹(此项目中为:src/main/com/albert/thrift)。文件复制完毕后,额外需要手动操作的就是给这些个接口文件添加 package 信息,不然无法在其他的源文件中import引用。
实现thrift的接口

源文件如下:

package com.albert.thriftHandler;

import com.albert.service.CalculatorService;
import com.albert.thrift.TCalculatorService;
import com.albert.thrift.TDivisionByZeroException;
import com.albert.thrift.TOperation;
import org.apache.thrift.TException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * Created by albert on 16-7-26.
 */

@Component
public class CalculatorServiceHandler implements TCalculatorService.Iface {

    @Autowired
    CalculatorService calculatorService;

    @Override
    public int calculate(int num1, int num2, TOperation op) throws TException {
        switch (op) {
            case ADD:
                return calculatorService.add(num1, num2);
            case SUBTRACT:
                return calculatorService.subtract(num1, num2);
            case MULTIPLY:
                return calculatorService.multiply(num1, num2);
            case DIVIDE:
                try {
                    return calculatorService.divide(num1, num2);
                } catch (IllegalArgumentException e) {
                    throw new TDivisionByZeroException();
                }
            default:
                 throw new TException("Unknown operation " + op);
        }
    }
}

其中:

  • calculatorService是一个具体的逻辑业务类,单独分开写。通过@Autowired可以自动注入到当前的类中。
  • 添加@Component注解充分利用spring作为容器的优势。
启动thrift服务的包装类,方便thrift服务的启动

源码如下:

package com.albert;

import com.albert.thrift.TCalculatorService;
import com.albert.thriftHandler.CalculatorServiceHandler;
import org.apache.log4j.Logger;
import org.apache.thrift.TProcessor;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TThreadPoolServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TServerSocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * Created by albert on 16-7-26.
 */

@Component
public class ThriftServer {
    private static Logger logger = Logger.getLogger(ThriftServer.class);

    @Value("${thrift.server.port}")
    private int thriftServerPort;

    @Autowired
    CalculatorServiceHandler calculatorServiceHandler;

    public void startServer() {
        try {
            TProcessor tProcessor = new TCalculatorService.Processor<>(calculatorServiceHandler);
            TServerSocket tServerSocket = new TServerSocket(thriftServerPort);
            TThreadPoolServer.Args tpsArgs = new TThreadPoolServer.Args(tServerSocket);

            tpsArgs.processor(tProcessor);
            tpsArgs.transportFactory(new TFramedTransport.Factory());
            tpsArgs.protocolFactory(new TCompactProtocol.Factory());

            TServer server = new TThreadPoolServer(tpsArgs);
            logger.info("start thrift server at: " + thriftServerPort);
            server.serve();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其中:

  • thriftServerPort也是自动注入,其配置在文件src/main/resources/application.properties中设置
  • thrift的服务器类型采用了TThreadPoolServer,关于thrift中各种服务器之前的对比说明可以参看本文最后的附录
springBoot配置及启动服务

为了可以在 src/main/resources/application.properties中设置 系统的参数,需要添加一下的配置文件:

package com.albert.config;

import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

/**
 * Created by albert on 16-7-26.
 */

@Configuration
@EnableAutoConfiguration
@ComponentScan("com.albert")
public class AppConfig {
    @Bean
    public static PropertyPlaceholderConfigurer properties() {
        PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
        ClassPathResource[] resources = new ClassPathResource[]
                {new ClassPathResource("application.properties")};
        ppc.setLocations(resources);
        ppc.setIgnoreUnresolvablePlaceholders(true);
        return ppc;
    }
}

配置完毕后,需要在src/main/java/com/albert/thriftSpringBootApplication.java中启动thrift服务器:

package com.albert;

import com.albert.config.AppConfig;
import org.apache.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@SpringBootApplication
public class ThriftSpringBootApplication {

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

    public static void main(String[] args) {
        SpringApplication.run(ThriftSpringBootApplication.class, args);

        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ThriftServer thriftServer = (ThriftServer) context.getBean("thriftServer");
        thriftServer.startServer();
    }
}

经过以上的开发,我们就可以在idea中把spring服务启动起来,选中thriftSpringBootApplication.java文件,右键选择‘Run’

最后还差单元测试

这个不是一个比选项,你可以自己写一个thrift client测试你的接口服务,但是强烈建议写单元测试,这个是代码是否可靠的保障

测试代码如下:

package com.albert;

import com.albert.thrift.TCalculatorService;
import com.albert.thrift.TDivisionByZeroException;
import com.albert.thrift.TOperation;
import org.apache.thrift.protocol.TCompactProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.junit.Assert.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ThriftSpringBootApplication.class)
@WebAppConfiguration
@IntegrationTest("server.port:0")
public class ThriftSpringBootApplicationTests {

    @Test
    public void contextLoads() {
    }

    protected TCalculatorService.Client client;

    @Before
    public void setUp() throws Exception {
        TTransport transport = new TFramedTransport(new TSocket("localhost", 6666));
        TProtocol protocol = new TCompactProtocol(transport);
        transport.open();

        client = new TCalculatorService.Client(protocol);
    }

    @Test
    public void testAdd() throws Exception {
        assertEquals(5, client.calculate(2, 3, TOperation.ADD));
    }

    @Test
    public void testSubtract() throws Exception {
        assertEquals(3, client.calculate(5, 2, TOperation.SUBTRACT));
    }

    @Test
    public void testMultiply() throws Exception {
        assertEquals(10, client.calculate(5, 2, TOperation.MULTIPLY));
    }

    @Test
    public void testDivide() throws Exception {
        assertEquals(2, client.calculate(10, 5, TOperation.DIVIDE));
    }

    @Test(expected = TDivisionByZeroException.class)
    public void testDivisionByZero() throws Exception {
        client.calculate(10, 0, TOperation.DIVIDE);
    }
}

你可以直接用过右键跑一下上面的文件,或者可以在命令行项目的根目录下运行gradle build

附加说明
thrift各个服务器比较说明:
  • TSimpleServer

    TSimplerServer接受一个连接,处理连接请求,直到客户端关闭了连接,它才回去接受一个新的连接。正因为它只在一个单独的线程中以阻塞I/O的方式完成这些工作,所以它只能服务一个客户端连接,其他所有客户端在被服务器端接受之前都只能等待。TSimpleServer主要用于测试目的,不要在生产环境中使用它!

  • TNonblockingServer

    TNonblockingServer使用非阻塞的I/O解决了TSimpleServer一个客户端阻塞其他所有客户端的问题。它使用了java.nio.channels.Selector,通过调用select(),它使得你阻塞在多个连接上,而不是阻塞在单一的连接上。当一或多个连接准备好被接受/读/写时,select()调用便会返回。TNonblockingServer处理这些连接的时候,要么接受它,要么从它那读数据,要么把数据写到它那里,然后再次调用select()来等待下一个可用的连接。通用这种方式,server可同时服务多个客户端,而不会出现一个客户端把其他客户端全部“饿死”的情况。

  • THsHaServer

    所有消息是被调用select()方法的同一个线程处理的。假设有10个客户端,处理每条消息所需时间为100毫秒,那么,latency和吞吐量分别是多少?当一条消息被处理的时候,其他9个客户端就等着被select,所以客户端需要等待1秒钟才能从服务器端得到回应,吞吐量就是10个请求/秒。如果可以同时处理多条消息的话,会很不错吧?因此,THsHaServer(半同步/半异步的server)就应运而生了。它使用一个单独的线程来处理网络I/O,一个独立的worker线程池来处理消息。这样,只要有空闲的worker线程,消息就会被立即处理,因此多条消息能被并行处理。用上面的例子来说,现在的latency就是100毫秒,而吞吐量就是100个请求/秒。

  • TThreadedSelectorServer

    Thrift 0.8引入了另一种server实现,即TThreadedSelectorServer。它与THsHaServer的主要区别在于,TThreadedSelectorServer允许你用多个线程来处理网络I/O。它维护了两个线程池,一个用来处理网络I/O,另一个用来进行请求的处理。当网络I/O是瓶颈的时候,TThreadedSelectorServer比THsHaServer的表现要好。

  • TThreadPoolServer

    TThreadPoolServer与其他三种server不同的是有一个专用的线程用来接受连接。一旦接受了一个连接,它就会被放入ThreadPoolExecutor中的一个worker线程里处理。 worker线程被绑定到特定的客户端连接上,直到它关闭。一旦连接关闭,该worker线程就又回到了线程池中。 你可以配置线程池的最小、最大线程数,默认值分别是5(最小)和Integer.MAX_VALUE(最大)。这意味着,如果有1万个并发的客户端连接,你就需要运行1万个线程。所以它对系统资源的消耗不像其他类型的server一样那么“友好”。此外,如果客户端数量超过了线程池中的最大线程数,在有一个worker线程可用之前,请求将被一直阻塞在那里。TThreadPoolServer的表现非常优异。如果你提前知道了将要连接到你服务器上的客户端数量,并且你不介意运行大量线程的话,TThreadPoolServer对你可能是个很好的选择。

参考连接

http://sgq0085.iteye.com/blog/2213959
https://dzone.com/articles/building-microservices-spring
http://bsideup.blogspot.jp/2015/04/spring-boot-thrift-part2.html
https://github.com/jruyi/thrift-gradle-plugin
http://stackoverflow.com/questions/35232344/gradle-thrift-plugin-by-example
https://beyondalbert.com/thrift-with-java-server-and-ruby-client/

发表评论

邮箱地址不会被公开。 必填项已用*标注