MyBatis annotation SQL

In the enterprise-level development of Spring Boot combined with MyBatis-Plus, basic CRUD operations have been BaseMapper Automatically take over. However, in the face of complex single-table statistics, multi-table joint queries or specific business SQL, developers still need to manually write SQL statements.

Application scenarios

MyBatis provides @Select, @Insert, @Update, @Delete Annotations allow writing SQL statements directly on the methods of the Mapper interface.

  • When to use: When provided by MyBatis-Plus LambdaQueryWrapper It cannot meet complex query requirements (such as multi-table JOIN, complex GROUP BY statistics, subqueries), and the SQL logic is relatively simple and there is no need to dynamically splice a large number of XML tags.
  • Where to write: mapper Above the interface methods under the package.

Grammar rules and code templates (add/delete/modify/check)

// Must be marked above the interface @Mapper, Tell Spring This is the interface of the data access layer
@Mapper
// inherit BaseMapper<XxxEntity>, reserve MyBatis-Plus Built-in single table addition, deletion, modification and query capabilities
//    Here's XxxEntity Replace with the specific entity class in your project, for example User
public interface XxxMapper extends BaseMapper<XxxEntity> {
    // Write native directly in parentheses SQL statement
    // Method name: Name it according to the business meaning, and you will know the meaning when you see the name, for example selectByName, countByDeptId
    // Parameter list: used before method parameters @Param("Parameter name") annotation, with SQL inside #{Parameter name} Same name
    @Select("SQLstatement")
    Return type method name(@Param("Parameter name") Parameter type formal parameter name);
    
    // ========== check ==========
    @Select("SELECT * FROM table name WHERE Field = #{Parameter name}")
    Return type selectEntity(@Param("Parameter name") Parameter type entity);

    // ========== increase ==========
    @Insert("INSERT INTO table name(Field list) VALUES(#{Entity properties1}, #{Entity properties2})")
    int insertEntity(Entity class entity);  // You can omit it when passing entities directly. @Param

    // ========== change ==========
    @Update("UPDATE table name SET Field = #{property} WHERE id = #{id}")
    int updateEntity(Entity class entity);

    // ========== delete ==========
    @Delete("DELETE FROM table name WHERE id = #{id}")
    int deleteEntity(@Param("id") Long id);
}

Parameter binding rules:

  • Single parameter (basic type/string): Must be used @Param("name"), in SQL #{} Use that name.
  • Single entity object parameters: Can be omitted directly @Param, in SQL #{} Write the attribute name of the entity class, and MyBatis obtains the value through getter.
  • Multiple parameters (basic type + entity, etc.): All parameters are required @Param statement.
    Multi-parameter example (basic type + entity mix):
    scene: Update a user's mailbox, but you need to pass in the user ID (basic type) and update information (entity object) at the same time.
    // Wrong spelling (no @Param, MyBatis Unrecognized)
    @Update("UPDATE user SET email = #{user.email} WHERE id = #{id}")
    int updateEmail(Long id, User user);  // Report an error: BindingException
    
    // Correct writing (all parameters use @Param explicit naming)
    @Update("UPDATE user SET email = #{user.email} WHERE id = #{id}")
    int updateEmail(@Param("id") Long id, @Param("user") User user);
    

    Quotation rules in SQL:

    • #{id} → Take it directly @Param("id") marked Long id
    • #{user.email} → Find first @Param("user") marked User user, and then pass the object's getEmail() value

Example

  1. Conditional query
@Mapper
public interface TalentMapper extends BaseMapper<Talent> {

    // Query talent list based on city and education level
    @Select("SELECT * FROM talent WHERE city = #{city} AND education = #{education}")
    List<Talent> findByCityAndEducation(@Param("city") String city, 
                                        @Param("education") String education);
}
  1. Aggregate statistics (return Map)
// Count the number of talents by city, and use Map Receive (key: city name, value: quantity)
@Select("SELECT city, COUNT(*) AS count FROM talent GROUP BY city")
@MapKey("city")
Map<String, Map<String, Object>> countByCity();
  1. insert
@Insert("INSERT INTO talent(name, city, education) VALUES(#{name}, #{city}, #{education})")
int insertTalent(Talent talent);  // Pass entity directly, #{} Get attributes directly
  1. renew
@Update("UPDATE talent SET city = #{city} WHERE id = #{id}")
int updateCity(@Param("id") Long id, @Param("city") String city);
  1. delete
@Delete("DELETE FROM talent WHERE id = #{id}")
int deleteById(@Param("id") Long id);

Things to note

  1. SQL complexity control: @Select Wait for annotationOnly suitable for short, static SQL(Usually no more than 10 lines). If SQL needs to be dynamic <if>, <choose> tags, or the statement is dozens of lines long,SQL must be migrated to the same Mapper.xml document, to ensure readability and maintainability.
  2. Return type selection:
operateannotationCommon return typesillustrate
increase@InsertintReturns the number of affected rows, also available void or boolean
delete@DeleteintSame as above
change@UpdateintSame as above
Check (single item)@SelectEntity classReturn if not found null
Check (multiple items)@SelectList<Entity class>Returns an empty list if not found
Check (statistical value)@SelectLong / Integer / BigDecimalUsed for COUNT, SUM, AVG, etc.
Check (single column value list)@SelectList<String> or List<Long>For example, only check all the values ​​​​of a certain column
Check (aggregate grouping)@SelectList<Map<String, Object>>Each record is a Map, and the column name is used as the key.
Check (aggregate grouping, specify key)@Select + @MapKey("Field name")Map<String, Map<String, Object>>The key of the outer Map is the value of the specified field

Analysis of IDEA Injection Mapper Interface "Red Report" Mechanism

Phenomenon: In traditional SSM or Spring Boot projects, use @Autowired When injecting the Mapper interface, the IDEA editor will often mark a red wavy line below, prompting "Could not autowire. No beans of 'XXXMapper' type found." (Cannot autowire, no beans of this type found.). But the projectCompiles normally and runs perfectly.

Solutions and Specifications

This "red report" is a false positive of the IDE's static check and does not affect any actual operation. In enterprise-level development, it is usually handled in the following way:

  • Option A (recommended): Adjust IDEA inspection level
    Move the cursor to the red @Autowired at, press Alt + Enter(Windows) or Option + Enter(Mac), select Autowire for... -> Disable injection point validation Or lower the severity level of the check item (Severity) to Warning or Weak Warning.
  • Option B: Use constructor injection (completely eliminates red lines)
    Abandon field injection (@Autowired), use Lombok instead @RequiredArgsConstructor Perform constructor injection. This is not only the injection method officially recommended by Spring, but also can perfectly bypass IDEA's interface injection false positives.
    @Service
    @RequiredArgsConstructor // Lombok Annotations, automatically generated include final Field constructor
    public class TalentServiceImpl implements TalentService {
    
        // use final Decoration, injection via constructor, IDEA Will not report red
        private final TalentMapper talentMapper;
    
        @Override
        public List<Talent> getAll() {
            return talentMapper.selectList(null);
        }
    }
    

