Gradle 多模块 +SpringCloud 微服务实践

本贴最后更新于 1761 天前,其中的信息可能已经东海扬尘

1.Project

2.Cloud 简单配置

2.1.父模块配置

主要就是配置一下通用依赖和基本插件

buildscript {
    ext {
        springBootVersion = '2.2.2.RELEASE'
        springBootManagementVersion = '1.0.8.RELEASE'
        springCloudVersion = 'Hoxton.SR1'
    }
    repositories {
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        mavenCentral()
        maven { url 'https://repo.spring.io/snapshot' }
        maven { url 'https://repo.spring.io/milestone' }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("io.spring.gradle:dependency-management-plugin:${springBootManagementVersion}")
    }
}

allprojects {
    group "com.github.rep3.cloud"
    version "1.0.0"
}

subprojects {
    apply plugin: 'java'
    apply plugin: 'application'
    apply plugin: 'idea'
    apply plugin: 'eclipse'
    apply plugin: 'war'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
    jar {
        enabled = true
    }
    bootJar {
        classifier = 'boot'
    }
    repositories {
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        mavenCentral()
        maven { url 'https://repo.spring.io/snapshot' }
        maven { url 'https://repo.spring.io/milestone' }
    }
    dependencies {
        compile(
                'org.springframework.boot:spring-boot-starter-web',
                'org.springframework.boot:spring-boot-starter-tomcat',
                'org.springframework.boot:spring-boot-starter-actuator',
                'org.springframework.cloud:spring-cloud-starter',
                'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client',
                'com.google.guava:guava:23.0'

        )
        testCompile(
                "org.springframework:spring-test",
                "junit:junit:4.12"
        )
    }
    dependencyManagement {
        imports { mavenBom("org.springframework.boot:spring-boot-dependencies:${springBootVersion}") }
        imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" }
    }
}

2.2.EurekaServer 服务注册中心

配置一下 EurekaServer 端依赖

mainClassName = 'com.github.rep3.cloud.eureka.EurekaApplication'
springBoot {
    mainClassName = 'com.github.rep3.cloud.eureka.EurekaApplication'
    buildInfo {
        properties {
            artifact = 'eureka'
            version = '1.0.0'
            group = 'com.github.rep3.cloud'
            name = 'eureka'
        }
    }
}
bootJar {
    classifier = 'boot'
}
dependencies {
    compile 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
}

2.3.Zuul 网关

配置 zuul 依赖

mainClassName = 'com.github.rep3.cloud.zuul.Rep3ZuulApplication'
springBoot {
    mainClassName = 'com.github.rep3.cloud.zuul.Rep3ZuulApplication'
    buildInfo {
        properties {
            artifact = 'zuul'
            version = '1.0.0'
            group = 'com.github.rep3.cloud'
            name = 'zuul'
        }
    }
}
bootJar {
    classifier = 'boot'
}
dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
}

Cloud 基本的依赖就配置完成了,接下来就配置配置 YAML 跑起来试试看

3.Springboot

SpringBoot 不做过多赘述,主要看一下配置文件加载的优先级
配置加载顺序 1->12 越小优先级越高

1.命令行传入参数
2.SPRING_APPLICATION_JSON中的属性
3.java:comp/env中的JNDI属性
4.Java的系统属性:System.getProperties()
5.操作系统环境变量
6.random*配置的随机属性
7.位于当前应用jar包之外的application-profile.yaml配置文件
8.位于当前应用jar包之内的application-profile.yaml配置文件
9.位于jar包之外的appication.yml配置
10.位于jar包之内的application.yml配置
11.@Configuration配置类中
12.应用默认属性比如port默认8080

4.SpringBootActutor

微服务的监控和管理,将依赖配置在根模块中,所有的子模块 SpringBoot 就可以使用了

'org.springframework.boot:spring-boot-starter-actuator'

启动以后查看 /health 就可以看到监控信息了

放开所有监控点

