java - 使用Vaadin 7应用程序中的推送显示相同数据穿越多个客户端

标签 java vaadin vaadin7 web-push

我想将同一组数据共享给多个客户端。我需要使用“推送”来自动更新其在屏幕上的 View 。

我已经阅读了问答Minimal example of Push in Vaadin 7 app (“@Push”)。现在,我需要一个更可靠的现实示例。一方面,我知道在Servlet环境中拥有永无止境的Thread不是一个好主意。

而且我不希望每个用户都有自己的线程,每个线程都自己访问数据库。似乎让一个线程独自检查数据库中的新数据更合乎逻辑。找到该线程后,该线程应将新数据发布到所有用户的UI/布局中,等待更新。

最佳答案

充分发挥作用的例子

在下面,您将找到几个类的代码。他们共同使用新的内置Push功能,将Vaadin 7.3.8应用程序完整地实例化,以同时向任何数量的用户发布单个数据集。我们通过随机生成一组数据值来模拟检查数据库中是否有新鲜数据。

当您运行此示例应用程序时,将出现一个窗口,其中显示当前时间以及一个按钮。时间每秒更新一次,共一百次。

这个更新不是真实的例子。时间更新器有两个其他用途:

  • 它的简单代码检查在您的Vaadin应用程序,Web服务器和Web浏览器中是否正确配置了Push。
  • 遵循Server Push sectionThe Book Of Vaadin中给出的示例代码。此处的时间更新器几乎完全与该示例完全相同,除了它们每分钟更新chart的地方,我们更新了一段文本。

  • 要查看此应用程序的真实示例,请单击/点击“打开数据窗口”按钮。第二个窗口打开,显示三个文本字段。每个字段都包含一个随机生成的值,我们将其伪装成来自数据库查询。

    这样做是一项工作,需要几个步骤。让我们来看看这些片段。



    在当前版本的Vaadin 7.3.8中,不需要插件或附加组件即可启用Push technology。甚至与Push相关的.jar文件都与Vaadin bundle 在一起。

    有关详细信息,请参见Book Of Vaadin。但是实际上您要做的就是将 @Push 批注添加到UI的子类中。

    使用Servlet容器和Web服务器的最新版本。推送是相对较新的,并且实现也在不断发展,尤其是对于WebSocket。例如,如果使用Tomcat,请确保使用对Tomcat 7或8的最新更新。

    定期检查新数据

    我们必须有某种方法可以重复查询数据库以获取新数据。

    在Servlet环境中,永无止境的线程并不是实现此目的的最佳方法,因为在取消部署Web应用程序或Servlet包含关闭程序时,线程不会结束。线程将继续在JVM中运行,浪费资源,导致内存泄漏和其他问题。

    Web App启动/关机 Hook

    理想情况下,我们希望在Web应用程序启动(部署)以及Web应用程序关闭(或取消部署)时得到通知。收到通知后,我们可以启动或中断该数据库查询线程。幸运的是,每个Servlet容器都提供了这样的钩子(Hook)。 Servlet spec需要一个支持 ServletContextListener 接口(interface)的容器。

    我们可以编写一个实现此接口(interface)的类。部署我们的网络应用程序(我们的Vaadin应用程序)后,将调用我们的监听器类 contextInitialized 。取消部署后,将调用 contextDestroyed 方法。

    执行人服务

    从这个钩子(Hook)我们可以启动一个线程。但是有更好的方法。 Java配备了 ScheduledExecutorService 。此类具有可用的线程池,以避免实例化和启动线程的开销。您可以将一个或多个任务(Runnable)分配给执行程序,以使其定期运行。

    Web App监听器

    这是我们的Web应用程序监听器类,使用Java 8中可用的Lambda语法。
    package com.example.pushvaadinapp;
    
    import java.time.Instant;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.ScheduledFuture;
    import java.util.concurrent.TimeUnit;
    import javax.servlet.ServletContext;
    import javax.servlet.ServletContextEvent;
    import javax.servlet.ServletContextListener;
    import javax.servlet.annotation.WebListener;
    
    /**
     * Reacts to this web app starting/deploying and shutting down.
     *
     * @author Basil Bourque
     */
    @WebListener
    public class WebAppListener implements ServletContextListener
    {
    
        ScheduledExecutorService scheduledExecutorService;
        ScheduledFuture<?> dataPublishHandle;
    
        // Constructor.
        public WebAppListener ()
        {
            this.scheduledExecutorService = Executors.newScheduledThreadPool( 7 );
        }
    
        // Our web app (Vaadin app) is starting up.
        public void contextInitialized ( ServletContextEvent servletContextEvent )
        {
            System.out.println( Instant.now().toString() + " Method WebAppListener::contextInitialized running." );  // DEBUG logging.
    
            // In this example, we do not need the ServletContex. But FYI, you may find it useful.
            ServletContext ctx = servletContextEvent.getServletContext();
            System.out.println( "Web app context initialized." );   // INFO logging.
            System.out.println( "TRACE Servlet Context Name : " + ctx.getServletContextName() );
            System.out.println( "TRACE Server Info : " + ctx.getServerInfo() );
    
            // Schedule the periodic publishing of fresh data. Pass an anonymous Runnable using the Lambda syntax of Java 8.
            this.dataPublishHandle = this.scheduledExecutorService.scheduleAtFixedRate( () -> {
                System.out.println( Instant.now().toString() + " Method WebAppListener::contextDestroyed->Runnable running. ------------------------------" ); // DEBUG logging.
                DataPublisher.instance().publishIfReady();
            } , 5 , 5 , TimeUnit.SECONDS );
        }
    
        // Our web app (Vaadin app) is shutting down.
        public void contextDestroyed ( ServletContextEvent servletContextEvent )
        {
            System.out.println( Instant.now().toString() + " Method WebAppListener::contextDestroyed running." ); // DEBUG logging.
    
            System.out.println( "Web app context destroyed." );  // INFO logging.
            this.scheduledExecutorService.shutdown();
        }
    
    }
    

    数据发布者

    在该代码中,您将看到DataPublisher实例被定期调用,要求它检查新数据,如果找到,则将其交付给所有感兴趣的Vaadin布局或小部件。
    package com.example.pushvaadinapp;
    
    import java.time.Instant;
    import net.engio.mbassy.bus.MBassador;
    import net.engio.mbassy.bus.common.DeadMessage;
    import net.engio.mbassy.bus.config.BusConfiguration;
    import net.engio.mbassy.bus.config.Feature;
    import net.engio.mbassy.listener.Handler;
    
    /**
     * A singleton to register objects (mostly user-interface components) interested
     * in being periodically notified with fresh data.
     *
     * Works in tandem with a DataProvider singleton which interacts with database
     * to look for fresh data.
     *
     * These two singletons, DataPublisher & DataProvider, could be combined into
     * one. But for testing, it might be handy to keep them separated.
     *
     * @author Basil Bourque
     */
    public class DataPublisher
    {
    
        // Statics
        private static final DataPublisher singleton = new DataPublisher();
    
        // Member vars.
        private final MBassador<DataEvent> eventBus;
    
        // Constructor. Private, for simple Singleton pattern.
        private DataPublisher ()
        {
            System.out.println( Instant.now().toString() + " Method DataPublisher::constructor running." );  // DEBUG logging.
            BusConfiguration busConfig = new BusConfiguration();
            busConfig.addFeature( Feature.SyncPubSub.Default() );
            busConfig.addFeature( Feature.AsynchronousHandlerInvocation.Default() );
            busConfig.addFeature( Feature.AsynchronousMessageDispatch.Default() );
            this.eventBus = new MBassador<>( busConfig );
            //this.eventBus = new MBassador<>( BusConfiguration.SyncAsync() );
            //this.eventBus.subscribe( this );
        }
    
        // Singleton accessor.
        public static DataPublisher instance ()
        {
            System.out.println( Instant.now().toString() + " Method DataPublisher::instance running." );   // DEBUG logging.
            return singleton;
        }
    
        public void register ( Object subscriber )
        {
            System.out.println( Instant.now().toString() + " Method DataPublisher::register running." );   // DEBUG logging.
            this.eventBus.subscribe( subscriber ); // The event bus is thread-safe. So hopefully we need no concurrency managament here.
        }
    
        public void deregister ( Object subscriber )
        {
            System.out.println( Instant.now().toString() + " Method DataPublisher::deregister running." );   // DEBUG logging.
            // Would be unnecessary to deregister if the event bus held weak references.
            // But it might be a good practice anyways for subscribers to deregister when appropriate.
            this.eventBus.unsubscribe( subscriber ); // The event bus is thread-safe. So hopefully we need no concurrency managament here.
        }
    
        public void publishIfReady ()
        {
            System.out.println( Instant.now().toString() + " Method DataPublisher::publishIfReady running." );   // DEBUG logging.
    
            // We expect this method to be called repeatedly by a ScheduledExecutorService.
            DataProvider dataProvider = DataProvider.instance();
            Boolean isFresh = dataProvider.checkForFreshData();
            if ( isFresh ) {
                DataEvent dataEvent = dataProvider.data();
                if ( dataEvent != null ) {
                    System.out.println( Instant.now().toString() + " Method DataPublisher::publishIfReady…post running." );   // DEBUG logging.
                    this.eventBus.publishAsync( dataEvent ); // Ideally this would be an asynchronous dispatching to bus subscribers.
                }
            }
        }
    
        @Handler
        public void deadEventHandler ( DeadMessage event )
        {
            // A dead event is an event posted but had no subscribers.
            // You may want to subscribe to DeadEvent as a debugging tool to see if your event is being dispatched successfully.
            System.out.println( Instant.now() + " DeadMessage on MBassador event bus : " + event );
        }
    
    }
    

    访问数据库

    该DataPublisher类使用DataProvider类访问数据库。在我们的例子中,我们只需要生成随机数据值,而不是实际访问数据库。
    package com.example.pushvaadinapp;
    
    import java.time.Instant;
    import java.util.Random;
    import java.util.UUID;
    
    /**
     * Access database to check for fresh data. If fresh data is found, package for
     * delivery. Actually we generate random data as a way to mock database access.
     *
     * @author Basil Bourque
     */
    public class DataProvider
    {
    
        // Statics
        private static final DataProvider singleton = new DataProvider();
    
        // Member vars.
        private DataEvent cachedDataEvent = null;
        private Instant whenLastChecked = null; // When did we last check for fresh data.
    
        // Other vars.
        private final Random random = new Random();
        private Integer minimum = Integer.valueOf( 1 ); // Pick a random number between 1 and 999.
        private Integer maximum = Integer.valueOf( 999 );
    
        // Constructor. Private, for simple Singleton pattern.
        private DataProvider ()
        {
            System.out.println( Instant.now().toString() + " Method DataProvider::constructor running." );   // DEBUG logging.
        }
    
        // Singleton accessor.
        public static DataProvider instance ()
        {
            System.out.println( Instant.now().toString() + " Method DataProvider::instance running." );   // DEBUG logging.
            return singleton;
        }
    
        public Boolean checkForFreshData ()
        {
            System.out.println( Instant.now().toString() + " Method DataProvider::checkForFreshData running." );   // DEBUG logging.
    
            synchronized ( this ) {
                // Record when we last checked for fresh data.
                this.whenLastChecked = Instant.now();
    
                // Mock database access by generating random data.
                UUID dbUuid = java.util.UUID.randomUUID();
                Number dbNumber = this.random.nextInt( ( this.maximum - this.minimum ) + 1 ) + this.minimum;
                Instant dbUpdated = Instant.now();
    
                // If we have no previous data (first retrieval from database) OR If the retrieved data is different than previous data --> Fresh.
                Boolean isFreshData = ( ( this.cachedDataEvent == null ) ||  ! this.cachedDataEvent.uuid.equals( dbUuid ) );
    
                if ( isFreshData ) {
                    DataEvent freshDataEvent = new DataEvent( dbUuid , dbNumber , dbUpdated );
                    // Post fresh data to event bus.
                    this.cachedDataEvent = freshDataEvent; // Remember this fresh data for future comparisons.
                }
    
                return isFreshData;
            }
        }
    
        public DataEvent data ()
        {
            System.out.println( Instant.now().toString() + " Method DataProvider::data running." );   // DEBUG logging.
    
            synchronized ( this ) {
                return this.cachedDataEvent;
            }
        }
    
    }
    

    包装数据

    DataProvider打包新数据以传递给其他对象。我们定义一个DataEvent类作为该包。或者,如果您需要交付多组数据或对象,而不是单个,请在您的DataHolder版本中放置一个Collection。打包对于想要显示此新数据的布局或小部件有意义的所有内容。
    package com.example.pushvaadinapp;
    
    import java.time.Instant;
    import java.util.UUID;
    
    /**
     * Holds data to be published in the UI. In real life, this could be one object
     * or could hold a collection of data objects as might be needed by a chart for
     * example. These objects will be dispatched to subscribers of an MBassador
     * event bus.
     *
     * @author Basil Bourque
     */
    public class DataEvent
    {
    
        // Core data values.
        UUID uuid = null;
        Number number = null;
        Instant updated = null;
    
        // Constructor
        public DataEvent ( UUID uuid , Number number , Instant updated )
        {
            this.uuid = uuid;
            this.number = number;
            this.updated = updated;
        }
    
        @Override
        public String toString ()
        {
            return "DataEvent{ " + "uuid=" + uuid + " | number=" + number + " | updated=" + updated + " }";
        }
    
    }
    

    分发数据

    将新数据打包成DataEvent之后,DataProvider会将其交给DataPublisher。因此,下一步是将数据获取到感兴趣的Vaadin布局或小部件中,以呈现给用户。但是,我们如何知道哪些布局/小部件对此数据感兴趣?以及我们如何将这些数据传递给他们?

    一种可能的方法是Observer Pattern。我们在Java Swing和Vaadin中都可以看到这种模式,例如Vaadin中的 ClickListener Button 。这种模式意味着观察者和被观察者彼此了解。这意味着在定义和实现接口(interface)方面需要做更多的工作。

    Activity 巴士

    在我们的案例中,我们不需要数据的生产者(DataPublisher)和使用者(Vaadin布局/小部件)相互了解。所有小部件只需要数据,而无需与生产者进行进一步的交互。因此,我们可以使用不同的方法,即事件总线。在事件总线中,某些对象在发生有趣的事件时会发布“事件”对象。当事件对象发布到总线上时,其他对象表示有兴趣得到通知。发布后,总线通过调用某种方法并传递事件,将该事件发布给所有注册的订户。在我们的示例中,将传递DataEvent对象。

    但是,将调用已注册的订阅对象上的哪种方法?通过Java的注释,反射和自省(introspection)技术的魔力,任何方法都可以标记为要调用的方法。仅用注释标记所需的方法,然后让总线在发布事件时在运行时找到该方法。

    无需自己构建任何此事件总线。在Java世界中,我们可以选择事件总线实现。

    Google Guava EventBus

    最著名的可能是Google Guava EventBusGoogle Guava是Google内部开发的一堆各种实用程序项目,然后将其开源以供其他人使用。 EventBus软件包是其中的一个项目。我们可以使用Guava EventBus。确实,我最初确实是使用该库构建此示例的。但是Guava EventBus有一个局限性:它拥有强大的引用。

    引用文献薄弱

    当对象注册其被通知的兴趣时,任何事件总线都必须通过保留对注册对象的引用来保留这些订阅的列表。理想情况下,这应该是weak reference,这意味着如果订阅对象达到其用途的尽头并成为garbage collection的候选者,则该对象可以这样做。如果事件总线具有强引用,则该对象无法继续进行垃圾回收。弱引用告诉JVM我们并不真正在乎对象,我们在乎一点但不足以坚持要保留对象。对于弱引用,事件总线在尝试将新事件通知给订户之前检查空引用。如果为null,则事件总线可以将该插槽放入其对象跟踪集合中。

    您可能会认为,作为解决保留强引用问题的一种解决方法,您可以让已注册的Vaadin小部件覆盖detach方法。当不再使用Vaadin小部件时,系统会通知您,然后您的方法将从事件总线中注销。如果将订阅对象从事件总线中删除,则不再需要强引用,也不会出现问题。但是,就像Java Object方法 finalize is not always called一样,Vaadin detach方法也不总是被调用。有关详细信息,请参见Vaadin专家this threadHenri Sara上发布的信息。依赖detach可能会导致内存泄漏和其他问题。

    MBassador Activity 巴士

    有关事件总线库的各种Java实现的讨论,请参见my blog post。在这些示例中,我选择了MBassador以便在此示例应用程序中使用。其存在的理由是使用弱引用。

    UI类

    线程之间

    要实际更新Vaadin布局和小部件的值,有一个大问题。这些小部件在它们自己的用户界面处理线程(该用户的主要Servlet线程)中运行。同时,您的数据库检查和数据发布以及事件总线调度都是在由执行程序服务管理的后台线程上进行的。 切勿从单独的线程访问或更新Vaadin小部件! 这个规则是绝对关键的。为了使它更加棘手,在开发过程中这样做可能实际上是可行的。但是,如果在生产中这样做,您将遭受重创。

    那么,我们如何从后台线程获取数据以传递到在主Servlet线程中运行的小部件中呢? UI类提供了一种专门用于此目的的方法: access 。您将Runnable传递给access方法,并且Vaadin计划将Runnable安排在主用户界面线程上执行。十分简单。

    剩余类(class)

    为了总结这个示例应用程序,下面是剩余的类。 “MyUI”类将替换由new Maven archetype for Vaadin 7.3.7创建的默认项目中的同名文件。
    package com.example.pushvaadinapp;
    
    import com.vaadin.annotations.Push;
    import com.vaadin.annotations.Theme;
    import com.vaadin.annotations.VaadinServletConfiguration;
    import com.vaadin.annotations.Widgetset;
    import com.vaadin.server.BrowserWindowOpener;
    import com.vaadin.server.VaadinRequest;
    import com.vaadin.server.VaadinServlet;
    import com.vaadin.ui.Button;
    import com.vaadin.ui.Label;
    import com.vaadin.ui.UI;
    import com.vaadin.ui.VerticalLayout;
    import java.time.Instant;
    import javax.servlet.annotation.WebServlet;
    
    /**
     * © 2014 Basil Bourque. This source code may be used freely forever by anyone
     * absolving me of any and all responsibility.
     */
    @Push
    @Theme ( "mytheme" )
    @Widgetset ( "com.example.pushvaadinapp.MyAppWidgetset" )
    public class MyUI extends UI
    {
    
        Label label = new Label( "Now : " );
        Button button = null;
    
        @Override
        protected void init ( VaadinRequest vaadinRequest )
        {
            // Prepare widgets.
            this.button = this.makeOpenWindowButton();
    
            // Arrange widgets in a layout.
            VerticalLayout layout = new VerticalLayout();
            layout.setMargin( Boolean.TRUE );
            layout.setSpacing( Boolean.TRUE );
            layout.addComponent( this.label );
            layout.addComponent( this.button );
    
            // Put layout in this UI.
            setContent( layout );
    
            // Start the data feed thread
            new FeederThread().start();
        }
    
        @WebServlet ( urlPatterns = "/*" , name = "MyUIServlet" , asyncSupported = true )
        @VaadinServletConfiguration ( ui = MyUI.class , productionMode = false )
        public static class MyUIServlet extends VaadinServlet
        {
        }
    
        public void tellTime ()
        {
            label.setValue( "Now : " + Instant.now().toString() ); // If before Java 8, use: new java.util.Date(). Or better, Joda-Time.
        }
    
        class FeederThread extends Thread
        {
    
            // This Thread class is merely a simple test to verify that Push works.
            // This Thread class is not the intended example.
            // A ScheduledExecutorService is in WebAppListener class is the intended example.
            int count = 0;
    
            @Override
            public void run ()
            {
                try {
                    // Update the data for a while
                    while ( count < 100 ) {
                        Thread.sleep( 1000 );
    
                        access( new Runnable() // Special 'access' method on UI object, for inter-thread communication.
                        {
                            @Override
                            public void run ()
                            {
                                count ++;
                                tellTime();
                            }
                        } );
                    }
    
                    // Inform that we have stopped running
                    access( new Runnable()
                    {
                        @Override
                        public void run ()
                        {
                            label.setValue( "Done. No more telling time." );
                        }
                    } );
                } catch ( InterruptedException e ) {
                    e.printStackTrace();
                }
            }
        }
    
        Button makeOpenWindowButton ()
        {
            // Create a button that opens a new browser window.
            BrowserWindowOpener opener = new BrowserWindowOpener( DataUI.class );
            opener.setFeatures( "height=300,width=440,resizable=yes,scrollbars=no" );
    
            // Attach it to a button
            Button button = new Button( "Open data window" );
            opener.extend( button );
    
            return button;
        }
    }
    

    在此示例Vaadin应用程序中,“DataUI”和“DataLayout”完成了7个.java文件。
    package com.example.pushvaadinapp;
    
    import com.vaadin.annotations.Push;
    import com.vaadin.annotations.Theme;
    import com.vaadin.annotations.Widgetset;
    import com.vaadin.server.VaadinRequest;
    import com.vaadin.ui.UI;
    import java.time.Instant;
    import net.engio.mbassy.listener.Handler;
    
    @Push
    @Theme ( "mytheme" )
    @Widgetset ( "com.example.pushvaadinapp.MyAppWidgetset" )
    public class DataUI extends UI
    {
    
        // Member vars.
        DataLayout layout;
    
        @Override
        protected void init ( VaadinRequest request )
        {
            System.out.println( Instant.now().toString() + " Method DataUI::init running." );   // DEBUG logging.
    
            // Initialize window.
            this.getPage().setTitle( "Database Display" );
            // Content.
            this.layout = new DataLayout();
            this.setContent( this.layout );
    
            DataPublisher.instance().register( this ); // Sign-up for notification of fresh data delivery.
        }
    
        @Handler
        public void update ( DataEvent event )
        {
            System.out.println( Instant.now().toString() + " Method DataUI::update (@Subscribe) running." );   // DEBUG logging.
    
            // We expect to be given a DataEvent item.
            // In a real app, we might need to retrieve data (such as a Collection) from within this event object.
            this.access( () -> {
                this.layout.update( event ); // Crucial that go through the UI:access method when updating the user interface (widgets) from another thread.
            } );
        }
    
    }
    

    …和…
    /*
     * To change this license header, choose License Headers in Project Properties.
     * To change this template file, choose Tools | Templates
     * and open the template in the editor.
     */
    package com.example.pushvaadinapp;
    
    import com.vaadin.ui.TextField;
    import com.vaadin.ui.VerticalLayout;
    import java.time.Instant;
    
    /**
     *
     * @author brainydeveloper
     */
    public class DataLayout extends VerticalLayout
    {
    
        TextField uuidField;
        TextField numericField;
        TextField updatedField;
        TextField whenCheckedField;
    
        // Constructor
        public DataLayout ()
        {
            System.out.println( Instant.now().toString() + " Method DataLayout::constructor running." );   // DEBUG logging.
    
            // Configure layout.
            this.setMargin( Boolean.TRUE );
            this.setSpacing( Boolean.TRUE );
    
            // Prepare widgets.
            this.uuidField = new TextField( "UUID : " );
            this.uuidField.setWidth( 22 , Unit.EM );
            this.uuidField.setReadOnly( true );
    
            this.numericField = new TextField( "Number : " );
            this.numericField.setWidth( 22 , Unit.EM );
            this.numericField.setReadOnly( true );
    
            this.updatedField = new TextField( "Updated : " );
            this.updatedField.setValue( "<Content will update automatically>" );
            this.updatedField.setWidth( 22 , Unit.EM );
            this.updatedField.setReadOnly( true );
    
            // Arrange widgets.
            this.addComponent( this.uuidField );
            this.addComponent( this.numericField );
            this.addComponent( this.updatedField );
        }
    
        public void update ( DataEvent dataHolder )
        {
            System.out.println( Instant.now().toString() + " Method DataLayout::update (via @Subscribe on UI) running." );   // DEBUG logging.
    
            // Stuff data values into fields. For simplicity in this example app, using String directly rather than Vaadin converters.
            this.uuidField.setReadOnly( false );
            this.uuidField.setValue( dataHolder.uuid.toString() );
            this.uuidField.setReadOnly( true );
    
            this.numericField.setReadOnly( false );
            this.numericField.setValue( dataHolder.number.toString() );
            this.numericField.setReadOnly( true );
    
            this.updatedField.setReadOnly( false );
            this.updatedField.setValue( dataHolder.updated.toString() );
            this.updatedField.setReadOnly( true );
        }
    
    }
    

    关于java - 使用Vaadin 7应用程序中的推送显示相同数据穿越多个客户端,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27872756/

    相关文章:

    java - 按坐标索引对象列表的最惯用方法是什么

    java - 单个应用程序有两个 list 文件?

    java - 从对象的字符串表示形式确定类

    java - Vaadin 向导插件事件触发两次

    gradle - Spring Boot Vaadin静态内容

    java - 为什么Hibernate要改变DataBase中存储的DateTime?

    java - 使用 Vaadin 生成缓慢的 HTML

    java - Hibernate 进行正确的数据库查询但返回空实体

    java - Vaadin - 行修改后刷新网格

    java - Vaadin 设置表格的选定行