How to change the corresponding business logic in springboot without changing other people’s code

I think many people are like me and have the same confusion as the title. This problem has puzzled me for a long time, and now I have finally solved it. Next, let me take you through my thinking and solution process.

Step one: Let’s build a simple springboot web project.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot</name>
    <description>springboot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.33</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>

</project>

Database table

create table chapter
(
    id int auto_increment comment 'primary key' primary key,
    fiction_id int not null comment 'novel id',
    chapter_title varchar(50) not null comment 'chapter title',
    content_id int not null comment 'contentid',
    create_date datetime not null on update CURRENT_TIMESTAMP comment 'Creation time',
    sort int not null comment 'serial number',
    chapter_url varchar(200) null comment 'Article link'
)comment 'Chapter' ;
INSERT INTO book.chapter (id, fiction_id, chapter_title, content_id, create_date, sort, chapter_url) VALUES (1, 6, 'Volume 1 The Bird in the Cage Chapter 1 Awakening of Insect', 1, '2020-09-09 01:28:29', 1, 'http://www.shuquge.com/txt/8659/2324752.html');
INSERT INTO book.chapter (id, fiction_id, chapter_title, content_id, create_date, sort, chapter_url) VALUES (2, 6, 'Volume 1 The Bird in the Cage Chapter 2 Opens the Door', 2, '2020-09-09 01:28:30', 2, 'http://www.shuquge.com/txt/8659/2324753.html');
INSERT INTO book.chapter (id, fiction_id, chapter_title, content_id, create_date, sort, chapter_url) VALUES (3, 6, 'Volume 1 The Bird in the Cage Chapter 3 Sunrise', 3, '2020-09- 09 01:28:30', 3, 'http://www.shuquge.com/txt/8659/2324754.html');
INSERT INTO book.chapter (id, fiction_id, chapter_title, content_id, create_date, sort, chapter_url) VALUES (4, 6, 'Volume 1 The Bird in the Cage Chapter 4 The Yellow Bird', 4, '2020-09- 09 01:28:30', 4, 'http://www.shuquge.com/txt/8659/2324755.html');
INSERT INTO book.chapter (id, fiction_id, chapter_title, content_id, create_date, sort, chapter_url) VALUES (5, 6, 'Volume 1: The Sparrow in the Cage Chapter 5: The Truth', 5, '2020-09-09 01:28:30', 5, 'http://www.shuquge.com/txt/8659/2324756.html');

Then we use mybatisX to generate mapper, service, entity related code

ChapterMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="springboot.mapper.ChapterMapper">

    <resultMap id="BaseResultMap" type="springboot.entity.Chapter">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="fictionId" column="fiction_id" jdbcType="INTEGER"/>
            <result property="chapterTitle" column="chapter_title" jdbcType="VARCHAR"/>
            <result property="contentId" column="content_id" jdbcType="INTEGER"/>
            <result property="createDate" column="create_date" jdbcType="TIMESTAMP"/>
            <result property="sort" column="sort" jdbcType="INTEGER"/>
            <result property="chapterUrl" column="chapter_url" jdbcType="VARCHAR"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,fiction_id,chapter_title,
        content_id,create_date,sort,
        chapter_url
    </sql>
</mapper>

ChaperMapper.java
package springboot.mapper;

import springboot.entity.Chapter;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
* @description Database operation Mapper for table [chapter (chapter)]
* @createDate 2023-11-12 13:49:37
* @Entity springboot.entity.Chapter
*/
public interface ChapterMapper extends BaseMapper<Chapter> {<!-- -->

}
ChapterService.java
package springboot.service;

import springboot.entity.Chapter;
import com.baomidou.mybatisplus.extension.service.IService;

/**
* @description Database operation Service for table [chapter (chapter)]
* @createDate 2023-11-12 13:49:37
*/
public interface ChapterService extends IService<Chapter> {<!-- -->

}
ChapterServiceImpl.java
package springboot.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import springboot.entity.Chapter;
import springboot.service.ChapterService;
import springboot.mapper.ChapterMapper;
import org.springframework.stereotype.Service;

/**
* @description Database operation Service implementation for table [chapter (chapter)]
* @createDate 2023-11-12 13:49:37
*/
@Service
public class ChapterServiceImpl extends ServiceImpl<ChapterMapper, Chapter>
    implements ChapterService{<!-- -->

}

