mirror of https://github.com/alibaba/easyexcel
20 changed files with 2745 additions and 0 deletions
@ -0,0 +1,136 @@
|
||||
package com.alibaba.easytools.spring.cache; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.function.Supplier; |
||||
|
||||
import javax.annotation.Resource; |
||||
|
||||
import com.alibaba.easytools.spring.cache.wrapper.CacheWrapper; |
||||
|
||||
import com.google.common.collect.Lists; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.data.redis.core.HashOperations; |
||||
import org.springframework.data.redis.core.StringRedisTemplate; |
||||
import org.springframework.data.redis.core.ValueOperations; |
||||
|
||||
/** |
||||
* 缓存操作 |
||||
* |
||||
* @author qiuyuyu |
||||
* @date 2022/03/08 |
||||
*/ |
||||
public class EasyCache<V> { |
||||
/** |
||||
* 超时时间 |
||||
*/ |
||||
private static final long DEFAULT_TIMEOUT = Duration.ofMinutes(2L).toMillis(); |
||||
/** |
||||
* 同步锁的前缀 |
||||
*/ |
||||
private static final String SYNCHRONIZED_PREFIX = "_EasyCache:"; |
||||
@Resource |
||||
private HashOperations<String, String, CacheWrapper<V>> hashOperations; |
||||
@Resource |
||||
private ValueOperations<String, CacheWrapper<V>> valueOperations; |
||||
@Resource |
||||
private StringRedisTemplate stringRedisTemplate; |
||||
|
||||
/** |
||||
* 去缓存里面获取一个值 并放入缓存 |
||||
* |
||||
* @param key 缓存的key |
||||
* @param queryData 查询数据 |
||||
* @return 缓存的值 |
||||
*/ |
||||
public V get(String key, Supplier<V> queryData) { |
||||
return get(key, queryData, DEFAULT_TIMEOUT); |
||||
} |
||||
|
||||
/** |
||||
* 去缓存里面获取一个值 并放入缓存 |
||||
* |
||||
* @param key 缓存的key |
||||
* @param queryData 查询数据 |
||||
* @param timeout 超时时长 ms |
||||
* @return 缓存的值 |
||||
*/ |
||||
public V get(String key, Supplier<V> queryData, Long timeout) { |
||||
if (key == null) { |
||||
return null; |
||||
} |
||||
// 先去缓存获取
|
||||
CacheWrapper<V> cacheWrapper = valueOperations.get(key); |
||||
if (cacheWrapper != null) { |
||||
return cacheWrapper.getData(); |
||||
} |
||||
// 没有则锁住 然后第一个去获取
|
||||
String lockKey = SYNCHRONIZED_PREFIX + key; |
||||
synchronized (lockKey.intern()) { |
||||
// 重新获取
|
||||
cacheWrapper = valueOperations.get(key); |
||||
if (cacheWrapper != null) { |
||||
return cacheWrapper.getData(); |
||||
} |
||||
|
||||
// 真正的去查询数据
|
||||
V value = queryData.get(); |
||||
|
||||
// 构建缓存
|
||||
CacheWrapper<V> cacheWrapperData = new CacheWrapper<>(); |
||||
cacheWrapperData.setData(value); |
||||
valueOperations.set(key, cacheWrapperData, timeout, TimeUnit.MILLISECONDS); |
||||
return value; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 去缓存里面获取一个值 并放入缓存 |
||||
* |
||||
* @param key 缓存的key |
||||
* @param hashKey 缓存的hashKey |
||||
* @param queryData 查询数据 |
||||
* @return 缓存的值 |
||||
*/ |
||||
public V hashGet(String key, String hashKey, Supplier<V> queryData) { |
||||
if (key == null || hashKey == null) { |
||||
return null; |
||||
} |
||||
// 先去缓存获取
|
||||
CacheWrapper<V> cacheWrapper = hashOperations.get(key, hashKey); |
||||
if (cacheWrapper != null && System.currentTimeMillis() < cacheWrapper.getExpireTimeMillis()) { |
||||
return cacheWrapper.getData(); |
||||
} |
||||
|
||||
// 没有则锁住 然后第一个去获取
|
||||
String lockKey = SYNCHRONIZED_PREFIX + key + ":" + hashKey; |
||||
synchronized (lockKey.intern()) { |
||||
// 重新获取
|
||||
cacheWrapper = hashOperations.get(key, hashKey); |
||||
if (cacheWrapper != null && System.currentTimeMillis() < cacheWrapper.getExpireTimeMillis()) { |
||||
return cacheWrapper.getData(); |
||||
} |
||||
|
||||
// 真正的去查询数据
|
||||
V value = queryData.get(); |
||||
|
||||
// 构建缓存
|
||||
CacheWrapper<V> cacheWrapperData = new CacheWrapper<>(); |
||||
cacheWrapperData.setData(value); |
||||
cacheWrapperData.setExpireTimeMillis(System.currentTimeMillis() + DEFAULT_TIMEOUT); |
||||
hashOperations.put(key, hashKey, cacheWrapperData); |
||||
stringRedisTemplate.boundHashOps(key).expire(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS); |
||||
return value; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 移除缓存 |
||||
* |
||||
* @param keys |
||||
*/ |
||||
public void delete(String... keys) { |
||||
stringRedisTemplate.delete(Lists.newArrayList(keys)); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,276 @@
|
||||
package com.alibaba.easytools.spring.cache; |
||||
|
||||
import java.time.Duration; |
||||
import java.util.concurrent.TimeUnit; |
||||
import java.util.function.Supplier; |
||||
|
||||
import javax.annotation.Resource; |
||||
|
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
|
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.redisson.api.RBucket; |
||||
import org.redisson.api.RLock; |
||||
import org.redisson.api.RedissonClient; |
||||
import org.springframework.cache.support.NullValue; |
||||
|
||||
/** |
||||
* 缓存服务v2 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
@Slf4j |
||||
public class EasyCacheV2 { |
||||
/** |
||||
* 超时时间 |
||||
*/ |
||||
private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(2L); |
||||
/** |
||||
* 同步锁的前缀 |
||||
*/ |
||||
private static final String SYNCHRONIZED_PREFIX = "_EasyCacheV2:"; |
||||
|
||||
@Resource |
||||
private RedissonClient redissonClient; |
||||
|
||||
/** |
||||
* 去缓存里面获取一个值 并放入缓存 |
||||
* |
||||
* @param key 缓存的key |
||||
* @param queryData 查询数据 |
||||
* @return 缓存的值 |
||||
*/ |
||||
public <T> T computeIfAbsent(String key, Supplier<T> queryData) { |
||||
return computeIfAbsent(key, queryData, DEFAULT_TIMEOUT); |
||||
} |
||||
|
||||
/** |
||||
* 去缓存里面获取一个值 并放入缓存 |
||||
* |
||||
* @param key 缓存的key |
||||
* @param queryData 查询数据 |
||||
* @param duration 超时时长 |
||||
* @return 缓存的值 |
||||
*/ |
||||
public <T> T computeIfAbsent(String key, Supplier<T> queryData, Duration duration) { |
||||
if (key == null) { |
||||
return null; |
||||
} |
||||
// 先去缓存获取
|
||||
T data = get(key); |
||||
if (data != null) { |
||||
return data; |
||||
} |
||||
// 没有则锁住 然后第一个去获取
|
||||
String lockKey = SYNCHRONIZED_PREFIX + key; |
||||
synchronized (lockKey.intern()) { |
||||
// 重新获取
|
||||
data = get(key); |
||||
if (data != null) { |
||||
return data; |
||||
} |
||||
|
||||
// 真正的去查询数据
|
||||
T value = queryData.get(); |
||||
|
||||
// 构建缓存
|
||||
set(key, value, duration); |
||||
return value; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 移除缓存 |
||||
* |
||||
* @param keys |
||||
*/ |
||||
public void delete(String... keys) { |
||||
redissonClient.getKeys().delete(keys); |
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务,等待时间为一分钟,执行释放锁为一分钟,推荐mq回调执行简单任务使用 |
||||
* 在等待超时后抛出异常 |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param runnable 同步执行的任务 |
||||
*/ |
||||
public void synchronousExecuteOneMinuteAndThrowException(String lockKey, Runnable runnable) { |
||||
synchronousExecute(lockKey, runnable, () -> { |
||||
log.warn("经过60秒没有抢到锁"); |
||||
throw BusinessException.of("经过60秒没有抢到锁"); |
||||
}, Duration.ofMinutes(1L), Duration.ofMinutes(1L)); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务,等待时间为一分钟,执行释放锁为一分钟,推荐mq回调执行简单任务使用 |
||||
* 在等待超时后抛出异常 |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param runnable 同步执行的任务 |
||||
*/ |
||||
public void synchronousExecuteThrowException(String lockKey, Runnable runnable, Duration waitDuration, |
||||
Duration leaseDuration) { |
||||
synchronousExecute(lockKey, runnable, () -> { |
||||
log.warn("经过{}秒没有抢到锁", waitDuration.getSeconds()); |
||||
throw BusinessException.of("经过" + waitDuration.getSeconds() + "秒没有抢到锁"); |
||||
}, waitDuration, leaseDuration); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务,等待时间为一分钟,执行释放锁为一分钟,推荐mq回调执行简单任务使用 |
||||
* 在等待超时后抛出异常 |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param supplier 同步执行的任务 |
||||
*/ |
||||
public <T> T synchronousExecuteOneMinuteAndThrowException(String lockKey, Supplier<T> supplier) { |
||||
return synchronousExecute(lockKey, supplier, () -> { |
||||
log.warn("经过60秒没有抢到锁"); |
||||
throw BusinessException.of("经过60秒没有抢到锁"); |
||||
}, Duration.ofMinutes(1L), Duration.ofMinutes(1L)); |
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务,等待时间为一分钟,执行释放锁为一分钟,推荐mq回调执行简单任务使用 |
||||
* 在等待超时后抛出异常 |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param supplier 同步执行的任务 |
||||
* @param waitDuration 等待超时时间 |
||||
* @param leaseDuration 执行任务多少时间后释放锁 |
||||
*/ |
||||
public <T> T synchronousExecuteThrowException(String lockKey, Supplier<T> supplier, |
||||
Duration waitDuration, Duration leaseDuration) { |
||||
return synchronousExecute(lockKey, supplier, () -> { |
||||
log.warn("经过{}秒没有抢到锁", waitDuration.getSeconds()); |
||||
throw BusinessException.of("经过" + waitDuration.getSeconds() + "秒没有抢到锁"); |
||||
}, waitDuration, leaseDuration); |
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务,等待0秒后立即执行waitTimeOutExecutor |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param runnable 同步执行的任务 |
||||
* @param waitTimeOutRunnable 等待超时以后的处理逻辑 |
||||
* @param leaseDuration 执行任务多少时间后释放锁 |
||||
*/ |
||||
public void synchronousExecuteFailFast(String lockKey, Runnable runnable, Runnable waitTimeOutRunnable, |
||||
Duration leaseDuration) { |
||||
synchronousExecute(lockKey, runnable, waitTimeOutRunnable, Duration.ZERO, leaseDuration); |
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务 |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param runnable 同步执行的任务 |
||||
* @param waitTimeOutRunnable 等待超时以后的处理逻辑 |
||||
* @param waitDuration 等待超时时间 |
||||
* @param leaseDuration 执行任务多少时间后释放锁 |
||||
*/ |
||||
public void synchronousExecute(String lockKey, Runnable runnable, Runnable waitTimeOutRunnable, |
||||
Duration waitDuration, Duration leaseDuration) { |
||||
//同一对象 不能并发
|
||||
RLock rLock = redissonClient.getLock(lockKey); |
||||
try { |
||||
// 尝试加锁 最多等待 waitTime , 并且在leaseTime 之后释放锁
|
||||
if (rLock.tryLock(waitDuration.getSeconds(), leaseDuration.getSeconds(), TimeUnit.SECONDS)) { |
||||
runnable.run(); |
||||
} else { |
||||
if (waitTimeOutRunnable != null) { |
||||
waitTimeOutRunnable.run(); |
||||
} |
||||
} |
||||
} catch (InterruptedException e) { |
||||
log.error("执行任务被打断:{}", lockKey, e); |
||||
if (waitTimeOutRunnable != null) { |
||||
waitTimeOutRunnable.run(); |
||||
} |
||||
} finally { |
||||
if (rLock.isHeldByCurrentThread()) { |
||||
rLock.unlock(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 同步执行任务 |
||||
* |
||||
* @param lockKey 锁 全局唯一 |
||||
* @param supplier 同步执行的任务 |
||||
* @param waitTimeOutRunnable 等待超时以后的处理逻辑 |
||||
* @param waitDuration 等待超时时间 |
||||
* @param leaseDuration 执行任务多少时间后释放锁 |
||||
*/ |
||||
public <T> T synchronousExecute(String lockKey, Supplier<T> supplier, Runnable waitTimeOutRunnable, |
||||
Duration waitDuration, Duration leaseDuration) { |
||||
//同一对象 不能并发
|
||||
RLock rLock = redissonClient.getLock(lockKey); |
||||
try { |
||||
// 尝试加锁 最多等待 waitTime , 并且在leaseTime 之后释放锁
|
||||
if (rLock.tryLock(waitDuration.getSeconds(), leaseDuration.getSeconds(), TimeUnit.SECONDS)) { |
||||
return supplier.get(); |
||||
} else { |
||||
if (waitTimeOutRunnable != null) { |
||||
waitTimeOutRunnable.run(); |
||||
} |
||||
return null; |
||||
} |
||||
} catch (InterruptedException e) { |
||||
log.error("执行任务被打断:{}", lockKey, e); |
||||
if (waitTimeOutRunnable != null) { |
||||
waitTimeOutRunnable.run(); |
||||
} |
||||
return null; |
||||
} finally { |
||||
if (rLock.isHeldByCurrentThread()) { |
||||
rLock.unlock(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 查询操作 |
||||
* |
||||
* @param key 缓存key |
||||
* @param <T> |
||||
* @return |
||||
*/ |
||||
public <T> T get(String key) { |
||||
if (StringUtils.isBlank(key)) { |
||||
return null; |
||||
} |
||||
RBucket<T> rBucket = redissonClient.getBucket(key); |
||||
T data = rBucket.get(); |
||||
// 在存储空的数据的时候 存储了一个空的对象
|
||||
if (data instanceof NullValue) { |
||||
return null; |
||||
} |
||||
return data; |
||||
} |
||||
|
||||
/** |
||||
* 设置一个值 |
||||
* |
||||
* @param key |
||||
* @param value |
||||
* @param duration |
||||
*/ |
||||
public void set(String key, Object value, Duration duration) { |
||||
Object cacheValue = value; |
||||
if (cacheValue == null) { |
||||
cacheValue = NullValue.INSTANCE; |
||||
} |
||||
if (duration == null) { |
||||
duration = DEFAULT_TIMEOUT; |
||||
} |
||||
redissonClient.getBucket(key) |
||||
// 为了兼容老版本
|
||||
.set(cacheValue, duration.getSeconds(), TimeUnit.SECONDS); |
||||
} |
||||
} |
@ -0,0 +1,25 @@
|
||||
package com.alibaba.easytools.spring.cache.wrapper; |
||||
|
||||
import java.io.Serializable; |
||||
|
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* 缓存包装 |
||||
* |
||||
* @author qiuyuyu |
||||
* @date 2022/03/08 |
||||
*/ |
||||
@Data |
||||
public class CacheWrapper<T> implements Serializable { |
||||
private static final long serialVersionUID = 1L; |
||||
/** |
||||
* 超时时间戳 |
||||
*/ |
||||
private Long expireTimeMillis; |
||||
|
||||
/** |
||||
* 数据 |
||||
*/ |
||||
private T data; |
||||
} |
@ -0,0 +1,95 @@
|
||||
package com.alibaba.easytools.spring.exception; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.validation.ConstraintViolationException; |
||||
|
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
import com.alibaba.easytools.base.excption.SystemException; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
import com.alibaba.easytools.spring.exception.convertor.ExceptionConvertorUtils; |
||||
import com.alibaba.fastjson.JSON; |
||||
|
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.catalina.connector.ClientAbortException; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.converter.HttpMessageNotReadableException; |
||||
import org.springframework.validation.BindException; |
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException; |
||||
import org.springframework.web.HttpMediaTypeNotSupportedException; |
||||
import org.springframework.web.HttpRequestMethodNotSupportedException; |
||||
import org.springframework.web.bind.MethodArgumentNotValidException; |
||||
import org.springframework.web.bind.MissingRequestHeaderException; |
||||
import org.springframework.web.bind.MissingServletRequestParameterException; |
||||
import org.springframework.web.bind.annotation.ControllerAdvice; |
||||
import org.springframework.web.bind.annotation.ExceptionHandler; |
||||
import org.springframework.web.bind.annotation.ResponseBody; |
||||
import org.springframework.web.bind.annotation.ResponseStatus; |
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; |
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException; |
||||
import org.springframework.web.multipart.MultipartException; |
||||
|
||||
/** |
||||
* 拦截Controller异常 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
@ControllerAdvice |
||||
@Slf4j |
||||
public class EasyControllerExceptionHandler { |
||||
|
||||
/** |
||||
* 业务异常 |
||||
* |
||||
* @param request request |
||||
* @param exception exception |
||||
* @return return |
||||
*/ |
||||
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class, IllegalArgumentException.class, |
||||
MissingServletRequestParameterException.class, MethodArgumentTypeMismatchException.class, |
||||
BusinessException.class, MaxUploadSizeExceededException.class, ClientAbortException.class, |
||||
HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, |
||||
MultipartException.class, MissingRequestHeaderException.class, HttpMediaTypeNotSupportedException.class, |
||||
ConstraintViolationException.class, HttpMessageNotReadableException.class}) |
||||
@ResponseStatus(value = HttpStatus.OK) |
||||
@ResponseBody |
||||
public ActionResult handleBusinessException(HttpServletRequest request, Exception exception) { |
||||
ActionResult result = ExceptionConvertorUtils.convert(exception); |
||||
log.info("发生业务异常{}:{}", request.getRequestURI(), result, exception); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* 系统异常 |
||||
* |
||||
* @param request request |
||||
* @param exception exception |
||||
* @return return |
||||
*/ |
||||
@ExceptionHandler({SystemException.class}) |
||||
@ResponseStatus(value = HttpStatus.OK) |
||||
@ResponseBody |
||||
public ActionResult handleSystemException(HttpServletRequest request, Exception exception) { |
||||
ActionResult result = ExceptionConvertorUtils.convert(exception); |
||||
log.error("发生业务异常{}:{}", request.getRequestURI(), result, exception); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* 未知异常 需要人工介入查看日志 |
||||
* |
||||
* @param request request |
||||
* @param exception exception |
||||
* @return return |
||||
*/ |
||||
@ExceptionHandler(Exception.class) |
||||
@ResponseStatus(value = HttpStatus.OK) |
||||
@ResponseBody |
||||
public ActionResult handledException(HttpServletRequest request, Exception exception) { |
||||
ActionResult result = ExceptionConvertorUtils.convert(exception); |
||||
log.error("发生未知异常{}:{}:{},请求参数:{}", request.getRequestURI(), |
||||
ExceptionConvertorUtils.buildHeaderString(request), result, |
||||
JSON.toJSONString(request.getParameterMap()), exception); |
||||
return result; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,20 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
import org.springframework.validation.BindException; |
||||
|
||||
/** |
||||
* BindException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class BindExceptionConvertor implements ExceptionConvertor<BindException> { |
||||
|
||||
@Override |
||||
public ActionResult convert(BindException exception) { |
||||
String message = ExceptionConvertorUtils.buildMessage(exception.getBindingResult()); |
||||
return ActionResult.fail(CommonErrorEnum.PARAM_ERROR, message); |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
/** |
||||
* BusinessException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class BusinessExceptionConvertor implements ExceptionConvertor<BusinessException> { |
||||
|
||||
@Override |
||||
public ActionResult convert(BusinessException exception) { |
||||
return ActionResult.fail(exception.getCode(), exception.getMessage()); |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import javax.validation.ConstraintViolationException; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
/** |
||||
* ConstraintViolationException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class ConstraintViolationExceptionConvertor implements ExceptionConvertor<ConstraintViolationException> { |
||||
|
||||
@Override |
||||
public ActionResult convert(ConstraintViolationException exception) { |
||||
String message = ExceptionConvertorUtils.buildMessage(exception); |
||||
return ActionResult.fail(CommonErrorEnum.PARAM_ERROR, message); |
||||
} |
||||
} |
@ -0,0 +1,23 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
/** |
||||
* 默认的异常处理 |
||||
* 直接抛出系统异常 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class DefaultExceptionConvertor implements ExceptionConvertor<Throwable> { |
||||
|
||||
@Override |
||||
public ActionResult convert(Throwable exception) { |
||||
if (exception instanceof BusinessException) { |
||||
BusinessException businessException = (BusinessException)exception; |
||||
return ActionResult.fail(businessException.getCode(), businessException.getMessage()); |
||||
} |
||||
return ActionResult.fail(CommonErrorEnum.COMMON_SYSTEM_ERROR); |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
/** |
||||
* 异常转换器 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public interface ExceptionConvertor<T extends Throwable> { |
||||
|
||||
/** |
||||
* 转换异常 |
||||
* |
||||
* @param exception |
||||
* @return |
||||
*/ |
||||
ActionResult convert(T exception); |
||||
} |
@ -0,0 +1,164 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import java.util.Enumeration; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.validation.ConstraintViolation; |
||||
import javax.validation.ConstraintViolationException; |
||||
|
||||
import com.alibaba.easytools.base.constant.SymbolConstant; |
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
import com.google.common.collect.Maps; |
||||
import org.springframework.http.converter.HttpMessageNotReadableException; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.validation.BindException; |
||||
import org.springframework.validation.BindingResult; |
||||
import org.springframework.validation.FieldError; |
||||
import org.springframework.validation.ObjectError; |
||||
import org.springframework.web.HttpRequestMethodNotSupportedException; |
||||
import org.springframework.web.bind.MethodArgumentNotValidException; |
||||
import org.springframework.web.bind.MissingServletRequestParameterException; |
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; |
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException; |
||||
|
||||
/** |
||||
* 转换工具类 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class ExceptionConvertorUtils { |
||||
|
||||
/** |
||||
* 所有的异常处理转换器 |
||||
*/ |
||||
public static final Map<Class<?>, ExceptionConvertor> EXCEPTION_CONVERTOR_MAP = Maps.newHashMap(); |
||||
|
||||
static { |
||||
EXCEPTION_CONVERTOR_MAP.put(MethodArgumentNotValidException.class, |
||||
new MethodArgumentNotValidExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(BindException.class, new BindExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(BusinessException.class, new BusinessExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(MissingServletRequestParameterException.class, new ParamExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(IllegalArgumentException.class, new ParamExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(MethodArgumentTypeMismatchException.class, |
||||
new MethodArgumentTypeMismatchExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(MaxUploadSizeExceededException.class, |
||||
new MaxUploadSizeExceededExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(HttpRequestMethodNotSupportedException.class, new BusinessExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(ConstraintViolationException.class, new ConstraintViolationExceptionConvertor()); |
||||
EXCEPTION_CONVERTOR_MAP.put(HttpMessageNotReadableException.class, |
||||
new ParamExceptionConvertor()); |
||||
} |
||||
|
||||
/** |
||||
* 默认转换器 |
||||
*/ |
||||
public static ExceptionConvertor DEFAULT_EXCEPTION_CONVERTOR = new DefaultExceptionConvertor(); |
||||
|
||||
/** |
||||
* 提取ConstraintViolationException中的错误消息 |
||||
* |
||||
* @param e |
||||
* @return |
||||
*/ |
||||
public static String buildMessage(ConstraintViolationException e) { |
||||
if (e == null || CollectionUtils.isEmpty(e.getConstraintViolations())) { |
||||
return null; |
||||
} |
||||
int index = 1; |
||||
StringBuilder msg = new StringBuilder(); |
||||
msg.append("请检查以下信息:"); |
||||
for (ConstraintViolation<?> constraintViolation : e.getConstraintViolations()) { |
||||
msg.append(index++); |
||||
// 得到错误消息
|
||||
msg.append(SymbolConstant.DOT); |
||||
msg.append(" 字段("); |
||||
msg.append(constraintViolation.getPropertyPath()); |
||||
msg.append(")传入的值为:\""); |
||||
msg.append(constraintViolation.getInvalidValue()); |
||||
msg.append("\",校验失败,原因是:"); |
||||
msg.append(constraintViolation.getMessage()); |
||||
msg.append(SymbolConstant.SEMICOLON); |
||||
} |
||||
return msg.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 提取BindingResult中的错误消息 |
||||
* |
||||
* @param result |
||||
* @return |
||||
*/ |
||||
public static String buildMessage(BindingResult result) { |
||||
List<ObjectError> errors = result.getAllErrors(); |
||||
if (CollectionUtils.isEmpty(errors)) { |
||||
return null; |
||||
} |
||||
|
||||
int index = 1; |
||||
StringBuilder msg = new StringBuilder(); |
||||
msg.append("请检查以下信息:"); |
||||
for (ObjectError e : errors) { |
||||
msg.append(index++); |
||||
// 得到错误消息
|
||||
msg.append(SymbolConstant.DOT); |
||||
msg.append(" "); |
||||
msg.append("字段("); |
||||
msg.append(e.getObjectName()); |
||||
if (e instanceof FieldError) { |
||||
FieldError fieldError = (FieldError)e; |
||||
msg.append(SymbolConstant.DOT); |
||||
msg.append(fieldError.getField()); |
||||
} |
||||
msg.append(")"); |
||||
if (e instanceof FieldError) { |
||||
FieldError fieldError = (FieldError)e; |
||||
msg.append("传入的值为:\""); |
||||
msg.append(fieldError.getRejectedValue()); |
||||
msg.append("\","); |
||||
} |
||||
msg.append("校验失败,原因是:"); |
||||
msg.append(e.getDefaultMessage()); |
||||
msg.append(SymbolConstant.SEMICOLON); |
||||
} |
||||
return msg.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 拼接头的日志信息 |
||||
* |
||||
* @param request |
||||
* @return |
||||
*/ |
||||
public static String buildHeaderString(HttpServletRequest request) { |
||||
StringBuilder stringBuilder = new StringBuilder(); |
||||
Enumeration<String> headerNames = request.getHeaderNames(); |
||||
while (headerNames.hasMoreElements()) { |
||||
String headName = headerNames.nextElement(); |
||||
stringBuilder.append(headName); |
||||
stringBuilder.append(SymbolConstant.COLON); |
||||
stringBuilder.append(request.getHeader(headName)); |
||||
stringBuilder.append(SymbolConstant.COMMA); |
||||
} |
||||
return stringBuilder.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 转换结果 |
||||
* |
||||
* @param exception |
||||
* @return |
||||
*/ |
||||
public static ActionResult convert(Throwable exception) { |
||||
ExceptionConvertor exceptionConvertor = EXCEPTION_CONVERTOR_MAP.get(exception.getClass()); |
||||
if (exceptionConvertor == null) { |
||||
exceptionConvertor = DEFAULT_EXCEPTION_CONVERTOR; |
||||
} |
||||
return exceptionConvertor.convert(exception); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,19 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException; |
||||
|
||||
/** |
||||
* MaxUploadSizeExceededException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class MaxUploadSizeExceededExceptionConvertor implements ExceptionConvertor<MaxUploadSizeExceededException> { |
||||
|
||||
@Override |
||||
public ActionResult convert(MaxUploadSizeExceededException exception) { |
||||
return ActionResult.fail(CommonErrorEnum.MAX_UPLOAD_SIZE); |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException; |
||||
|
||||
/** |
||||
* MethodArgumentNotValidException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class MethodArgumentNotValidExceptionConvertor implements ExceptionConvertor<MethodArgumentNotValidException> { |
||||
|
||||
@Override |
||||
public ActionResult convert(MethodArgumentNotValidException exception) { |
||||
String message = ExceptionConvertorUtils.buildMessage(exception.getBindingResult()); |
||||
return ActionResult.fail(CommonErrorEnum.PARAM_ERROR, message); |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; |
||||
|
||||
/** |
||||
* MethodArgumentTypeMismatchException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class MethodArgumentTypeMismatchExceptionConvertor |
||||
implements ExceptionConvertor<MethodArgumentTypeMismatchException> { |
||||
|
||||
@Override |
||||
public ActionResult convert(MethodArgumentTypeMismatchException exception) { |
||||
return ActionResult.fail(CommonErrorEnum.PARAM_ERROR, "请输入正确的数据格式"); |
||||
} |
||||
} |
@ -0,0 +1,21 @@
|
||||
package com.alibaba.easytools.spring.exception.convertor; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.wrapper.result.ActionResult; |
||||
|
||||
/** |
||||
* 参数异常 目前包括 |
||||
* ConstraintViolationException |
||||
* MissingServletRequestParameterException |
||||
* IllegalArgumentException |
||||
* HttpMessageNotReadableException |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public class ParamExceptionConvertor implements ExceptionConvertor<Throwable> { |
||||
|
||||
@Override |
||||
public ActionResult convert(Throwable exception) { |
||||
return ActionResult.fail(CommonErrorEnum.PARAM_ERROR, exception.getMessage()); |
||||
} |
||||
} |
@ -0,0 +1,56 @@
|
||||
package com.alibaba.easytools.spring.oss; |
||||
|
||||
import com.aliyun.oss.model.CannedAccessControlList; |
||||
import lombok.Data; |
||||
|
||||
/** |
||||
* 计算请求签名对象 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
@Data |
||||
public class CalculatePostSignature { |
||||
|
||||
/** |
||||
* 请求的host |
||||
*/ |
||||
private String host; |
||||
|
||||
/** |
||||
* 请求的策略 |
||||
*/ |
||||
private String policy; |
||||
|
||||
/** |
||||
* 授权id |
||||
*/ |
||||
private String accessId; |
||||
|
||||
/** |
||||
* 授权签名 |
||||
*/ |
||||
private String signature; |
||||
|
||||
/** |
||||
* 过期时间 时间戳 |
||||
*/ |
||||
private Long expire; |
||||
|
||||
/** |
||||
* 文件上传的key |
||||
*/ |
||||
private String key; |
||||
|
||||
|
||||
/** |
||||
* 文件连接 |
||||
*/ |
||||
private String url; |
||||
|
||||
/** |
||||
* 权限控制 |
||||
* |
||||
* @see CannedAccessControlList |
||||
*/ |
||||
private String objectAcl; |
||||
} |
@ -0,0 +1,45 @@
|
||||
package com.alibaba.easytools.spring.oss; |
||||
|
||||
import com.aliyun.oss.model.CannedAccessControlList; |
||||
import lombok.Getter; |
||||
|
||||
/** |
||||
* 默认的oss枚举 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
@Getter |
||||
public enum DefaultOssKindEnum implements OssKindEnum { |
||||
/** |
||||
* 默认 |
||||
*/ |
||||
DEFAULT("default", CannedAccessControlList.Private, null), |
||||
|
||||
// 分号
|
||||
; |
||||
|
||||
/** |
||||
* 样式的格式 |
||||
*/ |
||||
final String code; |
||||
/** |
||||
* 这里只支持 私有(每次访问都要授权 比如身份证)和共有读(获得连接了可以永久访问) |
||||
*/ |
||||
final CannedAccessControlList objectAcl; |
||||
|
||||
/** |
||||
* 默认的一些处理 目前主要是样式处理 |
||||
*/ |
||||
final String process; |
||||
|
||||
DefaultOssKindEnum(String code, CannedAccessControlList objectAcl, String process) { |
||||
this.code = code; |
||||
this.objectAcl = objectAcl; |
||||
this.process = process; |
||||
} |
||||
|
||||
@Override |
||||
public String getDescription() { |
||||
return this.code; |
||||
} |
||||
} |
@ -0,0 +1,826 @@
|
||||
package com.alibaba.easytools.spring.oss; |
||||
|
||||
import java.io.ByteArrayInputStream; |
||||
import java.io.File; |
||||
import java.io.InputStream; |
||||
import java.io.UnsupportedEncodingException; |
||||
import java.net.URL; |
||||
import java.net.URLEncoder; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.time.Duration; |
||||
import java.util.Calendar; |
||||
import java.util.Date; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.UUID; |
||||
|
||||
import com.alibaba.easytools.base.constant.SymbolConstant; |
||||
import com.alibaba.easytools.base.enums.oss.BaseOssKindEnum; |
||||
import com.alibaba.easytools.base.enums.oss.OssObjectAclEnum; |
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.excption.SystemException; |
||||
import com.alibaba.easytools.base.wrapper.ObjectWrapper; |
||||
import com.alibaba.easytools.common.util.EasyEnumUtils; |
||||
import com.alibaba.easytools.common.util.EasyOptionalUtils; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
|
||||
import cn.hutool.core.date.DateUtil; |
||||
import com.aliyun.oss.OSSClient; |
||||
import com.aliyun.oss.OSSErrorCode; |
||||
import com.aliyun.oss.OSSException; |
||||
import com.aliyun.oss.common.utils.BinaryUtil; |
||||
import com.aliyun.oss.common.utils.HttpUtil; |
||||
import com.aliyun.oss.internal.Mimetypes; |
||||
import com.aliyun.oss.internal.OSSUtils; |
||||
import com.aliyun.oss.internal.RequestParameters; |
||||
import com.aliyun.oss.model.CannedAccessControlList; |
||||
import com.aliyun.oss.model.GeneratePresignedUrlRequest; |
||||
import com.aliyun.oss.model.GetObjectRequest; |
||||
import com.aliyun.oss.model.MatchMode; |
||||
import com.aliyun.oss.model.ObjectMetadata; |
||||
import com.aliyun.oss.model.PolicyConditions; |
||||
import com.dtflys.forest.Forest; |
||||
import com.dtflys.forest.backend.ContentType; |
||||
import lombok.Data; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.collections4.MapUtils; |
||||
import org.apache.commons.lang3.BooleanUtils; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.jsoup.Jsoup; |
||||
import org.jsoup.nodes.Document; |
||||
import org.jsoup.nodes.Element; |
||||
import org.jsoup.select.Elements; |
||||
|
||||
import static com.aliyun.oss.internal.OSSConstants.DEFAULT_CHARSET_NAME; |
||||
|
||||
/** |
||||
* 自定义OSS的客户端 |
||||
* |
||||
* @author 是仪 |
||||
**/ |
||||
@Slf4j |
||||
@Data |
||||
public class EasyOssClient { |
||||
|
||||
/** |
||||
* 最大为10G |
||||
*/ |
||||
public static long maxContentLength = 10 * 1024 * 1024 * 1024L; |
||||
/** |
||||
* 最小为0 |
||||
*/ |
||||
public static long minContentLength = 0L; |
||||
|
||||
/** |
||||
* 超时时间 |
||||
*/ |
||||
public static Duration defaultTimeout = Duration.ofDays(1); |
||||
|
||||
public static Duration downloadConnectTimeout = Duration.ofMinutes(1); |
||||
public static Duration downloadReadTimeout = Duration.ofHours(1); |
||||
|
||||
/** |
||||
* 有些图片不规范 流也可以下载 |
||||
*/ |
||||
private static final String STREAM = "application/octet-stream"; |
||||
/** |
||||
* html类型 |
||||
*/ |
||||
private static final String HTML = "text/html"; |
||||
|
||||
/** |
||||
* https前缀 |
||||
*/ |
||||
private static final String HTTPS_PREFIX = "https:"; |
||||
|
||||
/** |
||||
* 如果不传kind 全部放到这里 |
||||
*/ |
||||
private static final String DEFAULT_KIND = "default"; |
||||
/** |
||||
* oss枚举的类 |
||||
*/ |
||||
private Class<? extends BaseOssKindEnum> ossKindEnumClass; |
||||
|
||||
/** |
||||
* 读文件的oss |
||||
*/ |
||||
private OSSClient ossClient; |
||||
|
||||
private String bucketName; |
||||
private String endpoint; |
||||
private String baseUrl; |
||||
|
||||
/** |
||||
* bucketName+endpoint |
||||
*/ |
||||
private String ossDomain; |
||||
|
||||
/** |
||||
* 绑定的域名 类似于 oss.alibaba.com 不要带任何前缀 |
||||
* 可以为空 |
||||
* 如果为空 则会生成 bucketName+endpoint 的连接 |
||||
* 如果不为空 则会生成 domain 的连接 |
||||
*/ |
||||
private String domain; |
||||
|
||||
/** |
||||
* 多媒体项目 |
||||
*/ |
||||
private String immProject; |
||||
|
||||
/** |
||||
* 是否需要替换域名 |
||||
* 生成私有的连接的时候 返回的是 bucketName+endpoint |
||||
* 这个时候 我们要把替换成 domain ,当然只有domain!=bucketName+endpoint 的情况 |
||||
*/ |
||||
private Boolean needReplaceDomain; |
||||
|
||||
/** |
||||
* accessKeyId |
||||
* 目前只有生成签名的时候需要 |
||||
*/ |
||||
private String accessKeyId; |
||||
|
||||
public EasyOssClient(OSSClient ossClient, String bucketName, String endpoint, |
||||
Class<? extends BaseOssKindEnum> ossKindEnumClass, String domain) { |
||||
this(ossClient, bucketName, endpoint, ossKindEnumClass, domain, null); |
||||
} |
||||
|
||||
public EasyOssClient(OSSClient ossClient, String bucketName, String endpoint, |
||||
Class<? extends BaseOssKindEnum> ossKindEnumClass, String domain, String accessKeyId) { |
||||
this.bucketName = bucketName; |
||||
this.endpoint = endpoint; |
||||
this.ossKindEnumClass = ossKindEnumClass; |
||||
this.ossDomain = bucketName + "." + endpoint; |
||||
this.domain = domain; |
||||
this.needReplaceDomain = this.domain != null; |
||||
// 为空 则拼接domain
|
||||
if (this.domain == null) { |
||||
this.domain = ossDomain; |
||||
} |
||||
this.baseUrl = "https://" + this.domain + "/"; |
||||
this.ossClient = ossClient; |
||||
this.accessKeyId = accessKeyId; |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file) { |
||||
return put(file, null, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file, String kind) { |
||||
return put(file, kind, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则根据file的名字下载 如果还为空 则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 为空则根据file的名字的后缀区分 如果还为空 则默认流application/octet-stream |
||||
* @param key 服务器的唯一key |
||||
* @param objectAcl 对象权限控制 默认私有的 |
||||
* @param contentDisposition 是否需要把文件名设置到contentDisposition里面 |
||||
* 这里默认不设置 |
||||
* 原因是chrome 在设置了contentDisposition的情况下 有可能是导致图片无法展示(特定的文件名),这个就很离谱,但是实际真的遇到了 |
||||
* 所以图片的情况下慎用 contentDisposition,除非自己指定名字,纯中文或者英文问题应该不大 |
||||
* 其他情况下不需要展示图片的可以直接设置为true |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
* @see CannedAccessControlList |
||||
*/ |
||||
public String put(File file, String kind, String fileName, String contentType, String key, |
||||
CannedAccessControlList objectAcl, Boolean contentDisposition) { |
||||
if (file == null) { |
||||
throw new BusinessException("文件不能为空!"); |
||||
} |
||||
if (!file.isFile()) { |
||||
throw new BusinessException("不能上传目录!"); |
||||
} |
||||
// 构建用来存储在服务器是上面的唯一目录值
|
||||
if (StringUtils.isBlank(key)) { |
||||
key = buildKey(kind, fileName); |
||||
} |
||||
// 构建用户下载的时候显示的文件名
|
||||
String fileNameDownload = buildFileName(file, fileName); |
||||
ossClient.putObject(bucketName, key, file, |
||||
buildObjectMetadata(fileNameDownload, contentType, objectAcl, contentDisposition, kind)); |
||||
return key; |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(InputStream inputStream) { |
||||
return put(inputStream, null, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(InputStream inputStream, String kind) { |
||||
return put(inputStream, kind, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 默认流application/octet-stream |
||||
* @param key 生成服务器文件存储的key (空的场景,需要生成) |
||||
* @param objectAcl 对象权限控制 默认私有的 |
||||
* @param contentDisposition 是否需要把文件名设置到contentDisposition里面 |
||||
* 这里默认不设置 |
||||
* 原因是chrome 在设置了contentDisposition的情况下 有可能是导致图片无法展示(特定的文件名),这个就很离谱,但是实际真的遇到了 |
||||
* 所以图片的情况下慎用 contentDisposition,除非自己指定名字,纯中文或者英文问题应该不大 |
||||
* 其他情况下不需要展示图片的可以直接设置为true |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
* @see CannedAccessControlList |
||||
*/ |
||||
public String put(InputStream inputStream, String kind, String fileName, String contentType, String key, |
||||
CannedAccessControlList objectAcl, Boolean contentDisposition) { |
||||
if (inputStream == null) { |
||||
throw new BusinessException("文件流不能为空!"); |
||||
} |
||||
// 构建用来存储在服务器是上面的唯一目录值
|
||||
if (StringUtils.isBlank(key)) { |
||||
key = buildKey(kind, fileName); |
||||
} |
||||
// 构建用户下载的时候显示的文件名
|
||||
String fileNameDownload = buildFileName(fileName); |
||||
// 上传文件
|
||||
ossClient.putObject(bucketName, key, inputStream, |
||||
buildObjectMetadata(fileNameDownload, contentType, objectAcl, contentDisposition, kind)); |
||||
return key; |
||||
} |
||||
|
||||
/** |
||||
* 根据url上传文件 |
||||
* |
||||
* @param url 文件地址 不能为空 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(String url) { |
||||
return put(url, null, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据url上传文件 |
||||
* |
||||
* @param url 文件地址 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(String url, String kind) { |
||||
return put(url, kind, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据url上传文件 |
||||
* |
||||
* @param url 文件地址 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 默认流application/octet-stream |
||||
* @param key 生成服务器文件存储的key (空的场景,需要生成) |
||||
* @param objectAcl 对象权限控制 默认私有的 |
||||
* @param contentDisposition 是否需要把文件名设置到contentDisposition里面 |
||||
* 这里默认不设置 |
||||
* 原因是chrome 在设置了contentDisposition的情况下 有可能是导致图片无法展示(特定的文件名),这个就很离谱,但是实际真的遇到了 |
||||
* 所以图片的情况下慎用 contentDisposition,除非自己指定名字,纯中文或者英文问题应该不大 |
||||
* 其他情况下不需要展示图片的可以直接设置为true |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
* @see CannedAccessControlList |
||||
*/ |
||||
public String put(String url, String kind, String fileName, String contentType, String key, |
||||
CannedAccessControlList objectAcl, Boolean contentDisposition) { |
||||
if (url == null) { |
||||
throw new BusinessException("url不能为空!"); |
||||
} |
||||
|
||||
BaseOssKindEnum kindEnum = EasyEnumUtils.getEnum(ossKindEnumClass, kind); |
||||
|
||||
ObjectWrapper<String> finalKey = ObjectWrapper.ofNull(); |
||||
|
||||
// 获取图片信息
|
||||
Forest.get(url) |
||||
.setDownloadFile(true) |
||||
.connectTimeout(downloadConnectTimeout) |
||||
.readTimeout(downloadReadTimeout) |
||||
.onSuccess((data, request, response) -> { |
||||
ContentType responseContentType = response.getContentType(); |
||||
String finalContentType = contentType; |
||||
if (finalContentType == null && responseContentType != null) { |
||||
finalContentType = responseContentType.toString(); |
||||
} |
||||
|
||||
// 在html & 为空的情况下 默认使用文件流
|
||||
// 攻击者可以上传恶意的HTML、JS等文件,受害者访问之后可能会绕过浏览器的同源限制或者是安全侧的一些域名白名单限制,最终导致CSRF、XSS等安全漏洞。
|
||||
if (StringUtils.isBlank(finalContentType) || HTML.equalsIgnoreCase(finalContentType)) { |
||||
finalContentType = STREAM; |
||||
} |
||||
|
||||
// 文件名称
|
||||
String finalFileName = fileName; |
||||
if (finalFileName == null) { |
||||
// 获取文件名
|
||||
finalFileName = response.getFilename(); |
||||
if (StringUtils.isBlank(finalFileName)) { |
||||
//最后的后缀
|
||||
String lastUrl = url.substring(url.lastIndexOf('/') + 1); |
||||
int index = lastUrl.indexOf('?'); |
||||
if (index > 0) { |
||||
finalFileName = lastUrl.substring(0, index); |
||||
} else { |
||||
finalFileName = lastUrl; |
||||
} |
||||
} |
||||
// 没有后缀
|
||||
if (!finalFileName.contains(".") && responseContentType != null |
||||
&& responseContentType.getSubType() != null) { |
||||
finalFileName += "." + responseContentType.getSubType(); |
||||
} |
||||
} |
||||
|
||||
// 构建用户下载的时候显示的文件名
|
||||
String fileNameDownload = buildFileName(fileName); |
||||
|
||||
// 构建用来存储在服务器是上面的唯一目录值
|
||||
finalKey.set(buildKey(EasyOptionalUtils.mapTo(kindEnum, BaseOssKindEnum::getCode), |
||||
finalFileName)); |
||||
|
||||
// 上传文件
|
||||
try { |
||||
// 上传文件
|
||||
ossClient.putObject(bucketName, finalKey.get(), new ByteArrayInputStream(response.getByteArray()), |
||||
buildObjectMetadata(fileNameDownload, finalContentType, objectAcl, contentDisposition, kind)); |
||||
} catch (Exception e) { |
||||
throw BusinessException.of(CommonErrorEnum.FAILED_TO_UPLOAD_FILE, "上传到文件服务器失败:" + url, e); |
||||
} |
||||
}) |
||||
.onError((exception, request, response) -> { |
||||
throw BusinessException.of(CommonErrorEnum.FAILED_TO_UPLOAD_FILE, "下载文件失败:" + url, exception); |
||||
}) |
||||
.executeAsByteArray(); |
||||
return finalKey.get(); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取文件流 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public InputStream get(String key) { |
||||
return get(key, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取文件流 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param process 格式转换 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public InputStream get(String key, String process) { |
||||
return getOssObject(key, process).getObjectContent(); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取OSS文件 如果需要获取文件名之类的请用此方法 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public OssObject getOssObject(String key) { |
||||
return getOssObject(key, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取OSS文件 如果需要获取文件名之类的请用此方法 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param process 格式转换 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public OssObject getOssObject(String key, String process) { |
||||
if (StringUtils.isBlank(key)) { |
||||
throw new BusinessException("获取文件key 不能为空"); |
||||
} |
||||
try { |
||||
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key); |
||||
if (StringUtils.isNotBlank(process)) { |
||||
getObjectRequest.setProcess(process); |
||||
} |
||||
return OssObject.buildWithAliyunOssObject(ossClient.getObject(getObjectRequest)); |
||||
} catch (OSSException e) { |
||||
String errorCode = e.getErrorCode(); |
||||
if (OSSErrorCode.NO_SUCH_KEY.equals(errorCode)) { |
||||
throw new BusinessException("找不到指定文件"); |
||||
} |
||||
throw new BusinessException(e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key) { |
||||
return getUrl(key, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param process 格式转换 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key, String process) { |
||||
return getUrl(key, null, process); |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param duration 超时时间 可以为空 共有读的不管传不传 都是永久的 私有的不传默认1天 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key, Duration duration) { |
||||
return getUrl(key, duration, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param duration 超时时间 可以为空 共有读的不管传不传 都是永久的 私有的不传默认1天 |
||||
* @param process 格式转换 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key, Duration duration, String process) { |
||||
if (StringUtils.isBlank(key)) { |
||||
return null; |
||||
} |
||||
String kind = StringUtils.split(key, SymbolConstant.SLASH)[0]; |
||||
BaseOssKindEnum kindEnum = EasyEnumUtils.getEnum(ossKindEnumClass, kind); |
||||
|
||||
// 使用默认的样式处理
|
||||
if (process == null) { |
||||
process = kindEnum.getProcess(); |
||||
} |
||||
|
||||
// 代表是公有读 直接拼接连接即可
|
||||
if (kindEnum != null && kindEnum.getOssObjectAcl() == OssObjectAclEnum.PUBLIC_READ) { |
||||
Map<String, String> params = new LinkedHashMap<>(); |
||||
if (StringUtils.isNotBlank(process)) { |
||||
params.put(RequestParameters.SUBRESOURCE_PROCESS, process); |
||||
} |
||||
if (MapUtils.isEmpty(params)) { |
||||
return baseUrl + OSSUtils.determineResourcePath(bucketName, key, |
||||
ossClient.getClientConfiguration().isSLDEnabled()); |
||||
} else { |
||||
String queryString = HttpUtil.paramToQueryString(params, DEFAULT_CHARSET_NAME); |
||||
return baseUrl + OSSUtils.determineResourcePath(bucketName, key, |
||||
ossClient.getClientConfiguration().isSLDEnabled()) + "?" + queryString; |
||||
} |
||||
} |
||||
|
||||
// 设置默认超时时间
|
||||
if (duration == null) { |
||||
duration = defaultTimeout; |
||||
} |
||||
|
||||
Date expirationDate = new Date(System.currentTimeMillis() + duration.getSeconds() * 1000); |
||||
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, key); |
||||
generatePresignedUrlRequest.setExpiration(expirationDate); |
||||
if (StringUtils.isNotBlank(process)) { |
||||
generatePresignedUrlRequest.setProcess(process); |
||||
} |
||||
URL url = ossClient.generatePresignedUrl(generatePresignedUrlRequest); |
||||
String urlString = url.toString(); |
||||
if (needReplaceDomain) { |
||||
urlString = urlString.replaceFirst(ossDomain, domain); |
||||
} |
||||
return urlString; |
||||
} |
||||
|
||||
/** |
||||
* 根据url生成key |
||||
* |
||||
* @param url 一个可以访问的url |
||||
* @return 唯一key |
||||
*/ |
||||
public String urlToKey(String url) { |
||||
if (StringUtils.isBlank(url)) { |
||||
throw new BusinessException("url转key url不能为空"); |
||||
} |
||||
try { |
||||
URL urlObject = new URL(url); |
||||
String path = urlObject.getPath(); |
||||
return path.substring(1); |
||||
} catch (Exception e) { |
||||
throw new BusinessException("请传入正确的url"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据key 移除文件 |
||||
* |
||||
* @param key |
||||
*/ |
||||
public void delete(String key) { |
||||
ossClient.deleteObject(bucketName, key); |
||||
} |
||||
|
||||
/** |
||||
* 构建 请求参数 |
||||
* |
||||
* @param fileNameDownload |
||||
* @param contentType |
||||
* @return |
||||
*/ |
||||
private ObjectMetadata buildObjectMetadata(String fileNameDownload, String contentType, |
||||
CannedAccessControlList objectAcl, Boolean contentDisposition, String kind) { |
||||
if (objectAcl == null) { |
||||
BaseOssKindEnum kindEnum = EasyEnumUtils.getEnum(ossKindEnumClass, kind); |
||||
if (kindEnum != null && kindEnum.getOssObjectAcl() == OssObjectAclEnum.PUBLIC_READ) { |
||||
objectAcl = CannedAccessControlList.PublicRead; |
||||
} else { |
||||
objectAcl = CannedAccessControlList.Private; |
||||
} |
||||
} |
||||
if (objectAcl != CannedAccessControlList.Private && objectAcl != CannedAccessControlList.PublicRead) { |
||||
throw new SystemException("目前仅仅支持私有和共有读类型。"); |
||||
} |
||||
ObjectMetadata objectMetadata = new ObjectMetadata(); |
||||
if (StringUtils.isBlank(contentType)) { |
||||
contentType = Mimetypes.getInstance().getMimetype(fileNameDownload); |
||||
} |
||||
objectMetadata.setContentType(contentType); |
||||
// 判断是否要设置 contentDisposition
|
||||
if (BooleanUtils.isTrue(contentDisposition)) { |
||||
try { |
||||
// 防止中文乱码
|
||||
fileNameDownload = URLEncoder.encode(fileNameDownload, StandardCharsets.UTF_8.name()).replaceAll("\\+", |
||||
"%20"); |
||||
} catch (UnsupportedEncodingException e) { |
||||
throw new SystemException(CommonErrorEnum.COMMON_SYSTEM_ERROR, "不支持的字符编码", e); |
||||
} |
||||
objectMetadata.setContentDisposition("filename*=utf-8''" + fileNameDownload); |
||||
} |
||||
// 默认私有
|
||||
objectMetadata.setObjectAcl(objectAcl); |
||||
return objectMetadata; |
||||
} |
||||
|
||||
/** |
||||
* 用来 下载时 显示给用户的名称 |
||||
* |
||||
* @param fileName |
||||
* @return |
||||
*/ |
||||
private String buildFileName(String fileName) { |
||||
if (StringUtils.isNotBlank(fileName)) { |
||||
return fileName; |
||||
} |
||||
return UUID.randomUUID().toString(); |
||||
} |
||||
|
||||
/** |
||||
* 用来 下载时 显示给用户的名称 |
||||
* |
||||
* @param file |
||||
* @param fileName |
||||
* @return |
||||
*/ |
||||
private String buildFileName(File file, String fileName) { |
||||
if (StringUtils.isNotBlank(fileName)) { |
||||
return fileName; |
||||
} |
||||
fileName = file.getName(); |
||||
if (StringUtils.isNotBlank(fileName)) { |
||||
return fileName; |
||||
} |
||||
return UUID.randomUUID().toString(); |
||||
} |
||||
|
||||
/** |
||||
* 生成服务器文件存储的key 统一看不到名字 看不到后缀 |
||||
* 有些场景需要提前生成唯一存储key |
||||
* |
||||
* @param kind |
||||
* @param fileName |
||||
* @return |
||||
*/ |
||||
public String buildKey(String kind, String fileName) { |
||||
if (StringUtils.isBlank(kind)) { |
||||
kind = DEFAULT_KIND; |
||||
} |
||||
StringBuilder key = new StringBuilder(); |
||||
key.append(kind); |
||||
key.append("/"); |
||||
Calendar calendar = Calendar.getInstance(); |
||||
key.append(calendar.get(Calendar.YEAR)); |
||||
key.append("/"); |
||||
key.append(StringUtils.leftPad(Integer.toString(calendar.get(Calendar.MONTH) + 1), 2, "0")); |
||||
key.append("/"); |
||||
key.append(UUID.randomUUID()); |
||||
|
||||
Optional.ofNullable(fileName).map(fileNameData -> { |
||||
int lastDotIndex = fileNameData.lastIndexOf(SymbolConstant.DOT); |
||||
if (lastDotIndex >= 0) { |
||||
return fileNameData.substring(lastDotIndex); |
||||
} |
||||
return null; |
||||
}).ifPresent(key::append); |
||||
return key.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 将一个html中需要授权的资源替换成授权完成 |
||||
* |
||||
* @param html html文本 |
||||
* @param ossKindEnum 需要处理的对象的oss 枚举 |
||||
* @return 授权完成的html |
||||
*/ |
||||
public String htmlSourceAuthorization(String html, OssKindEnum ossKindEnum) { |
||||
if (StringUtils.isBlank(html)) { |
||||
return html; |
||||
} |
||||
Document doc = Jsoup.parse(html); |
||||
// 查找所有image 标签
|
||||
Elements imgs = doc.getElementsByTag("img"); |
||||
for (Element img : imgs) { |
||||
img.attr("src", |
||||
getUrlFromUrl(img.attr("src"), EasyOptionalUtils.mapTo(ossKindEnum, OssKindEnum::getProcess))); |
||||
} |
||||
doc.outputSettings().prettyPrint(false); |
||||
return doc.body().html(); |
||||
} |
||||
|
||||
/** |
||||
* 将一个json中需要授权的资源替换成授权完成 |
||||
* |
||||
* 这里注意 这个json 文本很奇怪。类似于:["root",{},["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"测试"], |
||||
* ["span",{"italic":true,"data-type":"leaf"},"文本"]]]] |
||||
* |
||||
* @param json json文本 |
||||
* @param ossKindEnum 需要处理的对象的oss 枚举 |
||||
* @return 授权完成的html |
||||
*/ |
||||
public String jsonSourceAuthorization(String json, OssKindEnum ossKindEnum) { |
||||
if (StringUtils.isBlank(json)) { |
||||
return json; |
||||
} |
||||
JSONArray jsonArray = JSON.parseArray(json); |
||||
|
||||
// 给资源授权
|
||||
jsonSourceAuthorization(jsonArray, ossKindEnum); |
||||
return jsonArray.toString(); |
||||
} |
||||
|
||||
private void jsonSourceAuthorization(JSONArray jsonArray, OssKindEnum ossKindEnum) { |
||||
// 如果小于2级的标签 需要忽略
|
||||
if (jsonArray.size() < 2) { |
||||
return; |
||||
} |
||||
// 递归 第三个及其以后的节点
|
||||
for (int i = 2; i < jsonArray.size(); i++) { |
||||
Object data = jsonArray.get(i); |
||||
// 可能是 字符串 也是可能是数组
|
||||
// 这里只需要考虑是数组的情况
|
||||
if (data instanceof JSONArray) { |
||||
jsonSourceAuthorization((JSONArray)data, ossKindEnum); |
||||
} |
||||
} |
||||
|
||||
// 处理本节点的数据
|
||||
// 第0个数组代表标签名字
|
||||
String tagName = jsonArray.getString(0); |
||||
//代表不是图片
|
||||
if (!"img".equals(tagName)) { |
||||
return; |
||||
} |
||||
// 第1个数组代表 样式
|
||||
JSONObject style = jsonArray.getJSONObject(1); |
||||
if (style == null) { |
||||
return; |
||||
} |
||||
style.put("src", |
||||
getUrlFromUrl(style.getString("src"), EasyOptionalUtils.mapTo(ossKindEnum, OssKindEnum::getProcess))); |
||||
} |
||||
|
||||
/** |
||||
* 给一个路径资源授权 |
||||
* |
||||
* @param url 一个url |
||||
* @param process process处理 |
||||
* @return |
||||
*/ |
||||
public String getUrlFromUrl(String url, String process) { |
||||
if (StringUtils.isBlank(url)) { |
||||
return url; |
||||
} |
||||
try { |
||||
// 有空可能没有http 协议 默认https
|
||||
if (url.startsWith("//")) { |
||||
url = HTTPS_PREFIX + url; |
||||
} |
||||
URL sourceUrl = new URL(url); |
||||
// 代表不是当前 环境的资源
|
||||
if (!domain.equals(sourceUrl.getHost()) && !ossDomain.equals(sourceUrl.getHost())) { |
||||
return url; |
||||
} |
||||
return getUrl(sourceUrl.getPath().substring(1), process); |
||||
} catch (Exception e) { |
||||
log.error("错误的路径:{}", url, e); |
||||
return url; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 生成一个指定类型的文件上传签名 |
||||
* |
||||
* @param kind 文件类型 |
||||
* @param name 文件名 |
||||
* @return |
||||
*/ |
||||
public CalculatePostSignature calculatePostSignature(String kind, String name) { |
||||
BaseOssKindEnum kindEnum = EasyEnumUtils.getEnum(ossKindEnumClass, kind); |
||||
if (kindEnum == null) { |
||||
throw new BusinessException("不支持当前文件的上传类型:" + kind); |
||||
} |
||||
|
||||
CalculatePostSignature calculatePostSignature = new CalculatePostSignature(); |
||||
// 放入cal
|
||||
calculatePostSignature.setObjectAcl(kindEnum.getOssObjectAcl().getOssAcl()); |
||||
|
||||
// 获取请求的策略
|
||||
// 默认1小时有效期
|
||||
Date expiration = DateUtil.offsetHour(DateUtil.date(), 1); |
||||
calculatePostSignature.setExpire(expiration.getTime()); |
||||
|
||||
// 计算文件的key
|
||||
calculatePostSignature.setKey(buildKey(kind, name)); |
||||
|
||||
// 设置文件大小
|
||||
PolicyConditions policyConditions = new PolicyConditions(); |
||||
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, minContentLength, |
||||
maxContentLength); |
||||
policyConditions.addConditionItem(MatchMode.Exact, PolicyConditions.COND_KEY, |
||||
calculatePostSignature.getKey()); |
||||
String postPolicy = ossClient.generatePostPolicy(expiration, policyConditions); |
||||
// 签名
|
||||
calculatePostSignature.setSignature(ossClient.calculatePostSignature(postPolicy)); |
||||
|
||||
// 设置编码后的Policy
|
||||
byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8); |
||||
String encodedPostPolicy = BinaryUtil.toBase64String(binaryData); |
||||
calculatePostSignature.setPolicy(encodedPostPolicy); |
||||
|
||||
// host的格式为 bucketname.endpoint
|
||||
calculatePostSignature.setHost("https://" + bucketName + "." + endpoint); |
||||
|
||||
// 获取accessId
|
||||
calculatePostSignature.setAccessId(accessKeyId); |
||||
calculatePostSignature.setUrl(getUrl(calculatePostSignature.getKey())); |
||||
|
||||
return calculatePostSignature; |
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,759 @@
|
||||
package com.alibaba.easytools.spring.oss; |
||||
|
||||
import java.io.File; |
||||
import java.io.InputStream; |
||||
import java.io.UnsupportedEncodingException; |
||||
import java.net.URL; |
||||
import java.net.URLEncoder; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.Calendar; |
||||
import java.util.Date; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
import java.util.UUID; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import com.alibaba.easytools.base.constant.SymbolConstant; |
||||
import com.alibaba.easytools.base.excption.BusinessException; |
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.excption.SystemException; |
||||
import com.alibaba.easytools.common.util.EasyEnumUtils; |
||||
import com.alibaba.easytools.common.util.EasyOptionalUtils; |
||||
import com.alibaba.fastjson.JSON; |
||||
import com.alibaba.fastjson.JSONArray; |
||||
import com.alibaba.fastjson.JSONObject; |
||||
|
||||
import com.aliyun.oss.OSSClient; |
||||
import com.aliyun.oss.OSSErrorCode; |
||||
import com.aliyun.oss.OSSException; |
||||
import com.aliyun.oss.common.utils.HttpUtil; |
||||
import com.aliyun.oss.internal.Mimetypes; |
||||
import com.aliyun.oss.internal.OSSUtils; |
||||
import com.aliyun.oss.internal.RequestParameters; |
||||
import com.aliyun.oss.model.CannedAccessControlList; |
||||
import com.aliyun.oss.model.GeneratePresignedUrlRequest; |
||||
import com.aliyun.oss.model.GetObjectRequest; |
||||
import com.aliyun.oss.model.ObjectMetadata; |
||||
import com.aliyuncs.IAcsClient; |
||||
import com.aliyuncs.exceptions.ClientException; |
||||
import com.aliyuncs.imm.model.v20170906.CreateOfficeConversionTaskRequest; |
||||
import lombok.Data; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.apache.commons.collections4.MapUtils; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
import org.jsoup.Jsoup; |
||||
import org.jsoup.nodes.Document; |
||||
import org.jsoup.nodes.Element; |
||||
import org.jsoup.select.Elements; |
||||
|
||||
import static com.aliyun.oss.internal.OSSConstants.DEFAULT_CHARSET_NAME; |
||||
|
||||
/** |
||||
* 自定义OSS的客户端 |
||||
* |
||||
* @author 是仪 |
||||
* @deprecated 使用 EasyOssClient |
||||
**/ |
||||
@Slf4j |
||||
@Data |
||||
@Deprecated |
||||
public class OssClient { |
||||
/** |
||||
* https前缀 |
||||
*/ |
||||
private static final String HTTPS_PREFIX = "https:"; |
||||
|
||||
/** |
||||
* 如果不传kind 全部放到这里 |
||||
*/ |
||||
private static final String DEFAULT_KIND = "default"; |
||||
|
||||
/** |
||||
* 预览文件的后缀 |
||||
*/ |
||||
public static final String PREVIEW_SRC_SUFFIX = "_preview"; |
||||
/** |
||||
* oss枚举的类 |
||||
*/ |
||||
private Class<? extends OssKindEnum> ossKindEnumClass; |
||||
|
||||
/** |
||||
* 读文件的oss |
||||
*/ |
||||
private OSSClient readOss; |
||||
/** |
||||
* 写文件的oss |
||||
* 可能会使用 internalEndpoint |
||||
*/ |
||||
private OSSClient writeOss; |
||||
|
||||
private String projectName; |
||||
private String bucketName; |
||||
private String endpoint; |
||||
private String baseUrl; |
||||
|
||||
/** |
||||
* bucketName+endpoint |
||||
*/ |
||||
private String ossDomain; |
||||
|
||||
/** |
||||
* 绑定的域名 类似于 oss.alibaba.com 不要带任何前缀 |
||||
* 可以为空 |
||||
* 如果为空 则会生成 bucketName+endpoint 的连接 |
||||
* 如果不为空 则会生成 domain 的连接 |
||||
*/ |
||||
private String domain; |
||||
/** |
||||
* 处理预览的时候有用 |
||||
*/ |
||||
private IAcsClient iAcsClient; |
||||
|
||||
/** |
||||
* 多媒体项目 |
||||
*/ |
||||
private String immProject; |
||||
|
||||
/** |
||||
* 是否需要替换域名 |
||||
* 生成私有的连接的时候 返回的是 bucketName+endpoint |
||||
* 这个时候 我们要把替换成 domain ,当然只有domain!=bucketName+endpoint 的情况 |
||||
*/ |
||||
private Boolean needReplaceDomain; |
||||
|
||||
public OssClient(OSSClient oss, String projectName, String bucketName, String endpoint, |
||||
Class<? extends OssKindEnum> ossKindEnumClass) { |
||||
this(projectName, bucketName, endpoint, ossKindEnumClass, null, null, null); |
||||
this.readOss = oss; |
||||
this.writeOss = oss; |
||||
} |
||||
|
||||
/** |
||||
* 构建oss |
||||
* |
||||
* @param projectName 项目名 多个项目公用一个oss 可以传值 |
||||
* @param bucketName |
||||
* @param endpoint |
||||
* @param ossKindEnumClass |
||||
* @param iAcsClient |
||||
* @param immProject |
||||
* @param domain |
||||
*/ |
||||
public OssClient(String projectName, String bucketName, String endpoint, |
||||
Class<? extends OssKindEnum> ossKindEnumClass, IAcsClient iAcsClient, String immProject, String domain) { |
||||
this.projectName = projectName; |
||||
this.iAcsClient = iAcsClient; |
||||
this.bucketName = bucketName; |
||||
this.endpoint = endpoint; |
||||
if (ossKindEnumClass != null) { |
||||
this.ossKindEnumClass = ossKindEnumClass; |
||||
} else { |
||||
this.ossKindEnumClass = DefaultOssKindEnum.class; |
||||
} |
||||
this.immProject = immProject; |
||||
this.ossDomain = bucketName + "." + endpoint; |
||||
this.domain = domain; |
||||
this.needReplaceDomain = this.domain != null; |
||||
// 为空 则拼接domain
|
||||
if (this.domain == null) { |
||||
this.domain = ossDomain; |
||||
} |
||||
this.baseUrl = "https://" + this.domain + "/"; |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file) { |
||||
return put(file, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file, String kind) { |
||||
return put(file, kind, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param ossKindEnum 文件种类 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file, OssKindEnum ossKindEnum) { |
||||
return put(file, ossKindEnum.getCode(), null, null, null, ossKindEnum.getObjectAcl()); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则根据file的名字下载 如果还为空 则自动生成uuid |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file, String kind, String fileName) { |
||||
return put(file, kind, fileName, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则根据file的名字下载 如果还为空 则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 为空则根据file的名字的后缀区分 如果还为空 则默认流application/octet-stream |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(File file, String kind, String fileName, String contentType) { |
||||
return put(file, kind, fileName, contentType, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则根据file的名字下载 如果还为空 则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 为空则根据file的名字的后缀区分 如果还为空 则默认流application/octet-stream |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(String key, File file, String fileName, String contentType) { |
||||
return put(file, null, fileName, contentType, key, null); |
||||
} |
||||
|
||||
/** |
||||
* 上传文件 |
||||
* |
||||
* @param file 文件 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则根据file的名字下载 如果还为空 则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 为空则根据file的名字的后缀区分 如果还为空 则默认流application/octet-stream |
||||
* @param key 服务器的唯一key |
||||
* @param objectAcl 对象权限控制 默认私有的 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
* @see CannedAccessControlList |
||||
*/ |
||||
public String put(File file, String kind, String fileName, String contentType, String key, |
||||
CannedAccessControlList objectAcl) { |
||||
if (file == null) { |
||||
throw new BusinessException("文件不能为空!"); |
||||
} |
||||
if (!file.isFile()) { |
||||
throw new BusinessException("不能上传目录!"); |
||||
} |
||||
// 构建用来存储在服务器是上面的唯一目录值
|
||||
if (StringUtils.isBlank(key)) { |
||||
key = buildKey(kind, fileName); |
||||
} |
||||
// 构建用户下载的时候显示的文件名
|
||||
String fileNameDownload = buildFileName(file, fileName); |
||||
writeOss.putObject(bucketName, key, file, buildObjectMetadata(fileNameDownload, contentType, objectAcl)); |
||||
return key; |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(InputStream inputStream) { |
||||
return put(inputStream, null, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(InputStream inputStream, String kind) { |
||||
return put(inputStream, kind, null, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则自动生成uuid |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(InputStream inputStream, String kind, String fileName) { |
||||
return put(inputStream, kind, fileName, null, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 默认流application/octet-stream |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(InputStream inputStream, String kind, String fileName, String contentType) { |
||||
return put(inputStream, kind, fileName, contentType, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 默认流application/octet-stream |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
*/ |
||||
public String put(String key, InputStream inputStream, String fileName, String contentType) { |
||||
return put(inputStream, null, fileName, contentType, key, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据文件流上传文件 |
||||
* |
||||
* @param inputStream 文件流 不能为空 |
||||
* @param kind 文件种类 可以为空 在每个项目创建OSSKindConstants 尽量不用重复 为空默认放在default下面 |
||||
* @param fileName 用户下载时候的文件名 可以为空 为空则自动生成uuid |
||||
* @param contentType 文件类型 可以为空 用户打开时候浏览器的contentType 为空优先根据传入的fileName的后缀区分 如果fileName没有传入则 |
||||
* 默认流application/octet-stream |
||||
* @param key 生成服务器文件存储的key (空的场景,需要生成) |
||||
* @param objectAcl 对象权限控制 默认私有的 |
||||
* @return 返回文件的唯一key 可以获取文件url |
||||
* @see CannedAccessControlList |
||||
*/ |
||||
public String put(InputStream inputStream, String kind, String fileName, String contentType, String key, |
||||
CannedAccessControlList objectAcl) { |
||||
|
||||
if (inputStream == null) { |
||||
throw new BusinessException("文件流不能为空!"); |
||||
} |
||||
// 构建用来存储在服务器是上面的唯一目录值
|
||||
if (StringUtils.isBlank(key)) { |
||||
key = buildKey(kind, fileName); |
||||
} |
||||
// 构建用户下载的时候显示的文件名
|
||||
String fileNameDownload = buildFileName(fileName); |
||||
// 上传文件
|
||||
writeOss.putObject(bucketName, key, inputStream, buildObjectMetadata(fileNameDownload, contentType, objectAcl)); |
||||
return key; |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取文件流 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public InputStream get(String key) { |
||||
return get(key, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取文件流 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param process 格式转换 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public InputStream get(String key, String process) { |
||||
return getOssObject(key, process).getObjectContent(); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取OSS文件 如果需要获取文件名之类的请用此方法 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public OssObject getOssObject(String key) { |
||||
return getOssObject(key, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据 key获取OSS文件 如果需要获取文件名之类的请用此方法 |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param process 格式转换 |
||||
* @return 返回文件流 如果不存在 则会抛出RuntimeException |
||||
*/ |
||||
public OssObject getOssObject(String key, String process) { |
||||
|
||||
if (StringUtils.isBlank(key)) { |
||||
throw new BusinessException("获取文件key 不能为空"); |
||||
} |
||||
try { |
||||
GetObjectRequest getObjectRequest = new GetObjectRequest(bucketName, key); |
||||
if (StringUtils.isNotBlank(process)) { |
||||
getObjectRequest.setProcess(process); |
||||
} |
||||
return OssObject.buildWithAliyunOssObject(readOss.getObject(getObjectRequest)); |
||||
} catch (OSSException e) { |
||||
String errorCode = e.getErrorCode(); |
||||
if (OSSErrorCode.NO_SUCH_KEY.equals(errorCode)) { |
||||
throw new BusinessException("找不到指定文件"); |
||||
} |
||||
throw new BusinessException(e.getMessage(), e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key) { |
||||
return getUrl(key, null, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param process 格式转换 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key, String process) { |
||||
return getUrl(key, null, null, process); |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param expiration 超时时间 可以为空 共有读的不管传不传 都是永久的 私有的不传默认1小时 |
||||
* @param timeUnit 超时时间单位 和超时时间一起使用 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key, Long expiration, TimeUnit timeUnit) { |
||||
return getUrl(key, expiration, timeUnit, null); |
||||
} |
||||
|
||||
/** |
||||
* 根据Key 获取url |
||||
* |
||||
* @param key 文件key 在上传的时候获得 |
||||
* @param expiration 超时时间 可以为空 共有读的不管传不传 都是永久的 私有的不传默认1小时 |
||||
* @param timeUnit 超时时间单位 和超时时间一起使用 |
||||
* @param process 格式转换 |
||||
* @return 一个可以访问的url |
||||
*/ |
||||
public String getUrl(String key, Long expiration, TimeUnit timeUnit, String process) { |
||||
if (StringUtils.isBlank(key)) { |
||||
return null; |
||||
} |
||||
String kind = StringUtils.split(key, SymbolConstant.SLASH)[0]; |
||||
OssKindEnum kindEnum = EasyEnumUtils.getEnum(ossKindEnumClass, kind); |
||||
|
||||
// 使用默认的样式处理
|
||||
if (process == null) { |
||||
process = kindEnum.getProcess(); |
||||
} |
||||
|
||||
// 代表是公有读 直接拼接连接即可
|
||||
if (kindEnum != null && kindEnum.getObjectAcl() == CannedAccessControlList.PublicRead) { |
||||
Map<String, String> params = new LinkedHashMap<>(); |
||||
if (StringUtils.isNotBlank(process)) { |
||||
params.put(RequestParameters.SUBRESOURCE_PROCESS, process); |
||||
} |
||||
if (MapUtils.isEmpty(params)) { |
||||
return baseUrl + OSSUtils.determineResourcePath(bucketName, key, |
||||
readOss.getClientConfiguration().isSLDEnabled()); |
||||
} else { |
||||
String queryString = HttpUtil.paramToQueryString(params, DEFAULT_CHARSET_NAME); |
||||
return baseUrl + OSSUtils.determineResourcePath(bucketName, key, |
||||
readOss.getClientConfiguration().isSLDEnabled()) + "?" + queryString; |
||||
} |
||||
} |
||||
|
||||
// 设置默认超时时间
|
||||
if (expiration == null) { |
||||
expiration = 5L; |
||||
timeUnit = TimeUnit.MINUTES; |
||||
} else { |
||||
if (timeUnit == null) { |
||||
throw new BusinessException("超时时间和超时时间单位必须同时为空或者同时不为空"); |
||||
} |
||||
} |
||||
|
||||
Date expirationDate = new Date(System.currentTimeMillis() + timeUnit.toMillis(expiration)); |
||||
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, key); |
||||
generatePresignedUrlRequest.setExpiration(expirationDate); |
||||
if (StringUtils.isNotBlank(process)) { |
||||
generatePresignedUrlRequest.setProcess(process); |
||||
} |
||||
URL url = readOss.generatePresignedUrl(generatePresignedUrlRequest); |
||||
String urlString = url.toString(); |
||||
if (needReplaceDomain) { |
||||
urlString = urlString.replaceFirst(ossDomain, domain); |
||||
} |
||||
return urlString; |
||||
} |
||||
|
||||
/** |
||||
* 创建一个转换office 的任务。过一段时间会自动完成 |
||||
* |
||||
* @param key |
||||
*/ |
||||
public void createOfficeConversionTask(String key) { |
||||
String srcUri = "oss://" + bucketName + "/" + key; |
||||
CreateOfficeConversionTaskRequest createOfficeConversionTaskRequest = new CreateOfficeConversionTaskRequest(); |
||||
createOfficeConversionTaskRequest.setProject(immProject); |
||||
createOfficeConversionTaskRequest.setSrcUri(srcUri); |
||||
createOfficeConversionTaskRequest.setTgtType("vector"); |
||||
createOfficeConversionTaskRequest.setTgtUri(srcUri + PREVIEW_SRC_SUFFIX); |
||||
try { |
||||
iAcsClient.getAcsResponse(createOfficeConversionTaskRequest); |
||||
} catch (ClientException e) { |
||||
throw new SystemException("创建转换office任务失败.", e); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据url生成key |
||||
* |
||||
* @param url 一个可以访问的url |
||||
* @return 唯一key |
||||
*/ |
||||
public String urlToKey(String url) { |
||||
if (StringUtils.isBlank(url)) { |
||||
throw new BusinessException("url转key url不能为空"); |
||||
} |
||||
try { |
||||
URL urlObject = new URL(url); |
||||
String path = urlObject.getPath(); |
||||
return path.substring(1); |
||||
} catch (Exception e) { |
||||
throw new BusinessException("请传入正确的url"); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 根据key 移除文件 |
||||
* |
||||
* @param key |
||||
*/ |
||||
public void delete(String key) { |
||||
writeOss.deleteObject(bucketName, key); |
||||
} |
||||
|
||||
/** |
||||
* 构建 请求参数 |
||||
* |
||||
* @param fileNameDownload |
||||
* @param contentType |
||||
* @return |
||||
*/ |
||||
private ObjectMetadata buildObjectMetadata(String fileNameDownload, String contentType, |
||||
CannedAccessControlList objectAcl) { |
||||
if (objectAcl == null) { |
||||
objectAcl = CannedAccessControlList.Private; |
||||
} |
||||
if (objectAcl != CannedAccessControlList.Private && objectAcl != CannedAccessControlList.PublicRead) { |
||||
throw new SystemException("目前仅仅支持私有和共有读类型。"); |
||||
} |
||||
ObjectMetadata objectMetadata = new ObjectMetadata(); |
||||
if (StringUtils.isBlank(contentType)) { |
||||
contentType = Mimetypes.getInstance().getMimetype(fileNameDownload); |
||||
} |
||||
objectMetadata.setContentType(contentType); |
||||
try { |
||||
// 防止中文乱码
|
||||
fileNameDownload = URLEncoder.encode(fileNameDownload, StandardCharsets.UTF_8.name()).replaceAll("\\+", |
||||
"%20"); |
||||
} catch (UnsupportedEncodingException e) { |
||||
throw new SystemException(CommonErrorEnum.COMMON_SYSTEM_ERROR, "不支持的字符编码", e); |
||||
} |
||||
objectMetadata.setContentDisposition("filename*=utf-8''" + fileNameDownload); |
||||
// 默认私有
|
||||
objectMetadata.setObjectAcl(objectAcl); |
||||
return objectMetadata; |
||||
} |
||||
|
||||
/** |
||||
* 用来 下载时 显示给用户的名称 |
||||
* |
||||
* @param fileName |
||||
* @return |
||||
*/ |
||||
private String buildFileName(String fileName) { |
||||
if (StringUtils.isNotBlank(fileName)) { |
||||
return fileName; |
||||
} |
||||
return UUID.randomUUID().toString(); |
||||
} |
||||
|
||||
/** |
||||
* 用来 下载时 显示给用户的名称 |
||||
* |
||||
* @param file |
||||
* @param fileName |
||||
* @return |
||||
*/ |
||||
private String buildFileName(File file, String fileName) { |
||||
if (StringUtils.isNotBlank(fileName)) { |
||||
return fileName; |
||||
} |
||||
fileName = file.getName(); |
||||
if (StringUtils.isNotBlank(fileName)) { |
||||
return fileName; |
||||
} |
||||
return UUID.randomUUID().toString(); |
||||
} |
||||
|
||||
/** |
||||
* 生成服务器文件存储的key 统一看不到名字 看不到后缀 |
||||
* 有些场景需要提前生成唯一存储key |
||||
* |
||||
* @param kind |
||||
* @param fileName |
||||
* @return |
||||
*/ |
||||
public String buildKey(String kind, String fileName) { |
||||
if (StringUtils.isBlank(kind)) { |
||||
kind = DEFAULT_KIND; |
||||
} |
||||
StringBuilder key = new StringBuilder(); |
||||
if (StringUtils.isNotBlank(projectName)) { |
||||
key.append(projectName); |
||||
key.append("/"); |
||||
} |
||||
key.append(kind); |
||||
key.append("/"); |
||||
Calendar calendar = Calendar.getInstance(); |
||||
key.append(calendar.get(Calendar.YEAR)); |
||||
key.append("/"); |
||||
key.append(StringUtils.leftPad(Integer.toString(calendar.get(Calendar.MONTH) + 1), 2, "0")); |
||||
key.append("/"); |
||||
key.append(UUID.randomUUID()); |
||||
|
||||
Optional.ofNullable(fileName).map(fileNameData -> { |
||||
int lastDotIndex = fileNameData.lastIndexOf(SymbolConstant.DOT); |
||||
if (lastDotIndex >= 0) { |
||||
return fileNameData.substring(lastDotIndex); |
||||
} |
||||
return null; |
||||
}).ifPresent(key::append); |
||||
return key.toString(); |
||||
} |
||||
|
||||
/** |
||||
* 将一个html中需要授权的资源替换成授权完成 |
||||
* |
||||
* @param html html文本 |
||||
* @param ossKindEnum 需要处理的对象的oss 枚举 |
||||
* @return 授权完成的html |
||||
*/ |
||||
public String htmlSourceAuthorization(String html, OssKindEnum ossKindEnum) { |
||||
if (StringUtils.isBlank(html)) { |
||||
return html; |
||||
} |
||||
Document doc = Jsoup.parse(html); |
||||
// 查找所有image 标签
|
||||
Elements imgs = doc.getElementsByTag("img"); |
||||
for (Element img : imgs) { |
||||
img.attr("src", |
||||
getUrlFromUrl(img.attr("src"), EasyOptionalUtils.mapTo(ossKindEnum, OssKindEnum::getProcess))); |
||||
} |
||||
doc.outputSettings().prettyPrint(false); |
||||
return doc.body().html(); |
||||
} |
||||
|
||||
/** |
||||
* 将一个json中需要授权的资源替换成授权完成 |
||||
* |
||||
* 这里注意 这个json 文本很奇怪。类似于:["root",{},["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"测试"], |
||||
* ["span",{"italic":true,"data-type":"leaf"},"文本"]]]] |
||||
* |
||||
* @param json json文本 |
||||
* @param ossKindEnum 需要处理的对象的oss 枚举 |
||||
* @return 授权完成的html |
||||
*/ |
||||
public String jsonSourceAuthorization(String json, OssKindEnum ossKindEnum) { |
||||
if (StringUtils.isBlank(json)) { |
||||
return json; |
||||
} |
||||
JSONArray jsonArray = JSON.parseArray(json); |
||||
|
||||
// 给资源授权
|
||||
jsonSourceAuthorization(jsonArray, ossKindEnum); |
||||
return jsonArray.toString(); |
||||
} |
||||
|
||||
private void jsonSourceAuthorization(JSONArray jsonArray, OssKindEnum ossKindEnum) { |
||||
// 如果小于2级的标签 需要忽略
|
||||
if (jsonArray.size() < 2) { |
||||
return; |
||||
} |
||||
// 递归 第三个及其以后的节点
|
||||
for (int i = 2; i < jsonArray.size(); i++) { |
||||
Object data = jsonArray.get(i); |
||||
// 可能是 字符串 也是可能是数组
|
||||
// 这里只需要考虑是数组的情况
|
||||
if (data instanceof JSONArray) { |
||||
jsonSourceAuthorization((JSONArray)data, ossKindEnum); |
||||
} |
||||
} |
||||
|
||||
// 处理本节点的数据
|
||||
// 第0个数组代表标签名字
|
||||
String tagName = jsonArray.getString(0); |
||||
//代表不是图片
|
||||
if (!"img".equals(tagName)) { |
||||
return; |
||||
} |
||||
// 第1个数组代表 样式
|
||||
JSONObject style = jsonArray.getJSONObject(1); |
||||
if (style == null) { |
||||
return; |
||||
} |
||||
style.put("src", |
||||
getUrlFromUrl(style.getString("src"), EasyOptionalUtils.mapTo(ossKindEnum, OssKindEnum::getProcess))); |
||||
} |
||||
|
||||
/** |
||||
* 给一个路径资源授权 |
||||
* |
||||
* @param url 一个url |
||||
* @param process process处理 |
||||
* @return |
||||
*/ |
||||
public String getUrlFromUrl(String url, String process) { |
||||
if (StringUtils.isBlank(url)) { |
||||
return url; |
||||
} |
||||
try { |
||||
// 有空可能没有http 协议 默认https
|
||||
if (url.startsWith("//")) { |
||||
url = HTTPS_PREFIX + url; |
||||
} |
||||
URL sourceUrl = new URL(url); |
||||
// 代表不是当前 环境的资源
|
||||
if (!domain.equals(sourceUrl.getHost()) && !ossDomain.equals(sourceUrl.getHost())) { |
||||
return url; |
||||
} |
||||
return getUrl(sourceUrl.getPath().substring(1), process); |
||||
} catch (Exception e) { |
||||
log.error("错误的路径:{}", url, e); |
||||
return url; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,27 @@
|
||||
package com.alibaba.easytools.spring.oss; |
||||
|
||||
import com.alibaba.easytools.base.enums.BaseEnum; |
||||
|
||||
import com.aliyun.oss.model.CannedAccessControlList; |
||||
|
||||
/** |
||||
* oss枚举 |
||||
* |
||||
* @author 是仪 |
||||
*/ |
||||
public interface OssKindEnum extends BaseEnum<String> { |
||||
|
||||
/** |
||||
* 获取权限控制 |
||||
* |
||||
* @return |
||||
*/ |
||||
CannedAccessControlList getObjectAcl(); |
||||
|
||||
/** |
||||
* 样式处理 |
||||
* |
||||
* @return |
||||
*/ |
||||
String getProcess(); |
||||
} |
@ -0,0 +1,157 @@
|
||||
package com.alibaba.easytools.spring.oss; |
||||
|
||||
import java.io.Closeable; |
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.UnsupportedEncodingException; |
||||
import java.net.URLDecoder; |
||||
import java.nio.charset.StandardCharsets; |
||||
|
||||
import com.alibaba.easytools.base.excption.CommonErrorEnum; |
||||
import com.alibaba.easytools.base.excption.SystemException; |
||||
|
||||
import com.aliyun.oss.model.OSSObject; |
||||
import lombok.Data; |
||||
import org.apache.commons.lang3.StringUtils; |
||||
|
||||
/** |
||||
* oss返回对象 |
||||
* |
||||
* @author 是仪 |
||||
**/ |
||||
@Data |
||||
public class OssObject implements Closeable { |
||||
|
||||
/** |
||||
* 文件描述的文件名 |
||||
*/ |
||||
private static final String NORMAL_FILENAME = "filename="; |
||||
/** |
||||
* "RFC 5987",文件描述的文件名 |
||||
*/ |
||||
private static final String FILENAME = "filename*="; |
||||
/** |
||||
* utf8的前缀 |
||||
*/ |
||||
public static final String UTF8_PREFIX = "utf-8''"; |
||||
/** |
||||
* 默认文价类型 |
||||
*/ |
||||
private static final String DEFAULT_SUFFIX = "data"; |
||||
|
||||
/** |
||||
* 件key |
||||
*/ |
||||
private String key; |
||||
|
||||
/** |
||||
* 原文件名 上传文件时候的文件名 如果为空 返回uuid |
||||
*/ |
||||
private String originalFileName; |
||||
/** |
||||
* 原文件类型的后缀 如果没有指定 默认.data |
||||
*/ |
||||
private String originalFileSuffix; |
||||
/** |
||||
* 对象实体 |
||||
*/ |
||||
private InputStream objectContent; |
||||
/** |
||||
* 对象实体长度 |
||||
*/ |
||||
private Long objectContentLength; |
||||
/** |
||||
* 对象实体类型 |
||||
*/ |
||||
private String objectContentType; |
||||
/** |
||||
* 对象展示类型 |
||||
*/ |
||||
private String objectContentDisposition; |
||||
|
||||
public static OssObject buildWithAliyunOssObject(OSSObject aliyunOssObject) { |
||||
OssObject ossObject = new OssObject(); |
||||
if (aliyunOssObject == null) { |
||||
return ossObject; |
||||
} |
||||
ossObject.setKey(aliyunOssObject.getKey()); |
||||
ossObject.setObjectContent(aliyunOssObject.getObjectContent()); |
||||
ossObject.setObjectContentLength(aliyunOssObject.getObjectMetadata().getContentLength()); |
||||
ossObject.setObjectContentType(aliyunOssObject.getObjectMetadata().getContentType()); |
||||
ossObject.setObjectContentDisposition(aliyunOssObject.getObjectMetadata().getContentDisposition()); |
||||
String contentDisposition = aliyunOssObject.getObjectMetadata().getContentDisposition(); |
||||
|
||||
// 文件名
|
||||
String originalFileName = getFilename(contentDisposition); |
||||
if (originalFileName != null) { |
||||
ossObject.setOriginalFileName(originalFileName); |
||||
|
||||
// 从文件名解析文件后缀
|
||||
int fileSuffixIndex = StringUtils.lastIndexOf(originalFileName, "."); |
||||
if (fileSuffixIndex != -1) { |
||||
// 文件的后缀
|
||||
String serverFileSuffix = originalFileName.substring(fileSuffixIndex + 1); |
||||
ossObject.setOriginalFileSuffix(serverFileSuffix); |
||||
} |
||||
} |
||||
return ossObject; |
||||
} |
||||
|
||||
/** |
||||
* 从contentDisposition获取文件名 |
||||
* |
||||
* @param contentDisposition |
||||
* @return |
||||
*/ |
||||
private static String getFilename(String contentDisposition) { |
||||
int originalFileNameIndex = StringUtils.indexOf(contentDisposition, FILENAME); |
||||
if (originalFileNameIndex != -1) { |
||||
// 获取的文件名全称
|
||||
String originalFileName = contentDisposition.substring(originalFileNameIndex + FILENAME.length()); |
||||
if (originalFileName.startsWith(OssObject.UTF8_PREFIX)) { |
||||
originalFileName = originalFileName.substring(OssObject.UTF8_PREFIX.length()); |
||||
try { |
||||
originalFileName = URLDecoder.decode(originalFileName, StandardCharsets.UTF_8.name()); |
||||
} catch (UnsupportedEncodingException e) { |
||||
throw new SystemException(CommonErrorEnum.COMMON_SYSTEM_ERROR, "不支持的字符编码", e); |
||||
} |
||||
} |
||||
return originalFileName; |
||||
} |
||||
|
||||
originalFileNameIndex = StringUtils.indexOf(contentDisposition, NORMAL_FILENAME); |
||||
if (originalFileNameIndex != -1) { |
||||
String originalFileName = contentDisposition.substring(originalFileNameIndex + NORMAL_FILENAME.length()); |
||||
try { |
||||
originalFileName = URLDecoder.decode(originalFileName, StandardCharsets.UTF_8.name()); |
||||
} catch (UnsupportedEncodingException e) { |
||||
throw new SystemException(CommonErrorEnum.COMMON_SYSTEM_ERROR, "不支持的字符编码", e); |
||||
} |
||||
return originalFileName; |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
public String getOriginalFileName(String defaultFileName) { |
||||
if (StringUtils.isEmpty(originalFileName)) { |
||||
return defaultFileName; |
||||
} |
||||
return originalFileName; |
||||
} |
||||
|
||||
public String getOriginalFileName() { |
||||
return getOriginalFileName(getKey()); |
||||
} |
||||
|
||||
public String getOriginalFileSuffix() { |
||||
return originalFileSuffix; |
||||
} |
||||
|
||||
@Override |
||||
public void close() throws IOException { |
||||
if (objectContent != null) { |
||||
objectContent.close(); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue