SpringCloud-Alibaba之OSS對象存儲服務(wù)使用詳解
阿里云的OSS服務(wù)進(jìn)行云端的文件存儲
用戶認(rèn)證需要上傳圖片、首頁輪播需要上傳圖片,OSS分布式文件服務(wù)系統(tǒng)可以提供服務(wù)。
- 一般項(xiàng)目使用OSS對象存儲服務(wù),主要是對圖片、文件、音頻等對象集中式管理權(quán)限控制,管理數(shù)據(jù)生命周期等等,提供上傳,下載,預(yù)覽,刪除等功能。
- 通過OSS部署前端項(xiàng)目

一、依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
</dependency>
二、在配置文件中配置
OSS 服務(wù)對應(yīng)的 accessKey、secretKey 和 endpoint
alibaba.cloud.access-key=your-ak alibaba.cloud.secret-key=your-sk alibaba.cloud.oss.endpoint=***
阿里云 accessKey、secretKey 為例,獲取方式如下
①、在阿里云控制臺界面,單擊右上角頭像,選擇 accesskeys,或者直接登錄用戶信息管理界面:

②、獲取 accessKey、secretKey:

如果使用了阿里云 STS服務(wù) 進(jìn)行短期訪問權(quán)限管理,則除了 accessKey、secretKey、endpoint 以外,還需配置 securityToken
③、創(chuàng)建Bucket

進(jìn)入Bucket,上傳文件,可以查看上傳文件的詳情(URL用來訪問該文件)

三、注入OSSClient并進(jìn)行文件上傳下載等操作
大量文件對象操作的場景。
@Service
public class YourService{
@Autowired
private OSSClient ossClient;
public void saveFile(){
//下載文件到本地
ossClient.getObject(new GetObejctRequest(bucketName,objectName),new File("pathOfYourLocalFile"));
}
}
如果是僅僅讀取文件對象內(nèi)容,OSS Starter也支持以Resource方式讀取文件
①、在應(yīng)用的 /src/main/resources/application.properties 中添加基本配置信息和 OSS 配置
spring.application.name=oss-example server.port=18084 alibaba.cloud.access-key=your-ak alibaba.cloud.secret-key=your-sk alibaba.cloud.oss.endpoint=***
②、通過IDE直接啟動或者編譯打包后啟動應(yīng)用
- IDE直接啟動:找到主類 OSSApplication,執(zhí)行 main 方法啟動應(yīng)用
- 打包編譯后啟動,執(zhí)行 mvn clean package 將工程編譯打包;執(zhí)行 java -jar oss-example.jar啟動應(yīng)用應(yīng)用啟動后會自動在 OSS 上創(chuàng)建一個名為 aliyun-spring-boot-test 的 Bucke
@Value("oss://aliyun-spring-boot/oss-test")
private Resource file;
//文件內(nèi)容讀取
StreamUtils.copyToString(file.getInputStream(),Charset.forName(CharEncoding.UTF_8));
上傳或下載文件
#使用 curl 調(diào)用上傳接口 upload。該接口會上傳 classpath 下的的 oss-test.json 文件。文件內(nèi)容是一段 json: curl http://localhost:18084/upload #使用 curl 調(diào)用下載接口 download。該接口會下載剛才用 upload 接口上傳的 oss-test.json 文件,并打印文件內(nèi)容到結(jié)果中: curl http://localhost:18084/download
在OSS上驗(yàn)證結(jié)果
登陸OSS控制臺,可以看到左側(cè) Bucket 列表新增一個名字為aliyun-spring-boot-test的 Bucket。

單擊aliyun-spring-boot-test Bucket,選擇 文件管理 頁簽,發(fā)現(xiàn)上傳的 oss-test 文件。上傳的 objectName 為oss-test.json。目錄和文件以’/'符號分割。

