短链接管理
创建短链接数据库表
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
*/
@Override
public 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{
//数据库如果存在,则会报错,进入catch
baseMapper.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"
);
@Override
public 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
*/
@Override
public 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
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}