underlying principles:
1. Mapper is essentially a Java interface, the interface itself cannot be instantiated (it cannot new).
2. When the Spring container is started, it uses the dynamic proxy mechanism (JDK dynamic proxy) provided by MyBatis.Runtime Generate a proxy implementation class for the interface and register the proxy object in the Spring container.
3. IDEA’s static code analyzer is inCompile-time When scanning the code, only the interface definition can be seen, and the proxy class generated at runtime cannot be perceived, so it is mistakenly thought that "the implementation class cannot be found", thus triggering an error message.

Spring Boot unit testing and interface contract verification

In enterprise-level project development, code must undergo strict testing and verification after it is written. Testing is mainly divided into two dimensions:

  • For business logic layerUnit testing;
  • For the presentation layer (Controller) API interfaceInterface contract verification.

Simply put:Unit testing tests internal logic, and interface testing tests external commitments.

Spring Boot provides this @SpringBootTest Annotations can automatically identify the main startup class and load the complete Spring application context.

Unit testing

Core mechanics and syntax formulas

  • @SpringBootTest: Class-level annotations. Mark this class as a Spring Boot test class. When running tests, the framework automatically looks for files with @SpringBootApplication Annotated main startup class and starts a Spring container ready for testing.
  • @Autowired: Used to directly inject the Service or Mapper component to be tested in the test class.
  • @Test: Method-level annotations provided by JUnit, identifying the method as an independent test case.

syntax template

// Class-Level Annotations: Startup Spring test container
@SpringBootTest
class XxxServiceTest {

    // Inject the component to be tested(Service or Mapper)
    @Autowired
    private XxxService xxxService;

    // @Test Mark this method as a test case
    @Test
    void testMethodName() {
        // 1. Call business method
        // 2. Use assertions to verify the results (e.g. assertNotNull, assertEquals wait)
    }
}

assertion

Assertion is Compare expected and actual method execution results. If they are consistent, the test passes (green); if they are inconsistent, the test fails (red) and a specific error message is thrown (optional, but recommended).

Show where and what to do: Right-click to run the test class in IDEA, and the test panel will appear at the bottom. After each test method is run, a green check mark ✅ will be displayed if the assertion passes, and a red cross ❌ will be displayed if the assertion fails, along with the prompt information and actual value you wrote.There is no need to stare at the console log, you can see at a glance which method failed and why.

4 commonly used assertions

// 1. Assert is not null: Verify that the query result is not null
assertNotNull(object, "Prompt message on failure");

// 2. Assert equality: Verify that two values ​​are equal
assertEquals(expected value, actual value, "Prompt message on failure");

// 3. Assertion is true: Verification condition is true
assertTrue(conditional expression, "Prompt message on failure");

// 4. Assert exception: Verify that an exception of the specified type was thrown
assertThrows(Exception class.class, () -> { Code that may throw exceptions });

Example

@Test
void testGetById() {
    Talent talent = talentService.getById(1L);
    
    // Verify that the data found is not null
    assertNotNull(talent, "ID for 1 The talent does not exist, the query result is null");
    
    // Verify that the name is the expected value
    assertEquals("Zhang San", talent.getName(), "Name does not match, expected Zhang San, actual: " + talent.getName());
    
    // verify ID greater than 0
    assertTrue(talent.getId() > 0, "ID no greater than 0, actual value: " + talent.getId());
}

@Test
void testBusinessException() {
    // It was thrown when verifying that illegal parameters were passed in. BusinessException
    assertThrows(BusinessException.class, () -> {
        talentService.getById(-1L);  // Invalid input ID
    }, "Illegal input ID should throw BusinessException");
}

Notice: Assert that the order of execution within the method is linear.If the first assertion fails, an exception will be thrown directly, and subsequent assertions will not be executed. Therefore, usually assertNotNull Put it first and make sure the object is not empty before asserting its properties.

Why use assertion instead of System.out.println?

WayTest resultsquestion
System.out.printlnManual judgment by looking at the console output100 tests are hard to read and easy to miss
assertNotNull waitThe framework automatically determines pass/failRed marks fail, green passes, zero labor

a word: Assertion is to hand over "human eye judgment" to "machine judgment", so that the test results can be directly displayed as passed or failed, without manually checking the logs one by one.

Customize test method execution order

By default, JUnit executes tests in dictionary or random order of method names, and cannot guarantee a fixed order. When you need to strictly control the order of execution (such as testing new additions first, then testing queries, and finally testing deletions), use @TestMethodOrder(MethodOrderer.OrderAnnotation.class) Cooperate @Order(n) Annotation control, following template:

code template

@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)  // Statement press @Order Annotation sorting
class TalentServiceTest {

    @Autowired
    private TalentService talentService;

    @Test
    @Order(1)  // The smaller the number, the earlier it is executed.
    void testSave() {
        // Step one: add
    }

    @Test
    @Order(2)
    void testGetById() {
        // Step 2: Query
    }

    @Test
    @Order(3)
    void testDelete() {
        // Step 3: Delete
    }
}

rule

  • @TestMethodOrder(...):Written on the class to specify the sorting method
  • @Order(n): written on the method,n for 1, 2, 3..., The smaller the number, the earlier it is executed.
  • Other ways to sort:@TestMethodOrder(MethodOrderer.MethodName.class)Dictionary order by method name

Example

Location: src/test/java directory, the package path is consistent with the main code (such as com.itheima.test0705.service).

@SpringBootTest
class TalentServiceTest {

    @Autowired
    private TalentService talentService;  // Inject to be tested Service

    // Test: Based on ID Query a single record
    @Test
    void testGetById() {
        Talent talent = talentService.getById(1L);  // Call business method
        // Assertion: The result should not be null
        assertNotNull(talent, "Query results should not be empty");
    }

    // Test: Query all records
    @Test
    void testGetAll() {
        List<Talent> list = talentService.list();  // MyBatis-Plus Built-in method to get all data
        assertNotNull(list, "List should not be empty");
        // Assertion: The number of results should be greater than 0
        assertTrue(list.size() > 0, "There should be data in the data table");
    }
}

Things to note

1. The test class cannot find the main startup class

  • Phenomenon:thrown at runtime Unable to find a @SpringBootConfiguration abnormal.
  • reason: The package path of the test class is inconsistent with the main startup class, and Spring cannot automatically locate it.
  • solve:exist @SpringBootTest Explicitly specify the main startup class in .
    @SpringBootTest(classes = Test0705Application.class)
    

2. Test data pollutes the database

  • Phenomenon: After running the new or modified test method, test data remains in the database, affecting subsequent tests.
  • solve:Add on the test method @Transactional, the Spring test environment will automatically roll back the transaction after the method is executed, leaving no trace in the database.
    @Test
    @Transactional  // Automatic rollback at the end of the test without polluting the database
    void testSave() {
        talentService.save(new Talent());
    }
    
  • If you need to retain data(Confirm that the data is correctly logged into the database), plus additional @Rollback(false):
    @Test
    @Transactional
    @Rollback(false)  // No rollback, data retained
    void testSaveAndKeep() { ... }
    

Interface contract verification

core concepts

Interface contract verification is to confirm whether the HTTP API exposed by the Controller layer runs according to the rules agreed upon by the front and back ends - whether the URL path is correct, the request method is correct, and whether the returned JSON structure is unified. Result Format.

Verify what

