WildFly 27中Singleton EJB调用外部REST资源实现方案咨询
解决方案:在WildFly 27的Singleton EJB中安全调用外部API
方案一:使用Jakarta EE JAX-RS客户端(推荐)
直接用URLConnection的风险在于缺乏容器管理的连接池、超时控制和资源自动释放,而JAX-RS是Jakarta EE标准组件,WildFly原生支持,容器会统一管理资源,是最简洁的实现方式。
修改后的EJB代码示例
@Startup @Singleton public class QuotesJob { private static final Logger logger = LogManager.getLogger(QuotesJob.class); private static Map<String, QuotesListener> listeners = new ConcurrentHashMap<>(); private Client jaxRsClient; private Map<String, Float> quote = new HashMap<>(); @PostConstruct public void init() { quote.put("EURUSD", 0.00000f); quote.put("USDCAD", 0.00000f); quote.put("NZDUSD", 0.00000f); quote.put("AUDUSD", 0.00000f); // 初始化JAX-RS客户端,配置超时 jaxRsClient = ClientBuilder.newClient(); jaxRsClient.property(ClientProperties.CONNECT_TIMEOUT, 5000); jaxRsClient.property(ClientProperties.READ_TIMEOUT, 5000); } @Schedule(hour="*", minute="*", second="*/6", persistent=false) public void perform() { logger.info("quotes update..."); try { // 调用外部API,替换为真实接口地址 WebTarget target = jaxRsClient.target("https://api.example.com/quotes") .queryParam("pairs", "EURUSD,USDCAD,NZDUSD,AUDUSD"); QuotesResponse response = target.request(MediaType.APPLICATION_JSON).get(QuotesResponse.class); // 更新报价数据 response.getQuotes().forEach((k, v) -> quote.put(k, v.floatValue())); } catch (ProcessingException | WebApplicationException e) { logger.error("获取外部报价失败", e); // 可选:保留上次数据或设置默认值 } notifyListeners(); } // 原有notifyListeners、addQuotesListener、endJob方法不变 @PreDestroy public void endJob() { listeners.clear(); if (jaxRsClient != null) { jaxRsClient.close(); } } // 定义API响应对应的实体类 private static class QuotesResponse { private Map<String, Double> quotes; public Map<String, Double> getQuotes() { return quotes; } public void setQuotes(Map<String, Double> quotes) { this.quotes = quotes; } } }
说明
- JAX-RS客户端底层由WildFly托管,自带连接池,避免手动管理
URLConnection的资源泄漏问题 - 配置超时参数防止EJB线程被长时间阻塞
- 异常捕获确保定时任务不会因API调用失败而终止
方案二:使用WildFly HTTP资源适配器(复杂场景)
如果需要更细粒度的连接池配置、事务集成,可使用WildFly自带的HTTP资源适配器,适合高并发或严格资源管控的场景。
步骤1:配置WildFly资源适配器
在standalone.xml或domain.xml中添加以下配置:
<subsystem xmlns="urn:jboss:domain:resource-adapters:7.0"> <resource-adapters> <resource-adapter id="http-ra"> <archive>http-ra.rar</archive> <transaction-support>NoTransaction</transaction-support> <config-property name="ConnectionTimeout">5000</config-property> <config-property name="ReadTimeout">5000</config-property> <connection-definitions> <connection-definition class-name="org.jboss.resource.adapter.http.HttpManagedConnectionFactory" jndi-name="java:/HttpConnectionFactory" pool-name="HttpConnectionPool"> <pool> <min-pool-size>5</min-pool-size> <max-pool-size>20</max-pool-size> </pool> </connection-definition> </connection-definitions> </resource-adapter> </resource-adapters> </subsystem>
步骤2:在EJB中注入连接工厂调用API
@Startup @Singleton public class QuotesJob { @Resource(lookup = "java:/HttpConnectionFactory") private HttpConnectionFactory connectionFactory; // 原有成员变量不变 @Schedule(hour="*", minute="*", second="*/6", persistent=false) public void perform() { logger.info("quotes update..."); HttpConnection connection = null; try { connection = connectionFactory.getConnection(); URL url = new URL("https://api.example.com/quotes?pairs=EURUSD,USDCAD,NZDUSD,AUDUSD"); HttpURLConnection httpConn = connection.openConnection(url); httpConn.setRequestMethod("GET"); // 读取并解析响应 try (BufferedReader reader = new BufferedReader(new InputStreamReader(httpConn.getInputStream()))) { StringBuilder response = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { response.append(line); } // 使用Jackson解析JSON,需添加Jackson依赖 ObjectMapper mapper = new ObjectMapper(); Map<String, Double> apiQuotes = mapper.readValue(response.toString(), new TypeReference<Map<String, Double>>() {}); apiQuotes.forEach((k, v) -> quote.put(k, v.floatValue())); } } catch (Exception e) { logger.error("通过HTTP资源适配器获取报价失败", e); } finally { if (connection != null) { try { connection.close(); } catch (Exception e) { logger.warn("关闭HTTP连接失败", e); } } } notifyListeners(); } // 原有方法不变 }
方案三:CDI Bean封装HTTP调用(解耦设计)
将HTTP调用逻辑封装为独立CDI Bean,EJB依赖注入该Bean,便于后续替换实现或单元测试。
步骤1:创建CDI客户端Bean
@ApplicationScoped public class QuoteApiClient { private Client jaxRsClient; @PostConstruct public void init() { jaxRsClient = ClientBuilder.newClient(); jaxRsClient.property(ClientProperties.CONNECT_TIMEOUT, 5000); jaxRsClient.property(ClientProperties.READ_TIMEOUT, 5000); } public Map<String, Float> fetchQuotes(List<String> pairs) { try { WebTarget target = jaxRsClient.target("https://api.example.com/quotes") .queryParam("pairs", String.join(",", pairs)); QuotesResponse response = target.request(MediaType.APPLICATION_JSON).get(QuotesResponse.class); return response.getQuotes().entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().floatValue())); } catch (Exception e) { throw new RuntimeException("获取报价失败", e); } } @PreDestroy public void destroy() { jaxRsClient.close(); } private static class QuotesResponse { private Map<String, Double> quotes; // getter、setter方法 } }
步骤2:在EJB中注入使用
@Startup @Singleton public class QuotesJob { @Inject private QuoteApiClient quoteApiClient; // 原有成员变量不变 @Schedule(hour="*", minute="*", second="*/6", persistent=false) public void perform() { logger.info("quotes update..."); try { List<String> pairs = new ArrayList<>(quote.keySet()); Map<String, Float> newQuotes = quoteApiClient.fetchQuotes(pairs); quote.putAll(newQuotes); } catch (RuntimeException e) { logger.error("获取报价失败", e); } notifyListeners(); } // 原有方法不变 }
关键注意事项
- 避免阻塞EJB线程:外部API调用可能耗时,可使用
@Asynchronous注解或注入ExecutorService实现异步调用,防止定时任务线程被占用 - 异常处理:必须捕获所有API调用异常,确保定时任务持续执行
- 资源释放:所有外部连接需在
@PreDestroy或finally块中关闭,避免资源泄漏 - 配置解耦:API地址、超时等参数建议放在
microprofile-config.properties中,通过@ConfigProperty注入,避免硬编码
内容的提问来源于stack exchange,提问作者ranxero
相关产品推荐
相关产品推荐

