技术解析之策略模式
1、引出
- 在开发中遇到需要使用不同的方式进行上传以及登录。
- 采用if和switch结构可以解决,但是代码耦合度太高。
2、设计模式之策略模式
-
自定义接口
-
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ public interface SearchStrategy { /** * 搜索文章 * * @param keywords 关键字 * @return {@link List <ArticleSearchDTO>} 文章列表 */ List<ArticleSearchDTO> searchArticle(String keywords); } -
package com.lc.strategy; import com.lc.dto.UserInfoDTO; /** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ public interface SocialLoginStrategy { /** * 登录 * * @param data 数据 * @return {@link UserInfoDTO} 用户信息 */ UserInfoDTO login(String data); } -
package com.lc.strategy; import org.springframework.web.multipart.MultipartFile; /** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ public interface UploadStrategy { /** * 实现文件的上传 * * @param file 文件 * @param path 上传路径 * @return 文件地址 */ String uploadFile(MultipartFile file, String path); }
-
-
创建实现对应接口的类
-
创建抽象类来实现一些公共的方法
-
/** * 第三方登录抽象模板 * * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service public abstract class AbstractSocialLoginStrategyImpl implements SocialLoginStrategy { @Autowired private UserAuthMapper userAuthMapper; @Autowired private UserInfoMapper userInfoMapper; @Autowired private UserRoleMapper userRoleMapper; @Autowired private UserDetailServiceImpl userDetailsService; @Autowired private HttpServletRequest request; @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.isNull(user)) { // 返回数据库用户信息 userDetailDTO = getUserDetail(user, ipAddress, ipSource); } else { // 获取第三方用户信息,保存到数据库返回 userDetailDTO = saveUserDetail(socialToken, ipAddress, ipSource); } // 判断账号是否禁用 if (userDetailDTO.getIsDisable().equals(CommonConst.TRUE)) { throw new BizException("账号已被禁用!"); } // 将登录信息交给springSecurity管理 UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetailDTO, null, userDetailDTO.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); // 返回用户信息 return BeanCopyUtils.copyObject(userDetailDTO, UserInfoDTO.class); } /** * 获取用户信息 * * @param user 用户账号 * @param ipAddress ip地址 * @param ipSource ip源 * @return {@link UserDetailDTO} 用户信息 */ @Transactional(rollbackFor = Exception.class) 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.covertUserDetail(user, request); } /** * 获取用户账号 * * @return {@link UserAuth} 用户账号 */ private UserAuth getUserAuth(SocialTokenDTO socialTokenDTO) { return userAuthMapper.selectOne(new LambdaQueryWrapper<UserAuth>() .eq(UserAuth::getUsername, socialTokenDTO.getOpenId()) .eq(UserAuth::getLoginType, socialTokenDTO.getLoginType())); }
-
-
/**
* 获取第三方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);
/**
* 新增用户信息
*
* @param socialToken token信息
* @param ipAddress ip地址
* @param ipSource ip源
* @return {@link UserDetailDTO} 用户信息
*/
@Transactional(rollbackFor = Exception.class)
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.getOpenId())
.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.covertUserDetail(userAuth, request);
}
}
-
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service public abstract class AbstractUploadStrategyImpl implements UploadStrategy {
@Override
public String uploadFile(MultipartFile file, String path) {
try {
// 获取文件md5加密后的值
String md5 = FileUtils.getMd5(file.getInputStream());
// 获取文件拓展名
String extName = FileUtils.getExtName(file.getOriginalFilename());
// 重新生成文件名
String fileName = md5 + extName;
// 判断该文件是否已经存在
if (!exists(path + fileName)) {
// 不存在则上传
upload(path, fileName, file.getInputStream());
}
// 返回文件访问路径
return getFileAccessUrl(path + fileName);
} catch (Exception e) {
e.printStackTrace();
throw new BizException("文件上传失败!请检查文件格式与大小是否正确");
}
}
/**
* 判断文件是否存在
*
* @param filePath 文件路径
* @return {@link Boolean}
*/
public abstract Boolean exists(String filePath);
/**
* 上传
*
* @param path 路径
* @param fileName 文件名
* @param inputStream 输入流
* @throws IOException io异常
*/
public abstract void upload(String path, String fileName, InputStream inputStream) throws IOException;
/**
* 获取文件访问url
*
* @param filePath 文件路径
* @return {@link String}
*/
public abstract String getFileAccessUrl(String filePath);
}
-
创建抽象类继承类来拓展剩余的方法
-
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Log4j2 @Service("esSearchStrategyImpl") public class EsSearchStrategyImpl implements SearchStrategy { @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; @Override public List<ArticleSearchDTO> searchArticle(String keywords) { if (StringUtils.isBlank(keywords)) { return new ArrayList<>(); } return search(buildQuery(keywords)); } /** * 搜索文章构造 * * @param keywords 关键字 * @return es条件构造器 */ private NativeSearchQueryBuilder buildQuery(String keywords) { // 条件构造器 NativeSearchQueryBuilder nativeSearchQueryBuilder = new NativeSearchQueryBuilder(); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); // 根据关键词搜索文章标题或内容 boolQueryBuilder.must(QueryBuilders.boolQuery().should(QueryBuilders.matchQuery("articleTitle", keywords)) .should(QueryBuilders.matchQuery("articleContent", keywords))) .must(QueryBuilders.termQuery("isDelete", CommonConst.FALSE)) .must(QueryBuilders.termQuery("status", ArticleStatusEnum.PUBLIC.getStatus())); nativeSearchQueryBuilder.withQuery(boolQueryBuilder); return nativeSearchQueryBuilder; } /** * 文章搜索结果高亮 * * @param nativeSearchQueryBuilder es条件构造器 * @return 搜索结果 */ private List<ArticleSearchDTO> search(NativeSearchQueryBuilder nativeSearchQueryBuilder) { // 添加文章标题高亮 HighlightBuilder.Field titleField = new HighlightBuilder.Field("articleTitle"); titleField.preTags(CommonConst.PRE_TAG); titleField.postTags(CommonConst.POST_TAG); // 添加文章内容高亮 HighlightBuilder.Field contentField = new HighlightBuilder.Field("articleContent"); contentField.preTags(CommonConst.PRE_TAG); contentField.postTags(CommonConst.POST_TAG); contentField.fragmentSize(200); nativeSearchQueryBuilder.withHighlightFields(titleField, contentField); // 搜索 try { SearchHits<ArticleSearchDTO> search = elasticsearchRestTemplate.search(nativeSearchQueryBuilder.build(), ArticleSearchDTO.class); return search.getSearchHits().stream().map(hit -> { ArticleSearchDTO article = hit.getContent(); // 获取文章标题高亮数据 List<String> titleHighLightList = hit.getHighlightFields().get("articleTitle"); if (CollectionUtils.isNotEmpty(titleHighLightList)) { // 替换标题数据 article.setArticleTitle(titleHighLightList.get(0)); } // 获取文章内容高亮数据 List<String> contentHighLightList = hit.getHighlightFields().get("articleContent"); if (CollectionUtils.isNotEmpty(contentHighLightList)) { // 替换内容数据 article.setArticleContent(contentHighLightList.get(contentHighLightList.size() - 1)); } return article; }).collect(Collectors.toList()); } catch (Exception e) { log.error(e.getMessage()); } return new ArrayList<>(); } } -
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service("localUploadStrategyImpl") public class LocalUploadStrategyImpl extends AbstractUploadStrategyImpl{ /** * 本地路径 */ @Value("${upload.local.path}") private String localPath; /** * 访问url */ @Value("${upload.local.url}") private String localUrl; @Override public Boolean exists(String filePath) { return new File(localPath + filePath).exists(); } @Override public void upload(String path, String fileName, InputStream inputStream) throws IOException { // 判断目录是否存在 File directory = new File(localPath + path); if (!directory.exists()) { if (!directory.mkdirs()) { throw new BizException("创建目录失败!"); } } // 写入文件 File file = new File(localPath + path + fileName); if (file.createNewFile()) { BufferedInputStream bis = new BufferedInputStream(inputStream); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); byte[] bytes = new byte[1024]; int length; while ((length = bis.read(bytes)) != -1) { bos.write(bytes, 0, length); } bos.flush(); inputStream.close(); bis.close(); bos.close(); } } @Override public String getFileAccessUrl(String filePath) { return localUrl + filePath; } } -
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service("mySqlSearchStrategyImpl") public class MySqlSearchStrategyImpl implements SearchStrategy { @Autowired private ArticleMapper articleMapper; @Override public List<ArticleSearchDTO> searchArticle(String keywords) { // 校验 if (StringUtils.isNullOrEmpty(keywords)) { return new ArrayList<>(); } // 搜索文章 // select ta.* from tb_article ta where is_delete = 0 and status = 1 and (article_tile like %keyword% or article_content like %keyword%) List<Article> articleList = articleMapper.selectList(new LambdaQueryWrapper<Article>() .eq(Article::getIsDelete, CommonConst.FALSE) .eq(Article::getStatus, ArticleStatusEnum.PUBLIC.getStatus()) .and(item -> item.like(Article::getArticleTitle, keywords)) .or() .like(Article::getArticleContent, keywords)); // 再次处理(高亮) return articleList.stream().map(item -> { // 获取关键词第一次出现的位置 String articleContent = item.getArticleContent(); int index = articleContent.indexOf(keywords); if (index != -1) { // 获取关键词前面的文字 int preIndex = index > 25 ? index - 25 : 0; String preText = articleContent.substring(preIndex, index); // 获取关键词到后面的文字 int last = index + keywords.length(); int postLength = articleContent.length(); int postIndex = postLength > 175 ? last + 175 : last + postLength; String postText = articleContent.substring(index, postIndex); // 文章内容高亮 articleContent = (preText + postText).replaceAll(keywords, CommonConst.PRE_TAG + keywords + CommonConst.POST_TAG); } // 文章标题高亮 String articleTitle = item.getArticleTitle().replaceAll(keywords, CommonConst.PRE_TAG + keywords + CommonConst.POST_TAG); return ArticleSearchDTO.builder() .id(item.getId()) .articleTitle(articleTitle) .articleContent(articleContent) .build(); }).collect(Collectors.toList()); } } -
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service("ossUploadStrategyImpl") public class OssUploadStrategyImpl extends AbstractUploadStrategyImpl { @Autowired private OssConfigProperties ossConfigProperties; @Override public Boolean exists(String filePath) { return getOssClient().doesObjectExist(ossConfigProperties.getBucketName(), filePath); } @Override public void upload(String path, String fileName, InputStream inputStream) { getOssClient().putObject(ossConfigProperties.getBucketName(), path + fileName, inputStream); } @Override public String getFileAccessUrl(String filePath) { return ossConfigProperties.getUrl() + filePath; } /** * 获取ossClient * * @return {@link OSS} ossClient */ private OSS getOssClient() { return new OSSClientBuilder().build(ossConfigProperties.getEndpoint(), ossConfigProperties.getAccessKeyId(), ossConfigProperties.getAccessKeySecret()); } } -
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service("qqLoginStrategyImpl") public class QQLoginStrategyImpl extends AbstractSocialLoginStrategyImpl { @Autowired private QQConfigProperties qqConfigProperties; @Autowired private RestTemplate restTemplate; @Override public SocialTokenDTO getSocialToken(String data) { QQLoginVO qqLoginVO = JSON.parseObject(data, QQLoginVO.class); // 校验QQ token信息 checkQQToken(qqLoginVO); // 返回token信息 return SocialTokenDTO.builder() .openId(qqLoginVO.getOpenId()) .accessToken(qqLoginVO.getAccessToken()) .loginType(LoginTypeEnum.QQ.getType()) .build(); } @Override public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) { // 定义请求参数 Map<String, String> formData = new HashMap<>(3); formData.put(SocialLoginConst.QQ_OPEN_ID, socialTokenDTO.getOpenId()); formData.put(SocialLoginConst.ACCESS_TOKEN, socialTokenDTO.getAccessToken()); formData.put(SocialLoginConst.OAUTH_CONSUMER_KEY, qqConfigProperties.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); } } } -
/** * @author Const * @version 1.0 * @email 1357042069@qq.com * @date 2022/03/06 */ @Service("weiboLoginStrategyImpl") public class WeiboLoginStrategyImpl extends AbstractSocialLoginStrategyImpl { @Autowired private WeiboConfigProperties weiboConfigProperties; @Autowired private RestTemplate restTemplate;
-
@Override
public SocialTokenDTO getSocialToken(String data) {
WeiboLoginVO weiBoLoginVO = JSON.parseObject(data, WeiboLoginVO.class);
// 获取微博token信息
WeiboTokenDTO weiboToken = getWeiboToken(weiBoLoginVO);
// 返回token信息
return SocialTokenDTO.builder()
.openId(weiboToken.getUid())
.accessToken(weiboToken.getAccess_token())
.loginType(LoginTypeEnum.WEIBO.getType())
.build();
}
@Override
public SocialUserInfoDTO getSocialUserInfo(SocialTokenDTO socialTokenDTO) {
// 定义请求参数
Map<String, String> data = new HashMap<>(2);
data.put(SocialLoginConst.UID, socialTokenDTO.getOpenId());
data.put(SocialLoginConst.ACCESS_TOKEN, socialTokenDTO.getAccessToken());
// 获取微博用户信息
WeiboUserInfoDTO weiboUserInfoDTO = restTemplate.getForObject(weiboConfigProperties.getUserInfoUrl(), WeiboUserInfoDTO.class, data);
// 返回用户信息
return SocialUserInfoDTO.builder()
.nickname(Objects.requireNonNull(weiboUserInfoDTO).getScreen_name())
.avatar(weiboUserInfoDTO.getAvatar_hd())
.build();
}
/**
* 获取微博token信息
*
* @param weiBoLoginVO 微博登录信息
* @return {@link WeiboTokenDTO} 微博token
*/
private WeiboTokenDTO getWeiboToken(WeiboLoginVO weiBoLoginVO) {
// 根据code换取微博uid和accessToken
MultiValueMap<String, String> weiboData = new LinkedMultiValueMap<>();
// 定义微博token请求参数
weiboData.add(SocialLoginConst.CLIENT_ID, weiboConfigProperties.getAppId());
weiboData.add(SocialLoginConst.CLIENT_SECRET, weiboConfigProperties.getAppSecret());
weiboData.add(SocialLoginConst.GRANT_TYPE, weiboConfigProperties.getGrantType());
weiboData.add(SocialLoginConst.REDIRECT_URI, weiboConfigProperties.getRedirectUrl());
weiboData.add(SocialLoginConst.CODE, weiBoLoginVO.getCode());
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(weiboData, null);
try {
return restTemplate.exchange(weiboConfigProperties.getAccessTokenUrl(), HttpMethod.POST, requestEntity, WeiboTokenDTO.class).getBody();
} catch (Exception e) {
throw new BizException(StatusCodeEnum.WEIBO_LOGIN_ERROR);
}
}
}
- **创建上下文Context类来维护对应的类的引用**
- **创建搜索策略执行类**
```
/**
* @author Const
* @version 1.0
* @email 1357042069@qq.com
* @date 2022/03/06
*/
@Service
public class SearchStrategyContext {
/**
* 搜索模式
*/
@Value("${search.mode}")
private String searchMode;
@Autowired
private Map<String, SearchStrategy> searchStrategyMap;
/**
* 执行搜索策略
*
* @param keywords 关键字
* @return {@link List < ArticleSearchDTO >} 搜索文章
*/
public List<ArticleSearchDTO> executeSearchStrategy(String keywords) {
return searchStrategyMap.get(SearchModeEnum.getStrategy(searchMode)).searchArticle(keywords);
}
}
```
- **创建登录方式策略执行类**
```
/**
* @author Const
* @version 1.0
* @email 1357042069@qq.com
* @date 2022/03/06
*/
@Service
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);
}
}
```
- **创建上传模式策略执行类**
```
/**
* @author Const
* @version 1.0
* @email 1357042069@qq.com
* @date 2022/03/06
*/
@Service
public class UploadStrategyContext {
/**
* 上传模式
*/
@Value("${upload.mode}")
private String uploadMode;
@Autowired
private Map<String, UploadStrategy> uploadStrategyMap;
/**
* 上传文件
*
* @param file 文件
* @param path 路径
* @return {@link String} 文件地址
*/
public String executeUploadStrategy(MultipartFile file, String path) {
return uploadStrategyMap.get(UploadModeEnum.getStrategy(uploadMode)).uploadFile(file, path);
}
}
```
### 3、总结
- 策略模式在一个**算法**的**多个实现**方面起到了很好的解耦作用。
- **步骤**
- 创建对应接口。
- 创建对应实现类(抽象或具体)。
- 创建Context上下文对象来执行对应的具体实现类的方法。