Why @Service on a method won't compile
While wiring a Spring AI controller, I put @Service on a method. Eclipse refused before I could run anything:
The annotation @Service is disallowed for this location
Spring hasn't started at this point, so this is the Java compiler talking. I'd used @Service for years without asking what it actually is. The answer is eight lines long, and it explains most of how Spring finds your classes.
1. An annotation declares where it's allowed
This is Service.java in the Spring Framework source:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}
| @Target(TYPE) | Allowed on classes, interfaces, enums and records only. Methods aren't listed, so javac rejects the code. This line is the error. |
| @Retention(RUNTIME) | Kept in the .class file. With SOURCE it would be thrown away during compilation, and Spring would never see it. |
| @Component | An annotation on an annotation, called a meta-annotation. This is the entire mechanism behind Spring stereotypes. |
2. What Spring does with it at startup
Take one class and follow it through the application context:
package com.example.advice;
@Service
public class OfferService {
OfferService(PricingRules rules) { ... }
}
.class under com/example/**@Component present, directly or meta?"offerService"PricingRules injected- Scan.
@SpringBootApplicationincludes@ComponentScanfor its own package and everything below it. AClassPathBeanDefinitionScannerresolves that package into a list of class files. - Read the bytes, not the class. Each file's annotation metadata is read directly from its bytecode. Loading every class into the JVM just to inspect it would be slow, and it would run static initializers that have no business running at scan time.
- Follow the meta-annotations. The filter doesn't look for
@Serviceat all. It asks "is@Componentpresent, on the class or on any of its annotations?"@Servicecarries@Component, so the answer is yes. - Register a recipe. Spring stores a bean definition: which class to build, its scope and its name. The name is the class name with a lowercase first letter,
offerService. No object exists yet. - Instantiate. When the context refreshes, Spring resolves the constructor, finds a
PricingRulesbean to inject, creates the object and runs the post-processors. Now it's a bean.
So @Service has no behavior of its own. It's @Component with a name that tells you which layer the class is in. And since the scanner works on classes, a method was never something it could pick up.
3. On a method, use @Bean
If a method should produce a bean, that's the job of @Bean, whose @Target includes METHOD:
@Configuration
public class AiConfig {
@Bean
ChatClient advisorClient(ChatClient.Builder builder) {
return builder.defaultSystem("You are a freight advisor.").build();
}
}
Now the recipe is "call advisorClient()". There's a subtlety: Spring subclasses a @Configuration class at runtime with CGLIB. If one @Bean method calls another, the call is intercepted and returns the existing singleton instead of building a second instance.
So why have stereotypes at all?
If @Service is just @Component, why not use @Component everywhere?
- Intent you can target. The name tells a reader which layer a class belongs to, and an aspect can target that layer, for example every
@Service. - Real behavior where it matters.
@Repositoryisn't only a label. A post-processor wraps those beans so that vendor-specific persistence exceptions are translated into Spring'sDataAccessExceptionhierarchy.
One @Target line revealed the whole model. Annotations are metadata. Retention decides who gets to see them. Spring reads them from bytecode, and a stereotype is a meta-annotated @Component. The next part of this series follows what happens after step 5: post-processors, proxies, and why @Transactional does nothing when a method calls another method on the same object.
Source
- stereotype/Service.java on GitHub
- ClassPathBeanDefinitionScanner.java on GitHub