上文我们分析到 loadBalancer 根据具体的算法选择相应的server。

protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}

loadBalancer是定义软件负载均衡器操作的接口,共有以下几个实现类

本文便从loadBalancer开始分析ribbon具体的负载均衡策略

LoadBalancer


首先看AbstractLoadBalancer 定义了几个基础的方法

public abstract class AbstractLoadBalancer implements ILoadBalancer {
public enum ServerGroup{
//所有服务实例
ALL,
//正常服务的实例
STATUS_UP,
//停止服务的实例
STATUS_NOT_UP
}
/**
* 选择具体的服务实例,key为null,忽略key的条件判断
*/
public Server chooseServer() {
return chooseServer(null);
}
/**
* 定义了根据分组类型来获取不同的服务实例的列表。
*/
public abstract List<Server> getServerList(ServerGroup serverGroup); /**
* 定义了获取LoadBalancerStats对象的方法,LoadBalancerStats对象被用来存储负载均衡器中
* 各个服务实例当前的属性和统计信息。这些信息非常有用,我们可以利用这些信息来观察负载均衡
* 的运行情况,同时这些信息也是用来制定负载均衡策略的重要依据。
*/
public abstract LoadBalancerStats getLoadBalancerStats(); }

再看BaseLoadBalancer

BaseLoadBalancer定义了两个数组分别用来缓存所有服务实例和正常服务实例

@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
.synchronizedList(new ArrayList<Server>());
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
.synchronizedList(new ArrayList<Server>());

我们再看BaseLoadBalancer的构造方法

public BaseLoadBalancer() {
this.name = DEFAULT_NAME;
this.ping = null;
setRule(DEFAULT_RULE);
setupPingTask();

lbStats = new LoadBalancerStats(DEFAULT_NAME);
}

设置默认的路由规则为 RoundRobinRule

private final static IRule DEFAULT_RULE = new RoundRobinRule();

启动定时任务,每隔10秒钟执行一次,检查检查服务实例是否正常服务。默认ping的策略是SerialPingStrategy,采用线性的遍历各个服务实例

    private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();

注意下面的提示,之所以采用SerialPingStrategy这种策略是因为下面的中ping是基于内存的变量获取,那么在真实环境多节点部署时,假如你有100个节点,你就要线性ping100次,采用这种ping策略耗时很长,所以建议生产环境实现IPingStrategy重写这个策略

private static class SerialPingStrategy implements IPingStrategy {

        @Override
public boolean[] pingServers(IPing ping, Server[] servers) {
int numCandidates = servers.length;
boolean[] results = new boolean[numCandidates]; if (logger.isDebugEnabled()) {
logger.debug("LoadBalancer: PingTask executing ["
+ numCandidates + "] servers configured");
} for (int i = 0; i < numCandidates; i++) {
results[i] = false; /* Default answer is DEAD. */
try {
// NOTE: IFF we were doing a real ping
// assuming we had a large set of servers (say 15)
// the logic below will run them serially
// hence taking 15 times the amount of time it takes
// to ping each server
// A better method would be to put this in an executor
// pool
// But, at the time of this writing, we dont REALLY
// use a Real Ping (its mostly in memory eureka call)
// hence we can afford to simplify this design and run
// this
// serially
if (ping != null) {
results[i] = ping.isAlive(servers[i]);
}
} catch (Throwable t) {
logger.error("Exception while pinging Server:"
+ servers[i], t);
}
}
return results;
}
}

我们再看最为重要的 chooseServer,可以看到服务实例的选择由rule来实现。关于rule的分析,我们下面再谈

public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Throwable t) {
return null;
}
}
}

再看DynamicServerListLoadBalancer

DynamicServerListLoadBalancer实现了服务实例清单在运行期的动态更新能力;同时,它还具备了对服务实例清单的过滤功能

这里先介绍一个成员变变量 ServerList<T> serverListImpl, serverListImpl 是一个服务实例清单,初始化的时候回去所有服务,然后没个30秒定时的检查服务状态

