4.【Spring Cloud Alibaba】服务容错-sentinel
雪崩效应
常见容错方案
- 超时
- 限流
- 仓壁模式
- 断路器模式
断路器三态转换
使用Sentinel实现容错
什么是Sentinel
https://github.com/alibaba/Sentinel
轻量级的流量控制、熔断降级 Java 库
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
搭建Sentinel控制台
下载控制台
https://github.com/alibaba/Sentinel/releases
项目整合sentinel控制台
application.yml
spring:
cloud:
sentinel:
filter:
# 打开/关闭掉对Spring MVC端点的保护
enabled: true
transport:
# 指定sentinel 控制台的地址
dashboard: localhost:8080
流控规则
直接
快速失败
Warm Up
排队等待
直接拒绝:(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任何规则的阈值后,新的请求就会立即拒绝,拒绝方式为抛出FlowException . 这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
Warm Up:(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处理低水平的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值的上限,给系统一个预热的时间,避免冷系统被压垮。通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
均速排队:(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式后严格控制请求通过的时间间隔,也即是让请求以均匀的速度通过,对应的是漏桶算法。该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
降级规则
热点规则详解
@GetMapping("test-hot")
@SentinelResource("hot")
public String testHot(
@RequestParam(required = false) String a,
@RequestParam(required = false) String b
) {
return a + " " + b;
}
在sentinel控制台配置热点规则
系统规则详解
授权规则
使用代码配置流控规则
@GetMapping("test-add-flow-rule")
public String testHot() {
this.initFlowQpsRule();
return "success";
}
private void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule("/shares/1");
// set limit qps to 20
rule.setCount(20);
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setLimitApp("default");
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
sentinel与控制台通信原理剖析
sentinel控制台相关配置
sentinel Api详解
@GetMapping("/test-sentinel-api")
public String testSentinelAPI(
@RequestParam(required = false) String a) {
String resourceName = "test-sentinel-api";
ContextUtil.enter(resourceName, "test-wfw");
// 定义一个sentinel保护的资源,名称是test-sentinel-api
Entry entry = null;
try {
entry = SphU.entry(resourceName);
// 被保护的业务逻辑
if (StringUtils.isBlank(a)) {
throw new IllegalArgumentException("a不能为空");
}
return a;
}
// 如果被保护的资源被限流或者降级了,就会抛BlockException
catch (BlockException e) {
log.warn("限流,或者降级了", e);
return "限流,或者降级了";
} catch (IllegalArgumentException e2) {
// 统计IllegalArgumentException【发生的次数、发生占比...】
Tracer.trace(e2);
return "参数非法!";
} finally {
if (entry != null) {
// 退出entry
entry.exit();
}
ContextUtil.exit();
}
}
@SentinelResource注解详解
@GetMapping("/test-sentinel-resource")
@SentinelResource(
value = "test-sentinel-api",
blockHandler = "block",
blockHandlerClass = TestControllerBlockHandlerClass.class,
fallback = "fallback"
)
public String testSentinelResource(@RequestParam(required = false) String a) {
if (StringUtils.isBlank(a)) {
throw new IllegalArgumentException("a cannot be blank.");
}
return a;
}
/**
* 1.5 处理降级
* - sentinel 1.6 可以处理Throwable
*
* @param a
* @return
*/
public String fallback(String a) {
return "限流,或者降级了 fallback";
}
TestControllerBlockHandlerClass
@Slf4j
public class TestControllerBlockHandlerClass {
/**
* 处理限流或者降级
*
* @param a
* @param e
* @return
*/
public static String block(String a, BlockException e) {
log.warn("限流,或者降级了 block", e);
return "限流,或者降级了 block";
}
}
RestTemplate整合sentinel
// 在spring容器中,创建一个对象,类型RestTemplate;名称/ID是:restTemplate
// <bean id="restTemplate" class="xxx.RestTemplate"/>
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate() {
RestTemplate template = new RestTemplate();
template.setInterceptors(
Collections.singletonList(
new TestRestTemplateTokenRelayInterceptor()
)
);
return template;
}
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test-rest-template-sentinel/{userId}")
public UserDTO test(@PathVariable Integer userId) {
return this.restTemplate
.getForObject(
"http://user-center/users/{userId}",
UserDTO.class, userId);
}
关闭@SentinelRestTemplate注解
resttemplate:
sentinel:
# 设置成false,表示关闭@SentinelRestTemplate注解
enabled: true
Feign整合sentinel
为feign整合sentinel
feign:
sentinel:
# 为feign整合sentinel
enabled: true
代码验证
不能获取异常
@FeignClient(name = "user-center",
fallback = UserCenterFeignClientFallback.class,
)
public interface UserCenterFeignClient {
/**
* http://user-center/users/{id}
*
* @param id
* @return
*/
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id);
}
private final ShareMapper shareMapper;
private final UserCenterFeignClient userCenterFeignClient;
public ShareDTO findById(Integer id) {
// 获取分享详情
Share share = this.shareMapper.selectByPrimaryKey(id);
// 发布人id
Integer userId = share.getUserId();
// 1. 代码不可读
// 2. 复杂的url难以维护:https://user-center/s?ie={ie}&f={f}&rsv_bp=1&rsv_idx=1&tn=baidu&wd=a&rsv_pq=c86459bd002cfbaa&rsv_t=edb19hb%2BvO%2BTySu8dtmbl%2F9dCK%2FIgdyUX%2BxuFYuE0G08aHH5FkeP3n3BXxw&rqlang=cn&rsv_enter=1&rsv_sug3=1&rsv_sug2=0&inputT=611&rsv_sug4=611
// 3. 难以相应需求的变化,变化很没有幸福感
// 4. 编程体验不统一
UserDTO userDTO = this.userCenterFeignClient.findById(userId);
ShareDTO shareDTO = new ShareDTO();
// 消息的装配
BeanUtils.copyProperties(share, shareDTO);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
UserCenterFeignClientFallback
@Component
public class UserCenterFeignClientFallback implements UserCenterFeignClient {
@Override
public UserDTO findById(Integer id) {
UserDTO userDTO = new UserDTO();
userDTO.setWxNickname("流控/降级返回的用户");
return userDTO;
}
}
可以捕获异常
@FeignClient(name = "user-center",
fallbackFactory = UserCenterFeignClientFallbackFactory.class
)
public interface UserCenterFeignClient {
/**
* http://user-center/users/{id}
*
* @param id
* @return
*/
@GetMapping("/users/{id}")
UserDTO findById(@PathVariable Integer id);
}
private final ShareMapper shareMapper;
private final UserCenterFeignClient userCenterFeignClient;
public ShareDTO findById(Integer id) {
// 获取分享详情
Share share = this.shareMapper.selectByPrimaryKey(id);
// 发布人id
Integer userId = share.getUserId();
// 1. 代码不可读
// 2. 复杂的url难以维护:https://user-center/s?ie={ie}&f={f}&rsv_bp=1&rsv_idx=1&tn=baidu&wd=a&rsv_pq=c86459bd002cfbaa&rsv_t=edb19hb%2BvO%2BTySu8dtmbl%2F9dCK%2FIgdyUX%2BxuFYuE0G08aHH5FkeP3n3BXxw&rqlang=cn&rsv_enter=1&rsv_sug3=1&rsv_sug2=0&inputT=611&rsv_sug4=611
// 3. 难以相应需求的变化,变化很没有幸福感
// 4. 编程体验不统一
UserDTO userDTO = this.userCenterFeignClient.findById(userId);
ShareDTO shareDTO = new ShareDTO();
// 消息的装配
BeanUtils.copyProperties(share, shareDTO);
shareDTO.setWxNickname(userDTO.getWxNickname());
return shareDTO;
}
UserCenterFeignClientFallbackFactory
@Component
@Slf4j
public class UserCenterFeignClientFallbackFactory implements FallbackFactory<UserCenterFeignClient> {
@Override
public UserCenterFeignClient create(Throwable cause) {
return new UserCenterFeignClient() {
@Override
public UserDTO findById(Integer id) {
log.warn("远程调用被限流/降级了", cause);
UserDTO userDTO = new UserDTO();
userDTO.setWxNickname("流控/降级返回的用户");
return userDTO;
}
};
}
}
sentinel使用总结
sentinel规则持久化01-拉模式
拉模式架构
原理简述
- FileRefreshableDataSource 定时从指定文件中读取规则JSON文件【图中的本地文件】,如果发现文件发生变化,就更新规则缓存。
- FileWritableDataSource 接收控制台规则推送,并根据配置,修改规则JSON文件【图中的本地文件】。
编写
3.1 加依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>
写代码
FileDataSourceInit
/**
* 拉模式规则持久化
*
* @author itmuch.com
*/
public class FileDataSourceInit implements InitFunc {
@Override
public void init() throws Exception {
// TIPS: 如果你对这个路径不喜欢,可修改为你喜欢的路径
String ruleDir = System.getProperty("user.home") + "/sentinel/rules";
String flowRulePath = ruleDir + "/flow-rule.json";
String degradeRulePath = ruleDir + "/degrade-rule.json";
String systemRulePath = ruleDir + "/system-rule.json";
String authorityRulePath = ruleDir + "/authority-rule.json";
String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
// 将可读数据源注册至FlowRuleManager
// 这样当规则文件发生变化时,就会更新规则到内存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
// 将可写数据源注册至transport模块的WritableDataSourceRegistry中
// 这样收到控制台推送的规则时,Sentinel会先更新到内存,然后将规则写入到文件中
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
配置
在项目的 resources/META-INF/services 目录下创建文件,名为 com.alibaba.csp.sentinel.init.InitFunc ,内容为:
# 改成上面FileDataSourceInit的包名类名全路径即可。
com.itmuch.contentcenter.FileDataSourceInit
优缺点分析
优点
- 简单易懂
- 没有多余依赖(比如配置中心、缓存等)
缺点
- 由于规则是用 FileRefreshableDataSource 定时更新的,所以规则更新会有延迟。如果FileRefreshableDataSource定时时间过大,可能长时间延迟;如果FileRefreshableDataSource过小,又会影响性能;
- 规则存储在本地文件,如果有一天需要迁移微服务,那么需要把规则文件一起迁移,否则规则会丢失。
sentinel规则持久化-推模式
推模式架构图
原理简述
- 控制台推送规则:
- 将规则推送到Nacos或其他远程配置中心
- Sentinel客户端链接Nacos,获取规则配置;并监听Nacos配置变化,如发生变化,就更新本地缓存(从而让本地缓存总是和Nacos一致)
- 控制台监听Nacos配置变化,如发生变化就更新本地缓存(从而让控制台本地缓存总是和Nacos一致)
微服务改造
加依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
添加配置
spring:
cloud:
sentinel:
datasource:
# 名称随意
flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
# 规则类型,取值见:
# org.springframework.cloud.alibaba.sentinel.datasource.RuleType
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
system:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-system-rules
groupId: SENTINEL_GROUP
rule-type: system
authority:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-authority-rules
groupId: SENTINEL_GROUP
rule-type: authority
param-flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-param-flow-rules
groupId: SENTINEL_GROUP
rule-type: param-flow
推模式改造详情请查看如下地址
https://www.imooc.com/article/289464
在生产环境使用
https://help.aliyun.com/document_detail/90323.html
集群流控
扩展sentinel01-错误页优化
@Component
public class MyUrlBlockHandler implements UrlBlockHandler {
@Override
public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
ErrorMsg msg = null;
if (ex instanceof FlowException) {
msg = ErrorMsg.builder()
.status(100)
.msg("限流了")
.build();
} else if (ex instanceof DegradeException) {
msg = ErrorMsg.builder()
.status(101)
.msg("降级了")
.build();
} else if (ex instanceof ParamFlowException) {
msg = ErrorMsg.builder()
.status(102)
.msg("热点参数限流")
.build();
} else if (ex instanceof SystemBlockException) {
msg = ErrorMsg.builder()
.status(103)
.msg("系统规则(负载/...不满足要求)")
.build();
} else if (ex instanceof AuthorityException) {
msg = ErrorMsg.builder()
.status(104)
.msg("授权规则不通过")
.build();
}
// http状态码
response.setStatus(500);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
// spring mvc自带的json操作工具,叫jackson
new ObjectMapper()
.writeValue(
response.getWriter(),
msg
);
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ErrorMsg {
private Integer status;
private String msg;
}
扩展sentinel02-实现区分来源
限流规则里的来源值要和请求参数origin的值相等,限流规则才生效。
import com.alibaba.csp.sentinel.adapter.servlet.callback.RequestOriginParser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class MyRequestOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
// 从请求参数中获取名为 origin 的参数并返回
// 如果获取不到origin参数,那么就抛异常
String origin = request.getParameter("origin");
if (StringUtils.isBlank(origin)) {
throw new IllegalArgumentException("origin must be specified");
}
return origin;
}
}
扩展sentinel03-RESTfulUrl
扩展sentinel04-通过现象看本质
配置项总结
https://www.imooc.com/article/289562
4.【Spring Cloud Alibaba】服务容错-sentinel的更多相关文章
- Spring Cloud (7) 服务容错保护-Hystrix服务降级
在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以互相调用,在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用.为了保证其高可用,单个服务通常会集群 ...
- Spring Cloud Hystrix 服务容错保护 5.1
Spring Cloud Hystrix介绍 在微服务架构中,通常会存在多个服务层调用的情况,如果基础服务出现故障可能会发生级联传递,导致整个服务链上的服务不可用为了解决服务级联失败这种问题,在分布式 ...
- 笔记:Spring Cloud Hystrix 服务容错保护
由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加 ...
- Spring Cloud Hystrix 服务容错保护
目录 一.Hystrix 是什么 二.Hystrix断路器搭建 三.断路器优化 一.Hystrix 是什么 在微服务架构中,我们将系统拆分成了若干弱小的单元,单元与单元之间通过HTTP或者TCP等 ...
- Spring Cloud (9) 服务容错保护-Hystrix断路器
断路器 断路器本身是一种开关装置,用于在电路上保护线路过载,当线路中又电路发生短路时,断路器能够及时的切断故障电路,放置发生过载.发热.甚至起火等严重后果. 在分布式架构中,断路器模式的作用也是类似, ...
- Spring Cloud (8) 服务容错保护-Hystrix依赖隔离
依赖隔离 docker使用舱壁模式来实现进程的隔离,使容器与容器之间不会互相影响.而Hystrix则使用该模式实现线程池的隔离,它会为每一个Hystrix命令创建一个独立的线程池,这样就算在某个Hys ...
- 【Spring Cloud】服务容错保护:Hystrix(四)
一.雪崩效应 在微服务架构中,由于服务和服务之间可以互相调用,一项工作的完成可能会依赖调用多个微服务模块,但由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就 ...
- Spring Cloud微服务Sentinel+Apollo限流、熔断实战总结
在Spring Cloud微服务体系中,由于限流熔断组件Hystrix开源版本不在维护,因此国内不少有类似需求的公司已经将眼光转向阿里开源的Sentinel框架.而以下要介绍的正是作者最近两个月的真实 ...
- Spring Cloud微服务限流之Sentinel+Apollo生产实践
Sentinel概述 在基于Spring Cloud构建的微服务体系中,服务之间的调用链路会随着系统的演进变得越来越长,这无疑会增加了整个系统的不可靠因素.在并发流量比较高的情况下,由于网络调用之间存 ...
- Spring Cloud Alibaba Nacos 服务注册与发现功能实现!
Nacos 是 Spring Cloud Alibaba 中一个重要的组成部分,它提供了两个重要的功能:服务注册与发现和统一的配置中心功能. 服务注册与发现功能解决了微服务集群中,调用者和服务提供者连 ...
随机推荐
- JDK 和JRE区别
JDK,开发java程序用的开发包,JDK里面有java的运行环境(JRE),包括client和server端的.需要配置环境变量.... JRE,运行java程序的环境,JVM,JRE里面只有cli ...
- lvs+keepalived部署k8s v1.16.4高可用集群
一.部署环境 1.1 主机列表 主机名 Centos版本 ip docker version flannel version Keepalived version 主机配置 备注 lvs-keepal ...
- Java入门 - 语言基础 - 14.String类
原文地址:http://www.work100.net/training/java-string.html 更多教程:光束云 - 免费课程 String类 序号 文内章节 视频 1 概述 2 创建字符 ...
- php代码没解析成功
在Apache中加载PHP模块 1.打开Apache的配置文件httpd.conf(位于Apache2\conf 目录下). 2.查找 “#LoadModule ssl_module modules/ ...
- 安装mysql遇到的问题
想在自己的PC上安装mysql服务器,首先在官网下载mysql的安装文件. MySQL安装文件分两种 .msi和.zip ,.msi需要安装,.zip文件需要配置环境变量. 我首先下载的是不需要安装的 ...
- influxdb的命令们
InfluxDB是一个开源的时序数据库,使用GO语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据.而InfluxDB自带的各种特殊函数如求标准差,随机取样数据,统计数据变化比等,使数据统计 ...
- influxdb基础那些事儿
InfluxDB是一个开源的时序数据库,使用GO语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据.而InfluxDB自带的各种特殊函数如求标准差,随机取样数据,统计数据变化比等,使数据统计 ...
- Spring注解开发系列Ⅳ --- 属性赋值
在Spring框架中,属性的注入我们有多种方式,我们可以通过构造方法注入,可以通过set方法注入,也可以通过p名称空间注入,方式多种多样,对于复杂的数据类型比如对象.数组.List集合.map集合.P ...
- static静态变量在c++类中的应用实例
这个static 如果写在类中,那么就可以得到一个局部的静态变量,也就是说可以实现在类内保存某个特殊值不随函数释放而消失的作用.应用中由于赋初值的位置不对而报错,错误提示为:“无法解析外部符号 ... ...
- CSS DIV重叠
<div style="position: relative"> <div>content</div> <div style=" ...