Spring Boot实战:用工厂+策略模式优雅实现多端登录

2,123 阅读7分钟

Spring Boot 实战:用工厂 + 策略模式优雅实现多端登录

各位开发者好,我是在项目里被登录功能折磨过无数次的老码农。还记得三年前接手一个多端登录项目,用户名密码、微信、手机号验证码三种登录方式挤在一个if-else里,新增支付宝登录时整整改了 17 处代码,最后还漏了一处校验 —— 从此发誓再也不用 "面条代码" 处理登录逻辑。今天就把结合工厂模式和策略模式的优雅实现分享出来,带你从需求分析到 Spring Boot 实战,彻底告别登录模块的维护噩梦。

一、需求分析:当产品说 "我们要支持 10 种登录方式"

1. 典型登录场景:代码里的 "联合国"

假设我们要实现一个支持三种登录方式的系统:

  • 用户名密码登录:需要校验密码加密、账号是否锁定
  • 微信扫码登录:需要调用微信开放平台 API,校验授权码
  • 手机号验证码登录:需要生成验证码、校验有效期和正确性

传统写法是写一个LoginService,用if-else判断登录类型:

public String login(String loginType, Map<String, Object> params) {
    if ("password".equals(loginType)) {
        // 用户名密码登录逻辑
    } else if ("wechat".equals(loginType)) {
        // 微信登录逻辑
    } else if ("sms".equals(loginType)) {
        // 手机号登录逻辑
    } else {
        throw new IllegalArgumentException("不支持的登录类型");
    }
}

这种写法的问题在于:

  • 扩展性差:新增登录方式要改if-else,违反开闭原则
  • 职责混乱:所有逻辑挤在一个类里,可读性差
  • 复用困难:不同登录方式的公共逻辑(如用户校验)无法抽取

2. 设计模式选择:策略模式解耦算法,工厂模式创建实例

  • 策略模式:将每种登录方式封装成独立策略类,实现统一接口,调用者无需关心具体实现
  • 工厂模式:通过工厂类根据登录类型创建对应的策略实例,避免调用者直接 new 对象

二、Spring Boot 项目搭建:先搭好 "舞台"

1. 创建 Spring Boot 项目

添加 Web 依赖,项目结构如下:

src/main/java/com/example/login
├── config
│   └── StrategyConfig.java  // 策略Bean配置
├── controller
│   └── LoginController.java // 登录控制器
├── factory
│   └── LoginStrategyFactory.java // 登录策略工厂
├── model
│   └── LoginRequest.java    // 登录请求参数
├── service
│   ├── impl
│   │   ├── PasswordLoginStrategy.java  // 用户名密码策略
│   │   ├── WechatLoginStrategy.java    // 微信策略
│   │   └── SmsLoginStrategy.java       // 手机号策略
│   └── LoginStrategy.java   // 登录策略接口
└── Application.java

2. 定义统一登录策略接口

public interface LoginStrategy {
    // 登录类型标识,如"password"、"wechat"
    String getLoginType();
    
    // 登录方法,参数用Map传递不同登录方式的参数
    String execute(Map<String, Object> params);
}

三、策略模式实现:每种登录方式都是 "独立演员"

1. 用户名密码登录策略

@Service
public class PasswordLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return "password";
    }
    @Override
    public String execute(Map<String, Object> params) {
        String username = (String) params.get("username");
        String password = (String) params.get("password");
        // 模拟密码校验(实际应从数据库查询+密码解密)
        if (!"123456".equals(password)) {
            throw new IllegalArgumentException("密码错误");
        }
        // 模拟用户校验
        checkUserLocked(username);
        return "登录成功(用户名密码)";
    }
    private void checkUserLocked(String username) {
        // 调用用户服务检查账号是否锁定
        System.out.println("检查用户" + username + "是否锁定");
    }
}

2. 微信扫码登录策略

@Service
public class WechatLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return "wechat";
    }
    @Override
    public String execute(Map<String, Object> params) {
        String authCode = (String) params.get("authCode");
        // 模拟调用微信接口获取用户信息
        String openId = callWechatApi(authCode);
        // 模拟数据库查询用户绑定关系
        String userId = getUserIdByOpenId(openId);
        if (userId == null) {
            throw new IllegalArgumentException("微信账号未绑定系统用户");
        }
        return "登录成功(微信扫码)";
    }
    private String callWechatApi(String authCode) {
        // 实际应调用微信开放平台API
        System.out.println("调用微信接口,authCode=" + authCode);
        return "wechat_open_id_123";
    }
}

3. 手机号验证码登录策略

@Service
public class SmsLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return "sms";
    }
    @Override
    public String execute(Map<String, Object> params) {
        String phone = (String) params.get("phone");
        String code = (String) params.get("code");
        // 模拟验证码校验(实际应从Redis获取)
        if (!"666888".equals(code)) {
            throw new IllegalArgumentException("验证码错误");
        }
        // 模拟用户校验
        checkPhoneRegistered(phone);
        return "登录成功(手机号验证码)";
    }
    private void checkPhoneRegistered(String phone) {
        // 检查手机号是否注册
        System.out.println("检查手机号" + phone + "是否注册");
    }
}

四、工厂模式实现:让 "导演" 决定用哪个 "演员"

1. 登录策略工厂类

