一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
原文:Annotation-free Spring (frankel.ch)
作者:frankel
翻译有改动
我们对技术栈的判断,如果不是大多数,都来自于第三方的意见或以前的经验。然而,我们似乎对它们很执着。很长时间以来(有时甚至是现在),我看到一些帖子详细说明Spring是如何糟糕的,因为它使用XML进行配置。不幸的是,他们懵懵懂懂地忽略了一个事实,即基于注解的配置已经存在了很久。可能是因为我最近读到的Spring不好的原因是一样的......因为注解。如果你属于这类人群,我有个消息要告诉你:你可以摆脱大多数注解,如果你使用Kotlin,那就更可以了。在这篇文章中,我想告诉你如何为Spring提供的不同功能删除注释。
我们对于不常用技术栈的判断大多来自外部的意见。并且这种成见难以消除。一直以来我都有看到一些发帖吐槽 Spring 如何糟糕,因为它使用 XML 做配置。然而,他们在不经意之间忽略了一个事实:基于注解的Spring 已经推出了很久。
不使用注解的 bean
我们倾向于使用注释的第一个地方是注册 Bean。 让我们看看如何去掉它们。 这涉及到几个步骤。我们将从以下代码开始。
@Service
public class MyService {}
@Service定型注解有两个功能。
它将MyService类标记为属于服务层。 它让框架知道这个类,这样它就会实例化一个新的对象,并使其在上下文中可用。
第一步是将注释从类中移到一个专门的配置类中。
public class MyService {}
@Configuration
public class MyConfiguration {
@Bean
public MyService service() {
return new MyService();
}
}
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
因为 @SpringBootApplication 为自身标注了 @Configuration, 所以我们可以进一步简化代码:
public class MyService {}
@SpringBootApplication
public class MyApplication {
@Bean
public MyService service() {
return new MyService();
}
// Run the app
}
在这里, MyService 类没有使用注解。对我来说这就足够了。 但我们最初的目标是移除所有的注解。
幸运的是 Kotlin 在这里提供了 Bean DSL. 上面的代码可以被重构成这样:
class MyService
fun beans() = beans {
bean<MyService>()
}
fun main(args: Array<String>) {
runApplication<MyApplication>(*args) {
addInitializers(beans())
}
}
@SpringBootApplication
class MyApplication
路由控制
我们的下一个功能在于 Web 路由控制。传统的 Spring 操作使用 Controller 注解:
@RestController
public class MyController {
@GetMapping("/hello")
public String hello() {
return "Hello";
}
}
重构不满足没有注解的目标。然而,从SpringWebMVC 5.0 开始,它提供了名为 routers 的代替控制器的方式。重构上述代码:
@Bean
RouterFunction<ServerResponse> hello() {
return route(GET("/hello"),
req -> ServerResponse.ok().body("Hello"));
}
你可以反对说这里仍然有一个注解。 但是我们可以借助 Kotlin来实现这个目标,Spring 为路由也提供DSL。
bean {
router {
GET("/") { ok().body("Hello") }
}
}
Cross-cutting concerns
许多甚至可以说全部的 Spring 的切点配置都是通过注解实现的。例如事务管理和缓存。在这里我们使用缓存作为例子,但是相关的功能实现是相似的。
@Cacheable("things")
public Thing getAddress(String key) {
// Get the relevant Thing from the data store
}
Spring 通过代理包装使用了cacheable 的方法,当我们使用了被代理的方法时,他会首先检查对象是否在缓存中。
如果在则直接返回缓存中的对象,否则先调用方法,然后缓存它。 我们可以通过自行实现该逻辑来回避注解。
public class ThingRepository {
private final Cache cache;
public ThingRepository(Cache cache) {
this.cache = cache;
}
public Thing getAddress(String key) {
var value = cache.get(key, Thing.class);
if (value == null) {
// Get Thing and return it
}
return value;
}
}
如果我们是函数式编程爱好者,我们可以把它重构为更符合我们口味的形式。
public class ThingRepository {
private final Cache cache;
public ThingRepository(Cache cache) {
this.cache = cache;
}
public Thing getAddress(String key) {
return Optional.ofNullable(cache.get(key, Thing.class))
.orElse(/* Get Thing */);
}
}
错误处理
Spring 通过注解提供了丰富的错误处理机制来降低开发者的负担。具体可以参考:Spring 错误处理 。
下面是使用相关注解的一个例子
@RestController
public class MyController {
private final MyService service;
public MyController(MyService service) {
this.service = service;
}
@GetMapping("/hello")
public String hello() {
service.hello();
}
@GetMapping("/world")
public String world() {
service.world();
}
@ErrorHandler
public ResponseEntity<String> handle(ServiceException e) {
return ResponseEntity(e.getMessage(),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Spring 在上述请求处理中抛出 ServiceException 时调用该方法
同样地,这里可以自行实现
@RestController
public class MyController {
private final MyService service;
public MyController(MyService service) {
this.service = service;
}
@GetMapping("/hello")
public ResponseEntity<String> hello() {
try {
return ResponseEntity(service.hello(), HttpStatus.OK);
} catch (ServiceException e) {
return handle(e);
}
}
@GetMapping("/world")
public String world() {
try {
return ResponseEntity(service.world(), HttpStatus.OK);
} catch (ServiceException e) {
return handle(e);
}
}
private ResponseEntity<String> handle(ServiceException e) {
return ResponseEntity(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
我认为这里稍微有些繁琐,因此使用路由再做些改进。
@Bean
public RouterFunction<ServerResponse> hello(MyService service) {
return route(GET("/hello"),
req -> {
try {
return ServerResponse.ok().body(service.hello());
} catch (ServiceException e) {
return handle(e);
}
}).andRoute(GET("/world"),
req -> {
try {
return ServerResponse.ok().body(service.world());
} catch (ServiceException e) {
return handle(e);
}
});
}
private ServerResponse handle(ServiceException e) {
return ServerResponse.status(500).body(e.getMessage());
}
但是我不认为上面的代码重构得好。Kotlin的DSl在这里也没有什么好效果。
router {
fun handle(e: ServiceException) = status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.message)
GET("/hello") {
try {
ok().body(ref<MyService>().hello())
} catch (e: ServiceException) {
handle(e)
}
}
GET("/world") {
try {
ok().body(ref<MyService>().world())
} catch (e: ServiceException) {
handle(e)
}
}
}
我们没有使用任何得注解了,然而,私以为这里的代码可读性不如最初的代码。
我们可以重新设计 MyService,使用函数方法代替抛出异常以改进代码。最简单的方式是使用 kotlin 的 Result 类型。它包括被请求的值类型或者异常类型。 替代的类型包括 Arrow 或者 Vavr Either 类型.
class MyService {
fun hello(): Result<String> = // compute hello
fun world(): Result<String> = // compute world
}
var routes = router {
fun handle(e: ServiceException) = status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.message)
GET("/hello") {
ref<MyService>().hello().fold(
{ ok().body(it) },
{ handle(it as ServiceException) }
)
}
GET("/world") {
ref<MyService>().world().fold(
{ ok().body(it) },
{ handle(it as ServiceException) }
)
}
}
启动应用
至此,我们已经去除了所有的注解,除了唯一的一个 @SpringBootApplication。
注解中实际上有相当多的魔法。但是我们仍然有机会移除这最后一个注解。
不过这有一个前提,我们需要使用实验性的API。 这个方法是 Spring Fu。 "Fu" 意思是 functional. 它分别提供了Java和kotlin的版本。
下面是一个示例:
val app = webApplication {
messageSource {
basename = "messages/messages"
}
webMvc {
thymeleaf()
converters {
string()
resource()
jackson {
indentOutput = true
}
}
router {
resources("/webjars/**", ClassPathResource("META-INF/resources/webjars/"))
}
}
jdbc(DataSourceType.Generic) {
schema = listOf("classpath*:db/h2/schema.sql")
data = listOf("classpath*:db/h2/data.sql")
}
enable(systemConfig)
enable(vetConfig)
enable(ownerConfig)
enable(visitConfig)
enable(petConfig)
}
fun main() {
app.run()
}
总结
在本文中,我们尝试了去掉 Spring 中的全部注解。甚至我们还使用了实验性的API。
种框架、工具发展迅速、日新月异。很多时候我们对它们留下的印象已经成为了偏见。如果可行的话,我们可以定期更新认知,再做出评价。
写在结尾
翻译这篇文章的目的并不是为了推荐大家去使用 Kotlin 进行服务端开发。完全的切换到一门新的语言需要相当大的成本。只是偶然间发现 Spring 也没有我想象中的那样固步自封,类似地,一直以来没有标准的网络库的 C++ 甚至都要推出相关的标准库了。所以也是想借此机会提醒自己放下成见,不要预设太多立场去看待各种技术。