In a front-end and back-end separation project, the back-end must ensure that the following four items of each interface are consistent with the agreement:

  • URL path: Whether the path complies with RESTful specifications, such as /api/talents, /api/talents/1
  • HTTP method: GET, POST, PUT, DELETE are correct?
  • Request parameters: Whether the field names and types of path variables, query parameters, and request body JSON are consistent with the convention
  • response structure: Whether the return value is uniformly packaged in Result middle,code, message, data Are the three fields complete?

Verification standards for each operation

Add (POST)

  • ask:POST
  • URL path example:/api/talents
  • Request body: DTO data in JSON format,Content-Type: application/json
  • Expected response:{"code":200, "message":"success", "data":null}

Modify (PUT)

  • ask:PUT
  • URL path example:/api/talents
  • Request body: JSON format (requires primary key ID),Content-Type: application/json
  • Expected response:{"code":200, "message":"success", "data":null}

DELETE

  • ask:DELETE
  • URL path example:/api/talents/1
  • Request body: None
  • Expected response:{"code":200, "message":"success", "data":null}

Query a single (GET)

  • ask:GET
  • URL path example:/api/talents/1
  • Request body: None
  • Expected response:{"code":200, "message":"success", "data":{...}}, data is the object queried in

Query all (GET)

  • ask:GET
  • URL path example:/api/talents
  • Request body: None
  • Expected response:{"code":200, "message":"success", "data":[...]}, data in array

Example (taking Postman as an example)

1. Verify the new interface

Do these operations in Postman:

  • Request method selection POST
  • URL bar input http://localhost:8080/api/talents
  • Click Headers, add two lines:
    • token : test123
    • Content-Type : application/json
  • Click Body,select raw, drop down on the right to select JSON, and then paste in the text box:
    {
        "name": "Test talent",
        "phone": "13912345678",
        "education": "master",
        "city": "Nanchang"
    }
    
  • Click Send Send a request.

Verification (just look at the returned results):

  • Look at the upper right corner, is the HTTP status code 200 OK
  • Look at the Body column below, the returned JSON,code Yes or no 200
  • Open the database client and check talent table, see if there is an extra piece of data

2. Verify and query a single interface

  • Create a new request and select the method GET
  • URL bar input http://localhost:8080/api/talents/1
  • Headers Riga token : test123
  • No need for Body, just click Send.

verify:

  • Look at the returned JSON data Is it an object (not an array)
  • data Is the field name in id, name, phone, education, city(Same as the name agreed on the front end)
  • data.id Yes or no 1

Things to note

1. The return value is not wrapped in Result

  • Phenomenon: The Controller method directly returns the entity class,List or boolean, causing the front end to receive {...}, [...] or true/false,No code and message field.
  • Consequences: The front end cannot pass unified res.code Determine whether the request is successful and the parsing logic crashes directly.
  • Correct approach: The return values ​​of all Controller methods must be Result<T> Package. Returns when query is empty Result.success(null), business exceptions are returned uniformly by the global exception handler Result.error(...).

2. Confusion between HTTP status code and business status code

  • Phenomenon: When a business error occurs (such as "talent already exists"), the backend directly throws an unhandled exception, causing the HTTP status code to become 500; or manually set the HTTP status code to 400.
  • question: Both approaches break the uniform response format. The former makes the front end mistakenly think that the server has crashed, and the latter makes the front end need to parse the HTTP status code and the business status code in the response body at the same time, increasing the processing complexity.
  • Correct approach: Agree on a unified method within the team and keep the front and back ends consistent. There are two common strategies:
    • Strategy A: All interfaces return HTTP 200, and the business status is completely determined by Result internal code Field distinction. The front end only needs to parse the response body to determine success or failure.
    • Strategy B: Make full use of HTTP status codes to express semantics (201 successfully created, 400 parameter error, 404 resource does not exist), while the response body is still retained Result structure. The front end handles both HTTP status codes and business status codes.
  • key principles: No matter which strategy is adopted,Must be unified across projects, you cannot use different rules for different interfaces of the same project.Result The packaging layer should always exist, and business exceptions should be handled by the global exception handler instead of manually intervening with HTTP status codes in the Controller.

Unified result packaging Result

effect

All data returned by Controller methods must be Result Packaging to ensure that the JSON structure received by the front end is always unified as {code, msg, data} With three fields, there will be no situation where sometimes it is an object, sometimes it is an array, and sometimes it is a Boolean value.

code

put on com.example.demo.common Included below:

@Data
public class Result<T> {
    // Business status code: 200 Indicates success, not 200 Indicates various types of failure
    private Integer code;
    // Prompt message: When successful, it is "Operation successful", In case of failure, the specific error reason is
    private String msg;
    // Business data: Store the returned data when the query is successful, or when there is no data or fails. null
    private T data;

    // Private constructor, forcing object creation through static factory method
    private Result() {}

    // Successful response (carrying data): used for query operations
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("Operation successful");
        result.setData(data);
        return result;
    }

    // Successful response (no data): used for new, modified, and deleted operations
    public static <T> Result<T> success() {
        return success(null);
    }

    // Failure response (custom status code and prompt information)
    public static <T> Result<T> error(Integer code, String msg) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }

    // Failure response (default 500 Server internal error)
    public static <T> Result<T> error(String msg) {
        return error(500, msg);
    }
}

Writing instructions

1. Why use generics <T>

data The field type is T, means that whatever type of data is put in will be the same type when it is taken out, no forced transfer is required. For example Result.success(userVO) What is returned is Result<UserVO>,front end data Here are the fields of UserVO.

2. Why use static factory methods?

Write directly in Controller Result.success(data) You can get a packaged object, no need to new Result() line by line set. The code is shorter and the intent is clearer.

3. Status code constantization

Maintain one uniformly in the project Code interface to avoid hardcoding numbers everywhere:

public interface Code {
    Integer SUCCESS = 200;          // success
    Integer BAD_REQUEST = 400;      // Parameter error
    Integer UNAUTHORIZED = 401;     // Not logged in or Token Expired
    Integer FORBIDDEN = 403;        // No permission
    Integer NOT_FOUND = 404;        // Resource does not exist
    Integer INTERNAL_ERROR = 500;   // Server internal error
}

When using:

Result.error(Code.NOT_FOUND, "Talent information does not exist");
Result.error(Code.BAD_REQUEST, "Parameter verification failed");

No injection or inheritance is required. Once created, it can be used directly anywhere. You can get 200 by writing Code.SUCCESS anywhere in the project.

Controller usage example

@RestController
@RequestMapping("/api/talents")
public class TalentController {

    @Autowired
    private TalentService talentService;

    // New: No data returned
    @PostMapping
    public Result<Void> save(@RequestBody @Validated TalentDTO dto) {
        talentService.save(dto);
        return Result.success();
    }

    // Query a single: carry data
    @GetMapping("/{id}")
    public Result<Talent> getById(@PathVariable Long id) {
        Talent talent = talentService.getById(id);
        if (talent == null) {
            return Result.error(Code.NOT_FOUND, "Talent information does not exist");
        }
        return Result.success(talent);
    }

    // Query all: carry collection data
    @GetMapping
    public Result<List<Talent>> getAll() {
        List<Talent> list = talentService.list();
        return Result.success(list);
    }
}

Things to note

1. Result must be used with the global exception handler

Used in Controller Result packaging, but if exceptions such as parameter verification failure and database error are not handled, the front end will still receive Tomcat's default error page or messy JSON, and the unified format will be broken.