@Component
public class LoginStrategyFactory {
    private final Map<String, LoginStrategy> strategyMap;
    // 通过Spring依赖注入获取所有LoginStrategy实现类
    public LoginStrategyFactory(Map<String, LoginStrategy> strategyMap) {
        this.strategyMap = strategyMap;
    }
    public LoginStrategy getStrategy(String loginType) {
        LoginStrategy strategy = strategyMap.get(loginType);
        if (strategy == null) {
            throw new IllegalArgumentException("不支持的登录类型:" + loginType);
        }
        return strategy;
    }
}

这里利用 Spring 的自动装配,将所有@Service标记的LoginStrategy实现类注入到strategyMap中,键为 Bean 名称(默认是类名首字母小写,如passwordLoginStrategy),但我们在策略类中通过getLoginType()返回自定义的类型标识,所以需要在配置类中调整 Bean 名称:

2. 策略 Bean 配置(关键!)

@Configuration
public class StrategyConfig {
    @Bean("passwordStrategy") // 自定义Bean名称
    public LoginStrategy passwordLoginStrategy() {
        return new PasswordLoginStrategy();
    }
    @Bean("wechatStrategy")
    public LoginStrategy wechatLoginStrategy() {
        return new WechatLoginStrategy();
    }
    @Bean("smsStrategy")
    public LoginStrategy smsLoginStrategy() {
        return new SmsLoginStrategy();
    }
}

然后在策略类中重写getLoginType()返回和前端约定的类型标识(如 "password"),并在工厂类中建立类型标识到 Bean 的映射:

// 修改工厂类的构造方法,建立正确映射
public LoginStrategyFactory(Map<String, LoginStrategy> strategyMap) {
    this.strategyMap = new HashMap<>();
    strategyMap.forEach((beanName, strategy) -> 
        this.strategyMap.put(strategy.getLoginType(), strategy)
    );
}

五、控制器集成:对外提供统一接口

1. 登录请求参数类

public class LoginRequest {
    private String loginType; // 登录类型,如"password"、"wechat"
    private Map<String, Object> params; // 具体参数,不同登录方式不同
    // 省略getter/setter
}

2. 登录控制器

@RestController
@RequestMapping("/login")
public class LoginController {
    private final LoginStrategyFactory factory;
    @Autowired
    public LoginController(LoginStrategyFactory factory) {
        this.factory = factory;
    }
    @PostMapping
    public String login(@RequestBody LoginRequest request) {
        String loginType = request.getLoginType();
        Map<String, Object> params = request.getParams();
        LoginStrategy strategy = factory.getStrategy(loginType);
        return strategy.execute(params);
    }
}

六、测试验证:三种登录方式轻松切换

1. 用户名密码登录请求

{
  "loginType": "password",
  "params": {
    "username": "user123",
    "password": "123456"
  }
}

2. 微信扫码登录请求

{
  "loginType": "wechat",
  "params": {
    "authCode": "wechat_auth_code_456"
  }
}

3. 新增登录方式:比如支付宝登录

只需新增AlipayLoginStrategy类并实现接口,无需修改现有代码:

@Service("alipayStrategy")
public class AlipayLoginStrategy implements LoginStrategy {
    @Override
    public String getLoginType() {
        return "alipay";
    }
    @Override
    public String execute(Map<String, Object> params) {
        // 支付宝登录逻辑
        return "登录成功(支付宝)";
    }
}

七、核心优势:让代码具备 "抗需求变化体质"

1. 策略模式的好处

  • 解耦算法:每种登录逻辑独立在策略类中,可读性强
  • 易于扩展:新增登录方式只需添加新策略类,符合开闭原则
  • 方便测试:可以单独测试每个策略类,无需关心其他逻辑

2. 工厂模式的好处

  • 封装创建逻辑:调用者无需知道策略类的创建细节
  • 集中管理实例:通过 Spring 管理策略 Bean,支持依赖注入和生命周期管理

3. 结合 Spring Boot 的优势

  • 自动装配:通过@Service和Map<String, LoginStrategy>自动收集所有策略 Bean
  • 类型安全:工厂类在运行时检查登录类型是否合法,避免NullPointerException

八、最佳实践:这些细节别忽略

1. 参数校验前置

在控制器中先对loginType和params做基础校验,比如必填参数检查,避免策略类中重复校验

2. 公共逻辑抽取

如果多种登录方式有公共逻辑(如登录成功后的 Token 生成),可以创建抽象策略类,让具体策略类继承

3. 日志和异常处理

在策略类中添加登录日志记录,统一捕获异常并返回友好的错误信息:

@Service
public class PasswordLoginStrategy implements LoginStrategy {
    @Override
    public String execute(Map<String, Object> params) {
        try {
            // 登录逻辑
        } catch (Exception e) {
            log.error("用户名密码登录失败:{}", e.getMessage());
            throw new LoginException("登录失败,请重试"); // 自定义业务异常
        }
    }
}

4. 配置化登录类型

将支持的登录类型存储在配置文件中,前端调用时先获取支持的登录类型列表,避免硬编码

九、总结:设计模式让代码更有 "尊严"

回顾三年前的面条代码,再看现在的实现,最大的感受是:好的设计模式能让代码在需求变化面前保持优雅。工厂模式和策略模式的组合,就像给登录模块装了一个 "热插拔" 接口,新增功能时不用改核心逻辑,只需要添加新的 "插件"。

最后送大家一句口诀:登录逻辑别硬刚,策略模式来帮忙,工厂负责创实例,开闭原则记心上,Spring Boot 搭舞台,依赖注入真叫爽,需求万变不用慌,代码优雅没商量!