技术阅读周刊,每周更新。
历史更新 A Comprehensive guide to Spring Boot 3.2 with Java 21, Virtual Threads, Spring Security, PostgreSQL, Flyway, Caching, Micrometer, Opentelemetry, JUnit 5, RabbitMQ, Keycloak Integration, and More! (10/17) | by Jonathan Chevalier | Nov, 2023 | Medium URL: https://medium.com/@jojoooo/exploring-a-base-spring-boot-application-with-java-21-virtual-thread-spring-security-flyway-c0fde13c1eca#551c
本文讲解了基于最新的 Spring Boot3.2 和 Java 21 所使用到的技术栈
数据库 数据库使用 Postgres15
和 flyway 来管理数据库 schema 的迁移。
异常处理 Spring6 实现了新的 RFC9457 规范,实现以下接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Slf4j @ControllerAdvice @RequiredArgsConstructor public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleMethodArgumentNotValid ( @NonNull final MethodArgumentNotValidException ex, @NonNull final HttpHeaders headers, @NonNull final HttpStatusCode status, @NonNull final WebRequest request) { log.info(ex.getMessage(), ex); final List<ApiErrorDetails> errors = new ArrayList <>(); for (final ObjectError err : ex.getBindingResult().getAllErrors()) { errors.add( ApiErrorDetails.builder() .pointer(((FieldError) err).getField()) .reason(err.getDefaultMessage()) .build()); } return ResponseEntity.status(BAD_REQUEST) .body(this .buildProblemDetail(BAD_REQUEST, "Validation failed." , errors)); } private ProblemDetail buildProblemDetail ( final HttpStatus status, final String detail, final List<ApiErrorDetails> errors) { final ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, StringUtils.normalizeSpace(detail)); if (CollectionUtils.isNotEmpty(errors)) { problemDetail.setProperty("errors" , errors); } return problemDetail; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "type" : "about:blank" , "title" : "Bad Request" , "status" : 400 , "detail" : "Validation failed." , "instance" : "/management/companies" , "errors" : [ { "pointer" : "name" , "reason" : "must not be blank" } , { "pointer" : "slug" , "reason" : "must not be blank" } ] }
应用异常 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Getter public class RootException extends RuntimeException { @Serial private static final long serialVersionUID = 6378336966214073013L ; private final HttpStatus httpStatus; private final List<ApiErrorDetails> errors = new ArrayList <>(); public RootException (@NonNull final HttpStatus httpStatus) { super (); this .httpStatus = httpStatus; } public RootException (@NonNull final HttpStatus httpStatus, final String message) { super (message); this .httpStatus = httpStatus; } } @ExceptionHandler(RootException.class) public ResponseEntity<ProblemDetail> rootException (final RootException ex) { log.info(ex.getMessage(), ex); final ProblemDetail problemDetail = this .buildProblemDetail( ex.getHttpStatus(), API_DEFAULT_REQUEST_FAILED_MESSAGE, ex.getErrors()); return ResponseEntity.status(ex.getHttpStatus()).body(problemDetail); } { "type" : "about:blank" , "title" : "Internal Server Error" , "status" : 500 , "detail" : "Request failed." , "instance" : "/back-office/hello-world" }
异常降级 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(Throwable.class) public ProblemDetail handleAllExceptions (final Throwable ex, final WebRequest request) { log.warn(ex.getMessage(), ex); this .slack.notify(format("[API] InternalServerError: %s" , ex.getMessage())); return this .buildProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, API_DEFAULT_ERROR_MESSAGE); } { "type" : "about:blank" , "title" : "Internal Server Error" , "status" : 500 , "detail" : "Something went wrong. Please try again later or enter in contact with our service." , "instance" : "/back-office/hello-world" }
当有无法处理的异常时,就需要配置一个兜底的异常。
缓存 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public interface CompanyRepository extends JpaRepository <Company, Long> { String CACHE_NAME = "company" ; @NonNull @Cacheable(value = CACHE_NAME, key = "{'byId', #id}") @Override Optional<Company> findById (@NonNull Long id) ; @Cacheable(value = CACHE_NAME, key = "{'bySlug', #slug}") Optional<Company> findBySlug (String slug) ; @Caching( evict = { @CacheEvict(value = CACHE_NAME, key = "{'byId', #entity.id}"), @CacheEvict(value = CACHE_NAME, key = "{'bySlug', #entity.slug}"), }) @Override <S extends Company > @NonNull S save (@NonNull S entity) ; @NonNull @CacheEvict(cacheNames = CACHE_NAME, allEntries = true) @Override <S extends Company > List<S> saveAll (@NonNull Iterable<S> entities) ; @Caching( evict = { @CacheEvict(value = CACHE_NAME, key = "{'byId', #entity.id}"), @CacheEvict(value = CACHE_NAME, key = "{'bySlug', #entity.slug}"), }) @Override void delete (@NonNull Company entity) ; @CacheEvict(cacheNames = CACHE_NAME, allEntries = true) @Override void deleteAll (@NonNull Iterable<? extends Company> entities) ; }
Spring 提供了标准的缓存接口,即便是后续需要切换到 Redis,使用的 API 和注解都不会发生改变。
线程 Java21 后支持了虚拟线程,几乎可以无限的实现线程,在 Spring Boot 3.2 需要单独开启。
1 spring.threads.virtual.enabled
可观测性 1 2 3 4 5 6 7 8 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-actuator</artifactId > </dependency > <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-registry-prometheus</artifactId > </dependency >
1 2 3 4 5 spring: endpoints: web: exposure: include: info, health, prometheus, metrics
注意在生成环境不要暴露管理 API
Trace 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <dependency > <groupId > io.micrometer</groupId > <artifactId > micrometer-tracing-bridge-otel</artifactId > </dependency > <dependency > <groupId > net.ttddyy.observation</groupId > <artifactId > datasource-micrometer-spring-boot</artifactId > <version > ${datasource-micrometer.version}</version > </dependency > <dependency > <groupId > io.opentelemetry</groupId > <artifactId > opentelemetry-exporter-otlp</artifactId > <version > ${opentelemetry-exporter-otlp.version}</version > </dependency >
同步请求的时候每个请求都会带上 traceId
和 spanId
,如果是异步请求时候需要配置:spring.reactor.context-propagation=true
如果使用 @Async
时:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration public class TaskExecutorConfig { @Bean public TaskExecutor simpleAsyncTaskExecutor () { final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor (); taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator ()); return taskExecutor; } }
本地测试时候可以使用 Otel Desktop Viewer
1 2 3 4 5 6 7 management: tracing: sampling: probability: 1 otlp: tracing: endpoint: http://localhost:4317
Rust Vs Go: A Hands-On Comparison URL: https://www.shuttle.rs/blog/2023/09/27/rust-vs-go-comparison
动手比较 Rust 和 Go
本文是通过编写一个 web 服务来进行比较的。
Go 更加简单易学,同时标准库非常强大,只需要配合 gin+sqlx 这两个第三方库就能实现一个 web 服务 Rust也可以快速的构建一个安全的 web 服务,但需要依赖许多第三方库,比如http/JSON/模板引擎/时间处理等 但 Rust 在异常处理方面心智负担更低,代码更容易阅读。 如果是一个初创小团队,使用 Go 的上手难度确实更低; 但如果团队愿意花时间投入到 Rust 中,结合他出色的错误处理,和强大的编译检查,长时间来看会得到更好的效果。 为什么要使用 Go 语言?Go 语言的优势在哪里? - 知乎 URL: https://www.zhihu.com/question/21409296/answer/1040884859
图文并茂,讲解了 G-M-P 各自之间的关系,以及调度模型。
G: Goroutine ,用户创建的协程,图中搬运的砖头。 M: Machine ,OS 内核的线程的抽象,代表真正执行的资源;对应到就是图中的地鼠,地鼠不能用户直接创建;得是砖头 G 太多,地鼠 M 本身太少,同时还有空闲的小车 P,此时就会从其他地方借一些地鼠 M 过来直到把小车 P 用完为止。 P: Processor 处理器,G 只有绑定到 P 才能被调度;P 是图中的小车,由用户设置的 GoMAXPROCS
决定小车的数量。 文章链接:
#Newletters