技术阅读周刊,每周更新。
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规范,实现以下接口:
@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Process @Valid
@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));
// Adds errors fields on validation errors, following RFC 9457 best practices.
if (CollectionUtils.isNotEmpty(errors)) {
problemDetail.setProperty("errors", errors);
}
return problemDetail;
}
{
"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"
}
]
}
@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);
// Uses default message, can be adapted to use ex.getMessage().
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"
}
@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"
}
当有无法处理的异常时,就需要配置一个兜底的异常。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
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);
/*
* This cache implementation is only valid if the table is not
* frequently updated since it will clear the cache at every update operation
* If you want to be more performant you can use something like https://github.com/ms100/cache-as-multi
* */
@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);
/*
* This cache implementation is only valid if the table is not
* frequently updated since it will clear the cache at every delete operation
* If you want to be more performant you can use something like https://github.com/ms100/cache-as-multi
* */
@CacheEvict(cacheNames = CACHE_NAME, allEntries = true)
@Override
void deleteAll(@NonNull Iterable<? extends Company> entities);
}
Spring 提供了标准的缓存接口,即便是后续需要切换到 Redis,使用的 API 和注解都不会发生改变。
Java21 后支持了虚拟线程,几乎可以无限的实现线程,在 Spring Boot 3.2 需要单独开启。
spring.threads.virtual.enabled
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
spring:
endpoints:
web:
exposure:
include: info, health, prometheus, metrics
image.png
注意在生成环境不要暴露管理 API
<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
时:
@Configuration
public class TaskExecutorConfig {
/*
* Override default SimpleAsyncTaskExecutor to provide context propagation in @Async function
* */
@Bean
public TaskExecutor simpleAsyncTaskExecutor() {
final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
return taskExecutor;
}
}
本地测试时候可以使用 Otel Desktop Viewer
management:
tracing:
sampling:
probability: 1
otlp:
tracing:
endpoint: http://localhost:4317
image.png
URL: https://www.shuttle.rs/blog/2023/09/27/rust-vs-go-comparison
动手比较 Rust 和 Go
image.png
本文是通过编写一个 web 服务来进行比较的。
URL: https://www.zhihu.com/question/21409296/answer/1040884859
图文并茂,讲解了 G-M-P 各自之间的关系,以及调度模型。
image.png
GoMAXPROCS
决定小车的数量。文章链接:
点分享
点收藏
点点赞
点在看
本文分享自 crossoverJie 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!