服务端模块化架构设计 2.0|扩展模块实现技术解耦

我正在参加「掘金·启航计划」

本专栏 将通过以下几块内容来搭建一个 模块化:可以根据项目的功能需求和体量进行任意模块的组合或扩展 的后端服务

项目结构与模块化构建思路

RESTful与API设计&管理

网关路由模块化支持与条件配置

DDD领域驱动设计与业务模块化(概念与理解)

DDD领域驱动设计与业务模块化(落地与实现)

DDD领域驱动设计与业务模块化(薛定谔模型)

DDD领域驱动设计与业务模块化(优化与重构)

RPC模块化设计与分布式事务

v2.0:项目结构优化升级

v2.0:项目构建+代码生成「插件篇」

v2.0:扩展模块实现技术解耦(本文)

未完待续……

在上一篇文章 v2.0:项目结构优化升级 中,我们将项目的结构进行了优化,优化之后的项目结构如下

项目结构v2.png

简单说明

考虑到可能有读者是第一次看这个专栏,所以我还是先简单介绍一下,详细的内容大家可以看我之前的专栏文章

这里模块化的设想其实就是可插拔的功能模块,如果需要这个功能,就把这个模块用GradleMaven的方式编译进来,如果不需要,去掉对应的依赖就行了,避免改动代码,因为一旦涉及到代码改动的话,就会变得“改不断,理还乱”

当然了,需要达到每个模块都能够任意拆卸的程度其实并不简单,所以我借鉴了DDD的思想来达成这个目的,专栏中也有这一块的内容,另外后续也会再写一篇针对DDD优化的文章

所以当我们把所有模块都编译进来的时候,那么就是一个单体应用了,如果把多个模块分开编译,那么就变成了微服务

以我们juejin项目的三个模块(用户,沸点,通知)为例:

在最开始的时候,我们可以将这三个模块打包成一个服务,作为单体应用来运行,适合项目前期体量较小的情况

之后,当我们发现沸点的流量越来越大,就可以将沸点模块拆分出来作为单独的一个服务方便扩容,用户模块和通知模块打包在一起作为另一个服务,这样就从单体应用变成了微服务

注:从单体应用到微服务的切换是不需要修改代码的

扩展模块

v2.0:项目结构优化升级这篇文章中,我们还遗留了3个模块juejin-tokenjuejin-loginjuejin-rpc没有进行说明

这几个模块我称之为扩展模块,其核心思想是能够将技术概念与技术实现进行解耦

Token 扩展

首先我在juejin-token下定义了TokenCodec

/**






 * Token 编解码
 */






public interface TokenCodec {



    /**
     * 生成 Token
     */
    String encode(User user);



    /**
     * 解析 Token
     */
    User decode(String token);
}

用于抽象TokenUser的编码和解码

然后在juejin-token下创建一个token-jwt模块

JWT来实现具体的编解码

/**






 * JWT token 编解码实现
 */






public class JwtTokenCodec implements TokenCodec {



    @Override
    public String encode(User user) {
        //使用jwt生成token,将userId存到属性中
    }



    @Override
    public User decode(String token) {
        //从jwt中获得userId,然后获得对应的用户
    }
}

这样的一个好处就是

当我们需要生成Token或是通过Token获取用户的时候

只需要依赖juejin-token这个抽象模块就行了

具体的实现会放在application模块中方便更换

Token这个概念就和具体的JWT实现解耦开了

当需要扩展的时候

比如又实现了一个token-session

那么直接替换application模块中依赖的具体实现

也就是把token-jwt改为token-session即可

对于引用到TokenCodec的代码不需要任何的改动

Login 扩展

juejin-login依赖juejin-token来实现登录功能

定义LoginAuthorizer抽象登录授权

public interface LoginAuthorizer {

    LoginAuthorization authorize(User user);
}

然后提供一个默认实现

@Getter
@AllArgsConstructor
public class LoginAuthorizerImpl implements LoginAuthorizer {


    private TokenCodec tokenCodec;





    @Override
    public LoginAuthorization authorize(User user) {
        String token = tokenCodec.encode(user);
        LoginAuthorization la = new LoginAuthorization();
        la.setId(user.getId());
        la.setNickname(user.getNickname());
        la.setAvatar(user.getAvatar());
        la.setToken(token);
        return la;
    }

}

我们可以看到调用了TokenCodecencode方法

但是对于具体使用哪种方式生成Token并不在意

如果在application中依赖的是token-jwt那么就是使用JWT生成Token

如果在application中依赖的是token-session那么就是返回的session

juejin-login下创建一个login-username模块

表示使用用户名登录,添加UsernameLoginController

@Tag(name = "登录")
@RestController
@RequestMapping("/login")
public class UsernameLoginController {



    @Autowired
    protected UserRepository userRepository;


    @Autowired
    protected LoginAuthorizer loginAuthorizer;

    @Operation(summary = "用户名登录")
    @PostMapping("/username")
    public LoginAuthorization usernameLogin(@RequestParam String username, @RequestParam String password) {
        User user = userRepository.get(new LambdaConditions().equal(User::getUsername, username));
        if (user == null || !user.getPassword().equals(password)) {
            throw new IllegalArgumentException("login.username-or-password.error");
        }
        if (!user.getEnabled()) {
            throw new IllegalStateException("login.account.disabled");
        }
        return loginAuthorizer.authorize(user);
    }
}

我们拿到User之后通过LoginAuthorizer来进行授权登录

也是不需要关注具体的登录细节

另外我们可以在juejin-login下创建使用其他方式的登录模块

比如login-phone使用手机号登录

或是login-email使用邮箱登录

或是login-qrcode使用二维码登录

最后只需要在application中添加对应的依赖即可