實(shí)戰(zhàn)操作
一、導(dǎo)入依賴
<!--阿里云OSS依賴--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <!--日期工具欄依賴--> <dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> </dependency>
配置文件application.properties
server.port=8205 spring.application.name=service-oss spring.jackson.data-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8 spring.redis.host=192.168.44.165 spring.redis.port=6379 spring.redis.database=0 spring.redis.timeout=1800000 spring.redis.lettuce.pool.max-active=20 spring.redis.lettuce.pool.max-wait=-1 #最大阻塞等待時間(附屬表示沒限制) spring.redis.lettuce.pool.max-idle=5 spring.redis.lettuce.pool.min-idle=0 spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848 aliyun.oss.accessKeyId= aliyun.oss.secret=
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)//無需加載數(shù)據(jù)庫
@EnableDiscoveryClient
@ComponentScan(basePackages={"com.michael"})
public class ServiceOssApplication{
public static void main(String[] args){
SpringApplication.run(ServiceOssApplication.class,args);
}
}
二、網(wǎng)關(guān)模塊gateway中進(jìn)行對上傳oss模塊進(jìn)行配置
spring.cloud.gateway.routes[4].id=service-oss spring.cloud.gateway.routes[4].uri=lb://service-oss spring.cloud.gateway.routes[4].predicates=Path=/*/oss/**
三、文件流的方式上傳
Controller
@RestController
@RequestMapping("/api/oss/file")
public class FileApiController{
@Autowired
private FileService fileService;
//將本地文件上傳到阿里云
@PostMapping("fileUpload")
public Result fileUpLoad(MultipartFile file){//SpringMVC中的,可以得到要上傳的內(nèi)容
//返回上傳后的路徑
String url = fileService.upload(file);
return Result.ok(url);
}
}
Service
public interface FileService{
String upload(MultipartFile file);
}
@Service
public class FileServiceImpl implements FileService{
@Override
public String upload(MultipartFile file){
//以下變量可以設(shè)置到配置文件中,然后通過工具類讀取
String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
String accessKeyId = "";
String accessKeySecret = "";
String bucket = "";
//創(chuàng)建OSSClient實(shí)例
OSS occClient = new OSSClientBuilder().build(endpoint,accessKeyId,accessKeySecret);
//上傳文件流
try{
InputStream inputStream = file.getInputStream();//從Controller中獲取用戶上傳的文件流
String fileName = file.getOriginalFilename();//獲取文件名稱(包括路徑)
/**
由于同名原因會導(dǎo)致文件上傳的覆蓋問題,后上傳的覆蓋前上傳的
*/
//采用uuid生成唯一值,添加到文件名稱里(并且將里面的-用空串表示)
String uuid = UUID.randomUUID().toString().replaceAll("-","");
fileName = uuid+fileName;
//按照日期創(chuàng)建文件夾,上傳到創(chuàng)建文件夾的里面(joda-time日期工具依賴)
String timeUrl = new DateTime().toString("yyyy/MM/dd");
fileName = timeUrl + "/" + fileName;
ossClient.putObject(bucket,fileName,inputStream);
//關(guān)閉OSSClient
ossClient.shutdown();
//上傳之后的路徑,可以從阿里云工作臺的文件詳情獲取
String url = "https://"+bucketName+"."+endpoint+"/"+fileName;
return url;
}catch(IOException e){
e.printStackTrace();
}
}
}
基于AmazonS3實(shí)現(xiàn) Spring Boot Starter
Amazon Simple Storage Service(Amazon S3,Amazon簡便存儲服務(wù))是 AWS 最早推出的云服務(wù)之一,經(jīng)過多年的發(fā)展,S3 協(xié)議在對象存儲行業(yè)事實(shí)上已經(jīng)成為標(biāo)準(zhǔn)。
- 提供了統(tǒng)一的接口 REST/SOAP 來統(tǒng)一訪問任何數(shù)據(jù)
- 對 S3 來說,存在里面的數(shù)據(jù)就是對象名(鍵),和數(shù)據(jù)(值)
- 不限量,單個文件最高可達(dá) 5TB,可動態(tài)擴(kuò)容
- 高速。每個 bucket 下每秒可達(dá) 3500 PUT/COPY/POST/DELETE 或 5500 GET/HEAD 請求
- 具備版本,權(quán)限控制能力
- 具備數(shù)據(jù)生命周期管理能力
阿里云OSS兼容S3

七牛云對象存儲兼容S3

騰訊云COS兼容S3

Minio兼容S3

今天使用的是阿里云OSS對接阿里云OSS的SDK
后天我們使用的是騰訊COS對接是騰訊云COS,我們何不直接對接AmazonS3實(shí)現(xiàn)呢,這樣后續(xù)不需要調(diào)整代碼,只需要去各個云服務(wù)商配置就好了。
一、創(chuàng)建一個SpringBoot項(xiàng)目


添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws.version}</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
二、配置文件OssProperties
oss.endpoint=xxx oss.accessKey=xxx oss.secretKey=xxx
@ConfigurationProperties(prefix = “oss”):將配置文件中oss開頭的屬性綁定到此對象中
@Data
@ConfigurationProperties(prefix = "oss")
public class OssProperties{
/**
* 對象存儲服務(wù)的URL
*/
private String endpoint;
/**
* 區(qū)域
*/
private String region;
/**
* true path-style nginx 反向代理和S3默認(rèn)支持 pathStyle模式 {http://endpoint/bucketname}
* false supports virtual-hosted-style 阿里云等需要配置為 virtual-hosted-style 模式{http://bucketname.endpoint}
* 只是url的顯示不一樣
*/
private Boolean pathStyleAccess = true;
/**
* Access key
*/
private String accessKey;
/**
* Secret key
*/
private String secretKey;
/**
* 最大線程數(shù),默認(rèn):100
*/
private Integer maxConnections = 100;
}
三、創(chuàng)建接口OssTemplate
public interface OssTemplate{
void createBucket(String bucketName);//創(chuàng)建bucket
List<Bucket> getAllBuckets();//獲取所有的bucket
void removeBucket(String bucketName);//通過bucket名稱刪除bucket
//上傳文件
void putObject(String bucketName, String objectName, InputStream stream, String contextType) throws Exception;
void putObject(String bucketName, String objectName, InputStream stream) throws Exception;
//獲取文件
S3Object getObject(String bucketName, String objectName);
//獲取對象的url
String getObjectURL(String bucketName, String objectName, Integer expires);
//通過bucketName和objectName刪除對象
void removeObject(String bucketName, String objectName) throws Exception;
//根據(jù)文件前置查詢文件
List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive);
}
@RequiredArgsConstructor
public class OssTemplateImpl implements OssTemplate{
private final AmazonS3 amazonS3;
//創(chuàng)建Bucket
@Override
@SneakyThrows
public void createBucket(String bucketName){
if ( !amazonS3.doesBucketExistV2(bucketName) ) {
amazonS3.createBucket((bucketName));
}
}
//獲取所有的buckets
@Override
@SneakyThrows
public List<Bucket> getAllBuckets() {
return amazonS3.listBuckets();
}
//通過Bucket名稱刪除Bucket
@Override
@SneakyThrows
public void removeBucket(String bucketName) {
amazonS3.deleteBucket(bucketName);
}
//上傳對象
@Override
@SneakyThrows
public void putObject(String bucketName, String objectName, InputStream stream, String contextType) {
putObject(bucketName, objectName, stream, stream.available(), contextType);
}
@Override
@SneakyThrows
public void putObject(String bucketName, String objectName, InputStream stream) {
putObject(bucketName, objectName, stream, stream.available(), "application/octet-stream");
}
//通過bucketName和objectName獲取對象
@Override
@SneakyThrows
public S3Object getObject(String bucketName, String objectName) {
return amazonS3.getObject(bucketName, objectName);
}
//獲取對象的url
@Override
@SneakyThrows
public String getObjectURL(String bucketName, String objectName, Integer expires) {
Date date = new Date();
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.DAY_OF_MONTH, expires);
URL url = amazonS3.generatePresignedUrl(bucketName, objectName, calendar.getTime());
return url.toString();
}
//通過bucketName和objectName刪除對象
@Override
@SneakyThrows
public void removeObject(String bucketName, String objectName) {
amazonS3.deleteObject(bucketName, objectName);
}
//根據(jù)bucketName和prefix獲取對象集合
@Override
@SneakyThrows
public List<S3ObjectSummary> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) {
ObjectListing objectListing = amazonS3.listObjects(bucketName, prefix);
return objectListing.getObjectSummaries();
}
@SneakyThrows
private PutObjectResult putObject(String bucketName, String objectName, InputStream stream, long size,
String contextType) {
byte[] bytes = IOUtils.toByteArray(stream);
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(size);
objectMetadata.setContentType(contextType);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// 上傳
return amazonS3.putObject(bucketName, objectName, byteArrayInputStream, objectMetadata);
}
}
}
四、創(chuàng)建OssAutoConfiguration
OssAutoConfiguration:自動裝配配置類,自動裝配的bean有AmazonS3和OssTemplate
@Configuration
@RequiredArgsConstructor //lomnok的注解,替代@Autowired
@EnableConfigurationProperties(OssProperties.class) //自動裝配我們的配置類
public class OssAutoConfiguration{
@Bean
@ConditionalOnMissingBean//bean被注冊之后,注冊相同類型的bean,就不會成功,它會保證你的bean只有一個,即你的實(shí)例只有一個。多個會報錯
public AmazonS3 ossClient(OssProperties ossProperties){
//客戶端配置,主要是全局的配置信息
ClientConfiguration cientConfiguration = new ClientConfiguration();
clientConfiguration.setMaxConnections(ossProperties.getMaxConnections());
//url以及region配置
AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder
.EndpointConfiguration(ossProperties.getEndpoint(), ossProperties.getRegion());
//憑證配置
AWSCredentials awsCredentials = new BasicAWSCredentials(ossProperties.getAccessKey(),
ossProperties.getSecretKey());
AWSCredentialsProvider awsCredentialsProvider = new AWSStaticCredentialsProvider(awsCredentials);
// build amazonS3Client客戶端
return AmazonS3Client.builder().withEndpointConfiguration(endpointConfiguration)
.withClientConfiguration(clientConfiguration).withCredentials(awsCredentialsProvider)
.disableChunkedEncoding().withPathStyleAccessEnabled(ossProperties.getPathStyleAccess()).build();
}
@Bean
@ConditionalOnBean(AmazonS3.class)//當(dāng)給定的在bean存在時,則實(shí)例化當(dāng)前Bean
public OssTemplate ossTemplate(AmazonS3 amazonS3){
return new OssTemplateImpl(amazonS3);
}
}
五、ClientConfiguration對象
客戶端配置,主要是全局的配置信息