management:
  endpoints:
    web:
      exposure:
        include: '*'

查看所有监控点

http://192.168.0.101:8002/actuator

返回了所有监控点信息

{"_links":{"self":{"href":"http://192.168.0.101:8002/actuator","templated":false},"archaius":{"href":"http://192.168.0.101:8002/actuator/archaius","templated":false},"beans":{"href":"http://192.168.0.101:8002/actuator/beans","templated":false},"caches-cache":{"href":"http://192.168.0.101:8002/actuator/caches/{cache}","templated":true},"caches":{"href":"http://192.168.0.101:8002/actuator/caches","templated":false},"health-path":{"href":"http://192.168.0.101:8002/actuator/health/{*path}","templated":true},"health":{"href":"http://192.168.0.101:8002/actuator/health","templated":false},"info":{"href":"http://192.168.0.101:8002/actuator/info","templated":false},"conditions":{"href":"http://192.168.0.101:8002/actuator/conditions","templated":false},"configprops":{"href":"http://192.168.0.101:8002/actuator/configprops","templated":false},"env":{"href":"http://192.168.0.101:8002/actuator/env","templated":false},"env-toMatch":{"href":"http://192.168.0.101:8002/actuator/env/{toMatch}","templated":true},"loggers-name":{"href":"http://192.168.0.101:8002/actuator/loggers/{name}","templated":true},"loggers":{"href":"http://192.168.0.101:8002/actuator/loggers","templated":false},"heapdump":{"href":"http://192.168.0.101:8002/actuator/heapdump","templated":false},"threaddump":{"href":"http://192.168.0.101:8002/actuator/threaddump","templated":false},"metrics-requiredMetricName":{"href":"http://192.168.0.101:8002/actuator/metrics/{requiredMetricName}","templated":true},"metrics":{"href":"http://192.168.0.101:8002/actuator/metrics","templated":false},"scheduledtasks":{"href":"http://192.168.0.101:8002/actuator/scheduledtasks","templated":false},"mappings":{"href":"http://192.168.0.101:8002/actuator/mappings","templated":false},"refresh":{"href":"http://192.168.0.101:8002/actuator/refresh","templated":false},"features":{"href":"http://192.168.0.101:8002/actuator/features","templated":false},"service-registry":{"href":"http://192.168.0.101:8002/actuator/service-registry","templated":false}}}
1./beans: 所有bean信息
2./caches:缓存信息
3./health:服务状态
4./info:服务自定义信息
5./configprops:应用配置的属性信息
6./env:应用所有可用环境信息
7./mappings:所有SpringMvc映射关系信息
8./metrics:内存信息,线程信息,垃圾回收信息等
9./dump:运行中的线程信息
10./trace:http跟踪信息
...

5.Eureka 服务治理中心

服务注册 :主要用来开微服务架构中有多少服务都部署在哪里

服务发现 :服务之间调用关系的处理,比如 A 服务调用 C 服务,会直接像注册中心发出咨询请求,然后注册中心将 C服务的位置清单 返回给 A 服务,A 便获得了 多个C服务的位置 ,然后就可以调用其一。

SpringCloudEureka 使用 Netflix Eureka 来实现服务注册与发现,既包括了注册端也包括了服务端

服务端依赖

compile 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'

客户端依赖

compile 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client',

服务端 Application 开启 @EnableEurekaServer 注解标识为服务中心
同时要配置配置文件,给出 hostname 地址,客户端策略等

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false

客户端需要开启 @EnableEurekaClient 标示为 Eureka 客户端,同时配置出服务中心的地址

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/

5.1 高可用

上面搭建的只有一个服务注册中心 1 Eureka : N Springboot ,当这一台 Eureka 崩掉以后,所有服务将不能正常访问

在 Eureka 中,所有服务既是服务提供者,也是服务消费者,Eureka 本身也不例外,现在放开一下配置,并新建一个 Eureka 服务,两者互相注册成服务

  client:
    register-with-eureka: false
    fetch-registry: false

