摘要:JAVA SPI,基于接口的编程+策略模式+配置文件的动态加载机制。

本文分享自华为云社区《一文讲透Java核心技术之高可扩展利器SPI》,作者: 冰 河。

SPI的概念

JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制

SPI的使用场景

Java是一种面向对象语言,虽然Java8开始支持函数式编程和Stream,但是总体来说,还是面向对象的语言。在使用Java进行面向对象开发时,一般会推荐使用基于接口的编程,程序的模块与模块之前不会直接进行实现类的硬编码。而在实际的开发过程中,往往一个接口会有多个实现类,各实现类要么实现的逻辑不同,要么使用的方式不同,还有的就是实现的技术不同。为了使调用方在调用接口的时候,明确的知道自己调用的是接口的哪个实现类,或者说为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java中的SPI加载机制能够满足这样的需求,它能够自动寻找某个接口的实现类。

大量的框架使用了Java的SPI技术,如下:

(1)JDBC加载不同类型的数据库驱动
(2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类
(3)Spring中大量使用了SPI

  • 对servlet3.0规范
  • 对ServletContainerInitializer的实现
  • 自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

(4)Dubbo里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现

SPI的使用

当服务的提供者,提供了接口的一种实现后,需要在Jar包的**META-INF/services/**目录下,创建一个以接口的名称(包名.接口名的形式)命名的文件,在文件中配置接口的实现类(完整的包名+类名)。

当外部程序通过java.util.ServiceLoader类装载这个接口时,就能够通过该Jar包的**META/Services/**目录里的配置文件找到具体的实现类名,装载实例化,完成注入。同时,SPI的规范规定了接口的实现类必须有一个无参构造方法。

SPI中查找接口的实现类是通过java.util.ServiceLoader,而在java.util.ServiceLoader类中有一行代码如下:

// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";

这也就是说,我们必须将接口的配置文件写到Jar包的**META/Services/**目录下。

SPI实例

这里,给出一个简单的SPI使用实例,演示在Java程序中如何使用SPI动态加载接口的实现类。

注意:实例是基于Java8进行开发的。

1.创建Maven项目

在IDEA中创建Maven项目spi-demo,如下:

2.编辑pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<artifactId>spi-demo</artifactId>
<groupId>io.binghe.spi</groupId>
<packaging>jar</packaging>
<version>1.0.0-SNAPSHOT</version>
<modelVersion>4.0.0</modelVersion>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

3.创建类加载工具类

在io.binghe.spi.loader包下创建MyServiceLoader,MyServiceLoader类中直接调用JDK的ServiceLoader类加载Class。代码如下所示。

package io.binghe.spi.loader;
import java.util.ServiceLoader;
/**
* @author binghe
* @version 1.0.0
* @description 类加载工具
*/
public class MyServiceLoader {
/**
* 使用SPI机制加载所有的Class
*/
public static <S> ServiceLoader<S> loadAll(final Class<S> clazz) {
return ServiceLoader.load(clazz);
}
}

4.创建接口

在io.binghe.spi.service包下创建接口MyService,作为测试接口,接口中只有一个方法,打印传入的字符串信息。代码如下所示:

package io.binghe.spi.service;
/**
* @author binghe
* @version 1.0.0
* @description 定义接口
*/
public interface MyService {
/**
* 打印信息
*/
void print(String info);
}

5.创建接口的实现类

(1)创建第一个实现类MyServiceA

在io.binghe.spi.service.impl包下创建MyServiceA类,实现MyService接口。代码如下所示:

package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
/**
* @author binghe
* @version 1.0.0
* @description 接口的第一个实现
*/
public class MyServiceA implements MyService {
@Override
public void print(String info) {
System.out.println(MyServiceA.class.getName() + " print " + info);
}
}

(2)创建第二个实现类MyServiceB

在io.binghe.spi.service.impl包下创建MyServiceB类,实现MyService接口。代码如下所示:

package io.binghe.spi.service.impl;
import io.binghe.spi.service.MyService;
/**
* @author binghe
* @version 1.0.0
* @description 接口第二个实现
*/
public class MyServiceB implements MyService {
@Override
public void print(String info) {
System.out.println(MyServiceB.class.getName() + " print " + info);
}
}

6.创建接口文件

在项目的src/main/resources目录下创建**META/Services/**目录,在目录中创建io.binghe.spi.service.MyService文件,注意:文件必须是接口MyService的全名,之后将实现MyService接口的类配置到文件中,如下所示:

io.binghe.spi.service.impl.MyServiceA
io.binghe.spi.service.impl.MyServiceB

7.创建测试类

在项目的io.binghe.spi.main包下创建Main类,该类为测试程序的入口类,提供一个main()方法,在main()方法中调用ServiceLoader类加载MyService接口的实现类。并通过Java8的Stream将结果打印出来,如下所示:

package io.binghe.spi.main;
import io.binghe.spi.loader.MyServiceLoader;
import io.binghe.spi.service.MyService;
import java.util.ServiceLoader;
import java.util.stream.StreamSupport;
/**
* @author binghe
* @version 1.0.0
* @description 测试的main方法
*/
public class Main {
public static void main(String[] args){
ServiceLoader<MyService> loader = MyServiceLoader.loadAll(MyService.class);
StreamSupport.stream(loader.spliterator(), false).forEach(s -> s.print("Hello World"));
}
}

8.测试实例

运行Main类中的main()方法,打印出的信息如下所示:

io.binghe.spi.service.impl.MyServiceA print Hello World
io.binghe.spi.service.impl.MyServiceB print Hello World
Process finished with exit code 0

通过打印信息可以看出,通过Java SPI机制正确加载出接口的实现类,并调用接口的实现方法。

源码解析

这里,主要是对SPI的加载流程涉及到的java.util.ServiceLoader的源码的解析。

进入java.util.ServiceLoader的源码,可以看到ServiceLoader类实现了java.lang.Iterable接口,如下所示。

public final class ServiceLoader<S> implements Iterable<S> 

说明ServiceLoader类是可以遍历迭代的。

java.util.ServiceLoader类中定义了如下的成员变量:

// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";
// 需要加载的接口
private final Class<S> service;
// 类加载器,用于加载以接口命名的文件中配置的接口的实现类
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文环境
private final AccessControlContext acc;
// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用于延迟加载实现类的迭代器
private LazyIterator lookupIterator;

可以看到ServiceLoader类中定义了加载前缀为“META-INF/services/”,所以,接口文件必须要在项目的src/main/resources目录下的**META-INF/services/**目录下创建。

从MyServiceLoader类调用**ServiceLoader.load(clazz)**方法进入源码,如下所示:

//根据类的Class对象加载指定的类,返回ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//动态加载指定的类,将类加载到ServiceLoader中
return ServiceLoader.load(service, cl);
}

方法中调用了**ServiceLoader.load(service, cl)**方法,继续跟踪代码,如下所示:

//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}

可以看到**ServiceLoader.load(service, cl)**方法中,调用了ServiceLoader类的构造方法,继续跟进代码,如下所示:

//构造ServiceLoader对象
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果传入的Class对象为空,则判处空指针异常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}

继续跟**reload()**方法,如下所示。

//重新加载
public void reload() {
//清空保存加载的实现类的LinkedHashMap
providers.clear();
//构造延迟加载的迭代器
lookupIterator = new LazyIterator(service, loader);
}

继续跟进懒加载迭代器的构造函数,如下所示。

private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

可以看到,会将需要加载的接口的Class对象和类加载器赋值给LazyIterator的成员变量。

当我们在程序中迭代获取对象实例时,首先在成员变量**providers中查找是否有缓存的实例对象。如果存在则直接返回,否则调用lookupIterator**延迟加载迭代器进行加载。

迭代器进行逻辑判断的代码如下所示:

//迭代ServiceLoader的方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//获取保存实现类的LinkedHashMap<String,S>的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
//判断是否有下一个元素
public boolean hasNext() {
//如果knownProviders存在元素,则直接返回true
if (knownProviders.hasNext())
return true;
//返回延迟加载器是否存在元素
return lookupIterator.hasNext();
}
//获取下一个元素
public S next() {
//如果knownProviders存在元素,则直接获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//获取延迟迭代器lookupIterator中的元素
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}

LazyIterator加载类的流程如下代码所示

//判断是否拥有下一个实例
private boolean hasNextService() {
//如果拥有下一个实例,直接返回true
if (nextName != null) {
return true;
}
//如果实现类的全名为null
if (configs == null) {
try {
//获取全文件名,文件相对路径+文件名称(包名+接口名)
String fullName = PREFIX + service.getName();
//类加载器为空,则通过ClassLoader.getSystemResources()方法获取
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
//类加载器不为空,则直接通过类加载器获取
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
//如果configs中没有更过的元素,则直接返回false
if (!configs.hasMoreElements()) {
return false;
}
//解析包结构
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//加载类对象
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//通过c.newInstance()生成对象实例
S p = service.cast(c.newInstance());
//将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

最后,给出整个java.util.ServiceLoader的类,如下所示:

package java.util;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
public final class ServiceLoader<S> implements Iterable<S> {
// 加载具体实现类信息的前缀,也就是以接口命名的文件需要放到Jar包中的META-INF/services/目录下
private static final String PREFIX = "META-INF/services/";
// 需要加载的接口
private final Class<S> service;
// 类加载器,用于加载以接口命名的文件中配置的接口的实现类
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文环境
private final AccessControlContext acc;
// 用来缓存已经加载的接口实现类,其中,Key是接口实现类的完整类名,Value为实现类对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用于延迟加载实现类的迭代器
private LazyIterator lookupIterator;
//重新加载
public void reload() {
//清空保存加载的实现类的LinkedHashMap
providers.clear();
//构造延迟加载的迭代器
lookupIterator = new LazyIterator(service, loader);
}
//构造ServiceLoader对象
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//如果传入的Class对象为空,则判处空指针异常
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//如果传入的ClassLoader为空,则通过ClassLoader.getSystemClassLoader()获取,否则直接使用传入的ClassLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
// Parse a single line from the given configuration file, adding the name
// on the line to the names list.
//
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
// Private inner class implementing fully-lazy provider lookupload
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
//判断是否拥有下一个实例
private boolean hasNextService() {
//如果拥有下一个实例,直接返回true
if (nextName != null) {
return true;
}
//如果实现类的全名为null
if (configs == null) {
try {
//获取全文件名,文件相对路径+文件名称(包名+接口名)
String fullName = PREFIX + service.getName();
//类加载器为空,则通过ClassLoader.getSystemResources()方法获取
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
//类加载器不为空,则直接通过类加载器获取
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
//如果configs中没有更过的元素,则直接返回false
if (!configs.hasMoreElements()) {
return false;
}
//解析包结构
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//加载类对象
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//通过c.newInstance()生成对象实例
S p = service.cast(c.newInstance());
//将生成的对象实例保存到缓存中(LinkedHashMap<String,S>)
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
//迭代ServiceLoader的方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//获取保存实现类的LinkedHashMap<String,S>的迭代器
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
//判断是否有下一个元素
public boolean hasNext() {
//如果knownProviders存在元素,则直接返回true
if (knownProviders.hasNext())
return true;
//返回延迟加载器是否存在元素
return lookupIterator.hasNext();
}
//获取下一个元素
public S next() {
//如果knownProviders存在元素,则直接获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//获取延迟迭代器lookupIterator中的元素
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
//通过ClassLoader加载指定类的Class,并将返回结果封装到ServiceLoader对象中
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
//根据类的Class对象加载指定的类,返回ServiceLoader对象
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//动态加载指定的类,将类加载到ServiceLoader中
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
/**
* Returns a string describing this service.
*
* @return A descriptive string
*/
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}

