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.)