Your First Spring Application: XML and Annotation Wiring
No Spring Boot in this article — just Maven plus spring-context — so that you stand the Spring container up by hand exactly once: write one XML file saying "which objects exist and who needs whom", then ask the container for objects with getBean. There is only one comparison to master (XML versus annotations) and only two injection routes to tell apart (constructor injection via <constructor-arg>, property injection via <property>). Once this runs, every later claim of "Boot does it automatically" becomes a sequence of steps you can name out loud.
the Spring container is a vending machine. Building an object yourself means climbing inside the machine and manufacturing a bottle; using the container means the bottles are already stocked and you simply press the slot number (getBean("helloService")) and one drops out. The slot number is the bean's id — a wrong number, a mislabelled slot or an empty row is exactly the three classic failures in the error table of Section 17. And note the singleton case: a stocked item is not remade per press. The machine holds one bottle, so you and the next person pressing the same number get the very same one.
a BeanDefinition is the recipe, the bean is the dish on the table. At startup the container first copies every <bean> from your XML into a recipe (class name, constructor arguments, scope, whether it is served late), and files all of them in the registry (BeanDefinitionRegistry); only at refresh() does it cook, putting finished singletons into the warmer (singletonObjects). So the object you get from getBean is a finished dish — it is never cooked to order — which also explains why one wrong class ruins the whole table.