SPI总结

优点:

能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

缺点:

  • 多个并发多线程使用ServiceLoader类的实例是不安全的
  • 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。

参考:深入理解Java中的spi机制

点击关注,第一时间了解华为云新鲜技术~

SPI:Java的高可扩展利器的更多相关文章

  1. 15套java架构师、集群、高可用、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战视频教程

    * { font-family: "Microsoft YaHei" !important } h1 { color: #FF0 } 15套java架构师.集群.高可用.高可扩展. ...

  2. Android端IM应用中的@人功能实现:仿微博、QQ、微信,零入侵、高可扩展

    本文由“猫爸iYao”原创分享,感谢作者. 1.引言 最近有个需求:评论@人(没错,就是IM聊天或者微博APP里的@人功能),就像下图这样:   ▲ 微信群聊界面里的@人功能    ▲ QQ群聊界面里 ...

  3. java处理高并发高负载类网站的优化方法

    java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据) 一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF ...

  4. [转]java处理高并发高负载类网站的优化方法

    本文转自:http://www.cnblogs.com/pengyongjun/p/3406210.html java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,ja ...

  5. [项目机会]citrix 虚拟桌面对于java等高CPU占用率如何解决

    citrix 虚拟桌面对于java等高CPU占用率如何解决 问题1:java等客户端对于虚拟桌面cpu影响较大,但是有些用户的确需要使用java支持的程序,是否可以通过其他途径来解决? 问题2:对于其 ...

  6. 采用truelicense进行Java规划license控制 扩展可以验证后,license 开始结束日期,验证绑定一个给定的mac住址

    采用truelicense进行Java规划license控制 扩展可以验证后,license 开始结束日期,验证绑定一个给定的mac住址. Truelicense 它是一个开源java license ...

  7. Java安全套接字扩展——JSSE

    上节已经介绍了SSL/TLS协议的通信模式,而对于这些底层协议,如果要每个开发者都自己去实现显然会带来不必要的麻烦,正是为了解决这个问题Java为广大开发者提供了Java安全套接字扩展--JSSE,它 ...

  8. Java 多线程高并发编程 笔记(一)

    本篇文章主要是总结Java多线程/高并发编程的知识点,由浅入深,仅作自己的学习笔记,部分侵删. 一 . 基础知识点 1. 进程于线程的概念 2.线程创建的两种方式 注:public void run( ...

  9. php面试专题---19、MySQL高可扩展和高可用考点

    php面试专题---19.MySQL高可扩展和高可用考点 一.总结 一句话总结: 要区别分区和分库分表,分区的话对用户是透明的,分库分表的话需要程序员做点事情,主从数据库同步的话借助的是二进制日志 1 ...

随机推荐

  1. drools动态增加、修改、删除规则

    目录 1.背景 2.前置知识 1.如何动态构建出一个kmodule.xml文件 2.kmodule.xml应该被谁加载 3.我们drl规则内容如何加载 4.动态构建KieContainer 3.需求 ...

  2. 【可视化分析案例】用python分析B站Top100排行榜数据

    一.数据源 之前,我分享过一期爬虫,用python爬取Top100排行榜: 最终数据结果,是这样的: 在此数据基础上,做python可视化分析. 二.数据读取 首先,读取数据源: # 读取csv数据 ...

  3. Java开发学习(二)----IOC、DI入门案例

    一.IOC入门案例 1.1 思路分析 (1)Spring是使用容器来管理bean对象的,那么管什么? 主要管理项目中所使用到的类对象,比如(Service和Dao) (2)如何将被管理的对象告知IOC ...

  4. docker安装RabbitMQ及安装延迟插件

    我这个安装攻略首先得保证服务器上安装过docker了 如果没安装docker请先去安装docker 1.首先说一下什么是MQ MQ(message queue)字面意思上来说消息队列,FIFO先入先出 ...

  5. Eureka入门

    一个Eureka中分为eureka server和eureka client.其中eureka server是作为服务的注册与发现中心.eureka client既可以作为服务的生产者,又可以作为服务 ...

  6. 【python基础】第02回 计算机基础2

    上节内容回顾 1.绝对路径与相对路径 1.路径的概念 用来标识资源的位置 2.绝对路径 类似于全球GPS定位(给到任何人都可以顺利的找到相应的资源) eg: D:\aaa\a.txt 3.相对路径 需 ...

  7. 用Python实时获取Steam特惠游戏数据,我看看谁的钱包还有钱

    前言 大家好鸭, 我是小熊猫 Steam大家应该不陌生吧?不知道的话就让我们来了解一下吧~(一下简称"S") S是由美国电子游戏商Valve于2003年9月12日推出的数字发行平台 ...

  8. windows server2012R2 上 .net core IIS 部署--应用程序池 自动停止

    在windows server2016安装部署.NET CORE时,只需要将.net core应用程序池设置无托管,然后对应你项目的版本安装一个dotnet-hosting-2.2.6-win.exe ...

  9. 机器学习-K近邻(KNN)算法详解

    一.KNN算法描述   KNN(K Near Neighbor):找到k个最近的邻居,即每个样本都可以用它最接近的这k个邻居中所占数量最多的类别来代表.KNN算法属于有监督学习方式的分类算法,所谓K近 ...

  10. JavaWeb的技术体系

    客户端和服务器端的交互 browser/ server(B/S)浏览器/服务器. client/server(C/S)应用/服务器.