EurekaA

server:
  port:8001
spring:
  application:
    name:eureka1
eureka:
  instance:
    hostname: peer1
  client:
    serviceUrl:
      defaultZone: http://peer2:8000/eureka

EurekaB

server:
  port:8000
spring:
  application:
    name:eureka2
eureka:
  instance:
    hostname: peer2
  client:
    serviceUrl:
      defaultZone: http://peer1:8001/eureka

最后将 peer 的 ip 地址解析在/etc/hosts 中即可完成 eureka 高可用部署
高可用服务注册

spring:
  profiles: dev
  application:
    name: auth
server:
  port: 8002
eureka:
  client:
    serviceUrl:
      defaultZone: http://peer1/eureka/,http://peer2/eureka/
management:
  endpoints:
    web:
      exposure:
        include: '*'

5.2 负载均衡

ribbon 已经在 zuul 里了所以不必添加依赖
ribbon 和 eureka 联合使用时,在 eureka 的服务发现基础上实现了一套 服务选择 的策略,从而达到每个服务负载均衡

@EnableEurekaClient 替换为 @EnableDiscoveryClient
当使用服务时,ribbon 会轮询服务从而走负载较低的服务达到负载均衡的效果

5.3 Eureka 基础架构

服务注册中心: Eureka提供的服务端,提供服务注册与发现功能
服务提供: Eureka提供的客户端,将自己的服务注册到Eureka中
服务消费者: 消费者从应用中心获取服务列表,从而使消费者知道到何处调用服务,ribbon来实现服务消费,另外还有Feign消费方式

image.png

服务提供者
1.服务注册
	服务提供者在启动的时候`发送REST`请求到Eureka,其中包含了自身服务的信息
Eureka接收到请求后,将元数据信息存储在`双层Map`,第一层key为服务名称,第二层是具体服务实例名称.

2.服务同步
服务提供者可能注册到了不同的注册中心,他们的信息分别被多个eureka维护
所以访问其中一台eureka是无法调用到其余服务的,那么将eureka构建成集群模式
当服务提供者发送`REST请求`到其中一个注册中心时
eureka将请求转发给集群中其他的eureka,从而实现同步

3.服务续约
心跳检测来观测哪些服务死掉,死掉的服务会被剔除出注册中心集群中去
其中有两个配置项
服务续约任务调用时间间隔,默认为30
eureka.instance.lease-renewal-interval-in-seconds=30
服务失效时间默认为90
eureka.instance.lease-expiration-duration-in-seconds=90

服务消费者
1.获取服务
消费者访问Eureka中的服务时,Eureka会提供一份只读的服务清单,该清单被eureka缓存并每30秒更新一次
eureka.client.registry-fetch-interval-seconds配置缓存服务清单时间
2.服务调用
服务消费者获取服务清单后,通过服务名可以获得具体的提供服务的实例名称和服务元数据信息
客户端决定需要调用哪个服务实例,如果使用ribbon,则使用了轮询方式调用,从而实现客户端的负载均衡
3.服务下线
正常关闭服务实例时,服务实例发送`REST请求给EurekaServer`告诉服务中心我要下线了,服务中心收到后将服务状态设置为`Down`并传播下线事件

服务注册中心
1.失效剔除
发生不可预料的错误时导致服务崩溃但eureka没有收到下线消息的时候,由于服务没有及时续约,所以被剔除
2.自我保护
请求重试,断路器等机制

部分源码解析

//DiscoveryClient 为接口,抽象了一个服务中心基本需要实现的功能
// 所以注册中心可以换位任一实现了该接口的类,但不用修改代码
public class EurekaDiscoveryClient implements DiscoveryClient {

	// 一段描述
	public static final String DESCRIPTION = "Spring Cloud Eureka Discovery Client";
	// 客户端
	private final EurekaClient eurekaClient;
	// 客户端配置
	private final EurekaClientConfig clientConfig;