Correct approach:All exceptions are unified by @RestControllerAdvice Global exception handler interception, converted to Result.error(...) return. In Controller only use Result.success(), all errors are handled by the exception mechanism.

2. Do not write business judgments in the Controller

Don't write in Controller:

boolean flag = service.save(dto);
if (!flag) {
    return Result.error("Failed to add");
}

Correct approach:The service layer encounters business errors directly throw new BusinessException("reason"), the global exception handler automatically changes to Result.error. The Controller is only responsible for receiving parameters, calling Service, and returning Result, and does not make business judgments.

3. The serialization behavior remains unchanged when data is null

Result.success(null) In the returned JSON data The field value is null, this is normal. Look at the front-end judgment code, don’t watch data Whether it is null.data Being null only means that there is no data to be returned for this operation (such as a delete operation), but it does not mean that it failed.

4. The field serialization rules of internal data remain unchanged.

Result Just outer packaging, no intervention data Serialization of internal objects. if data The fields in are @JsonFormat date formatting,@JsonSerialize Annotations such as custom serialization still take effect normally.

In traditional SSM teaching documents, in order to demonstrate the concept of exception classification, it is usually passed at the Service layer. try-catch Catch the underlying exception and manually wrap it as SystemException Throw. In modern Spring Boot enterprise-level practice, this approach has been regarded asAnti-Pattern, so it was merged with unknown exceptions in the previous sorting and handed over to the global team for handling.

The core reasons for omitting (merging) are as follows:

  1. Violates AOP and code cleanliness principles:Written in each Service method try-catch To wrap system exceptions will cause the business code to be extremely bloated, and the core business logic will be overwhelmed by a large number of exception packaging codes.
  2. Break declarative transactions (@Transactional): If exceptions are manually captured and then thrown, it is very easy for the Spring transaction manager to be unable to correctly perceive the original exception due to improper handling, leading to serious production accidents in which the transaction is not rolled back.
  3. The global interception mechanism is complete enough: The bottom layer of the Spring framework will throw specific runtime exceptions (such as DataAccessException). Directly intercept these underlying system exceptions or unified exceptions through the global exception handler Exception.classBy cooperating with a log framework (such as SLF4J + Logback) to record the stack and connect to the alarm system, the need to "appease users and notify operation and maintenance" can be perfectly realized without manual conversion at the business layer.

Global exception handling

Why global exception handling is needed

Each layer of code in the project may make errors when running: Service layer verification fails, database connection times out, code has a null pointer exception, etc. If these errors are not handled uniformly, the backend will directly return the exception stack information to the frontend. The front end received a screen full of English error messages, which resulted in a very poor user experience. It also exposed sensitive information such as package paths and table names within the server.

The role of global exception handling is to Unify the interception of errors scattered at each layer, package them into a JSON format that can be recognized by the front end and return them. At the same time, decide whether to record logs and notify operation and maintenance personnel based on the exception type..

Anomaly classification

Exceptions in projects are divided into three categories according to their sources, and are handled in different ways:

1. Business abnormality

  • What is:Improper user operationorData does not comply with business rules, such as the user name already exists, duplicate declaration of talent, incorrect password format.
  • Solution: Give the specific error causeReturn directly to the front end, remind the user to modify. There is no need to record logs or notify operation and maintenance personnel.

2. System abnormality

  • What it is: Failure to call external services or system component failure, such as third-party AI interface timeout, SMS gateway disconnection, and brief database jitter.
  • How to deal with it: Return soothing words to the front end (such as "The system is busy, please try again later") and hide the technical details. Logs need to be recorded for subsequent investigation.
  • Control alarm: The system may have a large number of small jitters in edge services every day, which requires needAlert Identification to control: When core links (such as payment, core data warehousing) fail, an alarm is triggered, and operation and maintenance personnel are notified to handle it. When edge services (such as non-core weather query) fail, only logs are recorded.

3. Unknown exception

  • What it is: Code-level bugs, such as null pointer exceptions, array out-of-bounds, and SQL syntax errors.
  • Processing method: Return a unified prompt to the front end (such as "Internal system error, please contact the administrator"), complete stack logs must be recorded, and alarms must be triggered forcibly.

Core annotations

Spring Boot provides two annotations to implement global exception handling:

  • @RestControllerAdvice: Added to a class to identify this class as a global exception handler. It will intercept all exceptions thrown by the Controller layer, and automatically convert the object returned by the processing method into JSON and write it into the response body.
  • @ExceptionHandler(Exception class.class): Added to a method to specify what type of exception this method is responsible for handling. Whatever exception class is written in the parameter, this method will only handle the exception class.

syntax template:

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Specific exception class.class)
    public Result handleXxxException(Specific exception class e) {
        // Get the error message from the exception object
        // use Result.error Return after packaging
        return Result.error(error code, error message);
    }
}

Code implementation

Custom exception class

The function of the exception class is to allow The Service layer can package different errors into different categories of "error packages" Throw it out, and the global exception handler will handle it differently according to the package category.

Where to put it:exist main bag.exception Create these two new classes under the package.

Why inheritance is necessary RuntimeException:

  • inherit RuntimeException Finally, there is no need to write when the Service layer throws an exception. try-catch Wrapped, the code is cleaner.
  • Spring's @Transactional The transaction manager recognizes RuntimeException, automatically rollback the database transaction once thrown. If a non-runtime exception is inherited, the transaction will not be rolled back.

Business exception class:

package com.example.demo.exception;

public class BusinessException extends RuntimeException {

    private final Integer code;   // Business status code, consisting of Code Unified management of interfaces

    /**
     * Construction method: Pass in status code and error information
     * Usage scenario: When the error code needs to be explicitly specified
     */
    public BusinessException(Integer code, String message) {
        super(message);     // Store the error information in the parent class and let getMessage() available
        this.code = code;
    }

    /**
     * Construction method: only transmit error information, status code defaults 400(Parameter error)
     * Usage scenarios: Scenarios where most parameter verification fails
     */
    public BusinessException(String message) {
        this(400, message);       // Call the above two-parameter construct, code pass 400
    }

    public Integer getCode() {
        return code;
    }
}

this(...) It is a special syntax in Java called constructor call. It can only be written in the first line of the constructor method. Its function is to call another constructor method of this class and simplify the same logic.

System exception class:

package com.example.demo.exception;

public class SystemException extends RuntimeException {

    private final Integer code;
    private final boolean needAlert;  // Whether it is necessary to trigger an operation and maintenance alarm (Fa Dingding/Short message)

    /**
     * Construction method: Specify status code, error message, and whether to alert at the same time
     */
    public SystemException(Integer code, String message, boolean needAlert) {
        super(message);
        this.code = code;
        this.needAlert = needAlert;
    }

    /**
     * Construction method: only error information is passed, default code=500 and needAlert=true
     * Usage scenario: core link failure, developers need to be notified immediately
     */
    public SystemException(String message) {
        this(500, message, true);      // Call the three-parameter construct, needAlert pass true
    }

    public Integer getCode() {
        return code;
    }

    public boolean isNeedAlert() {
        return needAlert;
    }
}

needAlert Detailed explanation of logo:

needAlert yes SystemException one of the classes boolean Type field. It has only two values:true and false. This field determines when the global exception handler catches SystemException back,Should we notify the operation and maintenance personnel?.