ServerList有多个实现类,具体该使用那个主要由EurekaRibbonClientConfiguration配置类决定

由配置类我们可以看出ServerList默认的实习类是DomainExtractingServerList

   @Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}

现在来看 ServerList是如何实现服务的获取和更新的。这里看 DomainExtractingServerList类两个方法。注意这里的 this.list 是由DiscoveryEnabledNIWSServerList 传进去,所以真正的代码在DiscoveryEnabledNIWSServerList中

@Override
public List<DiscoveryEnabledServer> getInitialListOfServers() {
List<DiscoveryEnabledServer> servers = setZones(this.list
.getInitialListOfServers());
return servers;
} @Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
List<DiscoveryEnabledServer> servers = setZones(this.list
.getUpdatedListOfServers());
return servers;
}

所以我们看下DiscoveryEnabledNIWSServerList

两个方法都调用 obtainServersViaDiscovery

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
logger.warn("EurekaClient has not been initialized yet, returning an empty list");
return new ArrayList<DiscoveryEnabledServer>();
} EurekaClient eurekaClient = eurekaClientProvider.get();
if (vipAddresses!=null){
for (String vipAddress : vipAddresses.split(",")) {
// if targetRegion is null, it will be interpreted as the same region of client
List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
for (InstanceInfo ii : listOfInstanceInfo) {
if (ii.getStatus().equals(InstanceStatus.UP)) {
....省略部分代码
DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
des.setZone(DiscoveryClient.getZone(ii));
serverList.add(des);
}
}
if (serverList.size()>0 && prioritizeVipAddressBasedServers){
break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
}
}
}
return serverList;
}

这里的vipAddresses服务名,对这些服务名进行遍历,将状态为UP(正常服务)的实例转换成DiscoveryEnabledServer对象

现在我们已经知道了服务是怎么获取的了。但是 服务的定时更新策略我们还没提及。

这里我们看到 DynamicServerListLoadBalancer里的 ServerListUpdater

protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
@Override
public void doUpdate() {
updateListOfServers();
}
};

这个ServerListUpdater同样是个接口,看下它的注释就知道这个接口定义不同的策略去定时的更新ServerList

/**
* strategy for {@link com.netflix.loadbalancer.DynamicServerListLoadBalancer} to use for different ways
* of doing dynamic server list updates.
*
* @author David Liu
*/

他有两个实现类

默认的的是PollingServerListUpdater

很容易就能发现他的定时策略

public synchronized void start(final UpdateAction updateAction) {
if (isActive.compareAndSet(false, true)) {
final Runnable wrapperRunnable = new Runnable() {
@Override
public void run() {
try {
updateAction.doUpdate();
lastUpdated = System.currentTimeMillis();
} catch (Exception e) {
logger.warn("Failed one update cycle", e);
}
}
}; scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
wrapperRunnable,
initialDelayMs,//延迟1000ms触发
refreshIntervalMs,//每隔30 * 1000ms触发
TimeUnit.MILLISECONDS
);
} else {
logger.info("Already active, no-op");
}
}

之前我们提到过DynamicServerListLoadBalancer具有动态过滤的功能,我们在DynamicServerListLoadBalancer找到了ServerListFilter 接口,该接口允许用配置或动态获取的具有所需特性的候选服务器列表进行过滤。

public interface ServerListFilter<T extends Server> {

    public List<T> getFilteredListOfServers(List<T> servers);

}

com.netflix.loadbalancer.ZoneAffinityServerListFilter:该过滤器基于"区域感知(Zone Affinity)"的方式实现服务实例的过滤,也就说,它会根据提供服务的实例所处于的区域(Zone)与消费者自身所处区域(Zone)进行比较,过滤掉那些不是同处一个区域的实例
public List<T> getFilteredListOfServers(List<T> servers) {
if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){
List<T> filteredServers = Lists.newArrayList(Iterables.filter(
servers,
this.zoneAffinityPredicate.getServerOnlyPredicate()));
if (shouldEnableZoneAffinity(filteredServers)) {
return filteredServers;
} else if (zoneAffinity) {
overrideCounter.increment();
}
}
return servers;
}