六、創(chuàng)建spring.factories
在resources目錄下新增META-INF包,下面新建spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.qing.oss.OssAutoConfiguration
七、執(zhí)行install打包到本地倉庫
把springboot工程的啟動類,配置文件干掉,干掉Test包。
最重要的是干掉pom文件的spring-boot-maven-plugin,要不然install報錯。

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
這樣我們的一個oss-spring-boot-starter就完成了

執(zhí)行install打包成jar到我們的本地倉庫

到我們的本地倉庫就能看到我們的oss-spring-boot-starter

八、測試
創(chuàng)建一個spring-boot工程當(dāng)作我們的測試工程

①、pom文件新增我們的oss-spring-boot-starter依賴
<properties>
<oss.version>0.0.1-SNAPSHOT</oss.version>
</properties>
<dependency>
<groupId>com.qing</groupId>
<artifactId>oss-spring-boot-starter</artifactId>
<version>${oss.version}</version>
</dependency>
刷新maven后可以看到我們依賴加進(jìn)來了。

解決打包沒有注釋的問題
可以發(fā)現(xiàn)我們的依賴沒有注釋沒有Javadoc注釋,在oss-string-boot-starter的pom文件下加入下面插件,重新install一下就好了。
<build>
<plugins>
<!-- 在打好的jar包中保留javadoc注釋,實(shí)際會另外生成一個xxxxx-sources.jar -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
在我們的測試項(xiàng)目里面刷新一下maven可以看到已經(jīng)帶注釋了

配置文件添加oss-spring-boot-starter所需要的配置
阿里云,騰訊cos,七牛云,minio等等的配置。
以Minio為例
oss.endpoint=xxx oss.accessKey=xxx oss.secretKey=xxx
編寫測試方法
@SpringBootTest
class TestOssSpringBpptStarterApplicationTests {
@Autowired
private OssTemplate ossTemplate;
@Test
void contextLoads() {
ossTemplate.createBucket("oss02");
}
}

到我的Minio中查看發(fā)現(xiàn)測試成功。

