文章目录
- 1、需求背景
- 2、常规想法
- 3、工厂模式 + 配置文件解耦 + 策略模式
- 4、具体实现
- 5、其他场景
- 6、一点思考
1、需求背景
以gitee为例,登录验证的方式有多种:
- 用户名密码登录
- 短信验证码登录
- 微信登录
先写一个登录接口,适配所有方式,且要符合开闭原则,方便以后再新增其他登录方式。先定义下传参、响应以及接口API路径
//传参Dto
@Data
public class LoginDto {
private String name;
private String password;
private String phone;
private String validateCode;
private String wxCode;
/**
* account:账户密码登录
* sms:手机验证码登录
* we_chat:微信登录
*/
private String type;
}
//响应Vo
@Data
@AllArgsConstructor
public class LoginResp {
private boolean success;
}
//接口定义
@RestController
@RequestMapping("/api/")
public class LoginController {
@Resource
private UserService userService;
@PostMapping("/login")
public LoginResp login(@RequestBody LoginDto loginDto) {
return userService.login(loginDto);
}
}
//Service层接口抽象
public interface UserService {
LoginResp login(LoginDto dto);
}
2、常规想法
最先想到的是用户点击不同的登录方式图标,前端传不同的type,后端根据type走不同的验证逻辑,那Service层代码的实现大概长这样:
@Service
public class UserServiceImpl implements UserService {
@Override
public LoginResp login(LoginDto dto) {
if ("account".equals(dto.getType())) {
System.out.println("用户名密码登录");
//try执行用户名密码登录的验证逻辑
return new LoginResp(true);
} else if ("sms".equals(dto.getType())) {
System.out.println("短信验证码登录");
//try执行短信验证码登录的验证逻辑
return new LoginResp(true);
} else if ("we_chat".equals(dto.getType())) {
System.out.println("微信登录");
//try执行微信登录的验证逻辑
return new LoginResp(true);
} else {
return new LoginResp(false);
}
}
}
如此,繁琐的IF-else且不符合开闭原则。考虑使用设计模式来优化。
3、工厂模式 + 配置文件解耦 + 策略模式
每种登录就是实现登录这个目的的一种策略,因此先想到的应该是策略模式,所有具体策略类所需要实现的接口就是抽象策略类的login方法。其次,前端传不同的type,要调用不同的具体策略类对象,如此,再引入工厂模式。
这样写,以后再增加新的登录方式,工厂类还得改,为了解耦,使用配置文件,不同的登录方式的type,对应一个登录方式的具体策略类。
配置文件如:key为登录方式的type,value为具体策略类的Bean的名字。
login:
types:
account: accountGranter
sms: smsGranter
we_chat: weChatGranter
以后就把这个关系读到一个Map中使用。这里之所以给type和BeanName建立关系,是因为项目是Spring项目,如果不是,那我也可以给type和策略类的全类名建立映射关系存入Map,以后获取策略类对象,可通过反射,一样可以实现。
4、具体实现
上面提到要建立type和对应具体策略类的Bean的映射关系,这里通过实现 ApplicationContextAware 接口,去获取 ApplicationContext 对象,并通过它访问容器中的其他 bean。首先是读取yml配置,这里不要读login.types,这样以后加新的登录方式,又要改这个配置读取类,直接读login,得到一个types名字的数组
@Data
@Configuration
@ConfigurationProperties(prefix = "login")
public class GranterConfig {
private Map<String, String> types;
}
定义抽象策略类:
/**
* 抽象策略类
*/
public interface UserLoginGranter {
LoginResp login(LoginDto dto);
}
定义每种登录方式的具体策略类:
@Component
public class AccountGranter implements UserLoginGranter {
@Override
public LoginResp login(LoginDto dto) {
System.out.println("用户名密码登录");
//try执行用户名密码登录的验证逻辑
return new LoginResp(true);
}
}
@Component
public class SmsGranter implements UserLoginGranter {
@Override
public LoginResp login(LoginDto dto) {
System.out.println("短信验证码登录");
//try执行短信验证码登录的验证逻辑
return new LoginResp(true);
}
}
@Component
public class WeChatGranter implements UserLoginGranter {
@Override
public LoginResp login(LoginDto dto) {
System.out.println("微信登录");
//try执行微信登录的验证逻辑
return new LoginResp(true);
}
}
定义抽象工厂:这里定义一个static map,实现ApplicationContextAware接口(拿到容器上下文对象ApplicationContext去获取Bean),存入type和具体策略类Bean的映射关系:
/**
* 操作策略的上下文环境类 工具类
* 将策略整合起来 方便管理
*/
@Component
public class UserLoginFactory implements ApplicationContextAware {
@Resource
private GranterConfig granterConfig;
private static Map<String, UserLoginGranter> granterPool = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
granterConfig.getTypes().forEach((k, v) -> granterPool.put(k, applicationContext.getBean(v, UserLoginGranter.class)));
}
/**
* 获取具体策略类的对象
* @param type 登录方式
* @return 具体策略类的对象Bean
*/
public UserLoginGranter getGranter (String type) {
return granterPool.get(type);
}
}
修改之前繁琐的IF-else,Service层的实现类改为:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserLoginFactory factory;
@Override
public LoginResp login(LoginDto dto) {
UserLoginGranter loginGranter = factory.getGranter(dto.getType());
if (null == loginGranter) {
return new LoginResp(false);
}
return loginGranter.login(dto);
}
}
测试下效果:
此后,再扩展另外的登录方式,比如QQ登录认证,只需加个具体策略类以及在application.yaml加个配置
login:
types:
account: accountGranter
sms: smsGranter
we_chat: weChatGranter
# 扩展
qq: qqGranter
核心点:
- 提供多种具体策略的对象,让Spring容器管理
- 提供一个工厂,根据参数返回对应的具体策略对象
5、其他场景
类似的,做订单支付也可以用策略模式,具体支付策略有:
- 支付宝
- 微信
- 银联
再比如做解析不同类型的excel,可以针对不同的格式写具体策略类,所有策略类实现抽象策略类的解析接口:
- xls格式的解析具体策略类
- xlsx格式的解析具体策略类
总之,涉及不同的实现方式(策略),搭配冗长的if-else或者switch的场景,都可以使用策略模式 + 工厂模式做个优化。
6、一点思考
第三方供应商需要上架自己的产品到公司的交易平台,但用户使用产品时,最后一步请求的自然是供应商自己的服务器资源和API。关于这个需求的实现思路,大致是在交易平台需要做接口有效性校验、服务实例有效性校验等,以及消费数据记录。
因为不同的第三方供应商系统有不同的认证方式,想实现打通,就要有不同的具体策略类(比如策略类A是通过appid完成认证,策略类B是通过密钥完成认证),因此考虑使用了策略模式。抽象策略类:
public interface ApiRedirectHandler {
/**
* @param headerMap 请求头参数Map
* @param paramMap 对第三方接口的请求参数
* @return 返回第三方接用调用的结果
*/
Object redirect(Map<String, String> headerMap, Map<String, Object> paramMap);
}
前面提到,在交易平台要做一些校验和消费记录落库的操作,这些是对接所有三方系统的公共步骤,而后面请求第三方系统接口肯定要做的鉴权认证以及转发或者调用,则属于各个三方系统的定制化行为。因此,不直接写具体策略类,而是垫一个抽象类,实现这些公共步骤,只让最后定制化行为出现在具体策略类:
@Slf4j
public abstract class AbstractRedirectHandler implements ApiRedirectHandler {
//抽象类中实现接口的方法
@Override
public Object redirect(Map<String, String> headerMap, Map<String, Object> paramMap) {
//todo: 1.请求有效性验证
//从请求参数paramMap中拿到你要调用APIId,然后查到的三方系统接口的路径、host等信息
ApiInfo apiDetailVo = queryApiInfo(paramMap);
//API的ID用完了,它不是三方系统接口需要的请求参数,移除
paramMap.remove("apiId");
//todo: 2.服务实例有效性验证
//request中去写不同三方系统的鉴权、转发或调用逻辑
val responseData = request(headerMap, paramMap, apiDetailVo);
//todo: 3.记录消费记录
//返回第三方接口的响应结果
return responseData;
}
/**
* API转发请求,对接时,针对不同的三方系统去定制化实现
*
* @param headerMap 头信息
* @param paramMap 请求参数
* @Param apiDetailVo 接口信息,如接口路径、服务器host
* @return 返回第三方接用调用的结果
*/
protected abstract Object request(Map<String, String> headerMap, Map<String, Object> paramMap, ApiDetailVo apiDetailVo);
}
到此,不同的认证方式对应的不同的具体策略类只需实现这个抽象接口的request方法即可,之前用到的具体策略类:
- AppID认证
- AppSecret认证
- SDK反射(第三方系统自己开发认证方式,提供jar包,公司的交易系统去加载jar包并反射调用第三方系统开发者的request方法)
详见【策略模式 + 反射加载SDK】