1.概述
1.1 策略模式
策略模式是一种行为模式,它将对象和行为分开了,行为变成了一个接口以及这个行为的多个实现。策略模式可以让这些行为之间进行切换。
策略模式有3种角色,分别为:选择器、抽象策略、策略实例。
其中选择器selector
又被称为上下文context
,其作用为通过不同的标识来获取对应的策略实例。策略实例就是封装不同算法的实例对象,而抽象策略就是策略实例的顶层接口。

1.2 模板方法
模板方法通过继承来实现,顶层是一个抽象类,用于封装通用函数,并提供一个或多个抽象方法,下层是多个实现类,用于实现不同的业务逻辑分支,类图如下:

模板方法实现:
实际使用的时候,一般会通过子类的实例调用父类中的模板方法templateMethod
,在模板方法中调用抽象方法,最终还是会调用到子类中覆写的实例方法,这是一种常见的钩子函数使用方式。
-
抽象父类:
/** * 抽象父类 */ public abstract class BaseClass { final public void templateMethod() { System.out.println("执行模板方法"); method1(); method2(); } abstract protected void method1(); abstract protected void method2(); }
-
子类实现
/** * 子类1 */ public class ChildClass1 extends BaseClass { @Override protected void method1() { System.out.println("执行子类1的方法1"); } @Override protected void method2() { System.out.println("执行子类1的方法2"); } }
/** * 子类2 */ public class ChildClass2 extends BaseClass { @Override protected void method1() { System.out.println("执行子类2的方法1"); } @Override protected void method2() { System.out.println("执行子类2的方法2"); } }
2 实例
假设我们在用户登录模块中需要实现从不同的第三方登录的需求,这里假设分别是QQ登录和微信登录,下面用策略模式结合模板方法来进行实现这个需求。
2.1 实现思路
- 前置设计:通过定义常量来标识策略的类型,使用者调用时可以通过常量获取对应的策略实例。
- QQ登录和微信登录分别对应两个bean实例,在内部各自实现其登录逻辑,在两个bean实例的上层是抽象策略,有一个通用的接口(或抽象类)用于提供访问入口
- 选择器设计:可以通过
Map
来存储数据,调用者调用时可以通过策略标识来获取对应的策略实例。
2.2 实现代码
-
编写抽象策略接口和抽象策略实现类
// 抽象策略接口 public interface SocialLoginStrategy { /** * 登录 * * @param data 数据 * @return {@link UserInfoDTO} 用户信息 */ UserInfoDTO login(String data); }
// 抽象策略实现类 @Service public abstract class AbstractSocialLoginStrategyImpl implements SocialLoginStrategy { @Override public UserInfoDTO login(String data) { // 创建登录信息 UserDetailDTO userDetailDTO; // 获取第三方token信息 SocialTokenDTO socialToken = getSocialToken(data); // 获取用户ip信息 String ipAddress = IpUtils.getIpAddress(request); String ipSource = IpUtils.getIpSource(ipAddress); // 判断是否已注册 UserAuth user = getUserAuth(socialToken); if (Objects.nonNull(user)) { // 返回数据库用户信息 userDetailDTO = getUserDetail(user, ipAddress, ipSource); } else { // 获取第三方用户信息,保存到数据库返回 userDetailDTO = saveUserDetail(socialToken, ipAddress, ipSource); } // 判断账号是否禁用 if (userDetailDTO.getIsDisable().equals(TRUE)) { throw new BizException("账号已被禁用"); } // 将登录信息放入springSecurity管理 UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetailDTO, null, userDetailDTO.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); // 登录日志 LoginLog loginLog = UserUtils.getLoginLog(userDetailDTO); loginLog.setStatus(1); loginLog.setMessage("登录成功"); loginLogMapper.insert(loginLog); // 返回用户信息 return BeanCopyUtils.copyObject(userDetailDTO, UserInfoDTO.class); } /** * 获取第三方token信息 * * @param data 数据 * @return {@link SocialTokenDTO} 第三方token信息 */ public abstract SocialTokenDTO getSocialToken(String data); /** * 获取第三方用户信息 * * @param socialTokenDTO 第三方token信息 * @return {@link SocialUserInfoDTO} 第三方用户信息 */ public abstract SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO); /** * 获取用户账号 * * @return {@link UserAuth} 用户账号 */ private UserAuth getUserAuth(SocialTokenDTO socialTokenDTO) { return userAuthMapper.selectOne(new LambdaQueryWrapper<UserAuth>() .eq(UserAuth::getUsername, socialTokenDTO.getUnionId()) .eq(UserAuth::getLoginType, socialTokenDTO.getLoginType())); } /** * 获取用户信息 * * @param user 用户账号 * @param ipAddress ip地址 * @param ipSource ip源 * @return {@link UserDetailDTO} 用户信息 */ private UserDetailDTO getUserDetail(UserAuth user, String ipAddress, String ipSource) { // 更新登录信息 userAuthMapper.update(new UserAuth(), new LambdaUpdateWrapper<UserAuth>() .set(UserAuth::getLastLoginTime, LocalDateTime.now()) .set(UserAuth::getIpAddress, ipAddress) .set(UserAuth::getIpSource, ipSource) .eq(UserAuth::getId, user.getId())); // 封装信息 return userDetailsService.convertUserDetail(user, request); } /** * 新增用户信息 * * @param socialToken token信息 * @param ipAddress ip地址 * @param ipSource ip源 * @return {@link UserDetailDTO} 用户信息 */ private UserDetailDTO saveUserDetail(SocialTokenDTO socialToken, String ipAddress, String ipSource) { // 获取第三方用户信息 SocialUserInfoDTO socialUserInfo = getSocialUserInfo(socialToken); // 保存用户信息 UserInfo userInfo = UserInfo.builder() .nickname(socialUserInfo.getNickname()) .avatar(socialUserInfo.getAvatar()) .build(); userInfoMapper.insert(userInfo); // 保存账号信息 UserAuth userAuth = UserAuth.builder() .userInfoId(userInfo.getId()) .username(socialToken.getUnionId()) .password(socialToken.getAccessToken()) .loginType(socialToken.getLoginType()) .lastLoginTime(LocalDateTime.now(ZoneId.of(ZoneEnum.SHANGHAI.getZone()))) .ipAddress(ipAddress) .ipSource(ipSource) .build(); userAuthMapper.insert(userAuth); // 绑定角色 UserRole userRole = UserRole.builder() .userId(userInfo.getId()) .roleId(RoleEnum.USER.getRoleId()) .build(); userRoleMapper.insert(userRole); return userDetailsService.convertUserDetail(userAuth, request); } }
可以看到抽象策略实现类中实现了login方法,在login方法中调用了两个抽象方法(有没有很眼熟,这不就是模板方法嘛,再看一下模板方法的类图)
-
编写选择器
public class SocialLoginStrategyContext { @Autowired private Map<String, SocialLoginStrategy> socialLoginStrategyMap; /** * 执行第三方登录策略 * * @param data 数据 * @param loginTypeEnum 登录枚举类型 * @return {@link UserInfoDTO} 用户信息 */ public UserInfoDTO executeLoginStrategy(String data, LoginTypeEnum loginTypeEnum) { return socialLoginStrategyMap.get(loginTypeEnum.getStrategy()).login(data); } }
这里选择器就是用Map存储不同的登录策略,在执行时根据传入的loginType来决定调用。
-
策略实现,这里以实现QQ登录为例
@Service("qqLoginStrategyImpl") class QQLoginStrategyImpl extends AbstractSocialLoginStrategyImpl { @Autowired private QQConfigProperties qqConfigProperties; @Autowired private RestTemplate restTemplate; @Override public SocialTokenDTO getSocialToken(String data) { if (Objects.nonNull((JSON.parseObject(data).get("code")))) { QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class); // 定义请求参数 Map<String, String> formData = new HashMap<>(1); formData.put(CODE, JSON.parseObject(data).getString("code")); String result = restTemplate.getForObject(qqConfigProperties.getQqUniqueInfoUrl(), String.class, formData); return SocialTokenDTO.builder() .openId(JSON.parseObject(result).get("openid").toString()) .unionId(JSON.parseObject(result).get("unionid").toString()) .accessToken(JSON.parseObject(result).get("session_key").toString()) .platform(qqLoginVO.getPlatform()) .nickName(qqLoginVO.getNickname()) .avatar(qqLoginVO.getAvatar()) .loginType(LoginTypeEnum.QQ.getType()) .build(); } else { QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class); // 校验QQ token信息 checkQQToken(qqLoginVO); // 定义请求参数 Map<String, String> formData = new HashMap<>(1); formData.put(ACCESS_TOKEN, JSON.parseObject(data).getString("accessToken")); String result = restTemplate.getForObject(qqConfigProperties.getUniqueInfoUrl(), String.class, formData); qqUnionInfoDTO qqUnionInfoDTO = JSON.parseObject(result.substring(9,result.length()-3), qqUnionInfoDTO.class); // 返回token信息 return SocialTokenDTO.builder() .openId(qqLoginVO.getOpenId()) .unionId(qqUnionInfoDTO.getUnionid()) .accessToken(qqLoginVO.getAccessToken()) .appId(qqUnionInfoDTO.getClient_id()) .platform(qqLoginVO.getPlatform()) .loginType(LoginTypeEnum.QQ.getType()) .build(); } } @Override public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) { if (socialTokenDTO.getPlatform().equals("MP-QQ")) { // 返回用户信息 return SocialUserInfoDTO.builder() .nickname(socialTokenDTO.getNickName()) .avatar(socialTokenDTO.getAvatar()) .build(); } else { // 定义请求参数 Map<String, String> formData = new HashMap<>(3); formData.put(QQ_OPEN_ID, socialTokenDTO.getOpenId()); formData.put(ACCESS_TOKEN, socialTokenDTO.getAccessToken()); formData.put(OAUTH_CONSUMER_KEY, socialTokenDTO.getAppId()); // 获取QQ返回的用户信息 QQUserInfoDTO qqUserInfoDTO = JSON.parseObject(restTemplate.getForObject(qqConfigProperties.getUserInfoUrl(), String.class, formData), QQUserInfoDTO.class); // 返回用户信息 return SocialUserInfoDTO.builder() .nickname(Objects.requireNonNull(qqUserInfoDTO).getNickname()) .avatar(qqUserInfoDTO.getFigureurl_qq_1()) .build(); } } /** * 校验qq token信息 * * @param qqLoginVO qq登录信息 */ private void checkQQToken(QQLoginVO qqLoginVO) { // 根据token获取qq openId信息 Map<String, String> qqData = new HashMap<>(1); qqData.put(SocialLoginConst.ACCESS_TOKEN, qqLoginVO.getAccessToken()); try { String result = restTemplate.getForObject(qqConfigProperties.getCheckTokenUrl(), String.class, qqData); QQTokenDTO qqTokenDTO = JSON.parseObject(CommonUtils.getBracketsContent(Objects.requireNonNull(result)), QQTokenDTO.class); // 判断openId是否一致 if (!qqLoginVO.getOpenId().equals(qqTokenDTO.getOpenid())) { throw new BizException(StatusCodeEnum.QQ_LOGIN_ERROR); } } catch (Exception e) { e.printStackTrace(); throw new BizException(StatusCodeEnum.QQ_LOGIN_ERROR); } } }