我时不时地发现自己摸索了一些旧的代码,以找到示例“我在哪里做过工厂一样的事情”。
上周再次发生这种情况时,我决定只查找所有示例,并创建一个示例项目和有关它的博客文章。
所以在这篇文章中,我:
- 从简单的“原始” Java SE工厂示例开始
- 然后使用Java SE SPI
- Java SE上的CDI
- Java EE上的 CDI
- Java EE上的EJB
- Java SE上的动态SPI
- 最后是Java EE上的SPI
这个例子
这个示例应用程序是一个非常简单的“ Hello World”,您可以输入名称,并且有多种表达问候的方法。
Greeting Service
获取Greeting Factory
的实例。 然后,它可以按名称要求工厂提供Greeting
(接口),然后工厂将返回正确的实现。
有3种具体的实现方式:
-
English
将打招呼“好日子名字” 。 -
Afrikaans
将打招呼“ Goeie dag name” 。 (请参阅https://www.youtube.com/watch?v=CtxB4sbV0pA ) -
Bugs Bunny
会打招呼“ Eeee, 叫什么名字 ?” (请参阅https://www.youtube.com/watch?v=UeVtZjGII-I )
Github中提供了此博客的所有源代码 :
git clone https://github.com/phillip-kruger/factories-example
问候界面:
public interface Greeting {public String getName();public String sayHello(String to);}
香草
这个基本的Java SE应用程序有一个主要方法,该方法可让您传递名称和希望打招呼的方式。
工厂是获得正确实现的基本if-statement
:
public Greeting getGreeting(String name){if(name.equalsIgnoreCase("BugsBunny")){return new BugsBunny();}else if(name.equalsIgnoreCase("Afrikaans")){return new Afrikaans();}else {return new English();}}
一个具体的实现示例, 英语 :
public class English implements Greeting {@Overridepublic String sayHello(String to) {return "Good day " + to + ".";}@Overridepublic String getName() {return "English";}}
运行示例:
在vanilla文件夹中:
mvn clean install
这将构建项目并运行该应用程序。 日志将输出:
SEVERE: Good day Phillip.Goeie dag Phillip.Eeee, what's up Phillip ?
您也可以在Maven之外运行此命令:
java -jar target/vanilla-1.0.0-SNAPSHOT.jar World BugsBunnySEVERE: Eeee, what's up World ?
另见
- https://alvinalexander.com/java/java-factory-pattern-example
服务提供商接口(SPI)
上面的示例意味着我可以非常轻松地添加另一个实现,并在询问时更新if statement
以返回该实现。
但是,我们要改进的是if-statement
。 我们希望可以在不修改现有代码的情况下添加新的实现。 我要做的就是添加新的实现。
SPI是Java SE的一部分,是允许您构建可插入扩展的API。
将应用程序分解为模块。
我们要做的第一件事是将应用程序分解为模块 :
- API –这将包含Greeting接口(我们的合同)
- 引擎–其中将包含服务和工厂(以及默认的英语实现)
- 其他实现–所有其他实现都变成了自己的模块(因此,一种用于南非荷兰语,另一种用于Bugs Bunny等)
这已经意味着我可以通过创建一个新模块来添加新的实现,而不必触摸代码,只需更新依赖项即可:
<dependencies><dependency><groupId>${project.groupId}</groupId><artifactId>spi-api</artifactId><version>${project.version}</version></dependency><dependency><groupId>${project.groupId}</groupId><artifactId>spi-impl-afrikaans</artifactId><version>${project.version}</version></dependency><dependency><groupId>${project.groupId}</groupId><artifactId>spi-impl-bugsbunny</artifactId><version>${project.version}</version></dependency></dependencies>
映射文件
具体的实现需要通过在/src/main/resources/META-INF/services/
添加一个文件com.github.phillipkruger.factory.api.Greeting
(接口的全限定名),将其Greeting
类注册为实现。 )
文件的内容是实现的名称,例如Bugs Bunny :
com.github.phillipkruger.factory.impl.BugsBunny
该工厂
工厂现在需要获取所有Greetings实例:
ServiceLoader<Greeting> loader = ServiceLoader.load(Greeting.class);Iterator<Greeting> greetingIterator = loader.iterator();while (greetingIterator.hasNext()) {Greeting greeting = greetingIterator.next();loadedGreetings.put(greeting.getName(), greeting);}
现在我们摆脱了工厂中的if-statement
。
运行示例:
在spi文件夹中:
mvn clean install
这将构建项目并运行该应用程序。 日志将输出:
SEVERE: Good day Phillip.Goeie dag Phillip.Eeee, what's up Phillip ?
您也可以在Maven之外运行此命令:
java -jar spi-engine/target/spi-engine-1.0.0-SNAPSHOT-jar-with-dependencies.jar Johny AfrikaansSEVERE: Goeie dag Johny.
另见
- https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
上下文和依赖注入(CDI)
最新版本的CDI允许您在Java SE中使用CDI。 为了创建工厂,我们将创建自己的注释作为称为GreetingProvider
的API的一部分:
@Qualifier@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface GreetingProvider {String value();}
value
是实现的名称。
以上的文字实现 :
@AllArgsConstructorpublic class GreetingProviderLiteral extends AnnotationLiteral<GreetingProvider> implements GreetingProvider {private final String name;@Overridepublic String value() {return this.name;}}
这将允许我们使用@GreetingProvider
注释任何具体的实现(现在是RequestScoped
CDI Bean),例如英语:
@GreetingProvider("English")@RequestScopedpublic class English implements Greeting {@Overridepublic String sayHello(String to) {return "Good day " + to + ".";}@Overridepublic String getName() {return "English";}}
工厂更改以查找所有@GreetingProvider
类:
public class GreetingFactory {@Inject @Anyprivate Instance<Greeting> greetings;public Greeting getGreeting(String name) {Instance<Greeting> instance = greetings.select(new GreetingProviderLiteral(name));if(!instance.isUnsatisfied()){Greeting provider = instance.get();return provider;}else{return new English();}}}
因此,现在我们不再需要SPI映射文件,工厂中没有if-statement
,但是我们仍然必须更新依赖项以包含所需的所有实现。
运行示例:
在cdi文件夹中:
mvn clean install
这将构建项目并运行该应用程序。 日志将输出:
SEVERE: Good day Phillip.Goeie dag Phillip.Eeee, what's up Phillip ?
您也可以在Maven之外运行此命令:
java -jar cdi-engine/target/cdi-engine-1.0.0-SNAPSHOT-jar-with-dependencies.jar Charmaine BugsBunnySEVERE: Eeee, what's up Charmaine ?
另见
- http://www.mastertheboss.com/jboss-frameworks/cdi/building-a-cdi-2-standalone-java-application
- http://www.adam-bien.com/roller/abien/entry/injecting_classes_in_java_se
Java EE上的CDI
我们还可以使用CDI在Application Server上创建解决方案。 现在,我们将入口点设为REST服务(而不是主要方法),因此我们需要创建并添加ApplicationConfig
来启用JAX-RS:
@ApplicationPath("/api")public class ApplicationConfig extends Application {}
GreetingService
现在成为REST资源,使您可以执行GET
,将名称作为PathParam
传递,以及将可选方式作为QueryParam
进行问候:
@Path("/")@Produces(MediaType.APPLICATION_JSON)public class GreetingService {@Injectprivate GreetingFactory factory;@GET@Path("{to}")public String sayHello(@PathParam("to") String to, @QueryParam("way") List<String> way){//....}}
工厂和带注释的RequestScoped
CDI Bean实现与Java SE示例上的CDI完全相同。
运行示例:
该示例可以在3个不同的应用程序服务器上运行(为诚实起见)
- 野蜂群
- 开放自由
- Payara Micro
(您不必下载,安装或配置任何东西,Maven构建会做到这一点)
在javaee-cdi文件夹中:
mvn clean install -P wildfly
要么
mvn clean install -P liberty
要么
mvn clean install -P payara
在所有3种情况下,maven都会:
- 在部署了应用程序的情况下启动应用程序服务器
- 击中2个REST网址:
- http:// localhost:8080 / javaee-cdi-engine / api (此列表所有实现)
- 关闭应用程序服务器(Payara除外)
因此,在日志中,您将看到(类似):
===============================================
["BugsBunny","Afrikaans","English"]
==============================================================================================
["Eeee, what's up Phillip ?","Goeie dag Phillip.","Good day Phillip."]
===============================================
如果运行Payara,则服务器不会关闭,因此您也可以手动测试工厂:
wget -qO- http://localhost:8080/javaee-cdi-engine/api/Donald?way=BugsBunny["Eeee, what's up Donald ?"]
Java EE上的EJB
只是为了完成示例,这是在Java EE上使用EJB的方法(因此没有CDI –因此也没有自定义注释)
我们仅使用JNDI查找命名的EJB。
GreetingService
与Java EE CDI示例相同,因此我们仍然有一个REST入口点。 现在,具体的实现变为EJB,例如英语:
@Stateless@EJB(beanInterface = Greeting.class, beanName = "English", name = "English")public class English implements Greeting {@Overridepublic String sayHello(String to) {return "Good day " + to + ".";}@Overridepublic String getName() {return "English";}}
现在, 工厂将基于bean名称进行JNDI查找:
@Log@Statelesspublic class GreetingFactory {@EJB(lookup = "java:module/English")private Greeting english; // defaultpublic Greeting getGreeting(String name) {Greeting g = lookup("java:module/" + name);if(g==null)return english;return g;}public List<Greeting> getAll(){List<Greeting> greetings = new ArrayList<>();try {InitialContext context = new InitialContext();NamingEnumeration<Binding> list = (NamingEnumeration<Binding>)context.listBindings("java:global/javaee-ejb-engine"); while (list.hasMore()) {Binding next = list.next();if(next.getName().endsWith(Greeting.class.getName())){Greeting g = lookup("java:global/javaee-ejb-engine/" + next.getName());if(g!=null && !greetings.contains(g))greetings.add(g);}}} catch (NamingException e) {throw new RuntimeException(e);} return greetings;}private Greeting lookup(String jndi){try {InitialContext context = new InitialContext();Object o = context.lookup(jndi);return (Greeting)o;} catch (NamingException e) {log.log(Level.SEVERE, "Could not lookup [{0}]", jndi);return null;} }}
运行示例:
与Java EE CDI示例类似,它在Wildfly Swarm , Open Liberty和Payara Micro上运行
在javaee-ejb文件夹中:
mvn clean install -P wildfly
(或-P自由或-P payara)
在所有3种情况下,maven都会:
- 在部署了应用程序的情况下启动应用程序服务器
- 击中2个REST网址:
- http:// localhost:8080 / javaee-ejb-engine / api (此列表列出了所有实现)
- 关闭应用程序服务器(Payara除外)
因此,在日志中,您将看到(类似):
===============================================
["BugsBunny","Afrikaans","English"]
==============================================================================================
["Eeee, what's up Phillip ?","Goeie dag Phillip.","Good day Phillip."]
===============================================
如果运行Payara,则服务器不会关闭,因此您也可以手动测试工厂:
wget -qO- http://localhost:8080/javaee-ejb-engine/api/Barney?way=Afrikaans["Goeie dag Barney."]
动态SPI
到目前为止,添加新实现时,我们唯一要做的就是创建包含Greeting
实现的模块,并更新pom.xml
以包含新的依赖项。
接下来,让我们看看如何动态加载新的实现(因此无需更新依赖项)。
实现完全类似于Java SE SPI示例,包括映射文件,但是现在我们可以删除模块作为pom.xml
依赖项:
<dependencies><dependency><groupId>${project.groupId}</groupId><artifactId>dynamic-spi-api</artifactId><version>${project.version}</version></dependency><!-- This will be loaded dynamically<dependency><groupId>${project.groupId}</groupId><artifactId>dynamic-spi-impl-afrikaans</artifactId><version>${project.version}</version></dependency><dependency><groupId>${project.groupId}</groupId><artifactId>dynamic-spi-impl-bugsbunny</artifactId><version>${project.version}</version></dependency>--></dependencies>
工厂看起来像这样:
public class GreetingFactory {private final Map<String,Greeting> loadedGreetings = new HashMap<>();public GreetingFactory(){URLClassLoader classloader = getURLClassLoader();ServiceLoader<Greeting> loader = ServiceLoader.load(Greeting.class, classloader);Iterator<Greeting> greetingIterator = loader.iterator();while (greetingIterator.hasNext()) {Greeting greeting = greetingIterator.next();loadedGreetings.put(greeting.getName(), greeting);}}public Greeting getGreeting(String name){if(loadedGreetings.containsKey(name)){return loadedGreetings.get(name);}else {return new English();}}private URLClassLoader getURLClassLoader(){File[] pluginFiles = getPluginFiles();ArrayList<URL> urls = new ArrayList<>();for(File plugin:pluginFiles){try{URL pluginURL = plugin.toURI().toURL();urls.add(pluginURL);}catch(MalformedURLException m){log.log(Level.SEVERE, "Could not load [{0}], ignoring", plugin.getName());}}return new URLClassLoader(urls.toArray(new URL[]{}),GreetingFactory.class.getClassLoader());}private File[] getPluginFiles(){File loc = new File("plugins");File[] pluginFiles = loc.listFiles((File file) -> file.getPath().toLowerCase().endsWith(".jar"));return pluginFiles;}}
基本上,工厂仍将使用SPI的ServiceLoader
加载可用的问候语,但我们传入自定义的ClassLoader。我们在plugins
文件夹中查找任何jar文件,并使用URLClassloader
加载。
这意味着我现在可以只创建一个新的实现模块并将文件放在plugins
文件夹中。 美观且可插拔。
运行示例:
在dynamic-spi文件夹中:
mvn clean install
这将构建项目并运行该应用程序。 日志将输出:
SEVERE: Good day Phillip.Goeie dag Phillip.Eeee, what's up Phillip ?
您也可以在Maven之外运行此命令:
java -jar dynamic-spi-engine/target/dynamic-spi-engine-1.0.0-SNAPSHOT-jar-with-dependencies.jar Madonna BugsBunnySEVERE: Eeee, what's up Madonna ?
Java EE上的动态SPI
现在,这是否是个好主意,将进行另外的讨论,只是为了表明它是可能的,我们现在将使用动态SPI在应用程序服务器上加载实现。 这意味着我可以向正在运行的服务器添加新的实现。 因此,我不仅可以在不接触代码或依赖项的情况下添加新的实现,而且还可以启用该新实现而无需重新启动应用程序。
实现看起来完全像Java SE SPI示例, pom.xml
不包含任何实现模块,并且我们现在有了一个新类,可以将模块加载到plugins
文件夹中:
这是一个ApplicationScoped
CDI Bean,可在启动时加载模块。 这些模块也可以通过REST重新加载:
@Path("/pluginloader")@ApplicationScoped@Logpublic class PluginLoader {@Produces @Named("Greetings")private final Map<String,Greeting> loadedGreetings = new HashMap<>();public void init(@Observes @Initialized(ApplicationScoped.class) ServletContext context) {loadPlugins();}@GET@Path("/reload")public Response loadPlugins(){ClassLoader classloader = getClassLoader();ServiceLoader<Greeting> loader = ServiceLoader.load(Greeting.class, classloader);Iterator<Greeting> greetingIterator = loader.iterator();while (greetingIterator.hasNext()) {Greeting greeting = greetingIterator.next();log.log(Level.SEVERE, "Adding provider [{0}]", greeting.getName());if(!loadedGreetings.containsKey(greeting.getName())){loadedGreetings.put(greeting.getName(), greeting);}}return Response.ok("ok").build();}private ClassLoader getClassLoader(){File[] pluginFiles = getPluginFiles();if(pluginFiles!=null){ ArrayList<URL> urls = new ArrayList<>();for(File plugin:pluginFiles){try{URL pluginURL = plugin.toURI().toURL();urls.add(pluginURL);}catch(MalformedURLException m){log.log(Level.SEVERE, "Could not load [{0}], ignoring", plugin.getName());}}return new URLClassLoader(urls.toArray(new URL[]{}),this.getClass().getClassLoader());}return this.getClass().getClassLoader();}private File[] getPluginFiles(){File loc = getPluginDirectory();if(loc==null)return null;File[] pluginFiles = loc.listFiles((File file) -> file.getPath().toLowerCase().endsWith(".jar"));return pluginFiles;}private File getPluginDirectory(){File plugins = new File("plugins");if(plugins.exists())return plugins; return null;}}
所有加载的Greetings
在Map<String,Greeting>
中都可用,可以将其注入工厂 :
@RequestScoped@Logpublic class GreetingFactory {@Inject @Named("Greetings")private Map<String,Greeting> greetings;// ...}
运行示例:
与Java EE CDI和EJB示例类似,它在Wildfly Swarm , Open Liberty和Payara Micro上运行
在javaee-spi文件夹中:
mvn clean install -P wildfly
(或-P自由或-P payara)
在所有3种情况下,maven都会:
- 在部署了应用程序的情况下启动应用程序服务器
- 击中2个REST网址:
- http:// localhost:8080 / javaee-spi-engine / api (此列表列出了所有实现)
- 关闭应用程序服务器(Payara除外)
因此,在日志中,您将看到(类似):
===============================================
["BugsBunny","Afrikaans","English"]
==============================================================================================
["Eeee, what's up Phillip ?","Goeie dag Phillip.","Good day Phillip."]
===============================================
如果运行Payara,则服务器不会关闭,因此您也可以手动测试工厂:
wget -qO- http://localhost:8080/javaee-spi-engine/api/Frans?way=Afrikaans["Goeie dag Frans."]
当前,在plugins
文件夹中,您将看到2个已知的实现(南非荷兰语和Bugs Bunny):
ls javaee-spi-engine/plugins/javaee-spi-impl-afrikaans-1.0.0-SNAPSHOT.jar javaee-spi-impl-bugsbunny-1.0.0-SNAPSHOT.jar
当我们构建这些实现时,将其复制到那里。
现在,让服务器保持运行状态,并添加一种新的问候方式,称为AliG。 (请参阅https://www.youtube.com/watch?v=b00lc92lExw )
cd javaee-spi-impl-aligmvn clean install -P plugin
这会将Ali G实现复制到plugins
文件夹。
现在,让我们再次问候Frans:
wget -qO- http://localhost:8080/javaee-spi-engine/api/Frans?way=AliG["Booyakasha Frans !"]
因此,我们可以向正在运行的服务器添加新的具体实现。
结束
就是这样(现在)。 欢迎任何评论和您自己的示例!
翻译自: https://www.javacodegeeks.com/2017/12/some-factory-examples.html