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()?
The ApplicationContext interface itself doesn’t define close(), to keep the interface general (not all containers can/need to be closed, e.g. web containers). But its implementation AbstractApplicationContext implements Closeable, so you can cast and call it, or receive it via the ConfigurableApplicationContext interface.


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

@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
    }
}

⚠️ @PreDestroy doesn’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;

⚠️ @PropertySource doesn’t support wildcards (like classpath*:*.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 @Configuration classes fall inside the @ComponentScan package) causes all config classes to be “incidentally” scanned in, making management messy. @Import declares 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();
    }
}

⚠️ @Transactional by default only rolls back on RuntimeException and Error; 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 ClassCastException or class-loading errors. provided means “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; @EnableWebMvc fails, and Spring Boot’s WebMvcAutoConfiguration fails 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