0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot插件化开发模式

jf_ro2CN3Fa 来源:CSDN 2023-06-21 09:45 次阅读

一、前言

插件化开发模式正在很多编程语言或技术框架中得以广泛的应用实践,比如大家熟悉的jenkins,docker可视化管理平台rancher,以及日常编码使用的编辑器idea,vscode等,随处可见的带有热插拔功能的插件,让系统像插了翅膀一样,大大提升了系统的扩展性和伸缩性,也拓展了系统整体的使用价值,那么为什么要使用插件呢?

1.1 使用插件的好处

1.1.1 模块解耦

实现服务模块之间解耦的方式有很多,但是插件来说,其解耦的程度似乎更高,而且更灵活,可定制化、个性化更好。

举例来说,代码中可以使用设计模式来选择使用哪种方式发送短信给下单完成的客户,问题是各个短信服务商并不一定能保证在任何情况下都能发送成功,怎么办呢?这时候设计模式也没法帮你解决这个问题,如果使用定制化插件的方式,结合外部配置参数,假设系统中某种短信发送不出去了,这时候就可以利用插件动态植入,切换为不同的厂商发短信了。

1.1.2 提升扩展性和开放性

以spring来说,之所以具备如此广泛的生态,与其自身内置的各种可扩展的插件机制是分不开的,试想为什么使用了spring框架之后可以很方便的对接其他中间件,那就是spring框架提供了很多基于插件化的扩展点。

插件化机制让系统的扩展性得以提升,从而可以丰富系统的周边应用生态。

1.1.3 方便第三方接入

有了插件之后,第三方应用或系统如果要对接自身的系统,直接基于系统预留的插件接口完成一套适合自己业务的实现即可,而且对自身系统的侵入性很小,甚至可以实现基于配置参数的热加载,方便灵活,开箱即用。

1.2 插件化常用实现思路

java为例,这里结合实际经验,整理一些常用的插件化实现思路:

spi机制;

约定配置和目录,利用反射配合实现;

springboot中的Factories机制;

java agent(探针)技术;

spring内置扩展点;

第三方插件包,例如:spring-plugin-core;

spring aop技术;

二、Java常用插件实现方案

2.1 serviceloader方式

serviceloader是java提供的spi模式的实现。按照接口开发实现类,而后配置,java通过ServiceLoader来实现统一接口不同实现的依次调用。而java中最经典的serviceloader的使用就是Java的spi机制。

2.1.1 java spi

SPI全称 Service Provider Interface ,是JDK内置的一种服务发现机制,SPI是一种动态替换扩展机制,比如有个接口,你想在运行时动态给他添加实现,你只需按照规范给他添加一个实现类即可。比如大家熟悉的jdbc中的Driver接口,不同的厂商可以提供不同的实现,有mysql的,也有oracle的,而Java的SPI机制就可以为某个接口寻找服务的实现。

下面用一张简图说明下SPI机制的原理

eee64efe-0fd3-11ee-962d-dac502259ad0.png

2.1.2 java spi 简单案例

如下工程目录,在某个应用工程中定义一个插件接口,而其他应用工程为了实现这个接口,只需要引入当前工程的jar包依赖进行实现即可,这里为了演示我就将不同的实现直接放在同一个工程下;

eef438ac-0fd3-11ee-962d-dac502259ad0.png

定义接口

publicinterfaceMessagePlugin{

publicStringsendMsg(MapmsgMap);

}

定义两个不同的实现

publicclassAliyunMsgimplementsMessagePlugin{

@Override
publicStringsendMsg(MapmsgMap){
System.out.println("aliyunsendMsg");
return"aliyunsendMsg";
}
}
publicclassTencentMsgimplementsMessagePlugin{

@Override
publicStringsendMsg(MapmsgMap){
System.out.println("tencentsendMsg");
return"tencentsendMsg";
}
}

在resources目录按照规范要求创建文件目录,并填写实现类的全类名

ef053544-0fd3-11ee-962d-dac502259ad0.png

自定义服务加载类