	// 根据服务ID获取服务实例列表
	@Override
	public List<ServiceInstance> getInstances(String serviceId) {
		List<InstanceInfo> infos = this.eurekaClient.getInstancesByVipAddress(serviceId,
				false);
		List<ServiceInstance> instances = new ArrayList<>();
		for (InstanceInfo info : infos) {
			instances.add(new EurekaServiceInstance(info));
		}
		return instances;
	}

	//获取服务名称
	public List<String> getServices() {
		Applications applications = this.eurekaClient.getApplications();
		if (applications == null) {
			return Collections.emptyList();
		}
		List<Application> registered = applications.getRegisteredApplications();
		List<String> names = new ArrayList<>();
		for (Application app : registered) {
			if (app.getInstances().isEmpty()) {
				continue;
			}
			names.add(app.getName().toLowerCase());

		}
		return names;
	}
}
// EurekaClient抽象了许多接口来定义Client的行为
public interface EurekaClient extends LookupService {
   // 通过Region获取Applications,Region为yaml中设置的字符串
   // Applicaions中包含了appNameApplicationMap等
  // Applications 包装eureka服务器返回的所有注册表信息的类
    public Applications getApplicationsForARegion(@Nullable String region);
  // 通过地址获取 服务注册表
    public Applications getApplications(String serviceUrl);
  // 注册监听
    public void registerEventListener(EurekaEventListener eventListener);
  // 取消监听
    public boolean unregisterEventListener(EurekaEventListener eventListener);
  // 心跳检测
    public HealthCheckHandler getHealthCheckHandler();
  // 停止服务
    public void shutdown();
  // 获取Client的配置
    public EurekaClientConfig getEurekaClientConfig();
    public ApplicationInfoManager getApplicationInfoManager();
}
........

EndpointUtils.java

// 从属性文件中获取要与eureka客户端对话的所有eureka服务URL的列表。
 public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
        Map<String, List<String>> orderedUrls = new LinkedHashMap<>();
        // 从配置文件中读取Region返回
	// 通过eureka.client.region属性来定义
	String region = getRegion(clientConfig);
	// 读取 eureka.client.defaultZone
        String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
        if (availZones == null || availZones.length == 0) {
            availZones = new String[1];
            availZones[0] = DEFAULT_ZONE;
        }
        logger.debug("The availability zone for the given region {} are {}", region, availZones);
        int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);

        String zone = availZones[myZoneOffset];
	// 获取zone中所有配置的eurekaServer地址
        List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
        if (serviceUrls != null) {
            orderedUrls.put(zone, serviceUrls);
        }
        int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
        while (currentOffset != myZoneOffset) {
            zone = availZones[currentOffset];
            serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
            if (serviceUrls != null) {
		// 最后放入 服务列表中 
                orderedUrls.put(zone, serviceUrls);
            }
            if (currentOffset == (availZones.length - 1)) {
                currentOffset = 0;
            } else {
                currentOffset++;
            }
        }
        if (orderedUrls.size() < 1) {
            throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
        }
        return orderedUrls;
    }

服务注册

在 DiscoveryClient 的构造函数中调用了 initScheduledTasks 函数,初始化一些任务
image.png
函数中实力化了一个 InstanceInfoReplicator
image.png
该类中的 run 函数,这句就是调用注册了,
image.png
image.png
在往下就是 http 层了,层层专递的 InstanceInfo 就是实例的一些配置信息了
image.png

服务获取与续约

还在 initScheduledTasks

image.png

registryFetchIntervalSeconds:eureka.client.registry-fetch-interval-seconds
renewalIntervalInSecs:eureka.instance.lease-renewal-interval-in-seconds
其最后也是通过 RestTemplate 去请求服务端

6.Ribbon

负载均衡设备/模块 都会维护一个下挂可用的服务清单,发送心跳检测
剔除不可用设备,当客户端发送请求到负载均衡设备时,该设备按照某种
算法,从维护的清单中抽取出一台服务器,并进行转发。