Controller
@Slf4j
@RestController
@RequestMapping("/oss")
public class OSSController{
//上傳服務(wù)
@PostMapping("/upload")
public Response<Map<String,String>> upload(@RequestParam MultipartFile file, @RequestParam(required = false) String ossType)
throws IOException {
OSSTypeEnum ossTypeEnum = OSSTypeEnum.ALIYUN;//阿里云
if(StringUtils.isNotEmpty(ossType)){
ossTypeEnum = Enum.valueOf(OSSTypeEnum.class,ossType);
}
String originalFilename = file.getOriginalFilename();
InputStream inputStream = file.getInputStream();
final OSSTypeEnum finalOssTypeEnum = ossTypeEnum;
Optional<OSSService> optional = OSSContext.getBeans(OSSService.class).stream()//通過spring上下文獲取
.filter(item -> item.support(finalOssTypeEnum))
.findFirst();
//com.google.guava:guava:20.0
Map<String,String> result = Maps.newConcurrentMap();
if(optional.isPresent()){
String url = optional.get().uploadStream(inputStream,priginalFilename);
log.info("文件上傳的URL = {}", url);
result.put("name",originalFilename);
result.put("url",url);
}else{
return Response.toError(ResponseStatus.Code.FAIL_CODE, "未找到可用的云服務(wù)器");
}
return Reponse.toResponse(result);
}
//下載服務(wù)(參數(shù):文件路徑、文件名稱、云服務(wù)器類型、響應(yīng)流)
@ReqeustMapping("/download")
public void download(@RequestParam(required = false) String ossKey,
@RequestParam(required = false) String fileName,
@RequestParam(required = false) String ossType,
HttpServletResponse response)throws Exception{
//得到要下載的文件
String suffix = ossKey.substring(ossKey.lastIndexOf("."));
//hutool-all:4.1.14
String transFilename = DateUtil.formatDate(new Date()) + "-" + fileName + suffix;
//設(shè)置響應(yīng)頭
response.setContentType("application/octet-stream");
response.setCharacterEncoding("UTF-8");
response.setHeader("content-disposition", "attachment;filename=" + URLUtil.encode(transFilename, "UTF-8"));
final OSSTypeEnum finalOssTypeEnum = Enum.valueOf(OSSTypeEnum.class, ossType);
Optional<OSSService> optional = OSSContext.getBeans(OSSService.class).stream()
.filter(item -> item.support(finalOssTypeEnum)).findFirst();
if(optional.isPresent()){
//文件輸入流
InputStream in = optional.get().download(ossKey);
//創(chuàng)建輸出流
OutputStream out = response.getOutputStram();
FileUtil.write(in,out);
}
}
}
Service接口
public interface OSSService {
/**
* 支持ossType 類型
*
* @param ossType OSSType枚舉
* @return true 支持
*/
boolean support(final OSSTypeEnum ossType);
/**
* 上傳文件
*
* @param is 文件流
* @param fileName 文件名稱
* @return 上傳的文件路徑
*/
String uploadStream(final InputStream is, final String fileName);
/**
* 上傳文件
*
* @param file 源文件
* @param fileName 文件名稱
* @return 上傳的文件路徑
*/
String uploadFile(final File file, final String fileName);
/**
* 字節(jié)數(shù)組上傳
*
* @param data 字節(jié)數(shù)組
* @param fileName 文件名
* @return 上傳的文件路徑
*/
String uploadToByte(final byte[] data, final String fileName);
/**
* 根據(jù)文件key下載獲取文件流
*
* @param ossKey 文件key
* @return
*/
InputStream download(final String ossKey);
}
@Service("ALIYUN")
@Slf4j
public class AliyunServiceImpl implements OSSService{
@Autowired(required = false)
private OSS oss;//aliyun-sdk-oss:3.1.0
@Value("${alibaba.cloud.bucket}")
private String bucket;//桶
@Override
public boolean support(final OSSTypeEnum ossType){
return OSSTypeEnum.ALIYUN.equals(ossType);
}
@Override
public String uploadStream(final InputStream is,final String fileName){
String url = null;
String tempFileName = fileName;
try{
String suffix = tempFileName.substring(tempFileName.lastIndexOf(SConsts.POINT) + IConsts.ONE);
//上傳的文件key
tempFileName = suffix + SConsts.SLASH
+ DateUtils.format(DateUtils.dateTime(), DateUtils.NORM_DATE_PATTERN)
+ SConsts.SLASH + tempFileName;
//創(chuàng)建上傳Object的Metadata aliyun-sdk-oss:3.1.0
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(is.available());
objectMetadata.setCacheControl("no-cache");
objectMetadata.setHeader("Pragma", "no-cache");
objectMetadata.setContentType(FileUtil.getContentType(SConsts.POINT + suffix));
objectMetadata.setContentDisposition("inline;filename=" + tempFileName);
oss.putObject(bucket, tempFileName, is, objectMetadata);
// 獲取oss 文件地址
url = oss.generatePresignedUrl(bucket, tempFileName,
new Date(System.currentTimeMillis() + 3600L * 1000 * 24 * 365 * 10)).toString();
log.info("上傳后的地址 url = {}", url);
}catch(Exception e){
log.error("上傳文件失敗");
}
return url;
}
@Override
public String uploadFile(final File file, final String fileName) {
try {
return this.uploadStream(new FileInputStream(file), fileName);
} catch (FileNotFoundException e) {
log.error("源文件不存在,文件名稱 fileName = {}", fileName);
}
return null;
}
@Override
public String uploadToByte(final byte[] data, final String fileName) {
return this.uploadStream(new ByteArrayInputStream(data), fileName);
}
@Override
public InputStream download(final String ossKey) {
OSSObject ossObject = oss.getObject(bucket, ossKey);
return ossObject.getObjectContent();
}
}
@Service("QINIU")
@Slf4j
@EnableConfigurationProperties(QiniuProperties.class)
public class QiniuServiceImpl implements OSSService {
@Autowired(required = false)
private UploadManager uploadManager;
@Autowired(required = false)
private Auth auth;
@Autowired(required = false)
private QiniuProperties qiniuProperties;
@Override
public boolean support(final OSSTypeEnum ossType) {
return OSSTypeEnum.QINIU.equals(ossType);
}
@Override
public String uploadStream(InputStream is, String fileName) {
try {
String key = qiniuProperties.getBucket() + '/' + DateUtils.format(LocalDateTime.now(), DateUtils.NORM_DATE_PATTERN) + "/" + fileName;
Response response = uploadManager.put(is, key, this.getToken(), null, null);
if (response.isOK()) {
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return putRet.key;
}
} catch (QiniuException e) {
throw new RuntimeException("上傳文件失敗");
}
return null;
}
@Override
public String uploadFile(File file, String fileName) {
if (!file.exists()) {
throw new RuntimeException("上傳的文件不存在");
}
try {
return this.uploadStream(new FileInputStream(file), fileName);
} catch (FileNotFoundException e) {
log.error("文件不存在");
}
return null;
}
@Override
public String uploadToByte(byte[] data, String fileName) {
try {
Response response = uploadManager.put(data, fileName, this.getToken());
if (response.isOK()) {
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return putRet.key;
}
} catch (QiniuException e) {
e.printStackTrace();
}
return null;
}
@Override
public InputStream download(String ossKey) {
try {
String encodedFileName = URLEncoder.encode(ossKey, "utf-8").replace("+", "%20");
String publicUrl = String.format("%s/%s", qiniuProperties.getEndpoint(), encodedFileName);
String url = auth.privateDownloadUrl(publicUrl, 3600); //1小時,可以自定義鏈接過期時間
return FileUtil.getRemoteFile(url);
} catch (Exception e) {
log.error("獲取文件流失敗");
}
return null;
}
/**
* 獲取上傳token
*
* @return 上傳token
*/
private String getToken() {
return auth.uploadToken(qiniuProperties.getBucket());
}
}
七牛
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.2.0, 7.2.99]</version>
</dependency>
工具類
//七牛文件上傳
public class QiniuUpload{
private static final String URL = "http://pp7x7b2mm.bkt.clouddn.com/";
/**
* 上傳文件
*
* @param file 上傳的文件
* @param folder 文件路徑
* @return 文件全路徑
*/
public static String uploadToFile(File file, String folder) {
return URL + UploadConfiguration.uploadFile(file, folder);
}
/**
* 數(shù)據(jù)流文件上傳
*
* @param is 輸入流
* @param fileName 上傳文件名
* @return 文件全路徑
*/
public static String uploadToStream(InputStream is, String fileName) {
return URL + UploadConfiguration.uploadStream(is, fileName);
}
/**
* 字節(jié)數(shù)組上傳
*
* @param data 字節(jié)數(shù)組
* @param fileName 文件名
* @return 文件全路徑
*/
public static String uploadToByte(byte[] data, String fileName) {
return URL + UploadConfiguration.uploadToByte(data, fileName);
}
}
//七牛存儲配置
class UploadConfiguration {
/**
* 獲取上傳的token值
*/
private static String getToken() {
Auth auth = Auth.create(SysConfigMap.get(SysConfigKeys.QINIU_ACCESS_KEY), SysConfigMap.get(SysConfigKeys.QINIU_SECRET_KEY));
return auth.uploadToken(SysConfigMap.get(SysConfigKeys.QINIU_BUCKET));
}
/**
* 獲取設(shè)置的UploadManager配置中心
*/
private static UploadManager getUploadManager() {
Configuration configuration = new Configuration(Region.region0());
return new UploadManager(configuration);
}
static String uploadStream(InputStream is, String fileName) {
UploadManager uploadManager = UploadConfiguration.getUploadManager();
try {
String key = SysConfigMap.get(SysConfigKeys.QINIU_BUCKET) + '/' + DateUtils.format(LocalDateTime.now(), DateUtils.NORM_DATE_PATTERN) + "/" + fileName;
Response response = uploadManager.put(is, key, UploadConfiguration.getToken(), null, null);
if (response.isOK()) {
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return putRet.key;
}
} catch (QiniuException e) {
throw new RuntimeException("上傳文件失敗");
}
return null;
}
static String uploadFile(File file, String folder) {
UploadManager uploadManager = UploadConfiguration.getUploadManager();
if (!file.exists()) {
throw new RuntimeException("上傳的文件不存在");
}
StringBuilder builder = new StringBuilder();
if (!StringUtils.isNullOrEmpty(folder)) {
builder.append(folder);
}
builder.append(file.getName());
try {
Response response = uploadManager.put(file, builder.toString(), UploadConfiguration.getToken());
if (response.isOK()) {
return JSON.parseObject(response.bodyString(), DefaultPutRet.class).key;
}
} catch (QiniuException e) {
throw new RuntimeException("上傳文件失敗");
}
return null;
}
/**
* 字節(jié)數(shù)組上傳
*
* @param data 字節(jié)數(shù)組
* @param fileName 文件名
*/
static String uploadToByte(byte[] data, String fileName) {
UploadManager uploadManager = UploadConfiguration.getUploadManager();
try {
Response response = uploadManager.put(data, fileName, UploadConfiguration.getToken());
if (response.isOK()) {
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return putRet.key;
}
} catch (QiniuException e) {
e.printStackTrace();
}
return null;
}
}
阿里云
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>aliyun-oss-spring-boot-starter</artifactId>
</dependency>
騰訊云
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.162</version>
</dependency>
OSS項(xiàng)目實(shí)戰(zhàn)
①、Controller
- @HasPermissions(“sys:oss:config”) 注解配合切面
- ValidatorUtils工具類
@RestController
@RequestMapping("system/oss")
public class OssController extends BaseController {
private final static String KEY = CloudConstant.CLOUD_STORAGE_CONFIG_KEY;
@Autowired
private IOssService ossService;
@Autowired
private IConfigService configService;
//云存儲配置信息
@ReqeustMapping("config")
@HasPermissions("sys:oss:config")
public CloudStorageConfig config(){
String jsonConfig = configService.selectConfigByKey(CloudConstant.CLOUD_STORAGE_CONFIG_KEY);
//獲取云存儲配置信息(fastjson-1.2.74字符串轉(zhuǎn)換成對象)
CloudStorageConfig config = JSON.parseObject(jsonConfig,CloudStorageConfig.class);
return config;
}
//保存云存儲配置信息
@RequestMapping("saveConfig")
@HasPermissions("sys:oss:config")
public R SaveConfig(){
//校驗(yàn)類型
ValidatorUtils.validateEntity(config);
if (config.getType() == CloudService.QINIU.getValue()) {
// 校驗(yàn)七牛數(shù)據(jù)
ValidatorUtils.validateEntity(config, QiniuGroup.class);
} else if (config.getType() == CloudService.ALIYUN.getValue()) {
// 校驗(yàn)阿里云數(shù)據(jù)
ValidatorUtils.validateEntity(config, AliyunGroup.class);
} else if (config.getType() == CloudService.QCLOUD.getValue()) {
// 校驗(yàn)騰訊云數(shù)據(jù)
ValidatorUtils.validateEntity(config, QcloudGroup.class);
}
return toAjax(configService.updateValueByKey(KEY, new Gson().toJson(config)));
}
}
注解@HasPermissions(“sys:oss:config”) 和權(quán)限
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HasPermissions {
String value();
}
@Aspect
@Component
@Slf4j
public class PreAuthorizeAspect {
@Autowired
private IMenuService sysMenuService;
@Around("@annotation(com.ruoyi.system.auth.annotation.HasPermissions)")
public Object around(ProceedingJoinPoint point)throws Throwable{
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();//獲取切點(diǎn)(注解修飾的)方法
//根據(jù)方法,獲取注解類型
HasPermissions annotation = method.getAnnotation(HasPermissions.class);
if (annotation == null) {
return point.proceed();
}
String authority = new StringBuilder(annotation.value()).toString();//sys:oss:config
if(has(authority)){
return point.proceed();
}else{
throw new ForbiddenException();
}
}
private boolean has(String authority){
//用超管賬號方便測試
HttpServletRequest request = ServletUtils.getRequest();
String tmpUserKey = request.getHeader(Constants.CURRENT_ID);//current_id
if (Optional.ofNullable(tmpUserKey).isPresent()) {
Long userId = Long.valueOf(tmpUserKey);
log.debug("userid:{}", userId);
if (userId == 1L) {
return true;
}
return sysMenuService.selectPermsByUserId(userId).stream().anyMatch(authority::equals);
}
return false;
}
}
工具類
/**
* 客戶端工具類
*
* @author ruoyi
*/
public class ServletUtils {
/**
* 獲取String參數(shù)
*/
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
/**
* 獲取String參數(shù)
*/
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
/**
* 獲取Integer參數(shù)
*/
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
/**
* 獲取Integer參數(shù)
*/
public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
/**
* 獲取request
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* 獲取response
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
/**
* 獲取session
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
}
/**
* 將字符串渲染到客戶端
*
* @param response 渲染對象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try {
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 是否是Ajax異步請求
*
* @param request
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String accept = request.getHeader("accept");
if (accept != null && accept.indexOf("application/json") != -1) {
return true;
}
String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.indexOf("XMLHttpRequest") != -1) {
return true;
}
String uri = request.getRequestURI();
if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")) {
return true;
}
String ajax = request.getParameter("__ajax");
if (StringUtils.inStringIgnoreCase(ajax, "json", "xml")) {
return true;
}
return false;
}
}
/**
hibernate-validator校驗(yàn)工具類
*/
public class ValidatorUtils{
private static Validator validator;
static{
//validation-api-2.0.1.Final
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 校驗(yàn)對象
*
* @param object 待校驗(yàn)對象
* @param groups 待校驗(yàn)的組
* @throws BaseException 校驗(yàn)不通過,則報BaseException異常
*/
public static void validateEntity(Object object, Class<?>... groups)throws BaseException{
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
ConstraintViolation<Object> constraint = (ConstraintViolation<Object>) constraintViolations.iterator()
.next();
throw new BaseException(constraint.getMessage());
}
}
}
查詢權(quán)限的業(yè)務(wù)
- 主鍵緩存@RedisCache(key = “user_perms”, fieldKey = “#userId”)
/**
* 菜單 業(yè)務(wù)層處理
*
* @author ruoyi
*/
@Service
public class MenuServiceImpl implements IMenuService {
public static final String PREMISSION_STRING = "perms[\"{0}\"]";
@Autowired
private MenuMapper menuMapper;
@Autowired
private RoleMenuMapper roleMenuMapper;
/**
* 根據(jù)用戶查詢菜單
*
* @param user 用戶信息
* @return 菜單列表
*/
@Override
public List<Menu> selectMenusByUser(User user) {
List<Menu> menus = new LinkedList<>();
// 管理員顯示所有菜單信息
if (User.isAdmin(user.getUserId())) {
menus = menuMapper.selectMenuNormalAll();
} else {
menus = menuMapper.selectMenusByUserId(user.getUserId());
}
return menus;
}
/**
* 查詢菜單集合
*
* @return 所有菜單信息
*/
@Override
public List<Menu> selectMenuList(Menu menu) {
return menuMapper.selectMenuList(menu);
}
/**
* 查詢菜單集合
*
* @return 所有菜單信息
*/
@Override
@DataScope(parkAlias = "m")
public List<Menu> selectMenuAll() {
return menuMapper.selectMenuAll();
}
/**
* 根據(jù)用戶ID查詢權(quán)限
*
* @param userId 用戶ID
* @return 權(quán)限列表
*/
@Override
@RedisCache(key = "user_perms", fieldKey = "#userId")
public Set<String> selectPermsByUserId(Long userId) {
List<String> perms = menuMapper.selectPermsByUserId(userId);
Set<String> permsSet = new HashSet<>();
for (String perm : perms) {
if (StringUtils.isNotEmpty(perm)) {
permsSet.addAll(Arrays.asList(perm.trim().split(",")));
}
}
return permsSet;
}
@Override
public List<Menu> selectMenuIdsByRoleId(Long roleId) {
return menuMapper.selectMenuIdsByRoleId(roleId);
}
/**
* 根據(jù)角色I(xiàn)D查詢菜單
*
* @param role 角色對象
* @return 菜單列表
*/
@Override
public List<Ztree> roleMenuTreeData(Role role) {
Long roleId = role.getRoleId();
List<Ztree> ztrees = new ArrayList<Ztree>();
List<Menu> menuList = menuMapper.selectMenuAll();
if (StringUtils.isNotNull(roleId)) {
List<String> roleMenuList = menuMapper.selectMenuTree(roleId);
ztrees = initZtree(menuList, roleMenuList, true);
} else {
ztrees = initZtree(menuList, null, true);
}
return ztrees;
}
/**
* 查詢所有菜單
*
* @return 菜單列表
*/
@Override
public List<Ztree> menuTreeData() {
List<Menu> menuList = menuMapper.selectMenuAll();
List<Ztree> ztrees = initZtree(menuList);
return ztrees;
}
/**
* 查詢系統(tǒng)所有權(quán)限
*
* @return 權(quán)限列表
*/
@Override
public LinkedHashMap<String, String> selectPermsAll() {
LinkedHashMap<String, String> section = new LinkedHashMap<>();
List<Menu> permissions = menuMapper.selectMenuAll();
if (StringUtils.isNotEmpty(permissions)) {
for (Menu menu : permissions) {
section.put(menu.getMenuName(), MessageFormat.format(PREMISSION_STRING, menu.getPerms()));
}
}
return section;
}
/**
* 對象轉(zhuǎn)菜單樹
*
* @param menuList 菜單列表
* @return 樹結(jié)構(gòu)列表
*/
public List<Ztree> initZtree(List<Menu> menuList) {
return initZtree(menuList, null, false);
}
/**
* 對象轉(zhuǎn)菜單樹
*
* @param menuList 菜單列表
* @param roleMenuList 角色已存在菜單列表
* @param permsFlag 是否需要顯示權(quán)限標(biāo)識
* @return 樹結(jié)構(gòu)列表
*/
public List<Ztree> initZtree(List<Menu> menuList, List<String> roleMenuList, boolean permsFlag) {
List<Ztree> ztrees = new ArrayList<Ztree>();
boolean isCheck = StringUtils.isNotNull(roleMenuList);
for (Menu menu : menuList) {
Ztree ztree = new Ztree();
ztree.setId(menu.getMenuId());
ztree.setpId(menu.getParentId());
ztree.setName(transMenuName(menu, roleMenuList, permsFlag));
ztree.setTitle(menu.getMenuName());
if (isCheck) {
ztree.setChecked(roleMenuList.contains(menu.getMenuId() + menu.getPerms()));
}
ztrees.add(ztree);
}
return ztrees;
}
public String transMenuName(Menu menu, List<String> roleMenuList, boolean permsFlag) {
StringBuffer sb = new StringBuffer();
sb.append(menu.getMenuName());
if (permsFlag) {
sb.append("<font color=\"#888\"> " + menu.getPerms() + "</font>");
}
return sb.toString();
}
/**
* 刪除菜單管理信息
*
* @param menuId 菜單ID
* @return 結(jié)果
*/
@Override
public int deleteMenuById(Long menuId) {
return menuMapper.deleteMenuById(menuId);
}
/**
* 根據(jù)菜單ID查詢信息
*
* @param menuId 菜單ID
* @return 菜單信息
*/
@Override
public Menu selectMenuById(Long menuId) {
return menuMapper.selectMenuById(menuId);
}
/**
* 查詢子菜單數(shù)量
*
* @param parentId 父級菜單ID
* @return 結(jié)果
*/
@Override
public int selectCountMenuByParentId(Long parentId) {
return menuMapper.selectCountMenuByParentId(parentId);
}
/**
* 查詢菜單使用數(shù)量
*
* @param menuId 菜單ID
* @return 結(jié)果
*/
@Override
public int selectCountRoleMenuByMenuId(Long menuId) {
return roleMenuMapper.selectCountRoleMenuByMenuId(menuId);
}
/**
* 新增保存菜單信息
*
* @param menu 菜單信息
* @return 結(jié)果
*/
@Override
public int insertMenu(Menu menu) {
return menuMapper.insertMenu(menu);
}
/**
* 修改保存菜單信息
*
* @param menu 菜單信息
* @return 結(jié)果
*/
@Override
public int updateMenu(Menu menu) {
return menuMapper.updateMenu(menu);
}
/**
* 校驗(yàn)菜單名稱是否唯一
*
* @param menu 菜單信息
* @return 結(jié)果
*/
@Override
public String checkMenuNameUnique(Menu menu) {
Long menuId = StringUtils.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
Menu info = menuMapper.checkMenuNameUnique(menu.getMenuName(), menu.getParentId());
if (StringUtils.isNotNull(info) && info.getMenuId().longValue() != menuId.longValue()) {
return UserConstants.MENU_NAME_NOT_UNIQUE;
}
return UserConstants.MENU_NAME_UNIQUE;
}
/**
* 根據(jù)父節(jié)點(diǎn)的ID獲取所有子節(jié)點(diǎn)
*
* @param list 分類表
* @param parentId 傳入的父節(jié)點(diǎn)ID
* @return String
*/
public List<Menu> getChildPerms(List<Menu> list, int parentId) {
List<Menu> returnList = new ArrayList<Menu>();
for (Iterator<Menu> iterator = list.iterator(); iterator.hasNext(); ) {
Menu t = (Menu) iterator.next();
// 一、根據(jù)傳入的某個父節(jié)點(diǎn)ID,遍歷該父節(jié)點(diǎn)的所有子節(jié)點(diǎn)
if (t.getParentId() == parentId) {
recursionFn(list, t);
returnList.add(t);
}
}
return returnList;
}
/**
* 遞歸列表
*
* @param list
* @param t
*/
private void recursionFn(List<Menu> list, Menu t) {
// 得到子節(jié)點(diǎn)列表
List<Menu> childList = getChildList(list, t);
t.setChildren(childList);
for (Menu tChild : childList) {
if (hasChild(list, tChild)) {
// 判斷是否有子節(jié)點(diǎn)
Iterator<Menu> it = childList.iterator();
while (it.hasNext()) {
Menu n = (Menu) it.next();
recursionFn(list, n);
}
}
}
}
/**
* 得到子節(jié)點(diǎn)列表
*/
private List<Menu> getChildList(List<Menu> list, Menu t) {
List<Menu> tlist = new ArrayList<Menu>();
Iterator<Menu> it = list.iterator();
while (it.hasNext()) {
Menu n = (Menu) it.next();
if (n.getParentId().longValue() == t.getMenuId().longValue()) {
tlist.add(n);
}
}
return tlist;
}
/**
* 判斷是否有子節(jié)點(diǎn)
*/
private boolean hasChild(List<Menu> list, Menu t) {
return getChildList(list, t).size() > 0 ? true : false;
}
}
②、實(shí)體類
/**
* 云存儲配置信息
*
* @author jack
*/
@Data
public class CloudStorageConfig implements Serializable {
private static final long serialVersionUID = 9035033846176792944L;
// 類型 1:七牛 2:阿里云 3:騰訊云
@Range(min = 1, max = 3, message = "類型錯誤")//hibernate-validator-6.1.6.Final
private Integer type;
// 七牛綁定的域名(validation-api-2.0.1.Final)
@NotBlank(message = "七牛綁定的域名不能為空", groups = QiniuGroup.class)
@URL(message = "七牛綁定的域名格式不正確", groups = QiniuGroup.class)
private String qiniuDomain;
// 七牛路徑前綴
private String qiniuPrefix;
// 七牛ACCESS_KEY (validation-api-2.0.1.Final)
@NotBlank(message = "七牛AccessKey不能為空", groups = QiniuGroup.class)
private String qiniuAccessKey;
// 七牛SECRET_KEY
@NotBlank(message = "七牛SecretKey不能為空", groups = QiniuGroup.class)
private String qiniuSecretKey;
// 七牛存儲空間名
@NotBlank(message = "七??臻g名不能為空", groups = QiniuGroup.class)
private String qiniuBucketName;
// 阿里云綁定的域名(hibernate-validator-6.1.6.Final)
@NotBlank(message = "阿里云綁定的域名不能為空", groups = AliyunGroup.class)
@URL(message = "阿里云綁定的域名格式不正確", groups = AliyunGroup.class)
private String aliyunDomain;
// 阿里云路徑前綴
@Pattern(regexp = "^[^(/|\\)](.*[^(/|\\)])?$", message = "阿里云路徑前綴不能'/'或者'\'開頭或者結(jié)尾", groups = AliyunGroup.class)
private String aliyunPrefix;
// 阿里云EndPoint
@NotBlank(message = "阿里云EndPoint不能為空", groups = AliyunGroup.class)
private String aliyunEndPoint;
// 阿里云AccessKeyId
@NotBlank(message = "阿里云AccessKeyId不能為空", groups = AliyunGroup.class)
private String aliyunAccessKeyId;
// 阿里云AccessKeySecret
@NotBlank(message = "阿里云AccessKeySecret不能為空", groups = AliyunGroup.class)
private String aliyunAccessKeySecret;
// 阿里云BucketName
@NotBlank(message = "阿里云BucketName不能為空", groups = AliyunGroup.class)
private String aliyunBucketName;
// 騰訊云綁定的域名
@NotBlank(message = "騰訊云綁定的域名不能為空", groups = QcloudGroup.class)
@URL(message = "騰訊云綁定的域名格式不正確", groups = QcloudGroup.class)
private String qcloudDomain;
// 騰訊云路徑前綴
private String qcloudPrefix;
// 騰訊云SecretId
@NotBlank(message = "騰訊云SecretId不能為空", groups = QcloudGroup.class)
private String qcloudSecretId;
// 騰訊云SecretKey
@NotBlank(message = "騰訊云SecretKey不能為空", groups = QcloudGroup.class)
private String qcloudSecretKey;
// 騰訊云BucketName
@NotBlank(message = "騰訊云BucketName不能為空", groups = QcloudGroup.class)
private String qcloudBucketName;
// 騰訊云COS所屬地區(qū)
@NotBlank(message = "所屬地區(qū)不能為空", groups = QcloudGroup.class)
private String qcloudRegion;
}
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Security方法鑒權(quán)的實(shí)現(xiàn)
在Spring Security中,主要有兩種鑒權(quán)方式,一個是基于web請求的鑒權(quán),一個是基于方法的鑒權(quán),本文就來介紹一下Spring Security方法鑒權(quán)的實(shí)現(xiàn),感興趣的可以了解一下2023-12-12
springdata jpa使用Example快速實(shí)現(xiàn)動態(tài)查詢功能
這篇文章主要介紹了springdata jpa使用Example快速實(shí)現(xiàn)動態(tài)查詢功能,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
spring boot與spring mvc的區(qū)別及功能介紹
這篇文章主要介紹了spring boot與spring mvc的區(qū)別是什么以及spring boot和spring mvc功能介紹,感興趣的朋友一起看看吧2018-02-02
Java使用JSqlParser解析SQL語句實(shí)戰(zhàn)總結(jié)
JSqlParser?是一個用于解析?SQL?語句的?Java?庫,支持多種?SQL?方言,可以靈活操作?SQL?語句,并且易于集成到?Java?項(xiàng)目中,本文給大家介紹Java使用JSqlParser解析SQL語句實(shí)戰(zhàn)總結(jié),感興趣的朋友跟隨小編一起看看吧2026-01-01
Java使用CompletableFuture實(shí)現(xiàn)異步編程
在現(xiàn)代 Java 開發(fā)中,異步編程是一項(xiàng)重要技能,而 CompletableFuture 是從 Java 8 開始提供的一個功能強(qiáng)大的工具,用于簡化異步任務(wù)的編寫和組合,本文將詳細(xì)介紹 CompletableFuture 的基本使用和一些常見的應(yīng)用場景,需要的朋友可以參考下2025-01-01