publicstaticvoidmain(String[]args){
ServiceLoaderserviceLoader=ServiceLoader.load(MessagePlugin.class);
Iteratoriterator=serviceLoader.iterator();
Mapmap=newHashMap();
while(iterator.hasNext()){
MessagePluginmessagePlugin=iterator.next();
messagePlugin.sendMsg(map);
}
}

运行上面的程序后,可以看到下面的效果,这就是说,使用ServiceLoader的方式可以加载到不同接口的实现,业务中只需要根据自身的需求,结合配置参数的方式就可以灵活的控制具体使用哪一个实现。

ef2c9e2c-0fd3-11ee-962d-dac502259ad0.png

2.2 自定义配置约定方式

serviceloader其实是有缺陷的,在使用中必须在META-INF里定义接口名称的文件,在文件中才能写上实现类的类名,如果一个项目里插件化的东西比较多,那很可能会出现越来越多配置文件的情况。所以在结合实际项目使用时,可以考虑下面这种实现思路:

A应用定义接口;

B,C,D等其他应用定义服务实现;

B,C,D应用实现后达成SDK的jar;

A应用引用SDK或者将SDK放到某个可以读取到的目录下;

A应用读取并解析SDK中的实现类;

在上文中案例基础上,我们做如下调整;

2.2.1 添加配置文件

在配置文件中,将具体的实现类配置进去

server:
port:8081
impl:
name:com.congge.plugins.spi.MessagePlugin
clazz:
-com.congge.plugins.impl.TencentMsg
-com.congge.plugins.impl.AliyunMsg

2.2.2 自定义配置文件加载类

通过这个类,将上述配置文件中的实现类封装到类对象中,方便后续使用;

importlombok.Getter;
importlombok.Setter;
importlombok.ToString;
importorg.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("impl")
@ToString
publicclassClassImpl{
@Getter
@Setter
Stringname;

@Getter
@Setter
String[]clazz;
}

2.2.3 自定义测试接口

使用上述的封装对象通过类加载的方式动态的在程序中引入

importcom.congge.config.ClassImpl;
importcom.congge.plugins.spi.MessagePlugin;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.web.bind.annotation.GetMapping;
importorg.springframework.web.bind.annotation.RestController;

importjava.util.HashMap;

