1. 引言

作为开发者,我们总是在寻找使用某个技术或库的最佳实践。自然地,有时候也会产生一些争论。

其中一个常见的讨论点是:Spring 的 @Service 注解到底应该放在接口、抽象类还是具体实现类上?

由于 Spring 提供了多种方式来定义 Bean,因此合理使用这类“模板注解”(stereotype annotations)就显得尤为重要。

本文将深入探讨 @Service 注解的使用场景,并分析它放在接口、抽象类和具体类上的区别。

2. 将 @Service 放在接口上 ✅❌

有些开发者倾向于把 @Service 注解放在接口上,理由如下:

  • 明确表示该接口仅用于业务服务层
  • 便于后续新增实现类时自动被 Spring 扫描注册为 Bean

来看一个例子:

@Service
public interface AuthenticationService {

    boolean authenticate(String username, String password);
}

这样做的确能增强接口的语义性,明确它是一个服务接口。但问题也随之而来。

⚠️ 弊端在于:这会让我们的接口依赖于 Spring 框架本身,形成不必要的耦合。

为了验证这种做法是否有效,我们创建一个实现类:

public class InMemoryAuthenticationService implements AuthenticationService {

    @Override
    public boolean authenticate(String username, String password) {
        // 实现逻辑
    }
}

注意:这个实现类并没有标注 @Service,我们只在接口上加了。

然后运行一个基础的 Spring Boot 应用:

@SpringBootApplication
public class AuthApplication {

    @Autowired
    private AuthenticationService authService;

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

结果是:

❌ 启动失败,抛出异常:

org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type 'com.baeldung.annotations.service.interfaces.AuthenticationService' available: 
expected at least 1 bean which qualifies as autowire candidate.

所以,结论很明确:

❌ 在接口上加 @Service 并不能让其实现类被自动扫描注册为 Bean。

3. 将 @Service 放在抽象类上 ✅❌

在抽象类上使用 @Service 的情况比较少见。

我们来测试一下这种方式是否能让子类被自动识别为 Bean。

首先定义一个抽象类并加上 @Service

@Service
public abstract class AbstractAuthenticationService {

    public boolean authenticate(String username, String password) {
        return false;
    }
}

接着创建一个具体的实现类,但不加任何注解:

public class LdapAuthenticationService extends AbstractAuthenticationService {

    @Override
    public boolean authenticate(String username, String password) { 
        // 具体实现
    }
}

修改主应用类,尝试注入抽象类型:

@SpringBootApplication
public class AuthApplication {

    @Autowired
    private AbstractAuthenticationService authService;

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

运行后再次报错:

org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No qualifying bean of type 'com.baeldung.annotations.service.abstracts.AbstractAuthenticationService' available

❌ 同样无效!Spring 不会因为父类有 @Service 就把子类注册为 Bean。

因此:

❌ 把 @Service 加在抽象类上对组件扫描没有效果。

4. 将 @Service 放在具体类上 ✅✅

与前面两种方式不同,把 @Service 放在具体实现类上才是最常见也最推荐的做法。

这样做的目的是告诉 Spring:

  • 这个类是一个组件(Component)
  • 它属于服务层(Service Layer)

Spring 会在启动时通过组件扫描将其注册为 Bean。

来看两个示例:

@Service
public class InMemoryAuthenticationService implements AuthenticationService {

    @Override
    public boolean authenticate(String username, String password) {
        // 实现逻辑
    }
}

@Service
public class LdapAuthenticationService extends AbstractAuthenticationService {

    @Override
    public boolean authenticate(String username, String password) {
        // 实现逻辑
    }
}

注意:这两个类都各自标注了 @Service,即使它们继承自不同的父类或实现不同接口。

最后,在主应用中注入这两个 Bean:

@SpringBootApplication
public class AuthApplication {

    @Autowired
    private AuthenticationService inMemoryAuthService;

    @Autowired
    private AbstractAuthenticationService ldapAuthService;

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

✅ 成功启动!Spring 正确识别并注册了这两个 Bean。

5. 结论总结

经过以上几种方式的对比,我们可以得出如下结论:

✅ 只有在具体实现类上使用 @Service 注解,才能被 Spring 的组件扫描机制正确识别。

🚫 在接口或抽象类上使用 @Service 是无效的,即便这些类被实现或继承,Spring 也不会自动注册其子类。

📘 Spring 官方文档也指出,使用 @Service 的最佳实践是将其放在实际的服务实现类上,以便进行自动扫描。

6. 总结

在这篇文章中,我们分析了 Spring 中 @Service 注解的不同使用位置:

使用位置 是否生效 说明
接口 不会被扫描,增加框架依赖
抽象类 同样不会被扫描
具体实现类 最佳实践,可被自动注册

如你所见,只有将 @Service 注解放在具体类上,才能真正发挥它的作用。

📌 记住一句话:注解要贴到真正干活的类上,别贴到“蓝图”上。

所有代码示例均可在 GitHub 上找到。


原始标题:Where Should the Spring @Service Annotation Be Kept?

« 上一篇: Java周报,第370期
» 下一篇: JVM 中的常量池介绍