基於Vert.x和SpringBoot實現響應式開發

banq發表於2016-01-06
Vert.x是作為一個事件匯流排的設計,以保證應用中不同部分以一種非堵塞的執行緒安全方式通訊,其原理來自於Erlang和Akka,它是能充分利用多核處理器效能並實現高併發程式設計的需求。

所有Vert.x 的VERTICLE預設是一個單執行緒,不像Node.js只有一個單執行緒,vert.x能在很多執行緒中執行很多VERTICLE,每個執行緒一個VERTICLE,這樣你可以指定幾個VERTICLE作為"worker",能夠以多執行緒方式執行你的任務。

Vert.x可以透過使用Hazelcast實現底層的事件匯流排的多節點叢集。

下面以一個案例說明如何基於 Spring Boot, Spring Data JPA, 和 Spring REST開發一個聊天室系統?

下面沒有引入Vert.x而是使用傳統Spting語法實現的系統:

@SpringBootApplication
@EnableJpaRepositories
@EnableTransactionManagement
@Slf4j
public class Application {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Application.class, args);

        System.out.println("Let's inspect the beans provided by Spring Boot:");

        String[] beanNames = ctx.getBeanDefinitionNames();
        Arrays.sort(beanNames);
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }

    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        return builder.setType(EmbeddedDatabaseType.HSQL).build();
    }

    @Bean
    public EntityManagerFactory entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.zanclus.data.entities");
        factory.setDataSource(dataSource());
        factory.afterPropertiesSet();

        return factory.getObject();
    }

    @Bean
    public PlatformTransactionManager transactionManager(final EntityManagerFactory emf) {
        final JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(emf);
        return txManager;
    }
}
<p class="indent">

上述程式碼中 @Bean是提供了訪問JPA的EntityManager, TransactionManager, 和 DataSource. 這是一個標準的Springboot案例原始碼。前端使用CustomerEnpoints作為RESTful的控制器,接受客戶端的請求。

CustomerVerticle 類是作為@Component,意味著在啟動時,Spring會初始化這個類,它也有@PostConstruct標註的start方法,這樣Verticle 在啟動被載入時會執行其方法內容:

 @PostConstruct
    public void start() throws Exception {
        Router router = Router.router(vertx);
        router.route().handler(BodyHandler.create());
        router.get("/v1/customer/:id")
                .produces("application/json")
                .blockingHandler(this::getCustomerById);
        router.put("/v1/customer")
                .consumes("application/json")
                .produces("application/json")
                .blockingHandler(this::addCustomer);
        router.get("/v1/customer")
                .produces("application/json")
                .blockingHandler(this::getAllCustomers);
        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
    }
<p class="indent">

在這個啟動方法中,引入了vertx-web庫包:Router,能夠讓使用者定義將請求過濾轉換為HTTP URLs, methods, 和頭部header 等資訊,BodyHandler 是能將POST/PUT提交的內容轉變為一個JSON物件,Vert.x能夠將其作為RoutingContext的一部分進行處理;RoutingContext包含Vert.x請求物件和響應物件和任何Http請求中資料,包括POST內容資料等等,blockingHandler是接受RoutingContext作為輸入引數。

注意到blockingHandler方法使用了Java 8的方法引用,如this::getCustomerById,這比使用lambda將邏輯插入blockingHandler 中更具有程式碼可讀性。getCustomerById的方法如下:

 private void getCustomerById(RoutingContext rc) {
        log.info("Request for single customer");
        Long id = Long.parseLong(rc.request().getParam("id"));
        try {
            Customer customer = dao.findOne(id);
            if (customer==null) {
                rc.response().setStatusMessage("Not Found").setStatusCode(404).end("Not Found");
            } else {
                rc.response().setStatusMessage("OK").setStatusCode(200).end(mapper.writeValueAsString(dao.findOne(id)));
            }
        } catch (JsonProcessingException jpe) {
            rc.response().setStatusMessage("Server Error").setStatusCode(500).end("Server Error");
            log.error("Server error", jpe);
        }
    }
<p class="indent">

在getCustomerById方法中實現真正的業務處理,比如呼叫DAO訪問資料庫,序列化和反序列化物件,類似在MVC的控制器中實現一樣。

完整見Github

這段程式碼的主要問題是效能的擴充套件性有限,這段程式碼要麼可以在Tomcat中執行,要麼以微服務方式在嵌入式伺服器如Jetty或undertow中執行,總之,只能是一個請求捆綁一個執行緒,當在等待I/O等堵塞操作時,所有的資源都會消耗在等待上,這種區域性堵塞引發整體等待是一種資源浪費。

而如果我們引入了Vert.x,使用@Bean注入了Vertx例項:

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

    private Vertx vertx;

    /**
     * Create an {@link ObjectMapper} for use in (de)serializing objects to/from JSON
     * @return An instance of {@link ObjectMapper}
     */
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper(new JsonFactory());
    }

    /**
     * A singleton instance of {@link Vertx} which is used throughout the application
     * @return An instance of {@link Vertx}
     */
    @Bean
    public Vertx getVertxInstance() {
        if (this.vertx==null) {
            this.vertx = Vertx.vertx();
        }
        return this.vertx;
    }
....
<p class="indent">

訪問JPA的程式碼基本沒有變,主要是RESTful控制器更換了,使用CustomerVerticle替代了CustomerEnpoints。

完整程式碼見:Convert-To-Vert.x-Web

你可能覺得這比原來Spring程式碼更復雜了些,這裡只是介紹案例原始碼,真正實戰中可以使用Vert.x的元註解庫實現類似JAX-RS的REST端點方式,當然最主要是我們獲得了更好的可伸縮擴充套件性,在這段程式碼底層下面,Vertx使用了Netty作為非同步IO操作,這樣就可以處理更多併發請求,當然限制於資料庫連線池的大小。

上面展示了將前端RESTful的IO從傳統的同步模式如Jetty轉換到非同步IO入Netty方式,我們也可以將訪問後端資料庫的同步IO放入 Worker Verticles,這樣就會更有效率地處理來自客戶端的請求,程式碼可見:Convert-To-Worker-Verticles

原文參考:

Reactive Development Using Vert.x - Java Advent

相關文章