SpringCloudRibbon 客户端使用负载均衡

1.服务提供者向服务中心注册
2.服务消费者通过@LoadBalanced注解修饰过的RestTemplate访问服务

@LoadBalanced 注解给 RestTemplate 做标记,以使用负载均衡的客户端来配置它 LoadBalancerClient

ServiceInstance choose(String serviceId) 根据传入的服务名,从负载均衡器中挑选一个对应的服务实例

T execute(String serviceId,LoadBalancerRequest request): 从负载均衡器中提取服务来 执行 request

URI reconstructURI(ServiceInstance instance,URI original): 拼接请求地址

LoadBalancerInterceptor 的 Bean,用于对客户端发起的请求实时拦截

RestTemplateCstomizer 的 Bean,用于给 RestTemplate 增加 LoadBalancerInterceptor

维护了一个 RestTemplate 列表,然后给每一个 RestTemplate 实例去初始化拦截器

拦截器中注入了 LoadBalancerClientLoadBalancerRequestFactory
最后交给 LoadBalancerClient 去执行 request
最后走到了 RibbonLoadBalancerClient
image.png
Server server = getServer(loadBalacer,hint);

getLoadBalancer通过Factory构造了默认的ZoneAwaerLoadBalaner
image.png
然后调用了 ZoneAwaerLoadBalaner 中的 chooseServer 来选择一个服务,继而执行 request,选择策略在 ZoneAwaerLoadBalaner的chooseServer中

负载均衡器

AbstractLoadBalancer:
ILoadBalancer的抽象实现,定义了
分组枚举:ALL所有服务实例,STATUS_UP正常服务实例,STATUS_NOT_UP停止服务实例.
实现了一个chooseServer函数,key为null,表示在选择具体服务实例时忽略key的条件判断.
getServerList(ServerGroup goup): 根据不同的分组选择不同的服务实例列表.
getLoadBalancerStats():获取LoadBalancerStats对象,储存负载均衡器中各个服务实例当前的属性和统计信息.
BaseLoadBalancer:Ribbon负载均衡器的基础实现类
定义并维护了两个存储服务实例Server的对象列表,分别存储了所有服务实例和所有正常运行服务实例
定义了LoadBalancerStats
定义了IPing检查服务是否运行正常
定义了IPingStrategy Iping的执行策略对象
定义了IRule负载均衡的处理规则
...服务实例列表相关函数
DynamicServerListLoadBalancer继承BaseLoadBalancer,扩展了基础负载均衡器

负载均衡策略

AbstractLoadBalancerRule
RandomRule:随机选择策略
RoundRobinRule:线性轮询策略
RetryRule:重试机制策略
WeightedResponseTimeRule:权重计算策略

7.Hystrix 服务容错保护

单个服务出错断路

'org.springframework.cloud:spring-cloud-starterhystrix:1.4.7.RELEASE',  

SpringCloudApplication 注解聚合了,服务注册负载均衡熔断机制等注解,可以直接标示到 Application 上面

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@Service
public class DemoService {

    @Autowired
    RestTemplate restTemplate;
	// 如果函数出现错误,或者请求超时,那么将会出发熔断请求,并调用callback,返回结果
    @HystrixCommand(fallbackMethod = "helloFallback")
    public String toAuth() {
        return restTemplate.getForEntity("http://auth/home", String.class).getBody();
    }

    public String helloFallback(){
        return "error";
    }

}

工作流程

1.创建HystrixCommand/HystrixObservbleCommand Bean,用来表示对依赖服务的操作请求,同时传递所有需要的参数.
2.命令执行,execute同步执行,queue异步执行,observe()HotOb返回Ob对象,toObServable返回Ob对象ColdOb
3.结果是否被缓存
4.断路器是否打开
5.线程池/请求队列是否被占满
6.HystrixCommand.run()返回单一结果或跑一场
HystrixObservableCommand.construct()返回一个Ob来发射多个结果/通过onError发送错误通知
7.计算断路器健康程度,根据健康程度熔断/断路服务
8.fallback处理
9.返回成功的Response