@RestController
publicclassSendMsgController{

@Autowired
ClassImplclassImpl;

//localhost:8081/sendMsg
@GetMapping("/sendMsg")
publicStringsendMsg()throwsException{
for(inti=0;i

2.2.4 启动类

@EnableConfigurationProperties({ClassImpl.class})
@SpringBootApplication
publicclassPluginApp{

publicstaticvoidmain(String[]args){
SpringApplication.run(PluginApp.class,args);
}

}

启动工程代码后,调用接口:localhost:8081/sendMsg,在控制台中可以看到下面的输出信息,即通过这种方式也可以实现类似serviceloader的方式,不过在实际使用时,可以结合配置参数进行灵活的控制;

ef3e21a6-0fd3-11ee-962d-dac502259ad0.png

2.3 自定义配置读取依赖jar的方式

更进一步,在很多场景下,可能我们并不想直接在工程中引入接口实现的依赖包,这时候可以考虑通过读取指定目录下的依赖jar的方式,利用反射的方式进行动态加载,这也是生产中一种比较常用的实践经验。

具体实践来说,主要为下面的步骤:

应用A定义服务接口;

应用B,C,D等实现接口(或者在应用内部实现相同的接口);

应用B,C,D打成jar,放到应用A约定的读取目录下;

应用A加载约定目录下的jar,通过反射加载目标方法;

在上述的基础上,按照上面的实现思路来实现一下;

2.3.1 创建约定目录

在当前工程下创建一个lib目录,并将依赖的jar放进去

ef5405f2-0fd3-11ee-962d-dac502259ad0.png

2.3.2 新增读取jar的工具类

添加一个工具类,用于读取指定目录下的jar,并通过反射的方式,结合配置文件中的约定配置进行反射方法的执行;

@Component
publicclassServiceLoaderUtils{

@Autowired
ClassImplclassImpl;


publicstaticvoidloadJarsFromAppFolder()throwsException{
Stringpath="E:\code-self\bitzpp\lib";
Filef=newFile(path);
if(f.isDirectory()){
for(Filesubf:f.listFiles()){
if(subf.isFile()){
loadJarFile(subf);
}
}
}else{
loadJarFile(f);
}
}

publicstaticvoidloadJarFile(Filepath)throwsException{
URLurl=path.toURI().toURL();
//可以获取到AppClassLoader,可以提到前面,不用每次都获取一次
URLClassLoaderclassLoader=(URLClassLoader)ClassLoader.getSystemClassLoader();
//加载
//Methodmethod=URLClassLoader.class.getDeclaredMethod("sendMsg",Map.class);
Methodmethod=URLClassLoader.class.getMethod("sendMsg",Map.class);

method.setAccessible(true);
method.invoke(classLoader,url);
}

publicvoidmain(String[]args)throwsException{
System.out.println(invokeMethod("hello"));;
}

publicStringdoExecuteMethod()throwsException{
Stringpath="E:\code-self\bitzpp\lib";
Filef1=newFile(path);
Objectresult=null;
if(f1.isDirectory()){
for(Filesubf:f1.listFiles()){
//获取文件名称
Stringname=subf.getName();
StringfullPath=path+""+name;
//执行反射相关的方法
//ServiceLoaderUtilsserviceLoaderUtils=newServiceLoaderUtils();
//result=serviceLoaderUtils.loadMethod(fullPath);
Filef=newFile(fullPath);
URLurlB=f.toURI().toURL();
URLClassLoaderclassLoaderA=newURLClassLoader(newURL[]{urlB},Thread.currentThread()
.getContextClassLoader());
String[]clazz=classImpl.getClazz();
for(StringclaName:clazz){
if(name.equals("biz-pt-1.0-SNAPSHOT.jar")){
if(!claName.equals("com.congge.spi.BitptImpl")){
continue;
}
ClassloadClass=classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//获取实例
Objectobj=loadClass.newInstance();
Mapmap=newHashMap();
//获取方法
Methodmethod=loadClass.getDeclaredMethod("sendMsg",Map.class);
result=method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}elseif(name.equals("miz-pt-1.0-SNAPSHOT.jar")){
if(!claName.equals("com.congge.spi.MizptImpl")){
continue;
}
ClassloadClass=classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//获取实例
Objectobj=loadClass.newInstance();
Mapmap=newHashMap();
//获取方法
Methodmethod=loadClass.getDeclaredMethod("sendMsg",Map.class);
result=method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}
}
if(Objects.nonNull(result)){
break;
}
}
}
returnresult.toString();
}

publicObjectloadMethod(StringfullPath)throwsException{
Filef=newFile(fullPath);
URLurlB=f.toURI().toURL();
URLClassLoaderclassLoaderA=newURLClassLoader(newURL[]{urlB},Thread.currentThread()
.getContextClassLoader());
Objectresult=null;
String[]clazz=classImpl.getClazz();
for(StringclaName:clazz){
ClassloadClass=classLoaderA.loadClass(claName);
if(Objects.isNull(loadClass)){
continue;
}
//获取实例
Objectobj=loadClass.newInstance();
Mapmap=newHashMap();
//获取方法
Methodmethod=loadClass.getDeclaredMethod("sendMsg",Map.class);
result=method.invoke(obj,map);
if(Objects.nonNull(result)){
break;
}
}
returnresult;
}


publicstaticStringinvokeMethod(Stringtext)throwsException{
Stringpath="E:\code-self\bitzpp\lib\miz-pt-1.0-SNAPSHOT.jar";
Filef=newFile(path);
URLurlB=f.toURI().toURL();
URLClassLoaderclassLoaderA=newURLClassLoader(newURL[]{urlB},Thread.currentThread()
.getContextClassLoader());
Classproduct=classLoaderA.loadClass("com.congge.spi.MizptImpl");
//获取实例
Objectobj=product.newInstance();
Mapmap=newHashMap();
//获取方法
Methodmethod=product.getDeclaredMethod("sendMsg",Map.class);
//执行方法
Objectresult1=method.invoke(obj,map);
//TODOAccordingtotherequirements,writetheimplementationcode.
returnresult1.toString();
}

publicstaticStringgetApplicationFolder(){
Stringpath=ServiceLoaderUtils.class.getProtectionDomain().getCodeSource().getLocation().getPath();
returnnewFile(path).getParent();
}



}

2.3.3 添加测试接口

添加如下测试接口

@GetMapping("/sendMsgV2")
publicStringindex()throwsException{
Stringresult=serviceLoaderUtils.doExecuteMethod();
returnresult;
}

以上全部完成之后,启动工程,测试一下该接口,仍然可以得到预期结果;

ef688cf2-0fd3-11ee-962d-dac502259ad0.png

在上述的实现中还是比较粗糙的,实际运用时,还需要做较多的优化改进以满足实际的业务需要,比如接口传入类型参数用于控制具体使用哪个依赖包的方法进行执行等;

三、SpringBoot中的插件化实现

在大家使用较多的springboot框架中,其实框架自身提供了非常多的扩展点,其中最适合做插件扩展的莫过于spring.factories的实现;

3.1 Spring Boot中的SPI机制

在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化,这种自定义的SPI机制是Spring Boot Starter实现的基础。

3.2 Spring Factories实现原理

spring-core包里定义了SpringFactoriesLoader类,这个类实现了检索META-INF/spring.factories文件,并获取指定接口的配置的功能。在这个类中定义了两个对外的方法:

loadFactories 根据接口类获取其实现类的实例,这个方法返回的是对象列表;

loadFactoryNames 根据接口获取其接口类的名称,这个方法返回的是类名的列表;

上面的两个方法的关键都是从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表,具体代码如下:

publicstaticListloadFactoryNames(ClassfactoryClass,ClassLoaderclassLoader){
StringfactoryClassName=factoryClass.getName();
try{
Enumerationurls=(classLoader!=null?classLoader.getResources(FACTORIES_RESOURCE_LOCATION):
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
Listresult=newArrayList();
while(urls.hasMoreElements()){
URLurl=urls.nextElement();
Propertiesproperties=PropertiesLoaderUtils.loadProperties(newUrlResource(url));
StringfactoryClassNames=properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
returnresult;
}
catch(IOExceptionex){
thrownewIllegalArgumentException("Unabletoload["+factoryClass.getName()+
"]factoriesfromlocation["+FACTORIES_RESOURCE_LOCATION+"]",ex);
}
}

从代码中我们可以知道,在这个方法中会遍历整个ClassLoader中所有jar包下的spring.factories文件,就是说我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。

spring.factories的是通过Properties解析得到的,所以我们在写文件中的内容都是安装下面这种方式配置的:

com.xxx.interface=com.xxx.classname

如果一个接口希望配置多个实现类,可以使用’,’进行分割

3.3 Spring Factories案例实现

接下来看一个具体的案例实现来体验下Spring Factories的使用;

3.3.1 定义一个服务接口

自定义一个接口,里面添加一个方法;

publicinterfaceSmsPlugin{

publicvoidsendMessage(Stringmessage);

}

3.3.2 定义2个服务实现

实现类1

publicclassBizSmsImplimplementsSmsPlugin{

@Override
publicvoidsendMessage(Stringmessage){
System.out.println("thisisBizSmsImplsendMessage..."+message);
}
}

实现类2

publicclassSystemSmsImplimplementsSmsPlugin{

@Override
publicvoidsendMessage(Stringmessage){
System.out.println("thisisSystemSmsImplsendMessage..."+message);
}
}

3.3.3 添加spring.factories文件

在resources目录下,创建一个名叫:META-INF的目录,然后在该目录下定义一个spring.factories的配置文件,内容如下,其实就是配置了服务接口,以及两个实现类的全类名的路径;

com.congge.plugin.spi.SmsPlugin=
com.congge.plugin.impl.SystemSmsImpl,
com.congge.plugin.impl.BizSmsImpl

3.3.4 添加自定义接口

添加一个自定义的接口,有没有发现,这里和java 的spi有点类似,只不过是这里换成了SpringFactoriesLoader去加载服务;

@GetMapping("/sendMsgV3")
publicStringsendMsgV3(Stringmsg)throwsException{
ListsmsServices=SpringFactoriesLoader.loadFactories(SmsPlugin.class,null);
for(SmsPluginsmsService:smsServices){
smsService.sendMessage(msg);
}
return"success";
}

启动工程之后,调用一下该接口进行测试,localhost:8087/sendMsgV3?msg=hello,通过控制台,可以看到,这种方式能够正确获取到系统中可用的服务实现;

ef7a6242-0fd3-11ee-962d-dac502259ad0.png

利用spring的这种机制,可以很好的对系统中的某些业务逻辑通过插件化接口的方式进行扩展实现;

四、插件化机制案例实战

结合上面掌握的理论知识,下面基于Java SPI机制进行一个接近真实使用场景的完整的操作步骤;

4.1 案例背景

3个微服务模块,在A模块中有个插件化的接口;

在A模块中的某个接口,需要调用插件化的服务实现进行短信发送;

可以通过配置文件配置参数指定具体的哪一种方式发送短信;

如果没有加载到任何插件,将走A模块在默认的发短信实现;

4.1.1 模块结构

1、biz-pp,插件化接口工程;

2、bitpt,aliyun短信发送实现;

3、miz-pt,tencent短信发送实现;

4.1.2 整体实现思路

本案例完整的实现思路参考如下:

biz-pp定义服务接口,并提供出去jar被其他实现工程依赖;

bitpt与miz-pt依赖biz-pp的jar并实现SPI中的方法;

bitpt与miz-pt按照API规范实现完成后,打成jar包,或者安装到仓库中;

biz-pp在pom中依赖bitpt与miz-pt的jar,或者通过启动加载的方式即可得到具体某个实现;

4.2 biz-pp 关键代码实现过程

4.2.1 添加服务接口

publicinterfaceMessagePlugin{

publicStringsendMsg(MapmsgMap);

}

4.2.2 打成jar包并安装到仓库

这一步比较简单就不展开了

4.2.3 自定义服务加载工具类

这个类,可以理解为在真实的业务编码中,可以根据业务定义的规则,具体加载哪个插件的实现类进行发送短信的操作;

importcom.congge.plugin.spi.MessagePlugin;
importcom.congge.spi.BitptImpl;
importcom.congge.spi.MizptImpl;

importjava.util.*;

publicclassPluginFactory{

publicvoidinstallPlugin(){
Mapcontext=newLinkedHashMap();
context.put("_userId","");
context.put("_version","1.0");
context.put("_type","sms");
ServiceLoaderserviceLoader=ServiceLoader.load(MessagePlugin.class);
Iteratoriterator=serviceLoader.iterator();
while(iterator.hasNext()){
MessagePluginmessagePlugin=iterator.next();
messagePlugin.sendMsg(context);
}
}

publicstaticMessagePlugingetTargetPlugin(Stringtype){
ServiceLoaderserviceLoader=ServiceLoader.load(MessagePlugin.class);
Iteratoriterator=serviceLoader.iterator();
ListmessagePlugins=newArrayList<>();
while(iterator.hasNext()){
MessagePluginmessagePlugin=iterator.next();
messagePlugins.add(messagePlugin);
}
MessagePlugintargetPlugin=null;
for(MessagePluginmessagePlugin:messagePlugins){
booleanfindTarget=false;
switch(type){
case"aliyun":
if(messagePlugininstanceofBitptImpl){
targetPlugin=messagePlugin;
findTarget=true;
break;
}
case"tencent":
if(messagePlugininstanceofMizptImpl){
targetPlugin=messagePlugin;
findTarget=true;
break;
}
}
if(findTarget)break;
}
returntargetPlugin;
}

publicstaticvoidmain(String[]args){
newPluginFactory().installPlugin();
}


}

4.2.4 自定义接口

@RestController
publicclassSmsController{

@Autowired
privateSmsServicesmsService;

@Autowired
privateServiceLoaderUtilsserviceLoaderUtils;

//localhost:8087/sendMsg?msg=sendMsg
@GetMapping("/sendMsg")
publicStringsendMessage(Stringmsg){
returnsmsService.sendMsg(msg);
}

}

4.2.5 接口实现

@Service
publicclassSmsService{

@Value("${msg.type}")
privateStringmsgType;

@Autowired
privateDefaultSmsServicedefaultSmsService;

publicStringsendMsg(Stringmsg){
MessagePluginmessagePlugin=PluginFactory.getTargetPlugin(msgType);
MapparamMap=newHashMap();
if(Objects.nonNull(messagePlugin)){
returnmessagePlugin.sendMsg(paramMap);
}
returndefaultSmsService.sendMsg(paramMap);
}
}

4.2.6 添加服务依赖

在该模块中,需要引入对具体实现的两个工程的jar依赖(也可以通过启动加载的命令方式)




org.springframework.boot
spring-boot-starter-web


 

com.congge
biz-pt
1.0-SNAPSHOT



com.congge
miz-pt
1.0-SNAPSHOT



org.projectlombok
lombok



biz-pp的核心代码实现就到此结束了,后面再具体测试的时候再继续;

4.3 bizpt 关键代码实现过程

接下来就是插件化机制中具体的SPI实现过程,两个模块的实现步骤完全一致,挑选其中一个说明,工程目录结构如下:

ef91c1c6-0fd3-11ee-962d-dac502259ad0.png

4.3.1 添加对biz-app的jar的依赖

将上面biz-app工程打出来的jar依赖过来



com.congge
biz-app
1.0-SNAPSHOT


4.3.2 添加MessagePlugin接口的实现

publicclassBitptImplimplementsMessagePlugin{

@Override
publicStringsendMsg(MapmsgMap){
ObjectuserId=msgMap.get("userId");
Objecttype=msgMap.get("_type");
//TODO参数校验
System.out.println("====userId:"+userId+",type:"+type);
System.out.println("aliyunsendmessagesuccess");
return"aliyunsendmessagesuccess";
}
}

4.3.3 添加SPI配置文件

按照前文的方式,在resources目录下创建一个文件,注意文件名称为SPI中的接口全名,文件内容为实现类的全类名

com.congge.spi.BitptImpl

4.3.4 将jar安装到仓库中

完成实现类的编码后,通过maven命令将jar安装到仓库中,然后再在上一步的biz-app中引入即可;

4.4 效果演示

启动biz-app服务,调用接口:localhost:8087/sendMsg?msg=sendMsg,可以看到如下效果

efa30a3a-0fd3-11ee-962d-dac502259ad0.png

为什么会出现这个效果呢?因为我们在实现类配置了具体使用哪一种方式进行短信的发送,而加载插件的时候正好能够找到对应的服务实现,这样的话就给当前的业务提供了一个较好的扩展点。

efb5ac76-0fd3-11ee-962d-dac502259ad0.png

五、写在文末

从当前的趋势来看,插件化机制的思想已经遍布各种编程语言,框架,中间件,开源工具等领域,因此掌握插件化的实现机制对于当下做程序实现,或架构设计方面都有着很重要的意义,值得深入研究,本篇到此结束,感谢观看!




审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • SPI接口
    +关注

    关注

    0

    文章

    251

    浏览量

    33938
  • JAVA语言
    +关注

    关注

    0

    文章

    138

    浏览量

    19944
  • 解耦控制
    +关注

    关注

    0

    文章

    29

    浏览量

    10174
  • SpringBoot
    +关注

    关注

    0

    文章

    172

    浏览量

    106

原文标题:SpringBoot 插件化开发模式,强烈推荐!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    SpringBoot知识总结

    SpringBoot干货学习总结
    发表于 08-01 10:40

    怎么学习SpringBoot

    SpringBoot学习之路(X5)- 整合JPA
    发表于 06-10 14:52

    springboot集成mqtt

    springboot集成mqtt,大纲一.数据入库1.数据入库解决方案二.开发实时订阅发布展示页面1.及时通讯技术2.技术整合
    发表于 07-16 07:53

    怎样去使用springboot

    怎样去使用springboot呢?学习springboot需要懂得哪些?
    发表于 10-25 07:13

    面向无线传感网络的构件化开发方法

    构件化的开发模式使开发者在开发过程中能充分调用构件库中现有的构件为其服务。研究了构件化开发模式
    发表于 06-23 15:31 10次下载

    一种面向无线传感网络的构件化开发方法

    构件化的开发模式使开发者在开发过程中能充分调用构件库中现有的构件为其服务。研究了构件化开发模式
    发表于 07-14 17:12 19次下载

    jquery插件写法及用法(jQuery插件开发全解析)

    如今做web开发,jquery 几乎是必不可少的,同时jquery插件也是不断的被大家所熟知,以及运用。最近在搞这个jquery插件,发现它的牛逼之处,所以讲一讲jQuery插件的写法
    发表于 12-03 09:21 9860次阅读
    jquery<b class='flag-5'>插件</b>写法及用法(jQuery<b class='flag-5'>插件</b><b class='flag-5'>开发</b>全解析)

    如何使用Myeclipse进行java可视化开发

    本文档的主要内容详细介绍的是如何使用Myeclipse进行java可视化开发。实现Java的可视化开发
    发表于 01-10 10:38 5次下载
    如何使用Myeclipse进行java可视<b class='flag-5'>化开发</b>

    数据可视化开发平台PageNow上线了

    PageNow是一款基于SpringBoot+Vue构建的数据可视化开发平台。
    的头像 发表于 03-02 08:53 3446次阅读

    SpringBoot中MybatisX插件的简单使用教程

    MybatisX 是一款基于 IDEA 的快速开发插件,方便在使用mybatis以及mybatis-plus开始时简化繁琐的重复操作,提高开发速率。
    的头像 发表于 02-21 09:49 864次阅读

    什么是 SpringBoot

    本文从为什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里开始入手,逐步分析了 `SpringBoot` 自动装配的原理,最后手写了一个简单的 `start` 组件,通过实战来体会了 `
    的头像 发表于 04-07 11:28 1029次阅读
    什么是 <b class='flag-5'>SpringBoot</b>?

    SpringBoot常用注解及使用方法1

    基于 SpringBoot 平台开发的项目数不胜数,与常规的基于`Spring`开发的项目最大的不同之处,SpringBoot 里面提供了大量的注解用于快速
    的头像 发表于 04-07 11:51 445次阅读

    SpringBoot常用注解及使用方法2

    基于 SpringBoot 平台开发的项目数不胜数,与常规的基于Spring开发的项目最大的不同之处,SpringBoot 里面提供了大量的注解用于快速
    的头像 发表于 04-07 11:52 408次阅读

    什么是springBoot业务组件化开发?谈谈SpringBoot业务组件化

    首先,谈一谈什么是“springBoot业务组件化开发”,最近一直在开发一直面临这一个问题,就是相同的业务场景场景在一个项目中使用了,又需要再另外一个项目中复用,一遍又一遍的复制代码,然后想将该业务的代码在不同的项目中维护起来真
    的头像 发表于 07-20 11:30 586次阅读
    什么是<b class='flag-5'>springBoot</b>业务组件<b class='flag-5'>化开发</b>?谈谈<b class='flag-5'>SpringBoot</b>业务组件化

    javaweb和springboot能一起用吗

    JavaWeb 和 SpringBoot 是两种针对 Java 程序开发的框架,它们可以在一起使用。在本文中,我将详细介绍 JavaWeb 和 SpringBoot 的关系,并探讨如何结合使用这两个
    的头像 发表于 11-16 10:54 903次阅读