网易首页 > 网易号 > 正文 申请入驻

SpringBoot 插件化开发模式,真香啊!

0
分享至

Java精选面试题 (微信小程序): 5000+ 道面试题和选择题, 真实面经 , 简历模版 ,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计、大厂真题等,在线随时刷题!

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机制的原理

如下工程目录,在某个应用工程中定义一个插件接口,而其他应用工程为了实现这个接口,只需要引入当前工程的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目录按照规范要求创建文件目录,并填写实现类的全类名

自定义服务加载类

 public static void main(String[] args) {
        ServiceLoader
     
  serviceLoader = ServiceLoader.load(MessagePlugin .class);         Iterator  iterator = serviceLoader.iterator();         Map map =  new HashMap();          while (iterator.hasNext()){             MessagePlugin messagePlugin = iterator.next();             messagePlugin.sendMsg(map);         }     }

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

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 自定义配置文件加载类

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

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("impl")
@ToString
public class ClassImpl {
    @Getter
    @Setter
    String name;
 
    @Getter
    @Setter
    String[] clazz;
}
2.2.3 自定义测试接口

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

import com.congge.config.ClassImpl;
import com.congge.plugins.spi.MessagePlugin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.HashMap;
 
@RestController
public class SendMsgController {
 
    @Autowired
    ClassImpl classImpl;
 
     //localhost:8081/sendMsg 
    @GetMapping("/sendMsg")
    public String sendMsg() throws Exception{
        for (int i=0;i
     
new HashMap());         }          return  "success";     }   }
2.2.4 启动类

@EnableConfigurationProperties({ClassImpl.class}) @SpringBootApplication public class PluginApp {
 
    public static void main(String[] args) {
        SpringApplication.run(PluginApp.class,args);
    }
 
}

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

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

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

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

  • 应用A定义服务接口;

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

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

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

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

2.3.1 创建约定目录

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

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

@Component
public class ServiceLoaderUtils {
 
    @Autowired
    ClassImpl classImpl;
 
 
    public static void loadJarsFromAppFolder() throws Exception {
        String path = "E:\\code-self\\bitzpp\\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 = "E:\\code-self\\bitzpp\\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;
                 //执行反射相关的方法 
                 //ServiceLoaderUtils serviceLoaderUtils = new ServiceLoaderUtils(); 
                 //result = serviceLoaderUtils.loadMethod(fullPath); 
                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("biz-pt-1.0-SNAPSHOT.jar")){
                        if(!claName.equals("com.congge.spi.BitptImpl")){
                            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("miz-pt-1.0-SNAPSHOT.jar")){
                        if(!claName.equals("com.congge.spi.MizptImpl")){
                            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 = "E:\\code-self\\bitzpp\\lib\\miz-pt-1.0-SNAPSHOT.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.congge.spi.MizptImpl");
         //获取实例 
        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();
    }
}
2.3.3 添加测试接口

添加如下测试接口

@GetMapping("/sendMsgV2")
public String index() throws Exception {
    String result = serviceLoaderUtils.doExecuteMethod();
    return result;
}

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

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

三、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文件,并解析得到类名列表,具体代码如下:

public static List   loadFactoryNames (Class  factoryClass, ClassLoader classLoader)  {
    String factoryClassName = factoryClass.getName();
    try {
        Enumeration
     
  urls = (classLoader !=  null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));         List  result =  new ArrayList ();          while (urls.hasMoreElements()) {             URL url = urls.nextElement();             Properties properties = PropertiesLoaderUtils.loadProperties( new UrlResource(url));             String factoryClassNames = properties.getProperty(factoryClassName);             result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));         }          return result;     }      catch (IOException ex) {          throw  new IllegalArgumentException( "Unable to load [" + factoryClass.getName() +                  "] factories from location [" + 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 定义一个服务接口

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

public interface SmsPlugin {
 
    public void sendMessage(String message);
 
}
3.3.2 定义2个服务实现

实现类1

public class BizSmsImpl implements SmsPlugin {
 
    @Override
    public void sendMessage(String message) {
        System.out.println("this is BizSmsImpl sendMessage..." + message);
    }
}

实现类2

public class SystemSmsImpl implements SmsPlugin {
 
    @Override
    public void sendMessage(String message) {
        System.out.println("this is SystemSmsImpl sendMessage..." + 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")
public String sendMsgV3(String msg) throws Exception{
    List
     
  smsServices= SpringFactoriesLoader.loadFactories(SmsPlugin .class, null);      for(SmsPlugin smsService : smsServices){         smsService.sendMessage(msg);     }      return  "success"; }

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

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

四、插件化机制案例实战

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

4.1 案例背景

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

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

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

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

4.1.1 模块结构
  • biz-pp,插件化接口工程;

  • bitpt,aliyun短信发送实现;

  • 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 添加服务接口

public interface MessagePlugin {
 
    public String sendMsg(Map msgMap);
 
}
4.2.2 打成jar包并安装到仓库

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

4.2.3 自定义服务加载工具类

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

import com.congge.plugin.spi.MessagePlugin;
import com.congge.spi.BitptImpl;
import com.congge.spi.MizptImpl;
 
import java.util.*;
 
public class PluginFactory {
 
    public void installPlugin(){
        Map context = new LinkedHashMap();
        context.put("_userId","");
        context.put("_version","1.0");
        context.put("_type","sms");
        ServiceLoader
     
  serviceLoader = ServiceLoader.load(MessagePlugin .class);         Iterator  iterator = serviceLoader.iterator();          while (iterator.hasNext()){             MessagePlugin messagePlugin = iterator.next();             messagePlugin.sendMsg(context);         }     }        public static MessagePlugin getTargetPlugin(String type){         ServiceLoader  serviceLoader = ServiceLoader.load(MessagePlugin .class);         Iterator  iterator = serviceLoader.iterator();         List  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 BitptImpl){                         targetPlugin = messagePlugin;                         findTarget =  true;                          break;                     }                  case  "tencent":                      if (messagePlugin  instanceof MizptImpl){                         targetPlugin = messagePlugin;                         findTarget =  true;                          break;                     }             }              if(findTarget)  break;         }          return targetPlugin;     }        public static void main(String[] args) {          new PluginFactory().installPlugin();     }   }
4.2.4 自定义接口

@RestController
public class SmsController {
 
    @Autowired
    private SmsService smsService;
 
    @Autowired
    private ServiceLoaderUtils serviceLoaderUtils;
 
     //localhost:8087/sendMsg?msg=sendMsg 
    @GetMapping("/sendMsg")
    public String sendMessage(String msg){
        return smsService.sendMsg(msg);
    }
 
}
4.2.5 接口实现

@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);
    }
}
4.2.6 添加服务依赖

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


     

         

 org.springframework.boot groupId>         

 spring-boot-starter-web artifactId>      dependency>          

         

 com.congge groupId>         

 biz-pt artifactId>         

 1.0-SNAPSHOT version>      dependency>     

         

 com.congge groupId>         

 miz-pt artifactId>         

 1.0-SNAPSHOT version>      dependency>     

         

 org.projectlombok groupId>         

 lombok artifactId>      dependency>    dependencies>














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

