Spring Framework: Why Dependency Injection Changed Java Development
Spring Framework: Why Dependency Injection Changed Java Development
Rod Johnson published "Expert One-on-One J2EE Design and Development" in 2002. The code in that book became Spring. The framework was released as open source in 2003.
The central idea — dependency injection — sounds simple stated plainly: instead of an object constructing its own dependencies, the dependencies are provided to it from outside. The consequences of taking this idea seriously changed how Java applications were structured.
The Problem: Service Locator
Before Spring, the standard way to obtain a dependency was the Service Locator pattern: look it up by name from a registry.
public class DeviceService {
private DeviceDao dao;
public DeviceService() {
try {
// Look up from JNDI — the standard J2EE approach
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/NmsDS");
this.dao = new DeviceDao(ds);
} catch (NamingException e) {
throw new RuntimeException("Failed to look up DataSource", e);
}
}
}
Problems:
DeviceServicedepends on JNDI. To test it, you need a JNDI server or a mock.- The dependency on
DeviceDaois hidden inside the constructor. You cannot substitute it. - The class controls its own wiring. Changing the data source requires changing the class.
Dependency Injection
Spring inverted the control: the framework created objects and injected dependencies.
public class DeviceService {
private final DeviceDao dao; // declared, not constructed
// Constructor injection — dependency provided by Spring
public DeviceService(DeviceDao dao) {
this.dao = dao;
}
public Device findByIp(String ip) {
return dao.findByIp(ip);
}
}
Spring's XML configuration wired everything together:
<beans>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="org.gjt.mm.mysql.Driver"/>
<property name="url" value="jdbc:mysql://dbserver/nms"/>
<property name="username" value="nmsuser"/>
<property name="password" value="secret"/>
<property name="maxActive" value="10"/>
</bean>
<bean id="deviceDao" class="com.nms.DeviceDao">
<constructor-arg ref="dataSource"/>
</bean>
<bean id="deviceService" class="com.nms.DeviceService">
<constructor-arg ref="deviceDao"/>
</bean>
</beans>
Spring read this XML, created BasicDataSource, injected it into DeviceDao, and injected that into DeviceService. Your code never called new for these objects.
Why This Mattered for Testing
With dependencies injected through the constructor, testing was straightforward:
public class DeviceServiceTest {
public void testFindByIp_returnsDevice() throws Exception {
// Create a mock DAO — no database, no Spring context needed
DeviceDao mockDao = new DeviceDao(null) {
public Device findByIp(String ip) {
return new Device(ip, "test-device", "UP");
}
};
DeviceService service = new DeviceService(mockDao);
Device result = service.findByIp("192.168.1.1");
assert "test-device".equals(result.getName());
assert "UP".equals(result.getStatus());
}
}
No JNDI server. No database. No application server. A unit test that ran in milliseconds.
This was the transformative difference. J2EE integration tests required a running server, a database, and 30 seconds to deploy. Spring unit tests ran in the IDE in under a second.
AOP: Declarative Concerns
Spring also introduced aspect-oriented programming for cross-cutting concerns: logging, transactions, security. Instead of every method beginning with beginTransaction() and ending with commit(), you declared the transactional behaviour:
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="find*" read-only="true"/>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="txAdvice"
pointcut="execution(* com.nms.service.*.*(..))"/>
</aop:config>
All methods starting with save or update in any service class automatically ran in a transaction. No boilerplate in the business code.
Spring vs J2EE
Spring was not anti-J2EE — it ran inside application servers and could use JTA transactions, JNDI, and JMS. What it replaced was the heavyweight EJB component model, not the entire server.
By 2005 Spring had become the dominant way to build Java server applications, even in organisations that ran J2EE servers. EJB 3.0 in 2006 acknowledged Spring's success by adopting its core ideas — annotations, POJOs, injection — into the standard.
Annotation-based configuration (Spring 2.5, 2007) eliminated the XML. The principle — provide dependencies from outside rather than constructing them inside — remained the foundation.