@Service注解应该放在哪里?接口还是实现类?

202

我正在使用Spring开发应用程序。 我需要使用 @Service 注释。我有一个 ServiceI 和 ServiceImpl,其中 ServiceImpl 实现了 ServiceI。我现在很困惑:应该在接口还是实现中使用 @Service 注释?这两种方法之间有什么区别?

应该在实现类上使用 @Service 注释。如果你在接口上使用它,Spring将不会看到该注释并且应用程序将不会正常工作。使用接口和实现类进行注释的主要区别在于,使用接口注释时,任何实现都将被标记为服务,而使用实现类注释时,只有该具体实现会被标记为服务。


这是我对类似帖子的回答:https://stackoverflow.com/questions/38618995/need-of-dao-and-service-interfaces/48245959#48245959 - Agustí Sánchez
10个回答

220
我从不在接口上使用@Component(或@Service,...),因为这会使接口变得无用。让我解释一下原因。
主张1:如果您有一个接口,则要将该接口用作注入点类型。
主张2:接口的目的是定义可以由多个实现实现的合同。另一方面,您有注入点(@Autowired)。仅具有一个接口和仅有一个实现它的类是没有用的,并且违反了 YAGNI
事实:当您将以下内容放置时:
  • @Component(或@Service,...)在接口上,
  • 有多个实现它的类,
  • 至少有两个类成为Spring Bean,并且
  • 有一个注入点使用接口进行基于类型的注入,
那么您将获得NoUniqueBeanDefinitionException(或者您有非常特殊的配置设置,包括环境、配置文件或限定符...)
结论:如果您在接口上使用@Component(或@Service,...),则必须违反这两个声明中的至少一个。因此,我认为在接口级别上放置@Component并不有用(除了一些罕见的情况)。

Spring-Data-JPA仓库接口是完全不同的东西


4
你所写的内容非常有趣……那么哪种方法是正确的呢?是根本不注释接口,而将服务注释给实现吗?Spring是否仍能够使用接口类型进行自动装配?你对这个问题的回答是什么?https://dev59.com/Rmcs5IYBdhLWcg3wYy9T#12899432 - corlaez
9
为什么在只有一个类实现服务层时,我们需要为其创建接口呢?我已经看到很多项目都有控制器层、服务层(服务接口、服务实现)和存储层。 - Yubaraj
6
@Yubaraj:观点很中肯,但是你的问题是关于另一个话题的(我回答时假设:有一个接口,问题不是如何获得接口而是在哪里放置注释)。至于你的问题:对于一个永远不会有两个实现的业务服务类,几乎没有理由需要一个接口。(顺便提一下:只要你不为他人构建API,你总是可以重构代码并在需要时稍后引入接口) - Ralph
2
@Yubaraj,接口允许在需要时制作基于轻量级接口的JDK代理到bean。当没有接口时,Spring必须使用子类化或使用cglib修改bean来制作代理。@Transactional是使用bean代理的示例之一。AOP是另一个示例。 - Yoory N.
1
尽管有时您可能没有超过一个实现类,但我仍然更喜欢在接口中声明我的方法。同事开发人员只需查看声明和文档即可轻松检查可用的服务,而不必担心实现。 - HFSDev
显示剩余6条评论

37
基本上像@Service@Repository@Component这样的注释,它们都具有相同的目的:

当使用基于注解的配置和类路径扫描时进行自动检测。

根据我的经验,我总是在接口或抽象类上使用@Service注释,并在其实现中使用@Component@Repository等注释。我在那些服务基本目的、简单Spring bean的类上使用@Component注释。当我需要与数据库通信、执行某些事务等操作时,我会在DAO层中使用@Repository注释。

因此,我建议根据功能使用@Service注释您的接口和其他层。


