本文是 极客时间 杨波 《Spring Boot 与 Kubernetes 云原生微服务实践》 的学习笔记。如果对原课程有兴趣,欢迎扫码订阅(见文章末尾)
文章目录
强类型客户端
- Response 不可使用泛型,泛型在运行期信息会被擦除
- 使用Response 里边的code表示状态码,如果用Http status code表示,支持强类型客户端会更复杂
Spring Feign
Demo
@FeignClient(name = "account-service", path = "/v1/account", url = "${staffjoy.account-service-endpoint}")
// TODO Client side validation can be enabled as needed
// @Validated
public interface AccountClient {
@PostMapping(path = "/create")
GenericAccountResponse createAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid CreateAccountRequest request);
}
- AccountController in account-svc
@RestController
@RequestMapping("/v1/account")
@Validated
public class AccountController {
@PostMapping(path = "/create")
@Authorize(value = {
AuthConstant.AUTHORIZATION_SUPPORT_USER,
AuthConstant.AUTHORIZATION_WWW_SERVICE,
AuthConstant.AUTHORIZATION_COMPANY_SERVICE
})
public GenericAccountResponse createAccount(@RequestBody @Valid CreateAccountRequest request) {
AccountDto accountDto = accountService.create(request.getName(), request.getEmail(), request.getPhoneNumber());
GenericAccountResponse genericAccountResponse = new GenericAccountResponse(accountDto);
return genericAccountResponse;
}
}
数据校验
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}
public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber, String> {
@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) return true; // can be null
return phoneField != null && phoneField.matches("[0-9]+")
&& (phoneField.length() > 8) && (phoneField.length() < 14);
}
}
全局错误处理
DTO 和 DMO
View Object
DTO: Data Transfer Object
DMO: Data Model Object
Domain Object
Persistent Object
DTO
ModelMapper
public class AccountService {
private AccountDto convertToDto(Account account) {
return modelMapper.map(account, AccountDto.class);
}
private Account convertToModel(AccountDto accountDto) {
return modelMapper.map(accountDto, Account.class);
}
}
框架层分环境配置
异步
- 异步方法调用不能和调用方的Bean中(?)
- 调用和被调用方在不同的线程,需进行线程上下文信息复杂工作
AppConfig
@Configuration
@EnableAsync
@Import(value = {StaffjoyRestConfig.class})
@SuppressWarnings(value = "Duplicates")
public class AppConfig {
public static final String ASYNC_EXECUTOR_NAME = "asyncExecutor";
@Bean(name=ASYNC_EXECUTOR_NAME)
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// for passing in request scope context
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
ContextCopyingDecorator
// https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
RequestAttributes context = RequestContextHolder.currentRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(context);
runnable.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
};
}
}
AccountService
@Service
@RequiredArgsConstructor
public class AccountService {
public AccountDto create(String name, String email, String phoneNumber) {
//....
serviceHelper.syncUserAsync(account.getId());
//...
}
}
ServiceHelper
@RequiredArgsConstructor
@Component
public class ServiceHelper {
@Async(AppConfig.ASYNC_EXECUTOR_NAME)
public void syncUserAsync(String userId) {
//...
}
}
主流服务框架概览
网关
BFF: Backend for Frontend
- 优点
- 前后端独立变化
- 前端只需要知道BFF的域名即可,不需要知道后端众多微服务的域名
- BFF只需要一个对外域名,开销小
- 所有微服务都在BFF后面,不会直接暴露在公网,风险小
- 聚合裁剪、适配逻辑可在BFF实现
- 缺点
- 各业务线逻辑聚合,难维护
- 单点
网关 和 反向代理
- 网关,如Zuul
- 动态可配置
- 面向API及微服务
- 反向代理,如Nginx
- 静态配置,不灵活
- 运维配置,效率低效
- 网页流量
- 不同历史阶段主要产品
- Web时代:HAProxy、Nginx
- 微服务时代:网关(Zuul)、反向代理(Nginx)
- 云原生时代:Envoy、Traefik
Faraday 网关内核设计
生产级网关
- 限流熔断
- 动态路由和负载均衡
- 基于Path的路由
- 截获器链
- 日志采集 和 Metrics埋点
- 响应流优化
主流网关
安全架构
单体
- cookie+session:只会在登录过的服务器才有session
- Sticky Session:需要负载均衡器支持,统一session请求,打到一个server(nginx支持)
- session同步复制机制
- 无状态回话:数据存于浏览器端,但浏览器cookie有大小限制
- 集中状态会话:服务器端session统一存于一处(如 redis),高扩展,高可用
微服务
Token
Token+网关
JWT
- Json Web Token
- JWT: Header+Payload+Signature
- base64(Header)+”.”+base64(Payload)+”.”+base64(Signature)
- 不保证传输的安全性,但保证其不可篡改性
- jwt.io
- 优势
- 紧凑轻量
- 对AuthServer压力小
- 简化 AuthServer实现
- 不足
- 无状态和吊销无法两全
- 传输开销
权限参考模型
源码剖析
- Sessions in xyz.staffjoy.common.auth
包含 loginUser, logout, getToken, 把jwt存于cookie - Sign in xyz.staffjoy.common.crypto
生成jwt/token的相关代码 - AuthRequestInterceptor in xyz.staffjoy.faraday.core.interceptor
网关拦截器,获得sessionToken。如果需要登录,重定向到登录界面。登录用户的 userId会写入Header中 - FeignRequestHeaderInterceptor in xyz.staffjoy.common.auth
微服务拦截器,从header中获取userId - AuthContext in xyz.staffjoy.common.auth
帮助函数,从 RequestContextHolder.getRequestAttributes() 中获得 Request Header 的相关信息
调用网关必须进行Auth,内部服务之间,只需要带header,比较服务请求是谁就可以了(user_id)
测试
单元测试
+ junit、mockito
+ 测试对象未类或者函数
+ 使用h2内存数据库测试
集成测试
- 关注交互,不稳定性高
- 测试核心在于测试系统边界交互问题
组件测试:
- 内部mock:wiremock,mockbean、
- 外部mock:hoverfly, mbtest
端到端测试(End-to-End Test)
- web gui 测试工具:selenium webdriver
- api 测试工具:rest-assured
- 80/20,聚焦核心业务服务
- 用例测试
锲约测试
- pact、spring-cloud-contract
- 国内不流行
feign
调用链和监控
作者推荐CAT
结构化日志
- StaffjoyConfig
@PostConstruct
public void init() {
// init structured logging
StructLog4J.setFormatter(JsonFormatter.getInstance());
// global log fields setting, 所有日志都会包含env, service 信息
StructLog4J.setMandatoryContextSupplier(() -> new Object[]{
"env", activeProfile,
"service", appName});
}
- AccountService
public AccountDto create(String name, String email, String phoneNumber) {
// ...
LogEntry auditLog = LogEntry.builder()
.authorization(AuthContext.getAuthz())
.currentUserId(AuthContext.getUserId())
.targetType("account")
.targetId(account.getId())
.updatedContents(account.toString())
.build();
logger.info("created account", auditLog);
// ...
}
集中异常监控 和 Sentry
- StaffjoyConfig
@Bean
public SentryClient sentryClient() {
SentryClient sentryClient = Sentry.init(staffjoyProps.getSentryDsn());
sentryClient.setEnvironment(activeProfile);
sentryClient.setRelease(staffjoyProps.getDeployEnv());
sentryClient.addTag("service", appName);
return sentryClient;
}
@PreDestroy
public void destroy() {
sentryClient().closeConnection();
}
- StaffjoyProps
@ConfigurationProperties(prefix="staffjoy.common")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class StaffjoyProps {
@NotBlank
private String sentryDsn;
@NotBlank
// DeployEnvVar is set by Kubernetes during a new deployment so we can identify the code version
private String deployEnv;
}
- ServiceHelper
public void handleError(ILogger log, String errMsg) {
log.error(errMsg);
if (!envConfig.isDebug()) {
sentryClient.sendMessage(errMsg);
}
}
public void handleException(ILogger log, Exception ex, String errMsg) {
log.error(errMsg, ex);
if (!envConfig.isDebug()) {
sentryClient.sendException(ex);
}
}
SwitchHosts
Skywalking
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/34241.html