这里的zoneAffinityPredicate是guava的函数式接口,真正的判断封装如下


private final String zone = ConfigurationManager.getDeploymentContext().getValue(ContextKey.zone);
public boolean apply(PredicateKey input) {
Server s = input.getServer();
String az = s.getZone();
if (az != null && zone != null && az.toLowerCase().equals(zone.toLowerCase())) {
return true;
} else {
return false;
}
}

com.netflix.niws.loadbalancer.DefaultNIWSServerListFilter:该过滤器完全继承自ZoneAffinityServerListFilter,是默认的NIWS(Netfilx Internal Web Service)过滤器。

com.netflix.loadbalancer.ServerListSubsetFilter:该过滤器也继承自ZoneAffinityServerListFilter,它非常适用于拥有大规模服务器集群(上百或者更多)的系统。因为它可以产生一个“区域感知”

  • 获取“区域感知”的过滤结果,来作为候选的服务实例清单
  • 从当前消费者维护的服务实例子集中剔除那些相对不够健康的实例(同时也将这些实例从候选清单中剔除,防止第三步的时候又被选入),不够健康的标准如下:
    a. 服务实例的并发连接数超过客户端配置的值,默认为0,配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationConnectionThresold
    b. 服务实例的失败数超过客户端配置的值,默认为0,配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.eliminationFailureThresold
    c. 如果按符合上面任一规则的服务实例剔除后,剔除比例小于客户端默认配置的百分比,默认为0.1(10%),配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.forceEliminatePercent。那么就先对剩下的实例列表进行健康排序,再开始从最不健康实例进行剔除,直到达到配置的剔除百分比。
  • 在完成剔除后,清单已经少了至少10%(默认值)的服务实例,最后通过随机的方式从候选清单中选出一批实例加入到清单中,以保持服务实例子集与原来的数量一致,而默认的实例子集数量为20,其配置参数为:<clientName>.<nameSpace>.ServerListSubsetFilter.size
org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilterspring cloud整合时新增的过滤器,若使用Spring Cloud整合EurekaRibbon时会默认使用该过滤器,它实现了通过配置或者Eureka实例元数据的所属区域(Zone)来过滤同区域的服务实例,它的实现非常简单,首先通过ZoneAffinityServerListFilter的过滤器来获得"区域感知"的服务实例列表,然后遍历这个结果,取出根据消费者配置预设的区域Zone来进行过滤,如果过滤的结果是空就直接返回父类的结果,如果不为空就返回通过消费者的Zone过滤后的结果。
@Override
public List<Server> getFilteredListOfServers(List<Server> servers) {
List<Server> output = super.getFilteredListOfServers(servers);
if (this.zone != null && output.size() == servers.size()) {
List<Server> local = new ArrayList<Server>();
for (Server server : output) {
if (this.zone.equalsIgnoreCase(server.getZone())) {
local.add(server);
}
}
if (!local.isEmpty()) {
return local;
}
}
return output;
}

ZoneAwareLoadBalancer

ZoneAwareLoadBalancer负载均衡器是对DynamicServerListLoadBalancer的扩展。在DynamicServerListLoadBalancer中,我们可以看到它并没有重写选择具体服务实例的chooseServer函数,所以它依然会采用在BaseLoadBalancer中实现的算法,使用RoundRobinRule规则,以线性轮询的方式来选择调用的服务实例,该算法实现简单并没有区域(Zone)的概念,所以它会把所有实例视为一个Zone下的节点来看待,这样就会周期性的产生跨区域(Zone)访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问的实例,所以在多区域部署的情况下会有一定的性能问题,而该负载均衡器则可以避免这样的问题。

我们先看ZoneAwareLoadBalancer是如何选择server实录的