valuemeaningWhat action is triggered?
trueNeed to alertRecord log + send DingTalk/SMS/email to notify operation and maintenance personnel
falseNo alarm requiredOnly logs and does not send any notifications

Why is it designed like this?: After the company's project went online, the system had to handle a large number of requests every day. Some of the external services are core links (such as payment interfaces, core data storage). If there is a problem, the developers must be informed immediately, and they have to get up in the middle of the night to fix it. However, some external services are edge functions (such as querying the weather and sending irrelevant SMS notifications). It is normal to occasionally time out or fail. If an alarm is sent every time, operation and maintenance personnel will receive hundreds of messages a day, and truly important alarms will be drowned. This is the "alarm storm". So in terms of design,needAlert It is used to distinguish:If there is a problem with the core link, set it to true, if there is a problem with the edge function, set it to false.

Cooperation with other layers

How to pass values ​​in the Service layer:

// Scenario 1: Core business goes wrong → Construct with single parameter, needAlert Default is true
throw new SystemException("AIResume parsing service timed out");
// Equivalent to: throw new SystemException(500, "AIResume parsing service timed out", true);

// Scenario 2: Edge business goes wrong → Explicitly pass false, Avoid alert storms
throw new SystemException(500, "SMS sending failed", false);

How to use it in global exception handler:

if (e.isNeedAlert()) {
    // needAlert for true → Send alert
    alertService.sendAlert("Core system exception", e.getMessage());
}
// needAlert for false → No alarms are sent, only logs are recorded
Global exception handler

Where to put it:exist main bag.exception New under package GlobalExceptionHandler kind.

core logic: through three @ExceptionHandler The methods handle three types of exceptions respectively, and each returns a unified Result Format.

package com.example.demo.exception;

@RestControllerAdvice  // Declare this to be a global exception handler and intercept all Controller Exception thrown
public class GlobalExceptionHandler {

    // Log object, used to record stack information of system-level errors
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

     // Handling business exceptions: no logs or alarms, and error information directly returned to the front end
    @ExceptionHandler(BusinessException.class)
    public Result handleBusinessException(BusinessException e) {
        // Remove from exception object code and message, Return to the front end as is
        return Result.error(e.getCode(), e.getMessage());
    }

    // Handle system exceptions: record logs, based on needAlert Decide whether to give an alarm and return comfort words to the front end
    @ExceptionHandler(SystemException.class)
    public Result handleSystemException(SystemException e) {
        // Record logs to facilitate subsequent investigation
        log.error("System exception: {}", e.getMessage(), e);

        // according to needAlert The identification determines whether to trigger an alarm
        if (e.isNeedAlert()) {
            // Core link failure, hairpin/SMS notification to operation and maintenance personnel
            sendAlert("Core system exception", e.getMessage());
        }
        // needAlert for false At this time, only logs are recorded and no alarms are generated to avoid alarm storms caused by edge service jitters.

        // Return comfort words to the front end without exposing technical details
        return Result.error(e.getCode(), "The system is busy, please try again later");
    }

    // Handle unknown exceptions thoroughly: record complete stack, force alarms, and return unified prompts
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        // Record the complete stack log, this is the troubleshooting Bug key basis
        log.error("Unknown exception: ", e);

        // Unknown exceptions belong to the code level Bug, A mandatory warning is required
        sendAlert("seriousBug", e.getMessage());

        // Returning unified prompts to the front end must not be e.getMessage() Return directly (may contain sensitive information)
        return Result.error(Code.INTERNAL_ERROR, "System internal error, please contact the administrator");
    }

    // Simulated alarm service
    private void sendAlert(String title, String message) {
        // In the actual code, here is the docking DingTalk/Enterprise WeChat/Email and other alert channels
        // This is just a placeholder example
        log.warn("trigger alarm: {} - {}", title, message);
    }
}

The calling priority of the three processing methods:

  1. Throw BusinessException when, enter handleBusinessException.
  2. Throw SystemException when, enter handleSystemException.
  3. Throw other exceptions (such as NullPointerException, SQLException), the first two do not match, and finally enter handleException reveal all the details.

Spring matches exception types in order from specific to general. It first matches exceptions that are specially processed and uses them only if they cannot be found. Exception.class method.

How the Service layer throws exceptions

In the business code of the Service layer, different types of exceptions must be thrown when encountering different situations. The following examples show how to write three scenarios:

@Service
public class TalentServiceImpl implements TalentService {

    @Autowired
    private TalentMapper talentMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)  // Start the transaction and roll back any exceptions
    public void applySubsidy(Long talentId) {

        // Scenario 1: Business verification fails → throw BusinessException
        // This is a user-level error. Just tell the user the reason directly.
        Talent talent = talentMapper.selectById(talentId);
        if (talent == null) {
            throw new BusinessException("Declaration failed: The talent information does not exist");
        }
        if (talent.getStatus() == 1) {
            throw new BusinessException("Application failed: The talent has already applied for subsidies");
        }

        // Scenario 2: Failure to call core external services → throw SystemException(needAlert=true)
        // AI Resume parsing is core business and developers must be notified when it fails
        try {
            String result = callAiResumeParseService(talent.getFileUrl());
        } catch (TimeoutException e) {
            // Constructed with single parameter, default needAlert=true
            throw new SystemException("AIResume parsing service timed out");
        }

        // Scenario 3: Failure to call edge external services → throw SystemException(needAlert=false)
        // SMS notification is an auxiliary function. Occasionally sending failure is normal and no alarm is needed in the middle of the night.
        try {
            sendSms(talent.getPhone(), "Your declaration has been accepted");
        } catch (Exception e) {
            // Constructed with three parameters, explicitly specified needAlert=false, Avoid alert storms
            throw new SystemException(500, "SMS sending failed", false);
        }

        // Perform database operations
        // If this database is down, Spring The bottom layer throws DataAccessException
        // The exception does not belong to BusinessException or SystemException
        // will be GlobalExceptionHandler A covert method to capture and force an alarm
        talentMapper.updateStatus(talentId, 1);
    }
}

Notice: With @Transactional In the method,strictly prohibiteduse try-catch Do not rethrow an exception after catching it. because if catch exception but no throw, the Spring transaction manager will consider that the method has been executed normally and commit the transaction. This will result in database operations that are only half executed but are permanently saved, resulting in severely dirty data.

Complete workflow

When the Service layer throws an exception, Spring MVC's processing sequence is as follows:

  1. Exceptions are detected in the Service layer throw Throw, penetrating the Controller layer up the call stack.
  2. Controller no catch This exception continues to be thrown upward to DispatcherServlet.
  3. DispatcherServlet Found there @RestControllerAdvice Flagged global exception handler.
  4. Spring based on the actual type of the exception object, in GlobalExceptionHandler Find a method that can handle this exception. The matching rule isfrom specific to general, first find a professional to deal with it BusinessException or SystemException I didn't find any method, so I took the plunge. Exception.class method.
  5. Execute the matched processing method, retrieve the error information from the exception object, and construct Result.error(...) and return.
  6. Spring handle Result The object is automatically serialized into JSON and written into the response body, and the front end receives an error response in a unified format.

Things to note

1. Transaction rollback problem

