如果不允许使用XML也不允许使用注解,你会怎么使用 Spring 呢

95 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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++ 甚至都要推出相关的标准库了。所以也是想借此机会提醒自己放下成见,不要预设太多立场去看待各种技术。