springboot 如何在不改别人代码的情况下,改对应的业务逻辑

想必很多人都和我一样,有和标题一样的困惑,这个问题也困惑了我很久,如今终于解决了,接下来,就让我来带大家了解我的思考和解决过程。

第一步:我们先建一个简单的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,等等)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值