比如只支持手机号登录,那么就依赖login-phone

比如所有的方式都支持,那么就把全部的依赖都加上

RPC 扩展

和之前的思路也差不多

RPC有很多实现方式,FeignDubbogRPC等等

我们可以先定义用于远程传输的对象以及对应的转换器

以用户User为例

/**






 * 用户远程对象 remote object
 */






@Data
public class UserRO implements Identifiable {





    //ID
    protected String id;
    
    //用户名
    protected String username;


    //密码
    protected String password;

    //昵称
    protected String nickname;

    //头像
    protected String avatar;

    //启用
    protected Boolean enabled;

    //创建时间
    protected Long createTime;
}

然后用户远程对象和用户领域模型的转换器

/**






 * 用户领域模型和用户远程对象转换适配器
 */






public interface RPCUserFacadeAdapter extends RemoteObjectFacadeAdapter<User, UserRO> {
}





/**
 * 远程对象转换适配器
 *
 * @param <T>
 * @param <R>
 */
public interface RemoteObjectFacadeAdapter<T extends DomainObject, R extends Identifiable> {


    /**
     * 领域模型转远程对象
     */
    R do2ro(T object);

    /**
     * 远程对象转领域模型
     */
    T ro2do(R ro);
}

接下来我们来实现基于FeignRPC调用

juejin-rpc下创建rpc-feign

首先定义UserFeignClient(这里的name只需要填写user即可,在网关路由模块化支持与条件配置中实现了动态路由的思路)

/**






 * 用户 Feign 客户端
 */






@FeignClient(name = "user")
public interface UserFeignClient {





    /**
     * 根据 id 获得用户信息
     */
    @GetMapping("/feign/user/{id}")
    Response<UserRO> get(@PathVariable String id);
}

接着实现一个FeignUserController用于给UserFeignClient提供调用接口

/**






 * 提供给 Feign 调用的用户接口
 */






@RestController
@RequestMapping("/feign/user")
public class FeignUserController {

    @Autowired
    private UserRepository userRepository;



    @Autowired
    private RPCUserFacadeAdapter rpcUserFacadeAdapter;

    /**
     * 根据用户 id 获得用户信息
     */
    @GetMapping("/{id}")
    public UserRO get(@PathVariable String id) {
        User user = userRepository.get(id);
        if (user == null) {
            return null;
        }
        return rpcUserFacadeAdapter.do2ro(user);
    }
}

然后实现一个基于FeignUserRepository

/**






 * 基于 Feign 的用户存储
 */






@Component
public class FeignUserRepository extends QueryDomainRepository<User, Users, UserRO> implements UserRepository {





    @Autowired
    private RPCUserFacadeAdapter rpcUserFacadeAdapter;

    @Autowired
    private UserFeignClient userFeignClient;


    @Override
    public UserRO do2po(User user) {
        return rpcUserFacadeAdapter.do2ro(user);
    }


    @Override
    public User po2do(UserRO ro) {
        return rpcUserFacadeAdapter.ro2do(ro);
    }

    @Override
    protected UserRO doGet(String id) {
        Response<UserRO> response = userFeignClient.get(id);
        if (response.getResult()) {
            return response.getObject();
        }
        throw new RuntimeException(response.getMessage());
    }

    //省略其他代码
}

主要就是调用UserFeignClient来获取数据

最后,最重要的一步,添加配置类

@Configuration
@EnableFeignClients(basePackages = "com.bytedance.juejin.rpc.feign.*")
public class FeignAutoConfiguration {


    //方便分组不同的业务
    @Configuration
    public static class UserConfiguration {


        @Bean
        @ConditionalOnBean(UserRepository.class)
        public FeignUserController feignUserController() {
            return new FeignUserController();
        }


        @Bean
        @ConditionalOnMissingBean
        public UserRepository userRepository() {
            return new FeignUserRepository();
        }
    }
}

到这里可能有些读者已经有点迷糊了,这是怎么样的调用流程?

那就让我们以application-cloud-pinapplication-cloud-system为例来走一遍流程

前提,application-cloud-pin包含了沸点模块,application-cloud-system包含了用户和通知模块

  1. application-cloud-pin 启动注入

application-cloud-pin启动的时候,开始注入User Feign相关的实例

FeignUserController需要存在UserRepository实例才会注入,所以FeignUserController不会注入在application-cloud-pin

由于不存在UserRepository,所以会注入FeignUserRepository

  1. application-cloud-system 启动注入

application-cloud-system启动的时候,开始注入User Feign相关的实例

因为集成了用户模块所以存在UserRepository(基于MBP的实现)
所以会注入FeignUserController

由于存在UserRepository,所以不会注入FeignUserRepository

  1. 当前注入状态

application-cloud-pin注入了FeignUserRepository

application-cloud-system注入了FeignUserController

  1. 调用流程

现在我们调用沸点接口(沸点中需要获得发布沸点的用户信息)

application-cloud-pin通过UserRepository(FeignUserRepository)来获取用户接口

FeignUserRepository通过UserFeignClient调用application-cloud-system中的FeignUserController

FeignUserController调用自身的UserRepository(基于MBP的实现)获得数据

结束(指调用和文章)

总结

最后再说说RPC这块的设计,是怎么适配模块化的

我们通过domain-user来引用UserRepository

module-user会实现基于数据库的MBPUserRepository

rpc-feign会实现基于FeignFeignUserRepository

application中集成了module-user就会直接调用数据库

application中没有集成module-user就会通过Feign调用

所以我们只需要使用UserRepository的接口

不需要关注其具体实现,也没有办法关注具体实现

因为变化莫测的依赖会对应各种各样的实现

上一篇:v2.0:项目构建+代码生成「插件篇」

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYBBYVYA' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片