public Server chooseServer(Object key) {
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
//只有当负载均衡器中维护的实例所属的Zone区域的个数大于1的时候才会执行这里的策略
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
//调用ZoneAvoidanceRule.createSnapshot方法,当前的负载均衡器中所有的Zone区域分布创建快照,保存在Map zoneSnapshot中
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
} if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
//调用ZoneAvoidanceRule.getAvailableZones方法,来获取可用Zone区域集合,在该函数中会通过Zone区域快照的统计数据实现可用区的挑选。
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
//当获得的可用Zone区域集合不为空,并且个数小于Zone区域总数,就随机选择一个Zone区域
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) { String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
//在确定了某个Zone区域后,则获取了对应Zone区域服务均衡器,并调用zoneLoadBalancer.chooseServer来选择具体的服务实例,而在
//zoneLoadBalancer.chooseServer中将使用IRule接口的choose函数来选择具体的服务实例,在这里,IRule接口的实现会使用ZoneAvoidanceRule来挑选具体的服务实例。
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Throwable e) {
logger.error("Unexpected exception when choosing server using zone aware logic", e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
//否则实现父类的策略
return super.chooseServer(key);
}
}

以上我们便把几种loadbalnce介绍完毕,但是,在实际的选择server中,loadbalnce会交由rule实现

public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Throwable t) {
return null;
}
}
}

接下来,介绍下Rule

Rule


IRule是loadbalance选择服务的策略接口,IRule有多种实现,默认的rule采用轮训策略

private final static IRule DEFAULT_RULE = new RoundRobinRule();
策略名 策略声明 策略描述 实现说明
BestAvailableRule public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule 选择一个最小的并发请求的server 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server
AvailabilityFilteringRule public class AvailabilityFilteringRule extends PredicateBasedRule 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值) 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态
WeightedResponseTimeRule public class WeightedResponseTimeRule extends RoundRobinRule 根据响应时间分配一个weight,响应时间越长,weight越小,被选中的可能性越低。 一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权重。当刚开始运行,没有形成status时,使用roubine策略选择server。
RetryRule public class RetryRule extends AbstractLoadBalancerRule 对选定的负载均衡策略机上重试机制。 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server
RoundRobinRule public class RoundRobinRule extends AbstractLoadBalancerRule roundRobin方式轮询选择server 轮询index,选择index对应位置的server
RandomRule public class RandomRule extends AbstractLoadBalancerRule 随机选择一个server 在index上随机,选择index对应位置的server
ZoneAvoidanceRule public class ZoneAvoidanceRule extends PredicateBasedRule 复合判断server所在区域的性能和server的可用性选择server 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。

以上是系统提供的rule,如果你觉得这些还不能满足的的需求。我们可以自定义Rule,步骤如下

在配置文件中为指定

serviceName.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.MyRule

将rule交给容器管理

@Bean
public IRule ribbonRule() {
return new MyRule();//这里配置策略,和配置文件对应
}

参考


https://www.jianshu.com/p/1c02c1f8c0ff 【Spring Cloud源码分析(二)Ribbon】

