短链接管理
创建短链接数据库表
URI、URL和URN区别 :
URI 指的是一个资源 ;URL 用地址定位一个资源; URN 用名称定位一个资源。
举个例子: 去寻找一个具体的人(URI);如果用地址:XX省XX市XX区...XX单元XX室的主人 就是URL;如果用身份证号+名字去找就是URN(身份证号+名字 无法确认资源的地址) 。 在Java类库中,URI类不包含任何访问资源的方法,只能标识资源。URL类可以访问资源,可以获取指定资源的流信息。
新增短链接
由于describe在java中属于关键字,所以在实体对象中,对于describe属性应当:
记住是加了反引号的!
/*** 描述*/@TableField("`describe`")private String describe;
短链接区分大小写:将utf8m64改成utf8,相应的配置也改成utf8 bin。
将长连接hash模到短链接一定会存在冲突的问题,怎么解决?
重置。为了防止死循环,需要设置一个最大的重置次数。
由于海量数据,并且为了防止多次查询数据库,需要去查缓存,所以需要用分布式锁,从而这个用布隆过滤器实现(在用户注册时,同样使用了)。
private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;/*** 创建短链接** @param requestParam* @return*/@Overridepublic ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {String shortLinkSuffix=generateSuffix(requestParam);String fullShortUrl=requestParam.getDomain()+"/"+shortLinkSuffix;ShortLinkDO shortLinkDO=ShortLinkDO.builder().domain(requestParam.getDomain()).originUrl(requestParam.getOriginUrl()).gid(requestParam.getGid()).createdType(requestParam.getCreatedType()).validDateType(requestParam.getValidDateType()).validDate(requestParam.getValidDate()).describe(requestParam.getDescribe()).shortUri(shortLinkSuffix).enableStatus(0).fullShortUrl(fullShortUrl).build();try{//数据库如果存在,则会报错,进入catchbaseMapper.insert(shortLinkDO);}catch (DuplicateKeyException exp){//TODO 已经误判的短链接如何处理//第一种,短链接确实真实存在缓存中//第二种,短链接不一定存在缓存中//检查是否存在于数据库中,如果没存在,则说明布隆过滤器误判了。LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getFullShortUrl, fullShortUrl);ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);if(hasShortLinkDO!=null){log.warn("短链接:{} 重复入库",fullShortUrl);throw new ServiceException("短链接生成重复");}}shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);return ShortLinkCreateRespDTO.builder().fullShortUrl(shortLinkDO.getFullShortUrl()).originUrl(requestParam.getOriginUrl()).gid(requestParam.getGid()).build();}/*** 获取短链接的后缀* @param requestParam* @return*/private String generateSuffix(ShortLinkCreateReqDTO requestParam){int customGenerateCount=0;String shortUri;String originUrl = requestParam.getOriginUrl();while(true){if(customGenerateCount>10){throw new ServiceException("短链接频繁生成,请稍后再试");}//减小当前冲突的可能originUrl+=System.currentTimeMillis();shortUri=HashUtil.hashToBase62(originUrl);if(!shortUriCreateCachePenetrationBloomFilter.contains(requestParam.getDomain()+"/"+shortUri)){break;}customGenerateCount++;}return shortUri;}
开发用户登录验证拦截器返回友好提示信息:
@RequiredArgsConstructor
public class UserTransmitFilter implements Filter {private final StringRedisTemplate stringRedisTemplate;private static final List<String> IGNORE_URI= Lists.newArrayList("/api/short-link/admin/v1/user/login","/api/short-link/admin/v1/user/has-username");@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;String requestURI=httpServletRequest.getRequestURI();if(!IGNORE_URI.contains(requestURI)){String method=httpServletRequest.getMethod();if(!(Objects.equals(requestURI,"/api/short-link/admin/v1/user")&&Objects.equals(method,"POST"))){String username = httpServletRequest.getHeader("username");String token = httpServletRequest.getHeader("token");if (!StrUtil.isAllNotBlank(username,token)){returnJson(servletResponse,JSON.toJSONString(Results.failure(new ClientException(USER_TOKEN_FAIL))));return;}Object userInfoJsonStr = null;try{userInfoJsonStr=stringRedisTemplate.opsForHash().get("login_" + username, token);if(userInfoJsonStr==null){throw new ClientException(USER_TOKEN_FAIL);}}catch (Exception exp){returnJson(servletResponse,JSON.toJSONString(Results.failure(new ClientException(USER_TOKEN_FAIL))));return;}UserInfoDTO userInfoDTO = JSON.parseObject(userInfoJsonStr.toString(), UserInfoDTO.class);UserContext.setUser(userInfoDTO);}}try {filterChain.doFilter(servletRequest, servletResponse);} finally {UserContext.removeUser();}}private void returnJson(ServletResponse response, String json) {PrintWriter writer = null;response.setCharacterEncoding("UTF-8");response.setContentType("application/json; charset=utf-8");try {writer = response.getWriter();writer.print(json);} catch (IOException e) {} finally {if (writer != null)writer.close();}}}
打包工具,在admin和project的pom中添加,用于联调前端,这个打包插件会让打包有一个直接可以启动的spring文件。
<build><finalName>s{project.artifactId</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build>
分页查询短链接列表
/*** 分页查询短链接** @param requestParam* @return*/@Overridepublic IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class).eq(ShortLinkDO::getGid, requestParam.getGid()).eq(ShortLinkDO::getEnableStatus, 0).eq(ShortLinkDO::getDelFlag, 0);IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, queryWrapper);return resultPage.convert(eatch-> BeanUtil.toBean(eatch,ShortLinkPageRespDTO.class));}
请求的param有三个:gid、current(当前页)、size(每页数量)
由于返回响应中total有问题,这需要添加一个MySQL数据库分页插件:
@Configuration
public class DataBaseConfiguration {/*** 分页插件* @return*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}