Bringing @Transactional Catching exceptions in methods without rethrowing them is the most common serious mistake beginners make.

Wrong writing:

@Transactional
public void save(Talent talent) {
    try {
        talentMapper.insert(talent);
    } catch (Exception e) {
        e.printStackTrace();  // Exception was swallowed, Spring Don't know something went wrong
        // The transaction manager will commit the transaction and generate dirty data
    }
}

Correct writing: When encountering an unrecoverable error, you must throw it RuntimeException, allowing the transaction manager to sense the exception and roll back.

2. Leakage of sensitive information

Behind the scenes handleException In the method,NeverBundle e.getMessage() Return directly to the front end. System exception messages may contain sensitive information such as database table names, SQL statements, and server intranet IPs. Direct exposure may cause security vulnerabilities. A unified message like "Internal system error, please contact the administrator" must be returned.

3. Package path problem

GlobalExceptionHandler The class must be placed in the package where the main startup class is locatedSibling or child packagesDown.@SpringBootApplication By default, the package where the main startup class is located and its sub-packages are scanned. If the global exception handler is placed in the wrong location and Spring cannot scan it, the entire exception handling mechanism will fail.

Joint debugging of front-end and back-end agreements

In enterprise-level projects with separate front-end and back-end, the back-end provides RESTful API and unified response contract (Result), the front end calls the interface asynchronously through Ajax (such as Axios) and drives UI updates based on the response status. Understanding this complete joint debugging closed loop is the basis for back-end developers to design reasonable interfaces and troubleshoot front-end and back-end interaction problems.

Standard deployment of static resources in Spring Boot

  • Standard storage location: All front-end static resources (such as Vue packaged dist directory contents, or native HTML/JS files) should be placed directly in src/main/resources/static/ directory.
  • Automatic release mechanism: The bottom layer of Spring Boot automatically configures the resource processor and places it in static, public, resources Files in the directory will be automatically released without writing any Java configuration classes.
  • access path: Assume the file is located in src/main/resources/static/pages/books.html, pass directly after starting the project http://localhost:8080/pages/books.html can be accessed.

The core of interface joint debugging: based on Result Contract state driven

The core of front-end and back-end joint debugging lies inThe data structure returned by the backend must strictly match the front-end parsing logic. The front end does not directly determine the HTTP status code (such as 200, 404, 500), but uniformly reads the response body Result object, according to the internal code Fields to determine the feedback behavior of the UI.

state driven matrix:

Backend returns Result.codebusiness implicationFront-end UI feedback action
200 (or the agreed success code)Business operation successfulClose the pop-up window, refresh the list, and pop up the "operation successful" prompt (such as $message.success)
400 (Parameter verification failed)Client parameter passing errorKeep the pop-up window open and display error messages in red below the corresponding fields in the form.
500 / 600 (Business/system exception)Business interruption or system failure"Operation failed" or specific error message pops up (such as $message.error), do not refresh the list

Return value specifications of back-end data layer and service layer

Minimalist return value of MyBatis-Plus

In the development of the data persistence layer, determining whether a SQL statement (such as INSERT, UPDATE, DELETE) is executed successfully is the key to closed-loop business logic.
In projects combining Spring Boot with MyBatis-Plus, the bottom layer of the framework has automatically encapsulated the row number judgment logic.IService provided by the interface save, updateById, removeById etc.Return directly boolean, completely eliminating the redundant conversion code of the Service layer.

Pain points of traditional MyBatis

In native MyBatis, the addition, deletion and modification methods of the Mapper interface return by default int type, representsNumber of rows affected in the database. The Service layer needs to convert it to boolean For Controller to judge.

Developers must manually write judgment logic in the Service layer:

// Traditional writing method: You must manually determine whether the number of affected rows is greater than0
int rows = bookDao.save(book);
return rows > 0; 

This way of writing results in the Service layer being filled with a lot of code that has no business value. > 0 Determine the code and increase the redundancy of the code.

Minimalist packaging of MyBatis-Plus

Provided by MyBatis-Plus IService Interface (and its implementation class ServiceImpl) intercepts the execution results of MyBatis at the bottom level and automatically completes > 0 conversion.

  • save(T entity): The bottom layer executes INSERT, automatically determines the number of affected rows, and returns boolean.
  • updateById(T entity): The bottom layer executes UPDATE, automatically determines the number of affected rows, and returns boolean.
  • removeById(Serializable id): The bottom layer executes DELETE, automatically determines the number of affected rows, and returns boolean.

core meaning: The Service layer only needs to focus on the Boolean result of "success or not" and no longer needs to care about the specific number of rows returned by the underlying database, making the business code more pure.

Example

The following code shows how the Service layer and Controller layer use this mechanism for minimalist interaction in an environment where Spring Boot is combined with MyBatis-Plus.

Service layer: minimalist persistence call

In the Service layer, call directly IService Provided method to receive boolean As a result, a complete farewell > 0 judge.

@Service
public class TalentServiceImpl extends ServiceImpl<TalentMapper, Talent> implements TalentService {

    // scene 1: Basic new operations
    @Override
    public boolean addTalent(Talent talent) {
        // MyBatis-Plus The bottom layer has been implemented: execution INSERT Then automatically determine the number of affected rows and return true/false
        // Service layer direct call save Method, no need to write anymore baseMapper.insert(talent) > 0 logic
        return save(talent); 
    }

    // scene 2: Update operation combined with business verification
    @Override
    public boolean updateTalent(Talent talent) {
        // Business verification: for example, checking whether the talent is allowed to be modified
        if ("locking".equals(talent.getStatus())) {
            throw new BusinessException("This talent information has been locked and cannot be modified.");
        }
        
        // Return directly updateById of boolean As a result, the bottom layer automatically determines UPDATE Whether the statement affects data rows
        return updateById(talent);
    }
}

Controller layer: Boolean-based contract response

The Controller layer receives the response returned by the Service layer boolean value, you can directly use it to build a unified Result Respond to the contract.

@RestController
@RequestMapping("/api/talents")
public class TalentController {

    @Autowired
    private TalentService talentService;

    @PostMapping
    public Result<Void> add(@RequestBody @Validated TalentDTO dto) {
        Talent talent = new Talent();
        BeanUtils.copyProperties(dto, talent);
        
        // receive directly Service layer returns boolean value
        boolean success = talentService.addTalent(talent);
        
        // Build a unified response based on boolean values
        if (success) {
            return Result.success();
        }
        // If the number of rows affected by the bottom layer is0(For example, some special interceptors of the database are triggered, resulting in no insertion) and a failed contract is returned.
        return Result.error(Code.SAVE_ERR, "Data saving failed, please check the database status");
    }
}

Front-end and back-end joint debugging contract and F12 packet capture troubleshooting guide

In front-end and back-end separation projects, the essence of front-end and back-end joint debugging isContract verification of HTTP messages. Backend developers do not need to be proficient in front-end frameworks, but they must be able to use the Network panel of the browser's F12 developer tools to analyze HTTP requests and responses. This is a core skill for quickly defining front-end and back-end responsibilities and locating bugs.

Core concepts and joint debugging matrix

Front-end and back-end interaction completely relies on the HTTP protocol. The backend only needs to focus on two things:

  1. request messageWhether it complies with the Controller's receiving specifications.
  2. response messageIs it in compliance with the agreement? Result contract.