13
你能否说明在注解接口和注解实现之间有哪些区别? - TheKojuEffect
42
这个注解是@Component的一种特殊形式,目的是让实现类通过类路径扫描进行自动检测。因此建议在实现类上使用该注解。 - nbrooks
1
@TheKojuEffect,本篇文章详细解释了接口注释和实现注释之间的区别 - https://dev59.com/63A75IYBdhLWcg3wxb_V - Mahesh
1
@user3257644请注意,该帖子中回答所给出的建议仅适用于'@Transactional'注解,而非所有注解。 - Jonathan
3
在接口上使用@service注释没有任何效果,就像其他范畴注释一样。所有的范畴注释都应该放在抽象类或具体类上。 - bigfoot
@bigfoot,纠正一下你的说法,服务注释也不能与抽象类一起使用。只能与具体类一起使用。 - chepaiytrath

22
我只在实现类上使用了@Component、@Service、@Controller和@Repository注释,而没有在接口上使用。但是对于接口,仍可以使用@Autowired注释。如果您的接口只有一个实现,则Spring组件扫描会自动找到它,并且只需@Autowired注释即可。如果有多个实现,则需要在@Autowired注释中使用@Qualifier注释来在注入点注入正确的实现。

19

1. 接口上的@Service注解

@Service
public interface AuthenticationService {

    boolean authenticate(String username, String password);
}

通常情况下,这是可以的,但有一个缺点。通过在接口上使用Spring的@Service,我们创建了额外的依赖,并将我们的接口与外部库耦合起来。
接下来,为了测试我们新服务bean的自动检测功能,让我们创建一个AuthenticationService的实现:
public class InMemoryAuthenticationService implements AuthenticationService {

    @Override
    public boolean authenticate(String username, String password) {
        //...
    }
}

我们应该注意到,我们的新实现InMemoryAuthenticationService没有@Service注解。我们只在接口AuthenticationService上保留了@Service
因此,让我们使用基本的Spring Boot设置来运行我们的Spring上下文:
@SpringBootApplication
public class AuthApplication {

    @Autowired
    private AuthenticationService authService;

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

当我们运行应用程序时,可能会出现臭名昭著的NoSuchBeanDefinitionException错误,Spring上下文无法启动。
因此,在接口上放置@Service并不足以自动检测Spring组件。

2. 在抽象类上使用@Service注解

在抽象类上使用@Service注解并不常见。

我们将从头开始定义一个抽象类,并将@Service注解放在其中:

@Service
public abstract class AbstractAuthenticationService {

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

接下来,我们扩展AbstractAuthenticationService以创建一个具体的实现,而不需要对其进行注释:
public class LdapAuthenticationService extends AbstractAuthenticationService {

    @Override
    public boolean authenticate(String username, String password) { 
        //...
    }
}

因此,我们还需要更新AuthApplication,注入新的服务类:
@SpringBootApplication
public class AuthApplication {

