什么是服務?
今天,“ 服務 ”一詞已被過度使用,對不同的人可能意味著很多事情。 當我說Service時 ,我的定義是一個具有最小生命周期(例如init
, start
, stop
和destroy
)的軟件組件。 在編寫的每個服務中,您可能不需要生命周期的所有這些階段,但是您可以忽略那些不適用的服務。 在編寫旨在長期運行的大型應用程序(例如服務器組件)時,定義這些生命周期并確保按正確的順序執行它們是至關重要的!
我將引導您完成我準備的Java演示項目。 這是非常基礎的,應該獨立運行。 它具有的唯一依賴性是SLF4J記錄器。 如果您不知道如何使用記錄器,則只需將它們替換為System.out.println
。 但是,我強烈建議您學習在應用程序開發期間如何有效使用記錄器。 另外,如果您想嘗試與Spring相關的演示,那么顯然您也將需要其jar。
編寫基本的POJO服務
您可以在界面中如下所示快速定義具有生命周期的服務合同。
package servicedemo;public interface Service {void init();void start();void stop();void destroy();boolean isInited();boolean isStarted();
}
開發人員可以自由地在Service實現中做他們想做的事情,但是您可能想給他們一個適配器類,這樣他們就不必在每個Service上重寫相同的基本邏輯。 我將提供這樣的抽象服務:
package servicedemo;import java.util.concurrent.atomic.*;
import org.slf4j.*;
public abstract class AbstractService implements Service {protected Logger logger = LoggerFactory.getLogger(getClass());protected AtomicBoolean started = new AtomicBoolean(false);protected AtomicBoolean inited = new AtomicBoolean(false);public void init() {if (!inited.get()) {initService();inited.set(true);logger.debug('{} initialized.', this);}}public void start() {// Init service if it has not done so.if (!inited.get()) {init();}// Start service now.if (!started.get()) {startService();started.set(true);logger.debug('{} started.', this);}}public void stop() {if (started.get()) {stopService();started.set(false);logger.debug('{} stopped.', this);}}public void destroy() {// Stop service if it is still running.if (started.get()) {stop();}// Destroy service now.if (inited.get()) {destroyService();inited.set(false);logger.debug('{} destroyed.', this);}}public boolean isStarted() {return started.get();}public boolean isInited() {return inited.get();}@Overridepublic String toString() {return getClass().getSimpleName() + '[id=' + System.identityHashCode(this) + ']';}protected void initService() {}protected void startService() {}protected void stopService() {}protected void destroyService() {}
}
這個抽象類提供大多數服務需求的基礎。 它有一個記錄器,并指出了生命周期。 然后,它委托新的生命周期方法集,以便子類可以選擇重寫。 注意, start()
方法正在檢查是否自動調用init()
。 在destroy()
方法和stop()
方法中也是如此。 如果我們要在只有兩個階段生命周期調用的容器中使用它,則這一點很重要。 在這種情況下,我們可以簡單地調用start()
和destroy()
來匹配我們服務的生命周期。
一些框架可能會再進一步,對于生命周期,如每個階段創建獨立的接口InitableService
或StartableService
等,但我認為這將是太多了在一個典型的應用。 在大多數情況下,您想要簡單的東西,所以我只喜歡一個界面。 用戶可以選擇忽略不需要的方法,或僅使用適配器類。
在結束本節之前,我將提供一個愚蠢的Hello world服務,以后可以在我們的演示中使用它。
package servicedemo;public class HelloService extends AbstractService {public void initService() {logger.info(this + ' inited.');}public void startService() {logger.info(this + ' started.');}public void stopService() {logger.info(this + ' stopped.');}public void destroyService() {logger.info(this + ' destroyed.');}
}
使用容器管理多個POJO服務
現在我們已經定義了服務定義的基礎,您的開發團隊可能會開始編寫業務邏輯代碼! 不久之后,您將擁有自己的服務庫以供重新使用。 為了能夠有效地分組和控制這些服務,我們還希望提供一個容器來管理它們。 這個想法是,我們通常希望通過容器作為更高級別的組來控制和管理多個服務。 這是一個入門的簡單實現:
package servicedemo;import java.util.*;
public class ServiceContainer extends AbstractService {private List<Service> services = new ArrayList<Service>();public void setServices(List<Service> services) {this.services = services;}public void addService(Service service) {this.services.add(service);}public void initService() {logger.debug('Initializing ' + this + ' with ' + services.size() + ' services.');for (Service service : services) {logger.debug('Initializing ' + service);service.init();}logger.info(this + ' inited.');}public void startService() {logger.debug('Starting ' + this + ' with ' + services.size() + ' services.');for (Service service : services) {logger.debug('Starting ' + service);service.start();}logger.info(this + ' started.');}public void stopService() {int size = services.size();logger.debug('Stopping ' + this + ' with ' + size + ' services in reverse order.');for (int i = size - 1; i >= 0; i--) {Service service = services.get(i);logger.debug('Stopping ' + service);service.stop();}logger.info(this + ' stopped.');}public void destroyService() {int size = services.size();logger.debug('Destroying ' + this + ' with ' + size + ' services in reverse order.');for (int i = size - 1; i >= 0; i--) {Service service = services.get(i);logger.debug('Destroying ' + service);service.destroy();}logger.info(this + ' destroyed.');}
}
從上面的代碼中,您將注意到一些重要的事情:
- 我們擴展了AbstractService,因此容器本身就是服務。
- 在進入下一個服務之前,我們將調用所有服務的生命周期。 除非啟動所有其他服務,否則不會啟動任何服務。
- 對于大多數一般用例,我們應該以相反的順序停止和銷毀服務。
上面的容器實現很簡單,并且以同步方式運行。 這意味著,您啟動容器,然后所有服務將按照您添加它們的順序啟動。 停止應該相同,但順序相反。
我也希望您能夠看到有足夠的空間來改進此容器。 例如,您可以添加線程池以異步方式控制服務的執行。
運行POJO服務 通過一個簡單的運行程序運行服務。
以最簡單的形式,我們可以自己運行POJO服務,而無需任何高級服務器或框架。 Java程序是從靜態main
方法開始的,因此我們肯定可以在其中調用init
并start
我們的服務。 但是,當用戶關閉程序時(通常通過按CTRL+C
),我們還需要解決stop
和destroy
生命周期的問題。為此,Java具有java.lang.Runtime#addShutdownHook()
功能。 您可以創建一個簡單的獨立服務器來引導服務,如下所示:
package servicedemo;import org.slf4j.*;
public class ServiceRunner {private static Logger logger = LoggerFactory.getLogger(ServiceRunner.class);public static void main(String[] args) {ServiceRunner main = new ServiceRunner();main.run(args);}public void run(String[] args) {if (args.length < 1)throw new RuntimeException('Missing service class name as argument.');String serviceClassName = args[0];try {logger.debug('Creating ' + serviceClassName);Class<?> serviceClass = Class.forName(serviceClassName);if (!Service.class.isAssignableFrom(serviceClass)) {throw new RuntimeException('Service class ' + serviceClassName + ' did not implements ' + Service.class.getName());}Object serviceObject = serviceClass.newInstance();Service service = (Service)serviceObject;registerShutdownHook(service);logger.debug('Starting service ' + service);service.init();service.start();logger.info(service + ' started.');synchronized(this) {this.wait();}} catch (Exception e) {throw new RuntimeException('Failed to create and run ' + serviceClassName, e);}}private void registerShutdownHook(final Service service) {Runtime.getRuntime().addShutdownHook(new Thread() {public void run() {logger.debug('Stopping service ' + service);service.stop();service.destroy();logger.info(service + ' stopped.');}});}
}
使用優于跑步者,您應該可以使用以下命令運行它:
$ java demo.ServiceRunner servicedemo.HelloService
仔細查看,您會發現您可以使用上述運行器有很多選擇來運行多個服務。 讓我突出幾個:
- 直接改善上述運行器,并為每個新服務類名稱(而不是僅第一個元素)使用所有
args
。 - 或編寫一個
MultiLoaderService
來加載所需的多個服務。 您可以使用“系統屬性”控制參數傳遞。
您能想到其他改進此跑步者的方法嗎?
使用Spring運行服務
Spring框架是一個IoC容器,眾所周知,它易于使用POJO,而Spring可讓您將應用程序連接在一起。 這將非常適合在我們的POJO服務中使用。 但是,由于Spring提供了所有功能,因此錯過了易于使用的現成的主程序來引導spring config xml上下文文件。 但是就目前為止構建的內容而言,這實際上是一件容易的事。 讓我們編寫一個POJO 服務來引導一個Spring上下文文件。
package servicedemo;import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;public class SpringService extends AbstractService {private ConfigurableApplicationContext springContext;public void startService() {String springConfig = System.getProperty('springContext', 'spring.xml);springContext = new FileSystemXmlApplicationContext(springConfig);logger.info(this + ' started.');}public void stopService() {springContext.close();logger.info(this + ' stopped.');}
}
使用該簡單的SpringService
您可以運行和加載任何spring xml文件。 例如,嘗試以下操作:
$ java -DspringContext=config/service-demo-spring.xml demo.ServiceRunner servicedemo.SpringService
在config/service-demo-spring.xml
文件中,您可以輕松地創建我們的容器,該容器在Spring Bean中承載一項或多項服務。
<beans xmlns='http://www.springframework.org/schema/beans'xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'xsi:schemaLocation='http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd'><bean id='helloService' class='servicedemo.HelloService'></bean><bean id='serviceContainer' class='servicedemo.ServiceContainer' init-method='start' destroy-method='destroy'><property name='services'><list><ref bean='helloService'/></list></property></bean></beans>
注意,我只需要在serviceContainer
bean上設置一次init-method
和destroy-method
。 然后,您可以根據需要添加一個或多個其他服務,例如helloService
。 關閉Spring上下文時,它們將全部啟動,管理,然后關閉。
請注意,Spring上下文容器沒有明確地具有與我們的服務相同的生命周期。 Spring上下文將自動實例化您的所有依賴項Bean,然后調用所有設置了init-method
Bean。 所有這些操作都在FileSystemXmlApplicationContext
的構造函數中完成。 用戶未調用任何顯式的init方法。 但是最后,在服務停止期間,Spring提供了container#close()
來清理內容。 同樣,它們不區分stop
destroy
。 因此,我們必須合并我們的init
并start
進入Spring的init
狀態,然后合并stop
和destroy
進入Spring的close
狀態。 回想一下我們的AbstractService#destory
會自動調用stop
如果尚未執行的話)。 因此,為了有效使用Spring,我們需要了解這一技巧。
使用JEE應用服務器運行服務
在公司環境中,我們通常沒有自由運行獨立程序所需的內容。 相反,它們通常已經具有一些基礎設施和更嚴格的標準技術堆棧,例如使用JEE應用程序服務器。 在這種情況下,運行POJO服務最可移植的是war
Web應用程序。 在Servlet Web應用程序中,您可以編寫一個實現javax.servlet.ServletContextListener
的類,這將通過contextInitialized
和contextDestroyed
為您提供生命周期掛鉤。 在其中,您可以實例化ServiceContainer
對象,并相應地調用start
和destroy
方法。
您可以瀏覽以下示例:
package servicedemo;
import java.util.*;
import javax.servlet.*;
public class ServiceContainerListener implements ServletContextListener {private static Logger logger = LoggerFactory.getLogger(ServiceContainerListener.class);private ServiceContainer serviceContainer;public void contextInitialized(ServletContextEvent sce) {serviceContainer = new ServiceContainer();List<Service> services = createServices();serviceContainer.setServices(services);serviceContainer.start();logger.info(serviceContainer + ' started in web application.');}public void contextDestroyed(ServletContextEvent sce) {serviceContainer.destroy();logger.info(serviceContainer + ' destroyed in web application.');}private List<Service> createServices() {List<Service> result = new ArrayList<Service>();// populate services here.return result;}
}
您可以像上面這樣在WEB-INF/web.xml
配置:
<listener><listener-class>servicedemo.ServiceContainerListener</listener-class></listener></web-app>
該演示提供了一個占位符,您必須在代碼中添加服務。 但是您可以使用web.xml
作為上下文參數輕松地將其配置為可配置的。
如果要在Servlet容器中使用Spring,則可以直接使用其org.springframework.web.context.ContextLoaderListener
類,該類與上述功能大致相同,不同之處在于它們允許您使用contextConfigLocation
上下文參數指定其xml配置文件。 這就是典型的基于Spring MVC的應用程序的配置方式。 設置完成后,您可以像上面給出的Spring xml示例一樣嘗試我們的POJO服務以進行測試。 您應該在記錄器的輸出中看到我們的服務正在運行。
PS:實際上,我們在此描述的只是與Servlet Web應用程序相關,而不與JEE有關。 因此,您也可以使用Tomcat服務器。
服務生命周期的重要性及其在現實世界中的使用
我在這里提供的所有信息都不是新穎的,也不是殺手級的設計模式。 實際上,它們已在許多流行的開源項目中使用。 但是,根據我過去的工作經驗,人們總是設法使這些變得極為復雜,更糟糕的情況是,他們在編寫服務時完全無視生命周期的重要性。 的確,并非您要編寫的所有內容都需要安裝到服務中,但是,如果發現需要,請務必注意它們,并請務必小心它們的正確調用。 您想要的最后一件事是退出JVM,而不清理您為其分配了寶貴資源的服務。 如果您允許在部署過程中動態地重新加載應用程序而不退出JVM,這些操作將變得更加災難性,這將導致系統資源泄漏。
以上服務實踐已在TimeMachine項目中使用。 實際上,如果您查看timemachine.scheduler.service.SchedulerEngine
,它將只是許多運行在一起的服務的容器。 這就是用戶可以通過編寫Service來擴展調度程序功能的方式。 您可以通過一個簡單的屬性文件動態加載這些服務。
參考: 如何在A Programmer's Journal博客上從我們的JCG合作伙伴 Zemian Deng 編寫更好的POJO服務 。
翻譯自: https://www.javacodegeeks.com/2012/09/how-to-write-better-pojo-services.html