After this article you should be able to answer three questions:
- From the moment you write
new ClassPathXmlApplicationContext("applicationContext.xml"), which jobs has the container already done for me? - Both
<constructor-arg>and<property>"push a dependency in" — how do their creation timing and failure modes differ? - Which style suits which scenario, and can one container mix both?
Ask anyone how Spring wires a bean and the first answer is usually @SpringBootApplication. But that annotation hides too much — component scanning, auto-configuration and the embedded container are all folded into it, so you never see how the container is assembled layer by layer. In this article we deliberately avoid Spring Boot and stand up an ApplicationContext from scratch with Maven plus spring-context. Once you understand it, every piece of Boot "magic" becomes an explainable process.
Start with the dependency. The first question most people ask is: why not spring-core?
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>spring-xml-demo</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>6.1.6</spring.version> </properties> <dependencies> <!-- Declare just this one: the container itself --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> </dependencies></project>spring-contexttransitively pulls inspring-core,spring-beans,spring-aopandspring-expression; you never declare them by handspring-coreonly holds utilities (ClassUtils,Resource,OrderComparator) — it has no container;ApplicationContextlives inspring-context- Choosing
spring-contextgives you "container + bean factory + AOP + SpEL" in one shot, which is exactly what a Boot starter ends up assembling for you
Tip: to confirm the dependency tree, run mvn dependency:tree in the project root. You will see four sibling modules hanging off spring-context — that is "declare one, get a chain".
Fix the layout first so every file has a home:
spring-xml-demo/├── pom.xml└── src/main/ ├── java/com/example/ │ ├── HelloService.java │ ├── HelloServiceImpl.java │ ├── GreetingDao.java │ ├── SimpleDataSource.java │ └── Main.java └── resources/ └── applicationContext.xml
Interface first, then the implementation:
package com.example;public interface HelloService { String sayHello(String name);}The implementation deliberately keeps a constructor parameter — it is our probe for verifying that injection actually happened:
package com.example;public class HelloServiceImpl implements HelloService { private final String prefix; // No no-arg constructor: someone (the container) must pass prefix in public HelloServiceImpl(String prefix) { this.prefix = prefix; } @Override public String sayHello(String name) { return prefix + ", " + name + "!"; }}The entry class does only two things — start the container, fetch the bean:
package com.example;import org.springframework.context.ApplicationContext;import org.springframework.context.support.ClassPathXmlApplicationContext;public class Main { public static void main(String[] args) { // 1. Start the container: it reads applicationContext.xml from the classpath root ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); // 2. Fetch the bean by name -- note: NOT new HelloServiceImpl("Hello") HelloService service = ctx.getBean("helloService", HelloService.class); System.out.println(service.sayHello("Spring")); // 3. Close the container so all singleton destroy callbacks run ((ClassPathXmlApplicationContext) ctx).close(); }}Run Main and the console prints:
Hello, Spring!- The
Helloin the output is not hard-coded; it is theprefixinjected fromapplicationContext.xml - The second argument of
getBean("helloService", HelloService.class)performs a type check, failing fast instead of requiring a cast close()is not decoration: without it,@PreDestroyandDisposableBeancallbacks never fire
That one getBean call deserves its own look, because the same line has three possible endings: the name was never registered so it throws, a singleton comes straight out of the pool, or a prototype gets rebuilt from its blueprint every time. The animation walks all five steps, and the == check at the end is the quickest way to tell the first two apart:


Drop the following into src/main/resources/applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- (1) Simplest bean: id is its name in the container, class is the FQCN to instantiate --> <bean id="helloService" class="com.example.HelloServiceImpl"> <!-- (2) Constructor injection: index starts at 0, matching parameter position --> <constructor-arg index="0" value="Hello"/> </bean> <!-- (3) Property injection: no-arg construct first, then setter assignment --> <bean id="greetingDao" class="com.example.GreetingDao"> <property name="dataSource" ref="dataSource"/> <property name="timeout" value="3000"/> </bean> <!-- (4) The referenced bean (ref points at this id) --> <bean id="dataSource" class="com.example.SimpleDataSource" scope="singleton"/> <!-- (5) Prototype scope: every getBean returns a brand-new object --> <bean id="requestLogger" class="com.example.RequestLogger" scope="prototype"/></beans>id="helloService": the bean's unique name in the container;getBean("helloService")locates it by this<constructor-arg index="0" value="Hello"/>: constructor injection;indexis 0-based and most reliable when names repeat<property name="dataSource" ref="dataSource"/>: setter injection;refmeans "reference another bean", which must exist<property name="timeout" value="3000"/>:valueinjects a literal; Spring converts the string"3000"into anintscope="singleton": the default — one instance per container;prototypecreates a new one on every request
Trap: <property name="xxx"> matches a setter by method name via reflection — name="dataSource" requires a setDataSource(...). Omit it, misspell it, or expose only a field with no setter, and startup throws NotWritablePropertyException.
Those five lines describe the static shape. Reading one <bean> tag off the disk and filing it properly actually takes four steps — click through them, and pay attention to cell ③: until every blueprint is filed, not one object exists:
Beyond XML, Spring lets a Java class describe wiring. A @Configuration class is itself a bean, and a @Bean method's name is the bean name:
package com.example;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;@Configuration // declares a configuration class@ComponentScan("com.example") // scans this package and its subpackages for @Componentpublic class AppConfig { @Bean // the return value is registered; bean name = method name public HelloService helloService() { return new HelloServiceImpl("Hello"); }}Business classes carry @Service and declare what they need with @Autowired — note the use of constructor injection:
package com.example;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;@Service // a semantic variant of @Component: the service layerpublic class OrderService { private final HelloService helloService; @Autowired // optional for a single constructor since 4.3, but clearer public OrderService(HelloService helloService) { this.helloService = helloService; } public String greet(String user) { return helloService.sayHello(user); }}Startup switches to reading the config class:
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);OrderService orderService = ctx.getBean(OrderService.class);System.out.println(orderService.greet("Annotation"));@ComponentScanonly covers the configuration class's own package and below; a wrong package name is the number-one cause of "bean not found"@Service/@Repository/@Controllerare semantic aliases of@Componentand scan identically- Constructor injection lets
helloServicebefinal— once constructed, the dependencies are immutable and inherently thread-safe
| Aspect | XML wiring | Annotation wiring |
|---|---|---|
| Readability | Centralized; see everything at once | Lives beside the code; read locally |
| Refactoring | Breaks when a class/package is renamed | IDE rename and navigation work |
| Compile-time check | None; errors surface at startup | Missing class fails compilation |
| Cost of change | Edit config, no recompile | Edit code, rebuild |
| Best for | Infrastructure, third-party classes, ops-editable config | Your own business code, fast iteration |
the two are not mutually exclusive — XML and annotation beans can coexist in one container and share the same BeanDefinitionRegistry.

That table compares temperament. What actually trips people up is something far more concrete: for one given configuration, what is the word for it in each style. So play a round of pairing — the left column is what you will find in the legacy project you inherited, the right column is what you will write from article 6 onwards:
ApplicationContext is the high-level, user-facing interface. Its common implementations are categorized by where definitions come from:
| Implementation | Definition source | Typical use |
|---|---|---|
ClassPathXmlApplicationContext | XML on the classpath | Classic XML projects |
FileSystemXmlApplicationContext | XML at a filesystem path | External, editable config |
AnnotationConfigApplicationContext | Java config classes | Annotation / plain-Java projects |
AnnotationConfigWebApplicationContext | Java config classes | Web environments |
XmlWebApplicationContext | Referenced from web.xml | Legacy Servlet apps |
BeanFactory is the lowest-level container interface, with DefaultListableBeanFactory as the default implementation. The biggest difference is when instantiation happens:
// BeanFactory: create on demand (lazy)DefaultListableBeanFactory factory = new DefaultListableBeanFactory();// ^ only definitions are registered; no bean has been instantiated yet// ApplicationContext: pre-instantiate all singletons at startupApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");// ^ when construction returns, every non-lazy singleton already existsKey point: remember one sentence — BeanFactory is lazy; ApplicationContext pre-instantiates singletons at startup. On top of BeanFactory, ApplicationContext layers event publishing, i18n, resource loading and AOP auto-proxying, which is why real projects almost always use ApplicationContext.
With debug logging enabled, a typical startup looks like this:
10:21:33.114 [main] INFO o.s.c.s.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1b6d358610:21:33.182 [main] INFO o.s.b.f.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [applicationContext.xml]10:21:33.251 [main] INFO o.s.b.f.s.DefaultListableBeanFactory - Pre-instantiating singletons in ...: defining beans [helloService,greetingDao,dataSource,requestLogger]; root of factory hierarchyHello, Spring!Refreshing ...:refresh()is invoked, the single entry point that orchestrates every later phaseLoading XML bean definitions:XmlBeanDefinitionReaderturns each<bean>into aBeanDefinitiondefining beans [helloService, greetingDao, ...]: registration is done; the bracketed order mirrors "register, then instantiate"Pre-instantiating singletons: singleton pre-instantiation begins, so constructor logs appear after this lineHello, Spring!: main fetched the bean and called it — the whole wiring chain works
the component scan misses the package. @ComponentScan("com.example.service") scans only one subpackage, leaving an @Service under com.example.web orphaned; getBean then fails with NoSuchBeanDefinitionException: No qualifying bean of type .... First confirm where the class lives, then widen the scan path to the common parent.
a hand-constructed object is not managed by the container. new OrderService(new HelloServiceImpl("hi")) runs, but it is not in the container — @Autowired never fires, the AOP proxy never applies, and @Transactional is a no-op. Any class that needs container capabilities (injection, transactions, aspects) must be taken from the container, never built by hand.
your first circular-dependency error. A's constructor needs B and B's constructor needs A, so startup throws BeanCurrentlyInCreationException. Under XML, setter injection could break the cycle by exposing a half-built object early, so cycles often "looked fine"; switch to constructor injection and the cycle is exposed immediately. Before reaching for @Lazy, ask whether these two classes have tangled their responsibilities.
The demo below turns "how the container creates beans and when it skips them" into toggleable switches. Flip a condition and watch the wiring results change in a chain:
Once the buttons are done, ask the container yourself. This console is wired to the real in-browser kernel and every echoed line is computed on the spot — the chain it holds (dataSource → jdbcTemplate → userRepository → userService → dispatcherServlet) is a living specimen of the two injection routes from Section 3. The order below is the correct order for answering "did my bean actually get into the container":
cond changes a wiring condition, and the kernel swaps in a brand-new container and runs the creation flow again, so you do not have to type boot afterwards. The status column sliding back from "created" to "not created" in beans is the other face of the same fact — new container, empty pool. It is also why logs has to be re-read every time.
One step further: when you carry this wiring into a Boot project, the pom stops being "hand-pick three spring-* lines" and becomes "tick some starters". Do not copy it out — tick it once and read why the generator pairs each entry with what:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Section 7 read the log; this section puts the camera inside the container. This lab has four positions — click them in order. Start with "Read & parse" to watch a file on disk become something in memory; then "Register definitions" to see recipes clipped into the registry; then "getBean in action" for the instant you press the slot number; and finally "A typo in class", the most valuable of the four, because it explains why a single wrong letter stops the whole machine. After these four, the error table in Section 17 needs no memorising.
BeanDefinition shows up three times in this article, so it deserves its own look. It is the container's instruction sheet for a bean — everything you write in XML or annotations is first translated into it, and only then does instantiation begin. The four positions show: scanning and parsing, where scope / lazy end up, what the registry itself looks like, and the most useful of all — "Several candidates", which is the root of NoUniqueBeanDefinitionException when you call getBean(XxxService.class).

new ClassPathXmlApplicationContext(...) hides a refresh() — the single method that orchestrates container startup in twelve ordered actions. A beginner does not need to memorize all twelve; two facts carry almost everything: all definitions are registered before any object is built, and any failing step aborts the whole startup. Start with "The four that matter", then look at "When one step fails", and only expand to all twelve out of curiosity.
The difference between <constructor-arg> and <property> is not a style preference but two different creation routes: the first demands a matching constructor and fails outright when arguments are missing; the other calls the no-arg constructor for a half-built object and then assigns property by property, so the object always gets to exist. The fourth position, "With a cycle", explains the warning in Section 8 — why cycles booted fine in the setter era and explode immediately under constructor injection.
Before touching the code, change the config here. Two knobs: scope (singleton or prototype) and whether <constructor-arg> is present. The output tells you at once whether two getBean calls return the same object and whether the hashCode shapes line up — the quickest way to check "did my scope take effect".
getBean twice == truecom.example.HelloServiceImpl@1b6d3586com.example.HelloServiceImpl@1b6d3586# the constructor log prints only once
| Error text (fragment) | Real cause | 30-second fix | Deep dive |
|---|---|---|---|
org.springframework.beans.factory.BeanDefinitionStoreException: IOException parsing XML document from class path resource [beans.xml]; nested exception is java.io.FileNotFoundException: class path resource [beans.xml] cannot be opened because it does not exist | Wrong file name or path, or the XML sits in src/main/java and never gets packaged onto the classpath | Check the file really exists under src/main/resources; the constructor argument must match its path relative to the resources root exactly | Article 2 · Maven and directory layout |
java.lang.ClassNotFoundException: com.exmple.HelloServiceImpl (usually wrapped in a BeanCreationException) | The fully qualified class name in <bean class="..."> is misspelled, or that class's dependency is absent from the classpath | Hit Ctrl+N in your IDE and search the class name; if it shows up, copy the package name back. The package statement is the real package name | Article 1 · Packages and the classpath |
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userService' defined in class path resource [beans.xml]: Instantiation of bean failed; nested exception is ... No default constructor found | The container needs a no-arg constructor but the class only has a parameterized one (writing any parameterized constructor removes the implicit no-arg one) | Either add public UserService() {}, or write out <constructor-arg> in the XML | The refresh and di labs in Sections 12 and 14 |
org.springframework.beans.NotWritablePropertyException: Invalid property 'timeout' of bean class [com.example.GreetingDao]: Bean property 'timeout' is not writable or has an invalid setter method | <property name="timeout"> found no matching setTimeout(int): typo, wrong case, a field with no setter, or a mismatched parameter type | Look only at setter names: name="x" requires setX. Add the setter or rename; a private setter does not count | Article 6 · Properties and type conversion |
NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.HelloService' available | The class was never registered: the XML forgot the <bean>, an <import> did not pull that file in, or @ComponentScan does not cover the package | Print ctx.getBeanDefinitionNames() first and look for it in the roster before deciding whether to add config or widen the scan | Article 8 · Component scanning and auto-configuration |
NoSuchBeanDefinitionException: No bean named 'helloServcie' available (when looking up by id) | The id in getBean("...") is misspelled or differs in case — ids are case-sensitive strings | Keep the id in a constant instead of typing it: String ID = "helloService";; for an alias use <alias name="helloService" alias="hello"/> | The getbean position in Section 11 |
all six of these happen at the moment the container starts, so the debugging order is always "filename first → can the IDE jump to the class → are id and name exact → only then suspect Spring".
The first row of that table is what beginners hit before anything else: the container has not even started, and it already tells you a file cannot be found. Do not memorise the answer — pick out the culprit frame in that wall of text; picking wrong still explains itself:
You write the XML exactly as the tutorial says, drop the file somewhere in the project, then type new ClassPathXmlApplicationContext("beans.xml") — the program exits with code 1 without ever printing a single line of your own logging.
take away three things — spring-context is the container's minimal dependency; XML wires via constructor-arg / property while annotations wire via @Configuration / @ComponentScan / @Autowired; BeanFactory is lazy while ApplicationContext pre-instantiates singletons at startup. Once the container is visible, auto-configuration is no longer mysterious.