Let’s write another controller

ChapterController.java
package springboot.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import springboot.entity.Chapter;
import springboot.service.ChapterService;

@RestController
@RequestMapping("chapter")
public class ChapterController {<!-- -->
    @Autowired
    ChapterService chapterService;
    
}

Finally we write the database configuration information

application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/book?useUnicode=true & amp;characterEncoding=GBK & amp;serverTimezone=Asia/Shanghai & amp;useSSL=false & amp;allowMultiQueries=true
    username: root
    password: *****

Step 2: Fictional Scenario

For example, fictionId and sort can uniquely determine a novel chapter. fictionId indicates which novel it is, and sort indicates which chapter it is.

So what is the novel chapter query interface we provide?

//Add code in ChapterService.java
Chapter getFictionChapter(String fictionId,long sort);

//Add code in ChapterServiceImpl.java
public Chapter getFictionChapter(String fictionId,long sort){<!-- -->
    LambdaQueryChainWrapper<Chapter> wrapper = new LambdaQueryChainWrapper<>(baseMapper);
    wrapper.eq(Chapter::getFictionId,fictionId);
    wrapper.eq(Chapter::getSort,sort);
    return wrapper.list().get(0);
}
//Add code in ChapterController.java
@GetMapping("{fictionId}/{sort}")
Chapter getFiction(@PathVariable("fictionId") String fictionId,
                   @PathVariable("sort") long sort){<!-- -->
    return chapterService.getFictionChapter(fictionId,sort);
}

At this time, start the project and visit http://localhost:8080/chapter/6/1 to access the first chapter of the novel with ID 6.

Suddenly, one day, an unlucky guy executed an update statement

update chapter
set sort=sort + n
where true

The order of all chapters has been added to n. Then we cannot change the data in the database back. We also need to ensure that http://localhost:8080/chapter/6/1 is still the first chapter of the novel with ID 6. What should we do? . Some people will say that just sort + n in the getFictionChapter method logic. Yes, this is indeed possible. But if this interface is an external dependency package we introduced, we cannot change the original code, so how do we solve it? Which brings us back to the title.

The third step: Thinking

In my first thought, I thought of aop, which is non-invasive, but when I think about it carefully, doesn’t aop also need to add annotations to the original code? This does not meet the requirements. Finally, thinking about it, we still have to start with the birth of the bean. Isn’t it enough to exchange a civet cat for a prince and replace the original bean with my own bean?

Step 4 Solution

First we set the above n to be fixed to 2 as an example,

We write our own bean to replace ChapterServiceImpl.java

ChapterServiceReplaceImpl.java
package springboot.service.impl;

import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import springboot.entity.Chapter;
import springboot.mapper.ChapterMapper;
import springboot.service.ChapterService;

/**
* @description Database operation Service implementation for table [chapter (chapter)]
* @createDate 2023-11-12 13:49:37
*/
@Service
public class ChapterServiceReplaceImpl extends ServiceImpl<ChapterMapper, Chapter>
    implements ChapterService{<!-- -->

    public Chapter getFictionChapter(String fictionId,Long sort){<!-- -->
        LambdaQueryChainWrapper<Chapter> wrapper = new LambdaQueryChainWrapper<>(baseMapper);
        wrapper.eq(Chapter::getFictionId,fictionId);
        wrapper.eq(Chapter::getSort,sort + 2);
        return wrapper.list().get(0);
    }
}

At this time, an error will be reported when we start. ChapterService is not unique when injected. ChapterServiceReplaceImpl and ChapterServiceImpl are both implementation classes of ChapterService. Springboot does not know which one to inject, so it reports an error. We cannot rely on muddle-through to replace it.

It seems that our service cannot be the implementation class of ChapterService. Then remove implements ChapterService. It is indeed possible to start in this way, but it does not have the slightest impact on the business logic used. It was agreed that the civet cat should be replaced by the prince, but you should do it. Yes, how do I change it?

Fortunately, when I was watching a video, I saw someone else’s handwritten spring framework. There is a BeanPostProcessor that can operate on the bean after the bean is initialized, such as get, set, or even replace it with other beans. Replace! ! ! Hey, isn’t that what I want? I suddenly had an idea.

ChapterBeanPostProcessor.java
package springboot.config;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import springboot.service.impl.ChapterServiceReplaceImpl;