When encountering a problem, the back-end developer should instruct the front-end (or himself) to open the browser F12, enter the Network panel, find the request with the abnormal status code, and troubleshoot according to the following comparison table.

Front-end phenomenonF12 troubleshooting entry pointBackend code self-check points
Front-end prompt 404 / 405Check Headers(Header) Request URL and Request Method of the tabCheck on the Controller class @RequestMapping("path") and methodologically @PostMapping("path") Whether the splicing is correct; whether the verbs match strictly.
The parameters received by the backend are all null.Check Payload tab (JSON) or Form Data tab pageCheck whether the Key of the front-end JSON is exactly the same as the attribute name of the back-end DTO; check whether the method parameters are missing @RequestBody or @RequestParam.
The front end prompts "system exception" or the page crashesCheck Response Content returned by the tab pageCheck global exception handler (@RestControllerAdvice) is effective. If the response body is Tomcat's HTML error page or Java stack information, it means that the exception has not been handled uniformly and has been damaged. Result contract.
The front end prompts "operation successful" but there is no change in the databaseCheck Response The JSON returned in the tab, which code Is it 200?Check if the Service or DAO layer is blind return true, instead of judging based on the actual number of rows affected by the database. If the database returns 0 rows affected, a failure code should be returned or an exception should be thrown.

F12 packet capture and troubleshooting standard workflow

When joint debugging encounters a problem, follow the following three steps to troubleshoot in the Network panel of F12:

Verify request routing (Headers tab)
  • Where to look: Click on the specific HTTP request, at the bottom of the Headers tab on the right General area.
  • What to see:
    • Request URL: Verify the complete URL path. For example, the front-end request http://localhost:8080/api/talents, the class and method annotations of the backend Controller must match exactly after splicing.
    • Request Method: Check HTTP methods (GET, POST, PUT, DELETE). If the front end uses POST request, the backend corresponding method must be @PostMapping, otherwise a 405 error will be reported.
Verify request parameters (Payload tab)
  • Where to look: Click on the specific HTTP request and select Payload tab (may be called Request Payload in older browsers).
  • What to see:
    • This shows the JSON string actually sent by the front end.
    • Check if Key matches: Key in JSON (such as "talentName") must match the property name in the backend DTO class (talentName) are exactly the same, including capitalization.
    • Check whether the annotations are correct:
      • If it is JSON data, there must be before the backend method parameters @RequestBody.
      • If it is a URL ?key=value Form or form data, must be preceded by backend method parameters @RequestParam.
Verify response contract (Response tab)
  • Where to look: Click on the specific HTTP request and select Response tab page.
  • What to see:
    • View the response body content actually returned by the backend.
    • Format check: Regardless of business success or failure, the response body must be in standard JSON format and contain three fields:code, msg, data. For example:{"code":200, "msg":"Operation successful", "data":null}.
    • Exception check: If the response body displays a large Java exception stack (such as java.lang.NullPointerException) or Tomcat’s HTML error page, indicating that the exception has not been @RestControllerAdvice Global exception handler capture directly penetrates to the front end. This is a serious problem with backend code.

Interceptor

In the Spring MVC architecture, the interceptor is a dynamic interception mechanism based on the idea of ​​AOP (aspect-oriented programming). It allows custom business logic to be inserted before and after the request reaches the Controller method and after the view is rendered. In the Spring Boot front-end and back-end separation project, interceptors are the core components for building system security defense lines (such as login authentication and permission verification).

Core concepts and application scenarios

The essential difference between interceptors and filters

Although Interceptor and Filter are similar in function, there are essential differences between the two in terms of ownership level and capability boundaries:

  • Filter: Belongs to the Servlet specification level. Directly managed by web containers such as Tomcat, it can intercept all requests entering the server (including static resources such as HTML, CSS, JS, etc.). But it cannot directly use beans in the Spring container (for example, it cannot directly inject Redis tool classes or services).
  • Interceptor: Belongs to the Spring MVC framework level. Managed by Spring container,Only block entry DispatcherServlet dynamic request(Static resources are not intercepted by default). Its biggest advantage isAbility to seamlessly use all beans in the Spring container, very suitable for processing complex business authentication logic.

When to use

  • Login authentication: Verify whether the Token (such as JWT) in the request header is legal and determine whether the user is logged in.
  • Permission verification: Combined with the RBAC (role-based access control) model, verify whether the current user has the permission to access a specific interface.
  • Operation logging: Record the user's operation behavior, IP address and interface time before and after request processing.
  • Prevent duplicate submissions: Combined with Redis distributed lock, it prevents users from making malicious repeated clicks on the same interface in a short period of time.

Syntax formulas and code writing location

In Spring Boot, configuring interceptors must be implemented by WebMvcConfigurer interface to complete.

code writing location:

  1. Interceptor class: Usually placed in the project's interceptor Packaged, need to be implemented HandlerInterceptor interface and add @Component Annotations are managed by the Spring container.
  2. Configure registration class: Usually placed in the project's config package (such as WebMvcConfig), need to be implemented WebMvcConfigurer interface, rewrite addInterceptors method.

Grammar formula

// 1. Define interceptor
@Component
public class XxxInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(...) { ... }
}

// 2. Register interceptor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(Interceptor instance)
                .addPathPatterns("intercept path")
                .excludePathPatterns("Release path");
    }
}

Example

The following code shows how to implement a standard "Token login authentication interceptor" in the project and register it in the Spring MVC container.

1. When to block and verify logic - write authentication interceptor class

Where to write: main bag.interceptor.LoginInterceptor

package main bag.interceptor;

@Component // This annotation must be added to register the interceptor as Spring Bean, for subsequent injection in the configuration class or directly new come out
public class LoginInterceptor implements HandlerInterceptor {

    // Core method: in Controller Triggered before method execution
    // @return true Release, request to continue executing backwards; false Interception, the request ends here
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1. Get the front-end passed from the request header Token
        String token = request.getHeader("token");

        // 2. check Token Is it empty
        if (token == null || token.trim().isEmpty()) {
            // Manual setting HTTP The status code is 401 (Unauthorized), Indicates unauthorized
            response.setStatus(401);
            response.setContentType("application/json;charset=utf-8");
            // Error writing unified contract to frontend JSON response
            response.getWriter().write("{\"code\":401,\"msg\":\"Not logged in orTokenExpired, please log in again\"}");
            // return false, Block request, Controller method will not execute
            return false;
        }

        // 3. check Token Legality (specific details are omitted here) JWT Analyze and Redis Verification logic, simulated with hard coding)
        boolean isValid = checkToken(token);
        if (!isValid) {
            response.setStatus(401);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("{\"code\":401,\"msg\":\"Tokeninvalid\"}");
            return false;
        }

        // 4. Verification passed, request released
        // Advanced operation: The parsed users can beIDDeposit ThreadLocal, For follow-up Controller/Service Get current logged in user information
        return true;
    }

    // simulation Token Verification method, which should be parsed here in actual projects JWT or query Redis
    private boolean checkToken(String token) {
        return "admin_token_123".equals(token);
    }
}

Analysis: Why is "handwritten JSON" required in the interceptor? (response) instead of "throwing an exception" (throw)?

