1. The Spring IoC Container
1.1 Container architecture
BeanFactory (top-level interface)
└── ApplicationContext (the commonly used interface, extends BF)
├── ClassPathXmlApplicationContext (XML config)
├── FileSystemXmlApplicationContext (filesystem-path XML)
└── AnnotationConfigApplicationContext (annotation config)
Core differences between BeanFactory and ApplicationContext:
| Feature | BeanFactory | ApplicationContext |
|---|---|---|
| Bean init timing | Lazy (on the first getBean) | Eager (at container startup) |
| Features | Basic IoC | IoC + event publishing + i18n + AOP, etc. |
| Use case | Extremely resource-constrained embedded | 99% of business scenarios |
Why doesn’t ApplicationContext have close()?
TheApplicationContextinterface itself doesn’t defineclose(), to keep the interface general (not all containers can/need to be closed, e.g. web containers). But its implementationAbstractApplicationContextimplementsCloseable, so you can cast and call it, or receive it via theConfigurableApplicationContextinterface.
1.2 Lazy Loading
// Annotation way: @Lazy makes the Bean initialize only on first use
@Bean
@Lazy
public HeavyService heavyService() {
return new HeavyService();
}
// XML way:
// <bean id="heavyService" class="..." lazy-init="true"/>
Use case: Beans that are expensive to initialize and may not be used at startup (e.g. certain connection pools, third-party SDK clients).
1.3 Dependency injection methods
Constructor injection (recommended)
@Component
public class UserService {
private final UserMapper userMapper; // final ensures immutability
// Spring 4.3+ can omit @Autowired for a single constructor
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
}
<!-- XML constructor injection -->
<bean id="userService" class="com.example.UserService">
<!-- reference type -->
<constructor-arg name="userMapper" ref="userMapper"/>
<!-- simple type -->
<constructor-arg name="maxRetry" value="3"/>
</bean>
Setter injection
@Component
public class OrderService {
private UserService userService;
// The XML way requires a setter
public void setUserService(UserService userService) {
this.userService = userService;
}
}
<!-- XML setter injection -->
<bean id="orderService" class="com.example.OrderService">
<property name="userService" ref="userService"/>
<property name="timeout" value="3000"/>
</bean>
Collection injection
<bean id="dataConfig" class="com.example.DataConfig">
<!-- List -->
<property name="servers">
<list>
<value>192.168.1.1</value>
<value>192.168.1.2</value>
</list>
</property>
<!-- Map -->
<property name="params">
<map>
<entry key="timeout" value="3000"/>
<entry key="retry" value="3"/>
</map>
</property>
<!-- Properties -->
<property name="jdbcProps">
<props>
<prop key="url">jdbc:mysql://localhost:3306/db</prop>
</props>
</property>
</bean>
2. Spring Annotation-Based Development
2.1 Core annotations quick reference
| Annotation | Purpose | Equivalent XML |
|---|---|---|
@Component |
Generic Bean declaration | <bean> |
@Service |
Service layer | <bean> |
@Repository |
DAO layer | <bean> |
@Controller |
MVC control layer | <bean> |
@Configuration |
Config class | <beans> |
@ComponentScan |
Enable component scanning | <context:component-scan> |
@Bean |
A method produces a Bean | <bean> |
@Import |
Import other config classes | <import> |
@Scope("prototype") |
Scope | scope="prototype" |
2.2 Bean lifecycle callbacks
@Component
public class CacheService {
@PostConstruct // runs after Bean init completes (after property injection)
public void init() {
System.out.println("warming up the cache...");
// connect to Redis, load hot data
}
@PreDestroy // runs before Bean destruction (on container close)
public void destroy() {
System.out.println("cleaning up cache connections...");
// release connection resources
}
}
⚠️
@PreDestroydoesn’t fire on prototype-scoped Beans; the container doesn’t track the lifecycle of prototype Beans.
2.3 Dependency-injection annotations
@Service
public class UserService {
// @Autowired: inject by type
// If there are multiple Beans of the same type, then match by field name
@Autowired
private UserMapper userMapper;
// @Qualifier: specify which Bean to inject (required when there are multiple of the same type)
@Autowired
@Qualifier("mysqlUserMapper")
private UserMapper specificMapper;
// @Value: inject a simple value or a SpEL expression
@Value("${app.timeout:5000}") // read config, default 5000
private int timeout;
@Value("#{systemProperties['os.name']}") // SpEL expression
private String osName;
}
Prerequisite for @Value to read a .properties file:
// 1. Add @PropertySource on the config class
@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:application.properties") // no wildcards!
// Multiple-file form:
// @PropertySource({"classpath:db.properties", "classpath:app.properties"})
public class SpringConfig { }
// 2. Only then can you use @Value in a Bean
@Value("${jdbc.url}")
private String jdbcUrl;
⚠️
@PropertySourcedoesn’t support wildcards (likeclasspath*:*.properties); you must list the file paths one by one.
2.4 Managing third-party Beans
Recommended: @Configuration + @Import
// Data source config class
@Configuration
public class JdbcConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
// Reference-type injection: declare it on the method parameter, Spring injects it automatically
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
}
// Main config class: aggregate with @Import, don't scan config classes via @ComponentScan
@Configuration
@ComponentScan("com.example")
@PropertySource("classpath:application.properties")
@Import({JdbcConfig.class, MyBatisConfig.class}) // recommended
public class SpringConfig { }
Why prefer @Import over scanning?
Scanning (letting
@Configurationclasses fall inside the@ComponentScanpackage) causes all config classes to be “incidentally” scanned in, making management messy.@Importdeclares dependencies explicitly, at a glance.
2.5 Complete MyBatis integration config
@Configuration
public class MyBatisConfig {
// DataSource is auto-injected from the Spring container (method-parameter injection)
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// Set MyBatis global config (optional)
// factory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
// Enable camelCase mapping
org.apache.ibatis.session.Configuration config =
new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true);
factory.setConfiguration(config);
return factory;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
// Scan the package containing Mapper interfaces, auto-generate proxies registered into the container
msc.setBasePackage("com.example.mapper");
return msc;
}
}
2.6 Integrating JUnit 5 (modern style)
// JUnit 4 style (legacy projects)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testFind() { /* ... */ }
}
// JUnit 5 style (recommended, Spring Boot default)
@SpringJUnitConfig(classes = SpringConfig.class)
// equivalent to @ExtendWith(SpringExtension.class) + @ContextConfiguration
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testFind() { /* ... */ }
}
3. Spring AOP
3.1 Core concepts
JoinPoint → "any possible location" of program execution (all methods are join points)
Pointcut → the methods actually intercepted (a subset of join points)
Advice → the enhancement logic to run at the pointcut location
Aspect → the combination of pointcut + advice (the advice class)
Weaving → the process of applying an aspect to the target object
Analogy to Python decorators:
@around_advice ← advice
def save_user(): ← pointcut (the decorated function)
pass
Join point vs pointcut:
A join point is “theoretically all interceptable methods,” and a pointcut is “the methods you actually configure to intercept.” Every pointcut is a join point, but a join point isn’t necessarily a pointcut.
3.2 Pointcut expressions
// Syntax: execution(modifier? return-type package.class?.method(params) exceptions?)
// ? means optional
// Exact match
@Pointcut("execution(void com.example.service.UserService.save())")
// Match all methods of a class
@Pointcut("execution(* com.example.service.UserService.*(..))")
// ^any return ^any method name ^any params
// Match all methods of all classes in a package (not sub-packages)
@Pointcut("execution(* com.example.service.*.*(..))")
// Match all methods of all classes in a package and its sub-packages
@Pointcut("execution(* com.example.service..*.*(..))")
// ^^ the double dot means include sub-packages
// A common real-project form: intercept all methods of the service layer
@Pointcut("execution(* com.example.service.impl.*.*(..))")
3.3 Complete example of advice types
@Aspect
@Component
public class LogAspect {
// Define a reusable pointcut
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceMethods() {}
// Before advice: before the method runs
@Before("serviceMethods()")
public void before(JoinPoint jp) {
System.out.println("method starting: " + jp.getSignature().getName());
Object[] args = jp.getArgs(); // get the arguments
System.out.println("args: " + Arrays.toString(args));
}
// After advice: after the method runs (whether or not it threw)
@After("serviceMethods()")
public void after(JoinPoint jp) {
System.out.println("method ended: " + jp.getSignature().getName());
}
// AfterReturning advice: after normal return (not run on exception)
@AfterReturning(value = "serviceMethods()", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
System.out.println("return value: " + result);
}
// AfterThrowing advice: after the method throws
@AfterThrowing(value = "serviceMethods()", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) {
System.out.println("exception: " + ex.getMessage());
// Can send an alert here
}
// Around advice: the most powerful, can control whether the original method runs
@Around("serviceMethods()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// Get the method signature
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
long start = System.currentTimeMillis();
try {
// Execute the original method (if you don't call it, the original method won't run)
Object result = pjp.proceed();
long cost = System.currentTimeMillis() - start;
System.out.printf("[%s] took: %dms%n", methodName, cost);
return result;
} catch (Throwable e) {
System.out.println("[" + methodName + "] exception: " + e.getMessage());
throw e; // must rethrow, or the exception gets swallowed
}
}
}
Advice execution order (normal case):
Around(before) → Before → target method → Around(after) → AfterReturning → After
Advice execution order (exception case):
Around(before) → Before → target method (throws) → AfterThrowing → After
3.4 The underlying AOP proxy mechanism
// Spring AOP's two proxy approaches:
// 1. JDK dynamic proxy: the target class implements an interface → proxy the interface
// 2. CGLIB proxy: the target class has no interface → generate a subclass by inheriting the target class
// Spring Boot uses CGLIB by default (whether or not there's an interface)
// Reason: avoids the "must program to an interface" constraint, more flexible to use
// CGLIB note:
// final classes/methods can't be proxied by CGLIB (can't be inherited/overridden)
4. Spring Transactions
4.1 Basic usage
// 1. Configure the transaction manager (use DataSourceTransactionManager when integrating MyBatis)
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
// 2. Enable transaction-annotation support
@Configuration
@EnableTransactionManagement // enable @Transactional support
public class SpringConfig { }
// 3. Use it on a Service method
@Service
public class AccountService {
// On the interface: lower coupling (recommended), implementations inherit it automatically
// On the implementation class: more explicit, but couples the interface to the implementation
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
accountMapper.deduct(fromId, amount);
// If an exception is thrown here, the deduct above rolls back automatically
accountMapper.add(toId, amount);
}
}
4.2 Transaction propagation behavior
Scenario: method A (transaction manager) calls method B (transaction coordinator)
| Propagation | Description | Scenario |
|---|---|---|
| REQUIRED (default) | Join if there’s a transaction, create one if not | 99% of scenarios |
| REQUIRES_NEW | Always create an independent new transaction, isolated from the caller’s | Logging (main flow rolls back, log still saved) |
SUPPORTS |
Join if there’s a transaction, run non-transactionally if not | Read-only queries |
NOT_SUPPORTED |
Don’t use a transaction (suspend the current one) | Operations that don’t need a transaction |
MANDATORY |
Must run within an existing transaction, else throw | Force the caller to open a transaction |
NEVER |
Must not run within a transaction, else throw | - |
NESTED |
Create a savepoint within the current transaction (nested transaction) | Partial rollback |
@Service
public class OrderService {
@Autowired
private LogService logService;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
// Even if createOrder ultimately rolls back, the log is still written successfully
logService.writeLog("created order: " + order.getId());
// Simulate an exception to trigger rollback
if (order.getAmount().compareTo(BigDecimal.ZERO) < 0) {
throw new BusinessException("amount can't be negative");
}
}
}
@Service
public class LogService {
// REQUIRES_NEW: open an independent transaction, unaffected by the outer one
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void writeLog(String message) {
logMapper.insert(new Log(message, LocalDateTime.now()));
}
}
4.3 @Transactional caveats
// ❌ Wrong: the method isn't public, @Transactional doesn't take effect
@Transactional
private void internalTransfer() { }
// ❌ Wrong: same-class internal call bypasses the proxy, transaction doesn't take effect
@Service
public class UserService {
public void doA() {
this.doB(); // direct call, doesn't go through the AOP proxy!
}
@Transactional
public void doB() { }
}
// ✅ Correct: inject the class's own proxy (or use AopContext.currentProxy())
@Service
public class UserService {
@Autowired
private UserService self; // inject the proxy object
public void doA() {
self.doB(); // call through the proxy, transaction takes effect
}
}
// ❌ Wrong: the exception is caught, Spring doesn't know to roll back
@Transactional
public void save(User user) {
try {
userMapper.insert(user);
} catch (Exception e) {
log.error("save failed", e);
// no rethrow, the transaction won't roll back!
}
}
// ✅ Correct: or mark for rollback manually
@Transactional
public void save(User user) {
try {
userMapper.insert(user);
} catch (Exception e) {
log.error("save failed", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
⚠️
@Transactionalby default only rolls back onRuntimeExceptionandError; checked exceptions (IOException, etc.) don’t trigger rollback. To roll back on checked exceptions:@Transactional(rollbackFor = Exception.class)
5. Spring MVC
5.1 Basic workflow
HTTP request
↓
DispatcherServlet (front controller, the core)
↓
HandlerMapping (find the matching Controller method by URL)
↓
HandlerAdapter (call the Controller method, handle parameter binding)
↓
Controller method runs → returns ModelAndView or @ResponseBody data
↓
ViewResolver (resolve the view name; @ResponseBody skips this step)
↓
Render the response → return to the client
5.2 Receiving request parameters
@RestController // = @Controller + @ResponseBody
@RequestMapping("/users")
public class UserController {
// 1. Ordinary parameters: auto-bind by matching names
@GetMapping
public List<User> list(String name, Integer age) { }
// 2. POJO parameter: auto-bind same-named fields
@PostMapping
public User create(UserCreateDTO dto) { }
// 3. Array parameter: multiple values with the same name
@GetMapping("/batch")
public List<User> batch(String[] ids) { }
// 4. Collection parameter: must add @RequestParam
@GetMapping("/list")
public List<User> list(@RequestParam List<String> ids) { }
// 5. JSON parameter: must add @RequestBody, needs jackson-databind
@PostMapping("/json")
public Result createFromJson(@RequestBody UserCreateDTO dto) { }
// 6. Path parameter
@GetMapping("/{id}")
public User getById(@PathVariable Long id) { }
// 7. Date parameter: specify the format
@GetMapping("/born")
public List<User> byBirth(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate birthDate) { }
}
Differences among the three parameter annotations:
| Annotation | Value source | Typical scenario |
|---|---|---|
@RequestParam |
URL query params (?key=value) |
Pagination params, filter conditions |
@RequestBody |
Request body (JSON/XML) | POST/PUT submitted data |
@PathVariable |
URL path segment (/users/{id}) |
RESTful resource ID |
5.3 Unified response wrapping at the presentation layer
// Unified response body
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private Integer code; // business status code
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> fail(Integer code, String message) {
return new Result<>(code, message, null);
}
}
// Controller usage
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
User user = userService.getById(id);
return Result.success(user);
}
5.4 Global exception handling
// Custom business exception
public class BusinessException extends RuntimeException {
private final Integer code;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
}
}
// Custom system exception
public class SystemException extends RuntimeException {
private final Integer code;
public SystemException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
}
// Global exception handler
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
// Handle business exceptions (expected, e.g. validation failures, business-rule conflicts)
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
log.warn("business exception: {}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
// Handle system exceptions (unexpected, e.g. DB connection failures)
@ExceptionHandler(SystemException.class)
public Result<Void> handleSystem(SystemException e) {
log.error("system exception: ", e);
return Result.fail(500, "system busy, please retry later");
}
// Handle all uncaught exceptions (fallback)
@ExceptionHandler(Exception.class)
public Result<Void> handleAll(Exception e) {
log.error("unknown exception: ", e);
return Result.fail(500, "internal server error");
}
}
5.5 Interceptors
Interceptor vs Filter
| Comparison | Interceptor | Filter |
|---|---|---|
| Spec | Spring MVC | Servlet |
| Scope | Controller requests | All requests (including static resources) |
| Access Spring Beans | ✅ Can | ❌ Harder |
| Granularity | Finer (method level) | Coarser (URL level) |
| Typical use | Login check, authorization, logging | Encoding, CORS, rate limiting |
Interceptor implementation
// 1. Implement the HandlerInterceptor interface
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
// Before request handling: returning false interrupts the request chain
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !jwtUtil.verify(token)) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"message\":\"please log in first\"}");
return false; // interrupt, don't continue
}
return true; // let it through
}
// After request handling (after the Controller runs): can modify the ModelAndView
@Override
public void postHandle(HttpServletRequest req, HttpServletResponse res,
Object handler, ModelAndView mv) { }
// After view rendering completes: for resource cleanup
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
// clean up ThreadLocal and other resources
}
}
// 2. Register the interceptor
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // intercept everything
.excludePathPatterns( // let through
"/users/login",
"/users/register",
"/swagger-ui/**"
);
}
}
Execution order of multiple interceptors
Registration order: Interceptor1 → Interceptor2 → Interceptor3
Normal execution:
pre1 → pre2 → pre3 → [Controller] → post3 → post2 → post1
→ after3 → after2 → after1
pre3 returns false (those before pre3 already ran):
pre1 → pre2 → pre3(false) → after2 → after1
(Note: after only runs for interceptors whose preHandle returned true)
pre2 returns false:
pre1 → pre2(false) → after1
pre1 returns false:
pre1(false) → (nothing runs)
6. Maven Project Management
6.1 Dependency-conflict resolution rules
Priority (high to low):
1. Special priority: the same dependency with different versions in the same pom.xml → the later declaration overrides the earlier
2. Path priority: the shorter transitive path wins
A → B → C → log4j:1.1 (path length 3)
A → D → log4j:1.2 (path length 2) ← use 1.2
3. Declaration priority: for equal paths, the dependency declared earlier in pom.xml wins
Actively resolving conflicts:
<!-- Method 1: exclude a transitive dependency -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.20</version>
<exclusions>
<exclusion>
<!-- Exclude the old commons-logging brought in by spring-core -->
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Method 2: lock the version uniformly in the parent pom (dependencyManagement) -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
</dependencies>
</dependencyManagement>
6.2 Dependency scope
| scope | Compile | Test | Runtime | Description |
|---|---|---|---|---|
compile (default) |
✅ | ✅ | ✅ | Full scope, packaged into the jar/war |
test |
❌ | ✅ | ❌ | Test only, e.g. JUnit |
provided |
✅ | ✅ | ❌ | Provided by the container at runtime, e.g. servlet-api |
runtime |
❌ | ✅ | ✅ | Only needed at runtime, e.g. JDBC drivers |
Why must javax.servlet-api use provided?
The Tomcat container already includes an implementation of the Servlet API. If packaged into the war, it conflicts with Tomcat’s bundled version, causing
ClassCastExceptionor class-loading errors.providedmeans “needed at compile time, provided by the container at runtime, excluded at packaging.”
6.3 Inheritance and aggregation
<!-- Parent project pom.xml (packaging must be pom) -->
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<!-- Aggregation: the parent manages multiple modules (mvn package builds all modules in order) -->
<modules>
<module>common</module>
<module>service</module>
<module>web</module>
</modules>
<!-- Dependency management: declare versions so child modules don't need to write them -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Child project pom.xml -->
<parent>
<groupId>com.example</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>service</artifactId>
<!-- No need to write groupId and version, inherited from the parent -->
<dependencies>
<!-- No version needed, managed by the parent's dependencyManagement -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
6.4 Multi-environment configuration
<!-- Define profiles in pom.xml -->
<profiles>
<profile>
<id>dev</id>
<properties>
<profile.active>dev</profile.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault> <!-- active by default -->
</activation>
</profile>
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
</profile>
</profiles>
# Specify the environment at build time
mvn package -P prod
# Skip tests
mvn package -DskipTests
# or
mvn package -Dmaven.test.skip=true # even skips compiling the test code
7. A Deep Dive into SSM Integration
7.1 Spring’s dual-container architecture (key point)
Tomcat starts
↓
Discovers AbstractAnnotationConfigDispatcherServletInitializer
↓
Creates two Spring containers (parent-child relationship):
┌─────────────────────────────────────────────────────┐
│ Root ApplicationContext (parent container) │
│ Reads SpringConfig │
│ Contains: Services, Mapper proxies, DataSource, tx manager │
│ Responsibility: business logic + data access layer │
└─────────────────────────────────────────────────────┘
↑ (the child can access the parent, not vice versa)
┌─────────────────────────────────────────────────────┐
│ Servlet ApplicationContext (child/MVC container) │
│ Reads SpringMvcConfig │
│ Contains: Controllers, ViewResolver, HandlerMapping │
│ Responsibility: receive HTTP requests, dispatch │
└─────────────────────────────────────────────────────┘
Why this design?
Layer isolation: a Controller can inject a Service (child accesses parent), but a Service can’t inject a Controller (preventing coupling between the business layer and the presentation layer). It also lets different protocols share the same business container (e.g. supporting HTTP + WebSocket at once).
7.2 Bean loading control (preventing double scanning)
// SpringConfig: scan only non-Controller components
@Configuration
@ComponentScan(value = "com.example",
excludeFilters = @ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Controller.class
)
)
public class SpringConfig { }
// SpringMvcConfig: scan only Controllers
@Configuration
@ComponentScan("com.example.controller")
@EnableWebMvc
public class SpringMvcConfig { }
⚠️ If both configs scan the same package, Services are created twice (once in the parent, once in the child), causing AOP enhancements like transactions to fail (because the child’s Service didn’t go through the parent’s transaction proxy).
7.3 What does @EnableWebMvc do
// The essence of @EnableWebMvc:
// @Import(DelegatingWebMvcConfiguration.class)
// It registers a complete set of MVC components into the container:
// Enable @RequestMapping route mapping
// Enable @Controller / @ResponseBody annotation handling
// Enable automatic JSON conversion (needs jackson-databind on the classpath)
// Register parameter-binding converters (@DateTimeFormat, etc.)
// Register DispatcherServlet-related components (HandlerAdapter, etc.)
// Enable static-resource handling, view resolution, etc.
7.4 WebMvcConfigurer vs WebMvcConfigurationSupport
// Way 1: implement WebMvcConfigurer (recommended, non-intrusive)
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) { }
@Override
public void addCorsMappings(CorsRegistry registry) { }
// Override only the methods you need; the rest keep @EnableWebMvc's defaults
}
// Way 2: extend WebMvcConfigurationSupport (intrusive, use with caution)
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
// After extending this class, @EnableWebMvc's auto-configuration is disabled!
// You must manually configure all needed MVC components, or many defaults fail
}
Why is WebMvcConfigurationSupport intrusive?
Extending it means taking over the entire MVC configuration;
@EnableWebMvcfails, and Spring Boot’sWebMvcAutoConfigurationfails too. If you accidentally miss some configuration, you get problems like JSON conversion not working or static resources returning 404.
8. High-Frequency Q&A
Q1: The version relationship between Spring and Spring Boot?
Spring Boot 3.x → Spring Framework 6.x (requires JDK 17+)
Spring Boot 2.x → Spring Framework 5.x (supports JDK 8+)
Spring Boot 1.x → Spring Framework 4.x
Check the mapping: https://spring.io/projects/spring-boot#learn
(The Release Notes state it explicitly)
Q2: The difference between AOP Pointcut and JoinPoint?
JoinPoint = the candidates (all method execution points)
Pointcut = the selected candidates (the methods actually intercepted after filtering by expression)
Example: a Service class has 100 methods (100 join points)
you configure execution(* com.example.service.*.save*(..))
→ only the 5 methods starting with save are pointcuts
Q3: How do spring-jdbc and mybatis-spring work together?
spring-jdbc provides:
- DataSourceTransactionManager (the transaction manager)
- JdbcTemplate (optional)
- the infrastructure for integrating transactions
mybatis-spring provides:
- SqlSessionFactoryBean (creates MyBatis's core object and registers it into Spring)
- MapperScannerConfigurer (scans Mapper interfaces, generates proxy Beans)
- SqlSessionTemplate (a thread-safe SqlSession)
Relationship: mybatis-spring points MyBatis's DataSource to the Spring-managed DataSource,
so MyBatis operations participate in Spring's transaction management.
Q4: The difference between compile time and runtime?
Compile time: the process of javac turning .java → .class
- Syntax checking
- Type checking
- Annotation processing (e.g. Lombok generates code at compile time)
- Compile-time exceptions (checked exceptions)
Runtime: the process of the JVM loading and executing .class
- Dynamic proxy (Spring AOP generates proxy classes at runtime)
- Reflection
- Class loading
- Runtime exceptions (RuntimeException and its subclasses)
Q5: The difference between default and protected?
// default (package access, no modifier)
class DefaultClass {
void defaultMethod() {} // only classes in the same package can access
}
// protected
class Base {
protected void protectedMethod() {}
}
// The key difference: subclasses in a different package
// ✅ protected can be inherited across packages
package com.other;
import com.example.Base;
class Sub extends Base {
void test() {
protectedMethod(); // ✅ can access
}
}
// ❌ default can't be inherited across packages
package com.other;
import com.example.DefaultClass;
class Sub extends DefaultClass {
void test() {
defaultMethod(); // ❌ compile error: not visible
}
}
// Remember:
// Same package: both default and protected are accessible
// Subclass in a different package: only protected is accessible (default isn't)
// Non-subclass in a different package: neither default nor protected is accessible
Q6: The difference between @Value and direct assignment?
// Direct assignment: hardcoded, can't be configured externally
private int timeout = 5000;
// @Value: read from config files/environment variables at runtime, supports externalized config
@Value("${app.timeout:5000}") // default 5000
private int timeout;
// Other uses of @Value:
@Value("${server.port}") // read from application.properties
@Value("#{T(Math).PI}") // SpEL expression
@Value("#{systemProperties['java.home']}") // system property
@Value("${MY_ENV_VAR}") // environment variable