想必很多人都和我一样,有和标题一样的困惑,这个问题也困惑了我很久,如今终于解决了,接下来,就让我来带大家了解我的思考和解决过程。
第一步:我们先建一个简单的springboot的web项目。
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>
数据库表
create table chapter
(
id int auto_increment comment '主键' primary key,
fiction_id int not null comment '小说id',
chapter_title varchar(50) not null comment '章节标题',
content_id int not null comment '内容id',
create_date datetime not null on update CURRENT_TIMESTAMP comment '创建时间',
sort int not null comment '序号',
chapter_url varchar(200) null comment '文章链接'
)comment '章节' ;
INSERT INTO book.chapter (id, fiction_id, chapter_title, content_id, create_date, sort, chapter_url) VALUES (1, 6, '第一卷 笼中雀 第一章 惊蛰', 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, '第一卷 笼中雀 第二章 开门', 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, '第一卷 笼中雀 第三章 日出', 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, '第一卷 笼中雀 第四章 黄鸟', 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, '第一卷 笼中雀 第五章 道破', 5, '2020-09-09 01:28:30', 5, 'http://www.shuquge.com/txt/8659/2324756.html');
然后我们用mybatisX生成mapper,service,entity相关代码
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 针对表【chapter(章节)】的数据库操作Mapper
* @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 针对表【chapter(章节)】的数据库操作Service
* @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 针对表【chapter(章节)】的数据库操作Service实现
* @createDate 2023-11-12 13:49:37
*/
@Service
public class ChapterServiceImpl extends ServiceImpl<ChapterMapper, Chapter>
implements ChapterService{
}
我们再写一个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;
}
最后我们把数据库配置信息写上
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/book?useUnicode=true&characterEncoding=GBK&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true
username: root
password: *****
第二步 虚构场景
比如fictionId和sort可以唯一确定一个小说章节,fictionId表示是哪本小说,sort表示是哪个章节
那么我们我们提供小说章节查询接口是什么呢?
// 在 ChapterService.java里添加代码
Chapter getFictionChapter(String fictionId,long sort);
// 在 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);
}
// 在 ChapterController.java里添加代码
@GetMapping("{fictionId}/{sort}")
Chapter getFiction(@PathVariable("fictionId") String fictionId,
@PathVariable("sort") long sort){
return chapterService.getFictionChapter(fictionId,sort);
}
这时启动项目,访问http://localhost:8080/chapter/6/1就能访问小说id为6的第一章节。
突然,有一天,有个倒霉蛋执行一条更新语句
update chapter
set sort=sort+n
where true
所有章节顺序都加了n,然后我们不能去把数据库的数据更改回来,还要保证http://localhost:8080/chapter/6/1依旧是小说id为6的第一章节,要怎么做。有人就会说,直接getFictionChapter方法逻辑里sort+n就好了嘛,是的,这样确实可以。但是如果这个接口是我们引入的外部依赖包呢,我们就没办法更改原码了,那我们要怎么解决呢?这就回到了标题。
第三步 思考
在第一想法中,我想到的是aop,非入侵式的,但是仔细一想,aop不也要在原码上加注解吗,这就不符合要求了。最后想想,还是得从bean的诞生入手,用一手狸猫换太子,用自己的bean换掉原来的bean不就可以了。
第四步 解决方案
首先我们设定上述的n固定为2为例,
我们自己写一个用来替换ChapterServiceImpl.java的bean
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 针对表【chapter(章节)】的数据库操作Service实现
* @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);
}
}
这时我们启动就会报错,ChapterService注入的时候不唯一,ChapterServiceReplaceImpl和ChapterServiceImpl都是ChapterService的实现类,springboot不知道要注入哪个,就报错了,我们想靠蒙混过关来替换是不行了。
看来,我们的service就不能是ChapterService的实现类了。那就把implements ChapterService去掉。这样启动确实可以了,但是没有对用来的业务逻辑产生丝毫的影响。说好的狸猫换太子的,你倒是换啊。是啊,要怎么换呢?
有幸在刷视频的时候,看到别人手写spring框架,其中有个BeanPostProcessor可以在bean的初始化后对bean操作,比如get,set,甚至是替换为其他的bean,替换!!!诶,我不就是想要的吗?我顿时有了想法。
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);
}
}
我们判断bean的名称是不是chapterServiceImpl,如果是,就用我们的ChapterServiceReplaceImpl替换掉,不是就原样返回。
想法是好的,但是启动又报错了,错误信息大意是找不到ChapterService的bean,是啊,我们用ChapterServiceReplaceImpl替换了ChapterServiceImpl,但是ChapterServiceReplaceImpl不是ChapterService的实现类,注入的时候就找不到了。
那该怎么解决呢?没错,我们只能用代理类,用代理类来实现。
@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);
}
生成一个代理类,它的方法全部用ChapterServiceReplaceImpl的同名同构的方法来实现。到此,我们就实现了标题的内容。
但是,还有一个问题,就是我们要把整个原来的service都复制进来,假设service有1000个方法,我只要改1个,我不想复制其他999个方法,复制这1个方法,其他的还用原来的可以吗?
其实我们的逻辑就是,ChapterServiceReplaceImpl里有同名同构的方法,就用ChapterServiceReplaceImpl的方法,如果没有,就用原来ChapterServiceImpl的方法。
我们将代码优化如下:
@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);
}
到此就完美解决了。(当然,还留了个小坑,就是当原来的方法的参数不是类而是基本类型的时候,我们的同名同构的方法的参数要用基本类型对应的类,比如参数为int类型,要用Integer;参数为long类型,要用Long,等等)