@Component
public class ChapterBeanPostProcessor implements BeanPostProcessor {<!-- -->

    @Autowired
    ChapterServiceReplaceImpl replace;
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {<!-- -->
        if("chapterServiceImpl".equals(beanName)){<!-- -->
            return BeanPostProcessor.super.postProcessAfterInitialization(replace, beanName);
        }
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

We determine whether the bean name is chapterServiceImpl. If so, replace it with our ChapterServiceReplaceImpl. If not, return it as is.

The idea is good, but an error is reported at startup. The error message is to the effect that the bean of ChapterService cannot be found. Yes, we replaced ChapterServiceImpl with ChapterServiceReplaceImpl, but ChapterServiceReplaceImpl is not the implementation class of ChapterService and cannot be found when injected.

So how to solve it? That’s right, we can only use proxy classes to achieve this.

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {<!-- -->
    if("chapterServiceImpl".equals(beanName)){<!-- -->
        Object proxy = Proxy.newProxyInstance(SpringbootApplication.class.getClassLoader(),
             new Class[]{<!-- -->ChapterService.class}, new InvocationHandler() {<!-- -->
                  @Override
                  public Object invoke(Object proxy, Method method, Object[] args)
                  throws Throwable {<!-- -->
                         String name = method.getName();
                         if (args!=null) {<!-- -->
                               Class[] clazzes = new Class[args.length];
                               for (int i = 0; i < args.length; i + + ) {<!-- -->
                               clazzes[i] = args[i].getClass();
                               }
                               Method replaceMethod = replace.getClass().getDeclaredMethod(name,clazzes);
                               return replaceMethod.invoke(replace,args);
                          }else {<!-- -->
                                Method replaceMethod = replace.getClass().getDeclaredMethod(name);
                                return replaceMethod.invoke(replace,args);
                          }

                 }
             });
        return BeanPostProcessor.super.postProcessAfterInitialization(proxy, beanName);
    }
    return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}

Generate a proxy class, and all its methods are implemented using the isomorphic methods of ChapterServiceReplaceImpl with the same name. At this point, we have implemented the content of the title.

However, there is another problem, that is, we need to copy the entire original service. Suppose the service has 1000 methods. I only need to change one. I don’t want to copy the other 999 methods. I copy this one method and use the others. Is the original okay?

In fact, our logic is that if there is an isomorphic method with the same name in ChapterServiceReplaceImpl, use the method of ChapterServiceReplaceImpl. If not, use the method of the original ChapterServiceImpl.

We optimize the code as follows:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {<!-- -->
    if("chapterServiceImpl".equals(beanName)){<!-- -->
        Object proxy = Proxy.newProxyInstance(SpringbootApplication.class.getClassLoader(),
               new Class[]{<!-- -->ChapterService.class}, new InvocationHandler() {<!-- -->
                      @Override
                      public Object invoke(Object proxy, Method method, Object[] args)
                      throws Throwable {<!-- -->
                              String name = method.getName();
                              if (args!=null) {<!-- -->
                                    Class[] clazzes = new Class[args.length];
                                    for (int i = 0; i < args.length; i + + ) {<!-- -->
                                        clazzes[i] = args[i].getClass();
                                    }
                                    try {<!-- -->
                                        Method replaceMethod = replace.getClass().getDeclaredMethod(name,clazzes);
                                        return replaceMethod.invoke(replace,args);
                                    } catch (Exception e) {<!-- -->
                                        Method replaceMethod = bean.getClass().getDeclaredMethod(name,clazzes);
                                        return replaceMethod.invoke(bean,args);
                                    }
                              }else {<!-- -->
                                     try {<!-- -->
                                          Method replaceMethod = replace.getClass().getDeclaredMethod(name);
                                          return replaceMethod.invoke(replace,args);
                                      } catch (Exception e) {<!-- -->
                                          Method replaceMethod = bean.getClass().getDeclaredMethod(name);
                                          return replaceMethod.invoke(bean,args);
                                      }
                                 }

                    }
        });
        return BeanPostProcessor.super.postProcessAfterInitialization(proxy, beanName);
    }
    return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}

This is the perfect solution. (Of course, there is still a small pitfall, that is, when the parameters of the original method are not classes but basic types, the parameters of our isomorphic method with the same name must use the class corresponding to the basic type. For example, if the parameter is of type int, it must be Use Integer; the parameter is long type, use Long, etc.)