SpringCloud Ribbon的分析(二)的更多相关文章

  1. springcloud Ribbon学习笔记二

    之前介绍了如何搭建eureka服务并开发了一个用户服务成功注册到了eureka中,接下来介绍如何通过ribbon来从eureka中获取用户服务: springcloud ribbon提供客户端的负载均 ...

  2. SpringCloud Ribbon的分析

    Spring Cloud Ribbon主要用于客户端的负载均衡.最基本的用法便是使用RestTemplate进行动态的负载均衡.我们只需要加入如下的配置便能完成客户端的负载均衡. @Configura ...

  3. SpringCloud Feign的分析

    Feign是一个声明式的Web Service客户端,它使得编写Web Serivce客户端变得更加简单.我们只需要使用Feign来创建一个接口并用注解来配置它既可完成. @FeignClient(v ...

  4. SpringCloud学习系列之二 ----- 服务消费者(Feign)和负载均衡(Ribbon)使用详解

    前言 本篇主要介绍的是SpringCloud中的服务消费者(Feign)和负载均衡(Ribbon)功能的实现以及使用Feign结合Ribbon实现负载均衡. SpringCloud Feign Fei ...

  5. Spring Cloud之负载均衡组件Ribbon原理分析

    目录 前言 一个问题引发的思考 Ribbon的简单使用 Ribbon 原理分析 @LoadBalanced 注解 @Qualifier注解 LoadBalancerAutoConfiguration ...

  6. Spring Cloud 入门 之 Ribbon 篇(二)

    原文地址:Spring Cloud 入门 之 Ribbon 篇(二) 博客地址:http://www.extlight.com 一.前言 上一篇<Spring Cloud 入门 之 Eureka ...

  7. SNMP报文抓取与分析(二)

    SNMP报文抓取与分析(二) SNMP报文抓取与分析(二) 1.SNMP报文表示简介 基本编码规则BER 标识域Tag表示 长度域length表示 2.SNMP报文详细分析(以一个get-respon ...

  8. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  9. yhd日志分析(二)

    yhd日志分析(二) 继续yhd日志分析,统计数据 日期 uv pv 登录人数 游客人数 平均访问时长 二跳率 独立ip数 1 分析 登录人数 count(distinct endUserId) 游客 ...

随机推荐

  1. 本机是wifi,虚拟机无法连接外网问题

    1.首先看自己本机的各网口是否都启动. 2.在虚拟机中的虚拟网络编辑器中,选择桥接模式,并选择对应第一步的WLAN端口. 3.在虚拟机设置中选择自定义,选择第二部中选的VMnet2即可上网了.

  2. Linux指令集

    最近在学习Linux虚拟机,下面是我在学习虚拟机的过程中使用过的一些指令,及其作用. pwd-> 列出当前目录路径 ls-> 列出当前目录列表 cd-> 改变目录 mkdir-> ...

  3. 异步使用委托delegate --- BeginInvoke和EndInvoke方法

    当我们定义一个委托的时候,一般语言运行时会自动帮委托定义BeginInvoke 和 EndInvoke两个方法,这两个方法的作用是可以异步调用委托. 方法BeginInvoke有两个参数: Async ...

  4. 201771010118 马昕璐 《面向对象设计 java》第十七周实验总结

    1.实验目的与要求 (1) 掌握线程同步的概念及实现技术: (2) 线程综合编程练习 2.实验内容和步骤 实验1:测试程序并进行代码注释. 测试程序1: l 在Elipse环境下调试教材651页程序1 ...

  5. [LeetCode] Rectangle Overlap 矩形重叠

    A rectangle is represented as a list [x1, y1, x2, y2], where (x1, y1) are the coordinates of its bot ...

  6. Katalon Studio之请求响应中文乱码解决方法

    最近在用Katalon做接口测试过程中发现请求响应消息中返回的中文均为乱码,这是因为我们使用的系统环境在初始安装时选择的中文简体,导致windows系统默认编码格式为GBK,但是KS的编码格式是UTF ...

  7. 关于css中为什么要设置html和body的高度?

    1.在怪异模式下,也就是网页的头部不写DOCTYPE的时候,body作为根元素,设置高度为百分百的时候.可以是页面的高度和浏览高度相同,在标准模式下也就是有DOCTYPE的时候,html才是根元素这时 ...

  8. 兼容IE8,滚动加载下一页

    // 滚动加载下一页         var nowScrolledHeight = document.documentElement.scrollTop || document.body.scrol ...

  9. Python基础之函数参数

    一.实参 1.实参分类: 2.实参基础代码: def fun01(a, b, c): print(a) print(b) print(c) # 位置传参:实参与形参的位置依次对应 fun01(1, 2 ...

  10. hibernate框架搭建

    hibernate框架的搭建步骤: 1.导包 2.创建数据库准备表 3.书写orm元数据(对象与表的映射配置文件) 4.书写配置文件 5.书写代码测试 一.导包: 创建web-maven工程添加hib ...