断路器原理

image.png

HystrixCircuitBreaker 维护了三个抽象函数和三个类

allowRequest():每个Hystrix命令的请求通过该函数判断是否被执行

isOpen():断路器是否打开

markSuccess():闭合断路器

Factory:维护了一个Hystrix命令和HystrixCircuitBreaker的关系集合,key
通过HystrixCommandKey定义,每一个Hystrix命令都有一个key标识,同时一个Hystrix命令也会在该集合中找到它对应的断路器HystrixCirucuitBreaker

NoOpCirutiBreaker定义了一个什么都不做的断路器实现,允许所有请求,断路器始终闭合

HystrixCircuitBreakerImpl是本接口实现,定义了4个核心对象
HystrixCommandProperties:对应实例对象
HystrixCommandMetrics:记录度量指标对象
circuitOpen:是否打开标志
circuitOpenedOrLastTestedTime:打开或是上一次测试的时间

HystrixCircuitBreakerImpl 的 isOpen
image.png
HystrixCircuitBreakerImpl 的 allowRequest,通过 isOpen 判断是否允许被请求
image.png
!isOpen()||allowSingleTest() 通过配置文件中的 circuiBreakerSleepWindowInMilliseconds 属性休眠时间,对比时间戳判断请求是否可以访问,如果休眠时间到达后请求访问失败,则断路器再次进入打开状态,如果成功,断路器关闭,这句是切换断路器打开关闭状态的切换的实现。
image.png

8.Zuul 路由管理

单独使用可以进行路由转发,类似 nginx

zuul:
  routes:
    {serviceId}:
       path: /user/**
       url: http://www.baidu.com/user/

上面将 /user 的路由全部转发到下面对应的 url 中

多实例配置

zuul:
  routes:
    {serviceId}:
       path: /user/**
       serviceId: {serviceId}
// 没有整合eureka这里应该关闭
ribbon:
  eureka:
    enabled: false
{serviceId}:
  ribbon:
    listOfServers:  http://www.baidu1.com/,http://www.baidu2.com/

忽略路由

zuul.ignored.parttens=/**/hello/**

本地跳转

zuul.routes.{serviceId}.url=forward:/local

传递 Http Header 信息

zuul:
  routes:
    {router}:
      // 对制定路由开启自定义敏感头信息
      customSenstiveHeaders:true
      // 将制定路由敏感信息头设置为空
      senstiveHeaders

当遇到 302 跳转到具体微服务而不是网关时

zuul:
  // 跳转时设置Host为zuul
  addHostHeader : true

8.1 四个阶段的核心过滤器

image.png

Pre: 路由转发之前

name index desc
ServletDetectionFilter -3 检测当前请求为 DispatcherServelet/ZuulServlet 处理运行
Servlet30WrapperFilter -2 HttpServletRequest->Servlet30RequestWrapper
FormBodyWrapperFillter -1 将 ContentType 为 x-www-form-urlencoded/multipart-form-data 请求->FormBodyRequestWrapper
DebugFilter 1 debug 信息
PreDecorationFilter 5 设置请求信息,进行路由匹配

route:路由转发中

name index desc
RibbonRouterFilter 10 通过 serviceId 和 Ribbon/Hystrix 对应用实例发起请求并将结果返回
SimpleHostRoutingFiler 100 通过 routesHost 和路由规则向物理机器发送请求
SendForwardFillter 500 对 forward.to 参数的请求进行处理,forward 跳转

post route 和 error 过滤器调用之后进入

name index desc
SendErrorFillter 0 包装错误信息组成 forward 发送给网关来报错
SendResponseFiller 1000 返回响应发送给客户端
  • Gradle
    41 引用 • 20 回帖 • 2 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖 • 1 关注
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    942 引用 • 1459 回帖 • 31 关注
  • 云计算
    78 引用 • 91 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...