4.3 bizpt 关键代码实现过程

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

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


     

         

 com.congge groupId>         

 biz-app artifactId>         

 1.0-SNAPSHOT version>      dependency> dependencies>




4.3.2 添加MessagePlugin接口的实现

public class BitptImpl implements MessagePlugin {
 
    @Override
    public String sendMsg(Map msgMap) {
        Object userId = msgMap.get("userId");
        Object type = msgMap.get("_type");
         //TODO 参数校验 
        System.out.println(" ==== userId :" + userId + ",type :" + type);
        System.out.println("aliyun send message success");
        return "aliyun send message success";
    }
}
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,可以看到如下效果

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

五、写在文末

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

作者:小码农叔叔 来源:https://blog.csdn.net/zhangcongyi420

公众号“Java精选”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

最近有很多人问,有没有技术或摸鱼交流群!加入方式很简单,公众号Java精选,回复“加群”,即可入群!在线摸鱼:https://www.yoodb.com/

Java精选面试题(微信小程序):3000+道面试题,包含Java基础、并发、JVM、线程、MQ系列、Redis、Spring系列、Elasticsearch、Docker、K8s、Flink、Spark、架构设计等,在线随时刷题!

特别推荐:专注分享最前沿的技术与资讯,为弯道超车做好准备及各种开源项目与高效率软件的公众号,「大咖笔记」,专注挖掘好东西,非常值得大家关注。点击下方公众号卡片关注

文章有帮助的话,点在看,转发吧!

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
武汉16岁男生大桥上凭空消失?失联72小时,父亲最新回应令人揪心

武汉16岁男生大桥上凭空消失?失联72小时,父亲最新回应令人揪心

离离言几许
2026-01-30 11:24:52
缅北电诈头目明珍珍被判处死刑,她将要为之前的猖狂付出代价

缅北电诈头目明珍珍被判处死刑,她将要为之前的猖狂付出代价

听书的狐
2026-01-09 00:55:06
一场追思会,戳穿翁帆沉默两个月真相,原来杨振宁生前真的没说错

一场追思会,戳穿翁帆沉默两个月真相,原来杨振宁生前真的没说错

来科点谱
2025-12-15 09:06:33
台退役军人想享解放军优待?国台办马上回应,法理情理都说透了

台退役军人想享解放军优待?国台办马上回应,法理情理都说透了

始于初见见
2026-01-30 09:10:24
捧中国最猛的西方政客

捧中国最猛的西方政客

虚声
2026-01-30 20:25:20
又一款Max神机!2亿全焦段三摄+8000mAh,小米新机已就位

又一款Max神机!2亿全焦段三摄+8000mAh,小米新机已就位

小柱解说游戏
2026-01-30 20:26:59
塔帅:瓜帅说阿森纳是最强的球队是在玩心理战吗?我不觉得

塔帅:瓜帅说阿森纳是最强的球队是在玩心理战吗?我不觉得

懂球帝
2026-01-30 20:12:12
雷军刘强东同台出席中英企业家会议 小米京东发力英国市场

雷军刘强东同台出席中英企业家会议 小米京东发力英国市场

快科技
2026-01-30 17:21:06
西红柿立大功!医生研究发现:老人吃西红柿,或能缓解4种症状

西红柿立大功!医生研究发现:老人吃西红柿,或能缓解4种症状

医学原创故事会
2026-01-21 21:50:30
俄罗斯拒停火后,泽连斯基向普京提出直接谈判,并甩出一关键条件

俄罗斯拒停火后,泽连斯基向普京提出直接谈判,并甩出一关键条件

阿龙聊军事
2026-01-30 19:53:43
贸易战打到今天,事实证明:中国离得开美国,美国也离得开中国

贸易战打到今天,事实证明:中国离得开美国,美国也离得开中国

老范谈史
2026-01-29 19:39:07
湖南2名乡镇“一把手”被纪委带走

湖南2名乡镇“一把手”被纪委带走

时刻
2026-01-30 14:13:04
冲击个人第三座大满贯!中国金花张帅闯入澳网女双决赛

冲击个人第三座大满贯!中国金花张帅闯入澳网女双决赛

澎湃新闻
2026-01-29 19:05:03
1979年,党中央抓捕许世友儿子,许世友得知后:抓得好,抓得妙

1979年,党中央抓捕许世友儿子,许世友得知后:抓得好,抓得妙

大海的诺言
2024-10-04 08:55:11
美国大军逼近!特朗普发出最后通牒……伊朗:随时准备扣动扳机

美国大军逼近!特朗普发出最后通牒……伊朗:随时准备扣动扳机

新民周刊
2026-01-29 20:05:51
太顶了!明明什么都没露,却性感得要命!

太顶了!明明什么都没露,却性感得要命!

贵圈真乱
2025-12-20 12:02:06
越南人吐槽:我们学的历史课,“中国”占据了80%,还要背诵唐诗

越南人吐槽:我们学的历史课,“中国”占据了80%,还要背诵唐诗

铭记历史呀
2026-01-28 02:21:58
博主:目前海港的管理层确实与之前涉事的时间点全部都错开了

博主:目前海港的管理层确实与之前涉事的时间点全部都错开了

懂球帝
2026-01-30 12:45:13
郭包肉 | 首次授衔秘闻:张宗逊为何仅获授上将军衔?

郭包肉 | 首次授衔秘闻:张宗逊为何仅获授上将军衔?

郭包肉八世
2026-01-24 18:12:56
原来他们是两口子,戏里戏外都是夫妻,看不出年龄相差有16岁

原来他们是两口子,戏里戏外都是夫妻,看不出年龄相差有16岁

陈穟侃故事
2026-01-24 17:15:11
2026-01-30 21:08:49
Java精选
Java精选
一场永远也演不完的戏
1769文章数 3859关注度
往期回顾 全部

科技要闻

意念控制机器人不是科幻 1-2年就落地

头条要闻

泽连斯基:我邀请普京来基辅 不可能在莫斯科与他会面

头条要闻

泽连斯基:我邀请普京来基辅 不可能在莫斯科与他会面

体育要闻

“假赌黑”的子弹,还要再飞一会儿吗?

娱乐要闻

金晨出事前 曾灵魂发问未收到春晚邀请

财经要闻

金银闪崩,是调整还是趋势反转的开始?

汽车要闻

合资品牌首搭800V/5C快充 东风日产NX8将于3、4月上市

态度原创

教育
本地
艺术
数码
亲子

教育要闻

躺平的孩子疯狂要钱充值游戏,父母给还是不给?

本地新闻

云游中国|拨开云雾,巫山每帧都是航拍大片

艺术要闻

风景画选刊 | 中国油画学会三十年艺术展

数码要闻

马年焕新电竞房,四款27寸双模电竞显示器优选导购

亲子要闻

萌娃问爸爸:谁是你的宝贝?爸爸的回答让萌娃说男人都爱撒谎

无障碍浏览 进入关怀版