# 1、Java常用插件實現方案 ## 1.2、serviceloader方式 serviceloader是java提供的spi模式的實現。按照介面開發實現類,而後配置,java通過ServiceLoader來實現統一介面不同實現的依次調用。而java中最經典的serviceloader的使用就是J ...
1、Java常用插件實現方案
1.2、serviceloader方式
serviceloader是java提供的spi模式的實現。按照介面開發實現類,而後配置,java通過ServiceLoader來實現統一介面不同實現的依次調用。而java中最經典的serviceloader的使用就是Java的spi機制。
1.2.1、java spi
SPI全稱 Service Provider Interface ,是JDK內置的一種服務發現機制,SPI是一種動態替換擴展機制,比如有個介面,你想在運行時動態給他添加實現,你只需按照規範給他添加一個實現類即可。比如大家熟悉的jdbc中的Driver介面,不同的廠商可以提供不同的實現,有mysql的,也有oracle的,而Java的SPI機制就可以為某個介面尋找服務的實現。
下麵用一張簡圖說明下SPI機制的原理
1.2.2、java spi 簡單案例
如下工程目錄,在某個應用工程中定義一個插件介面,而其他應用工程為了實現這個介面,只需要引入當前工程的jar包依賴進行實現即可,這裡為了演示我就將不同的實現直接放在同一個工程下;
定義介面
public interface MessagePlugin {
public String sendMsg(Map msgMap);
}
定義兩個不同的實現
public class AliyunMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("aliyun sendMsg");
return "aliyun sendMsg";
}
}
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}
在resources目錄按照規範要求創建文件目錄(META-INF/services),文件名為介面的全限定名,並填寫實現類的全限定類名。
自定義服務載入類
public static void main(String[] args) {
ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
Iterator<MessagePlugin> iterator = serviceLoader.iterator();
Map map = new HashMap();
while (iterator.hasNext()){
MessagePlugin messagePlugin = iterator.next();
messagePlugin.sendMsg(map);
}
}
運行上面的程式後,可以看到下麵的效果,這就是說,使用ServiceLoader的方式可以載入到不同介面的實現,業務中只需要根據自身的需求,結合配置參數的方式就可以靈活的控制具體使用哪一個實現。
1.2、自定義配置約定方式
serviceloader其實是有缺陷的,在使用中必須在META-INF里定義介面名稱的文件,在文件中才能寫上實現類的類名,如果一個項目里插件化的東西比較多,那很可能會出現越來越多配置文件的情況。所以在結合實際項目使用時,可以考慮下麵這種實現思路:
- A應用定義介面;
- B,C,D等其他應用定義服務實現;
- B,C,D應用實現後達成SDK的jar;
- A應用引用SDK或者將SDK放到某個可以讀取到的目錄下;
- A應用讀取並解析SDK中的實現類;
在上文中案例基礎上,我們做如下調整;
1.2.1、添加配置文件
在配置文件中,將具體的實現類配置進去
server:
port: 8888
impl:
name: com.wq.plugins.spi.MessagePlugin
clazz:
- com.wq.plugins.impl.AliyunMsg
- com.wq.plugins.impl.TencentMsg
1.2.2、自定義配置文件載入類
通過這個類,將上述配置文件中的實現類封裝到類對象中,方便後續使用;
package com.wq.propertie;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Arrays;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/1
* @Author wandaren
*/
// 啟動類需要添加@EnableConfigurationProperties({ClassImpl.class})
@ConfigurationProperties("impl")
public class ClassImpl {
private String name;
private String[] clazz;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String[] getClazz() {
return clazz;
}
public void setClazz(String[] clazz) {
this.clazz = clazz;
}
public ClassImpl(String name, String[] clazz) {
this.name = name;
this.clazz = clazz;
}
public ClassImpl() {
}
@Override
public String toString() {
return "ClassImpl{" +
"name='" + name + '\'' +
", clazz=" + Arrays.toString(clazz) +
'}';
}
}
1.2.3、自定義測試介面
使用上述的封裝對象通過類載入的方式動態的在程式中引入
package com.wq.contorller;
import com.wq.plugins.spi.MessagePlugin;
import com.wq.propertie.ClassImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/1
* @Author wandaren
*/
@RestController
public class HelloController {
@Autowired
private ClassImpl classImpl;
@GetMapping("/sendMsg")
public String sendMsg() throws Exception{
for (int i=0;i<classImpl.getClazz().length;i++) {
Class pluginClass= Class.forName(classImpl.getClazz()[i]);
MessagePlugin messagePlugin = (MessagePlugin) pluginClass.newInstance();
messagePlugin.sendMsg(new HashMap());
}
return "success";
}
}
1.2.4、啟動類
package com.wq;
import com.wq.propertie.ClassImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
啟動工程代碼後,調用介面:localhost:8888/sendMsg,在控制臺中可以看到下麵的輸出信息,即通過這種方式也可以實現類似serviceloader的方式,不過在實際使用時,可以結合配置參數進行靈活的控制;
1.3、自定義配置讀取依賴jar的方式
更進一步,在很多場景下,可能我們並不想直接在工程中引入介面實現的依賴包,這時候可以考慮通過讀取指定目錄下的依賴jar的方式,利用反射的方式進行動態載入,這也是生產中一種比較常用的實踐經驗。
具體實踐來說,主要為下麵的步驟:
- 應用A定義服務介面(安裝到maven);
- 應用B,C,D等實現介面(或者在應用內部實現相同的介面),dependency引用服務A;
- 應用B,C,D打成jar,放到應用A約定的讀取目錄下;
- 應用A載入約定目錄下的jar,通過反射載入目標方法;
在上述的基礎上,按照上面的實現思路來實現一下;
- 應用A定義介面
public interface MessagePlugin {
public String sendMsg(Map msgMap);
}
安裝到本地maven倉庫
- 應用B,C實現介面
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-00</artifactId>
<version>1</version>
</dependency>
- 應用B
public class AliyunMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("aliyun sendMsg");
return "aliyun sendMsg";
}
}
- 應用C
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}
- 將應用B、C打成jar
在工程下創建一個lib目錄,並將依賴的jar放進去
1.3.2、新增讀取jar的工具類
添加一個工具類,用於讀取指定目錄下的jar,並通過反射的方式,結合配置文件中的約定配置進行反射方法的執行;
package com.wq.utils;
import com.wq.propertie.ClassImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Component
public class ServiceLoaderUtils {
@Autowired
ClassImpl classImpl;
public static void loadJarsFromAppFolder() throws Exception {
String path = "/Users/wandaren/develop/study/spi-00/lib";
File f = new File(path);
if (f.isDirectory()) {
for (File subf : f.listFiles()) {
if (subf.isFile()) {
loadJarFile(subf);
}
}
} else {
loadJarFile(f);
}
}
public static void loadJarFile(File path) throws Exception {
URL url = path.toURI().toURL();
// 可以獲取到AppClassLoader,可以提到前面,不用每次都獲取一次
URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
// 載入
//Method method = URLClassLoader.class.getDeclaredMethod("sendMsg", Map.class);
Method method = URLClassLoader.class.getMethod("sendMsg", Map.class);
method.setAccessible(true);
method.invoke(classLoader, url);
}
public void main(String[] args) throws Exception{
System.out.println(invokeMethod("hello"));;
}
public String doExecuteMethod() throws Exception{
String path = "/Users/wandaren/develop/study/spi-00/lib";
File f1 = new File(path);
Object result = null;
if (f1.isDirectory()) {
for (File subf : f1.listFiles()) {
//獲取文件名稱
String name = subf.getName();
String fullPath = path + "/" + name;
//執行反射相關的方法
File f = new File(fullPath);
URL urlB = f.toURI().toURL();
URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
.getContextClassLoader());
String[] clazz = classImpl.getClazz();
for(String claName : clazz){
if(name.equals("spi-01-1.jar")){
if(!claName.equals("com.wq.plugins.impl.AliyunMsg")){
continue;
}
Class<?> loadClass = classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//獲取實例
Object obj = loadClass.newInstance();
Map map = new HashMap();
//獲取方法
Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
result = method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}else if(name.equals("spi-02-1.jar")){
if(!claName.equals("com.wq.plugins.impl.TencentMsg")){
continue;
}
Class<?> loadClass = classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//獲取實例
Object obj = loadClass.newInstance();
Map map = new HashMap();
//獲取方法
Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
result = method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}
}
if(Objects.nonNull(result)){
break;
}
}
}
return result.toString();
}
public Object loadMethod(String fullPath) throws Exception{
File f = new File(fullPath);
URL urlB = f.toURI().toURL();
URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
.getContextClassLoader());
Object result = null;
String[] clazz = classImpl.getClazz();
for(String claName : clazz){
Class<?> loadClass = classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//獲取實例
Object obj = loadClass.newInstance();
Map map = new HashMap();
//獲取方法
Method method=loadClass.getDeclaredMethod("sendMsg",Map.class);
result = method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}
return result;
}
public static String invokeMethod(String text) throws Exception{
String path = "/Users/wandaren/develop/study/spi-00/lib/spi-01-1.jar";
File f = new File(path);
URL urlB = f.toURI().toURL();
URLClassLoader classLoaderA = new URLClassLoader(new URL[]{urlB}, Thread.currentThread()
.getContextClassLoader());
Class<?> product = classLoaderA.loadClass("com.wq.plugins.impl.AliyunMsg");
//獲取實例
Object obj = product.newInstance();
Map map = new HashMap();
//獲取方法
Method method=product.getDeclaredMethod("sendMsg",Map.class);
//執行方法
Object result1 = method.invoke(obj,map);
// TODO According to the requirements , write the implementation code.
return result1.toString();
}
public static String getApplicationFolder() {
String path = ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();
return new File(path).getParent();
}
}
1.3.3、添加測試介面
@Autowired
private ServiceLoaderUtils serviceLoaderUtils;
@GetMapping("/sendMsgV2")
public String index() throws Exception {
String result = serviceLoaderUtils.doExecuteMethod();
return result;
}
以上全部完成之後,啟動工程,測試一下該介面,仍然可以得到預期結果;
在上述的實現中還比較粗糙的,實際運用時,還需要做較多的優化改進以滿足實際的業務需要,比如介面傳入類型參數用於控制具體使用哪個依賴包的方法進行執行等;
2、SpringBoot中的插件化實現
在大家使用較多的springboot框架中,其實框架自身提供了非常多的擴展點,其中最適合做插件擴展的莫過於spring.factories的實現;
2.1、 Spring Boot中的SPI機制
在Spring中也有一種類似與Java SPI的載入機制。它在META-INF/spring.factories文件中配置介面的實現類名稱,然後在程式中讀取這些配置文件並實例化,這種自定義的SPI機制是Spring Boot Starter實現的基礎。
2.2、 Spring Factories實現原理
spring-core包里定義了SpringFactoriesLoader類,這個類實現了檢索META-INF/spring.factories文件,並獲取指定介面的配置的功能。在這個類中定義了兩個對外的方法:
- loadFactories 根據介面類獲取其實現類的實例,這個方法返回的是對象列表;
- loadFactoryNames 根據介面獲取其介面類的名稱,這個方法返回的是類名的列表;
上面的兩個方法的關鍵都是從指定的ClassLoader中獲取spring.factories文件,並解析得到類名列表,具體代碼如下:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
從代碼中我們可以知道,在這個方法中會遍歷整個ClassLoader中所有jar包下的spring.factories文件,就是說我們可以在自己的jar中配置spring.factories文件,不會影響到其它地方的配置,也不會被別人的配置覆蓋。
spring.factories的是通過Properties解析得到的,所以我們在寫文件中的內容都是安裝下麵這種方式配置的:
com.xxx.interface=com.xxx.classname
如果一個介面希望配置多個實現類,可以使用’,’進行分割
2.3、Spring Factories案例實現
接下來看一個具體的案例實現來體驗下Spring Factories的使用;
2.3.1、定義一個服務介面
自定義一個介面,裡面添加一個方法;
public interface SmsPlugin {
public void sendMessage(String message);
}
2.3.2、 定義2個服務實現
實現類1
package com.wq.plugin.impl;
import com.wq.plugin.SmsPlugin;
public class BizSmsImpl implements SmsPlugin {
@Override
public void sendMessage(String message) {
System.out.println("this is BizSmsImpl sendMessage..." + message);
}
}
實現類2
package com.wq.plugin.impl;
import com.wq.plugin.SmsPlugin;
public class SystemSmsImpl implements SmsPlugin {
@Override
public void sendMessage(String message) {
System.out.println("this is SystemSmsImpl sendMessage..." + message);
}
}
2.3.3、 添加spring.factories文件
在resources目錄下,創建一個名叫:META-INF的目錄,然後在該目錄下定義一個spring.factories的配置文件,內容如下,其實就是配置了服務介面,以及兩個實現類的全類名的路徑;
com.wq.plugin.SmsPlugin=\
com.wq.plugin.impl.BizSmsImpl,\
com.wq.plugin.impl.SystemSmsImpl
2.3.4、 添加自定義介面
添加一個自定義的介面,有沒有發現,這裡和java 的spi有點類似,只不過是這裡換成了SpringFactoriesLoader去載入服務;
package com.wq.controller;
import com.wq.plugin.SmsPlugin;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/2
* @Author wandaren
*/
@RestController
public class SmsController {
@GetMapping("/sendMsgV3")
public String sendMsgV3(String msg) throws Exception{
List<SmsPlugin> smsServices= SpringFactoriesLoader.loadFactories(SmsPlugin.class, null);
for(SmsPlugin smsService : smsServices){
smsService.sendMessage(msg);
}
return "success";
}
}
啟動工程之後,調用一下該介面進行測試,localhost:8080/sendMsgV3?msg=hello,通過控制台,可以看到,這種方式能夠正確獲取到系統中可用的服務實現;
利用spring的這種機制,可以很好的對系統中的某些業務邏輯通過插件化介面的方式進行擴展實現;
3、插件化機制案例實戰
結合上面掌握的理論知識,下麵基於Java SPI機制進行一個接近真實使用場景的完整的操作步驟;
3.1、 案例背景
- 3個微服務模塊,在A模塊中有個插件化的介面;
- 在A模塊中的某個介面,需要調用插件化的服務實現進行簡訊發送;
- 可以通過配置文件配置參數指定具體的哪一種方式發送簡訊;
- 如果沒有載入到任何插件,將走A模塊在預設的發簡訊實現;
3.1.1、 模塊結構
1、spi-00,插件化介面工程;
2、spi-01,aliyun簡訊發送實現;
3、spi-02,tncent簡訊發送實現;
3.1.2、 整體實現思路
本案例完整的實現思路參考如下:
- spi-00定義服務介面,並提供出去jar被其他實現工程依賴;
- spi-01與spi-02依賴spi-00的jar並實現SPI中的方法;
- spi-01與spi-02按照API規範實現完成後,打成jar包,或者安裝到倉庫中;
- spi-00在pom中依賴spi-01與的jar,spi-02或者通過啟動載入的方式即可得到具體某個實現;
3.2、spi-00添加服務介面
3.2.1、 添加服務介面
public interface MessagePlugin {
public String sendMsg(Map msgMap);
}
3.2.2、 打成jar包並安裝到倉庫
idea執行install
3.3、spi-01與spi-02實現
maven引入spi-00依賴坐標
<dependencies>
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-00</artifactId>
<version>1</version>
</dependency>
</dependencies>
3.3.1、spi-01
public class AliyunMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("aliyun sendMsg");
return "aliyun sendMsg";
}
}
3.3.2、spi-02
public class TencentMsg implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
System.out.println("tencent sendMsg");
return "tencent sendMsg";
}
}
3.3.3、將spi-01與spi-02打成jar
idea執行install
3.4、spi-00添加服務依賴與實現
3.4.1、添加服務依賴
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<spring-boot.version>2.7.4</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-01</artifactId>
<version>1</version>
</dependency>
<dependency>
<groupId>com.wq</groupId>
<artifactId>spi-02</artifactId>
<version>1</version>
</dependency>
</dependencies>
3.4.2、自定義服務載入工具類
package com.wq.spi;
import java.util.*;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/2
* @Author wandaren
*/
public class PluginFactory {
public void installPlugin(){
Map context = new LinkedHashMap();
context.put("_userId","");
context.put("_version","1.0");
context.put("_type","sms");
ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
Iterator<MessagePlugin> iterator = serviceLoader.iterator();
while (iterator.hasNext()){
MessagePlugin messagePlugin = iterator.next();
messagePlugin.sendMsg(context);
}
}
public static MessagePlugin getTargetPlugin(String type){
ServiceLoader<MessagePlugin> serviceLoader = ServiceLoader.load(MessagePlugin.class);
Iterator<MessagePlugin> iterator = serviceLoader.iterator();
List<MessagePlugin> messagePlugins = new ArrayList<>();
while (iterator.hasNext()){
MessagePlugin messagePlugin = iterator.next();
messagePlugins.add(messagePlugin);
}
MessagePlugin targetPlugin = null;
for (MessagePlugin messagePlugin : messagePlugins) {
boolean findTarget = false;
switch (type) {
case "aliyun":
if (messagePlugin instanceof AliyunMsg){
targetPlugin = messagePlugin;
findTarget = true;
break;
}
case "tencent":
if (messagePlugin instanceof TencentMsg){
targetPlugin = messagePlugin;
findTarget = true;
break;
}
default: break;
}
if(findTarget) {
break;
}
}
return targetPlugin;
}
public static void main(String[] args) {
new PluginFactory().installPlugin();
}
}
3.4.3、介面實現
package com.wq.service;
import com.wq.spi.MessagePlugin;
import com.wq.spi.PluginFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Service
public class SmsService {
@Value("${msg.type}")
private String msgType;
@Autowired
private DefaultSmsService defaultSmsService;
public String sendMsg(String msg) {
MessagePlugin messagePlugin = PluginFactory.getTargetPlugin(msgType);
Map paramMap = new HashMap();
if(Objects.nonNull(messagePlugin)){
return messagePlugin.sendMsg(paramMap);
}
return defaultSmsService.sendMsg(paramMap);
}
}
package com.wq.service;
import com.wq.spi.MessagePlugin;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* @Description TODO
* @Version 1.0.0
* @Date 2023/7/2
* @Author wandaren
*/
@Service
public class DefaultSmsService implements MessagePlugin {
@Override
public String sendMsg(Map msgMap) {
return "DefaultSmsService--------";
}
}
3.4.4、測試controller
package com.wq.controller;
import com.wq.service.SmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SmsController {
@Autowired
private SmsService smsService;
@GetMapping("/sendMsg")
public String sendMessage(String msg){
return smsService.sendMsg(msg);
}
}
3.4.5、測試
通過修改配置application.yml中msg.type的值切換不同實現
msg:
# type: tencent
type: aliyun