    @Autowired
    private AbstractAuthenticationService authService;

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

在运行AuthApplication后,Spring上下文没有启动。最终出现了相同的NoSuchBeanDefinitionException异常。

因此,在抽象类上使用@Service注释在Spring中没有任何效果。


3. 具体类上的@Service

与我们之前所见的相反,将实现类而不是抽象类或接口注释起来是一种很常见的做法。

这样,我们的目标主要是告诉Spring这个类将成为一个@Component并用一个特殊的构造型@Service进行标记。

因此,Spring将会自动从类路径中检测到这些类,并自动定义它们为托管bean。

因此,让我们这次在我们的具体服务类上放置@Service。我们将有一个实现我们接口的类和一个扩展我们之前定义的抽象类的第二个类:

@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) {
        //...
    }
}

我们需要注意的是,我们的AbstractAuthenticationService在这里没有实现AuthenticationService。因此,我们可以独立测试它们。
最后,我们将两个服务类添加到AuthApplication中并尝试使用:
@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。
您可以查看this页面获取其他解释。

3
感谢你对这个主题进行的精彩演示,正如其他有趣的帖子所描述的理论方法一样:它有助于理解必要的“关键”。 - Lemmy_Caution

7

将注解放在@Service上的优点是可以提示它是一个服务。我不知道是否有任何实现类会默认继承此注解。

缺点是您正在将接口与特定框架即Spring耦合,通过使用特定于spring的注解。由于接口应该与实现分离,因此我不建议在接口中使用任何框架特定的注解或对象。


1
我想我们都听过强耦合的论点很多次了,但请记住,注释可以存在于jar之外,因此只要您的耦合在注释上,它仍然可以解耦。 - Niels Bech Nielsen

7

我建议你在类上加上@Service注解,并将接口的名称作为参数放入注解中,例如:

interface ServiceOne {}

@Service("ServiceOne")
class ServiceOneImpl implements ServiceOne{}

这样做可以获得所有的好处,并且仍然可以注入接口,但是获得类。

@Autowired 
private ServiceOne serviceOne;

因此,您的接口并不与Spring框架绑定,您可以随时更改类而不必更新所有注入点。

所以,如果我想要更改实现类,我只需将新类注释并从第一个类中删除,但这就是要更改的全部内容。如果您注入该类,则每当要更改impl类时都需要进行大量工作。


2

Spring的一个好处是可以轻松切换服务(或其他)实现。为此,您需要在接口上进行注释,并声明变量,如下所示:

@Autowired
private MyInterface myVariable;

而不是:

@Autowired
private MyClassImplementationWhichImplementsMyInterface myVariable;

和第一种情况类似,您可以从实现唯一的时刻开始激活注入哪个实现。 在第二种情况下,您需要重构所有代码(新的类实现具有另一个名称)。 因此,尽可能将注解放在接口上。此外,JDK代理非常适合此操作:它们在应用程序启动时创建和实例化,因为运行时类型是事先已知的,与CGlib代理相反。


6
"我的类实现,它实现了我的接口" 哈哈 - inafalcao
1
第一个例子不需要在接口上进行注释才能工作。您可以使用@Service对实现进行注释并自动装配接口。Spring将检查实现此接口的任何对象。 - Marco

0
interface MyService {}

@Service
class MyServiceImpl implements MyService{}

@Autowired 
private MyService myService;

我在测试spring-boot 2.7.4上的结果是:

仅将@Service添加到接口不会创建名为MyService的Spring bean。这会导致Autowired出错。

@Service需要添加到实现类以创建bean com.*.service.impl.MyServiceImpl$$EnhancerBySpringCGLIB$$9140ae19。Spring将其连接到私有的MyService myService;


-3

有五个注释可用于创建Spring Bean。请参阅以下答案列表。

您真的需要接口吗? 如果您将为每个服务接口拥有一个实现,请只使用类,避免使用接口。当然,如果您没有RMI或需要接口代理时,则必须使用接口。

@Repository - 用于注入DAO层类。

@Service - 用于注入服务层类。在服务层中,您可能还需要使用@Transactional注释以进行数据库事务管理。

@Controller - 用于前端层控制器,例如作为Spring Bean注入的JSF托管Bean。

@RestController - 用于Spring REST控制器,这将帮助您避免在REST方法中每次都放置@ResponseBody和@RequestBody注释。

@Component - 在任何其他情况下使用它,当您需要注入不是控制器、服务或DAO类的Spring Bean时。


是的,您需要在层之间的边界(例如数据访问层和服务层)上使用接口。它们使包含这些层实现的模块之间解耦松散。如果没有它们,提到的层的客户端必须知道具体类型,并且当您想要将BasicDao装饰为CachingDao时,您需要更改它们... - Igand

-4
简单来说: @Service 是用于服务层的注释类型。 @Repository 是用于持久化层的注释类型。 @Component 是一个通用的注释类型,用于告诉Spring在应用程序上下文中创建对象实例。可以为实例定义任何名称,缺省值是类名作为驼峰式命名法。

5
这些注释的含义并不是被寻求,而是它们应该放在界面或其实现上的位置。 - nanosoft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接