In a Controller or Service, when encountering an error we usually directly throw new BusinessException(401, "Not logged in"),let @RestControllerAdvice Global exception handler to catch and convert to JSON. But in the interceptor, manual operation is highly recommended response Write JSON (as shown in the code above).

reason: The execution timing of the interceptor (Interceptor) is DispatcherServlet Distribute to Controller Before. and @RestControllerAdvice Mainly used to capture Controller and its internal call chain Exception thrown. If a business exception is thrown directly in the interceptor, Spring MVC's default exception handling mechanism may not be able to correctly convert it into the agreed JSON response (usually going to Tomcat's default error page or Spring Boot's BasicErrorController), causing the front end to receive inconsistent Result Format. So, in the interceptorDirect operation HttpServletResponse Manually writing JSON is the safest and most standard solution.

2. What to block - register the interceptor and configure path rules

Once blocked, you need to go to the interceptor class for verification and decide whether to release it.
Interception policy: deny by default, allow explicitly

Where to write: main bag.config.WebMvcConfig

package main bag.config;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // If other interceptor classes need to be injected Spring Bean(like RedisTemplate), Passable @Autowired Inject interceptor instance
    // Here is a simple demonstration, directly new out interceptor object
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                // intercept all /api/ Beginning request
                .addPathPatterns("/api/**")
                // Release login and registration interface
                .excludePathPatterns(
                    "/api/login",
                    "/api/register"
                );
    }
}

interception structure

Any number of implementations can be created in a project HandlerInterceptor The interceptor class for the interface. Each class is responsible for an independent verification logic, such as login authentication, permission verification, operation logs, prevention of repeated submissions, etc.

In actual projects, there are usually 3 to 5, divided according to responsibilities:

  • LoginInterceptor ——Verify Token to determine whether the user is logged in
  • PermissionInterceptor ——Verify whether the user has permission to access an interface
  • LogInterceptor ——Record the path, parameters and time consumption of each request
  • RateLimitInterceptor ——Prevent users from repeatedly submitting in a short period of time
    ……

then in WebMvcConfig of addInterceptors In the method, register in sequence according to the order of execution, and configure independent interception paths and release paths for each.

A project usually has only one WebMvcConfig Configuration class, a addInterceptors method. In this method, in order of execution registry.addInterceptor(...), each interceptor can be configured with its own independent addPathPatterns and excludePathPatterns.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // Interceptor 1: Only /api/admin/** Path permission verification
        registry.addInterceptor(new AdminInterceptor())
                .addPathPatterns("/api/admin/**");

        // Interceptor 2: for all /api/** Path for login verification
        registry.addInterceptor(new LoginInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/login", "/api/register");

        // Interceptor three: for all /api/** Path logging
        registry.addInterceptor(new LogInterceptor())
                .addPathPatterns("/api/**");
    }
}

The complete process of request interception (pre-processing)

  1. front-end GET /api/user/info, please take it with your head token: admin_token_123.
  2. The request arrives at Spring's DispatcherServlet.
  3. DispatcherServlet Found based on URL UserController.getUserInfo method.
  4. Before actually calling the Controller, Spring detects that there is an interceptor registered in /api/** on the path.
  5. Spring automatic call LoginInterceptor.preHandle(...).
  6. preHandle Here you use request.getHeader("token") Got it "admin_token_123".
  7. Verification passed, return true.
  8. Spring continues execution UserController.getUserInfo(), return data to the front end.

If the Token obtained in step 6 is empty or illegal,preHandle return false, step 8 will not happen, and the front end will directly receive a 401 error.

Should static resources be released?

In front-end and back-end separation architecture projects, it is most likely not needed.

The reason is simple: in the front-end and back-end separation architecture, the front-end code (Vue, React or ordinary HTML) is started independently and is not placed in Spring Boot. static directory. The backend only provides JSON interface and does not provide HTML pages.

Therefore, there are no static resources to be processed in the project, so naturally there is no need to write "/**/*.html" These release rules.

When do you need to release static resources?

There is only one situation: put the front-end packaged files in src/main/resources/static/ directory, the front-end and back-end are deployed in the same project. At this time the user accesses http://localhost:8080/index.html It is actually requesting static files in Spring Boot. If the interceptor .html, .css, .js If it is also blocked, the page will have a white screen and cannot be opened.

Judgment criteria:Open src/main/resources/static/ Directory, take a look to see if there is anything in it. If it is empty, it does not need to be released. If there is a file, it needs to be added.

Detailed explanation of path matching rules

exist addPathPatterns and excludePathPatterns , path matching follows Ant-style rules:

  • /books: Exact match only /books This path.
  • /books/*:match /books/ Any path to the next level (e.g. /books/1),butno matchMulti-level paths (e.g. /books/1/detail).
  • /books/**:match /books/ downAll levelsPath (most commonly used in enterprise-level projects, such as /api/** Represents interception of all API interfaces).

Interceptor life cycle and execution process

HandlerInterceptor The interface defines three core methods, which constitute a complete request life cycle:

  1. preHandle (pre-processing)
    • execution timing:Controller method executionBefore.
    • core role: Determine whether to release the request. return true Continue execution and return false Directly interrupt the request link. Most business logic (such as authentication, current limiting) is implemented in this method.
  2. postHandle (post-processing)
    • execution timing:Controller method executionafter, but when the view renders (or JSON serializes and writes the response body)Before.
    • core role: Yes ModelAndView Make changes. In projects with front-end and back-end separation (returning JSON directly), this methodRarely used.
  3. afterCompletion (Complete processing)
    • execution timing: Entire request processingcomplete(including response data has been written).
    • core role: used for resource cleanup (such as clearing ThreadLocal user information in to prevent memory leaks) or record global time-consuming logs. Regardless of whether an exception occurs in the request, as long as preHandle returned true, this methodMust be executed.

Interceptor chain configuration and execution order

When multiple interceptors are configured in the project (for example: execute the log interceptor first, and then execute the login interceptor), a problem will occur.interceptor chain. The order of execution is strictly followed "First in, last out (stack structure)" principle.

Assume there are interceptor A and interceptor B, and the configuration order is A first and then B:

  1. preHandle Stage (positive sequence):A's preHandle -> B's preHandle.
  2. Controller execution.
  3. postHandle Stages (reverse order):B's postHandle -> A's postHandle.
  4. afterCompletion Stages (reverse order):B's afterCompletion -> A's afterCompletion.

blocking mechanism: If in B's preHandle returned in false(Intercepted request):

  • Controller methods will not be executed.
  • A and B's postHandle allwill not be executed.
  • A's afterCompletion will still be executed(Because A’s preHandle has been successfully released, the Spring framework must ensure that A’s resource cleanup logic is executed), and B’s afterCompletion Will not be executed.

Things to note

1. Space-time pointer exception when interceptor injects Bean

  • Must be added to the interceptor class @Component annotation;
  • exist WebMvcConfig In the configuration class, pass @Autowired Inject the interceptor instance and then pass this injected instance to registry.addInterceptor(), rather than directly new.

2. ThreadLocal must be cleaned after use

  • Must be in the interceptor afterCompletion Called in method ThreadLocal.remove() Perform a forced cleanup.
  • afterCompletion The method does not matter whether an exception occurs in the request, as long as preHandle returned true, it will definitely be executed and it is cleaning ThreadLocal The most suitable location.