如何使用 Grails 3 上传文件
了解如何使用 Grails 3 上传文件;将其传输到一个文件夹,将其保存在数据库中的 byte[] 中,或将其上传到 AWS S3。
作者:Sergio del Amo
Grails 版本 3.3.6
1 Grails 培训
Grails 培训 - 由创建和积极维护 Grails 框架的人员开发和提供的!。
2 开始使用
在此指南中,您将编写三个不同的域类。此外,您将编写与之关联的控制器、服务和视图。
我们将探讨保存上传文件这种方法;数据库中的 byte[]、本地文件夹或远程服务器(AWS S3)。
我们将使用命令对象只允许图片(JPG 或 PNG)上传。
2.1 需要什么
要完成此指南,您需要以下内容
-
一些闲暇时间
-
一款不错的文本编辑器或 IDE
-
安装了 JDK 1.7 或更高版本,并已适当地配置了
JAVA_HOME
2.2 如何完成指南
要开始操作,请执行以下操作
-
下载并解压缩源代码
或
-
克隆 Git 仓库
git clone https://github.com/grails-guides/grails-upload-file.git
Grails 指南仓库包含两个文件夹
-
initial
初始项目。通常是一个带有附加代码的简单 Grails 应用,以此为您提供一个良好的开端。 -
complete
完成的示例。这是贯穿指南提供的步骤并对initial
文件夹应用这些更改的结果。
要完成指南,请转至 initial
文件夹
-
cd
转至grails-guides/grails-upload-file/initial
并按照下一部分中的说明进行操作。
如果您 cd 转至 grails-guides/grails-upload-file/complete ,可以转到已完成的示例 |
3 编写应用程序
initial
文件夹包含使用 web
概要文件创建的 Grails 3 应用程序。它有多个控制器、GORM 数据服务、域名类以及用 GSP 构建的简单 CRUD 界面。它是一个用于列出酒店、餐厅和重要景点等旅游资源的应用。
在接下来的部分中,您将为应用添加一项功能。这些资源中的每个资源可以包含一个精选图片。
-
对于餐厅,我们将直接将图片字节存储在域名类中。
-
对于重要景点,我们将文件传输至一个文件夹。
-
对于酒店,我们将文件上传至 AWS S3。
3.1 精选图片命令对象
我们应用程序中的每个实体(酒店、餐厅和重要景点)都将有精选图片。
为封装上传文件的验证,我们将使用 Grails 命令对象
package example.grails
import grails.validation.Validateable
import org.springframework.web.multipart.MultipartFile
class FeaturedImageCommand implements Validateable {
MultipartFile featuredImageFile
Long id
Integer version
static constraints = {
id nullable: false
version nullable: false
featuredImageFile validator: { val, obj ->
if ( val == null ) {
return false
}
if ( val.empty ) {
return false
}
['jpeg', 'jpg', 'png'].any { extension -> (1)
val.originalFilename?.toLowerCase()?.endsWith(extension)
}
}
}
}
1 | 仅允许 JPG 或 PNG 结尾的文件 |
3.2 增加允许的文件大小
Grails 3 的默认文件大小为 128000(约 128KB)。
我们将允许 25MB 的文件上传。
25 * 1024 * 1024 = 26.214.400 字节
grails:
controllers:
upload:
maxFileSize: 26214400
maxRequestSize: 26214400
4 餐厅 - 上传并保存为 byte[]
本指南部分展示了如何将文件上传至 Grails 服务器应用以及将文件的字节存储在数据库中。
4.1 餐厅域名类
修改 Restaurant
域名类。添加两个属性 featuredImageBytes
和 featuredImageContentType
。
package example.grails
class Restaurant {
String name
byte[] featuredImageBytes (1)
String featuredImageContentType (2)
static constraints = {
featuredImageBytes nullable: true
featuredImageContentType nullable: true
}
static mapping = {
featuredImageBytes column: 'featured_image_bytes', sqlType: 'longblob' (3)
}
}
1 | 使用域名类属性来在数据库中存储图像的字节 |
2 | 图像的内容类型。例如“image/jpeg” |
3 | 图像可能是大文件,我们使用映射闭包来配置 sqlType |
4.2 餐厅视图
我们将稍微修改视图,以包括可用于上传精选图片的功能。
在餐厅列表中,表中只显示餐厅名称。查看 Fields 插件 f:table 文档。
<f:table collection="${restaurantList}" properties="['name']"/>
编辑和创建表单不允许用户设置精选图片的内容类型或 byte[]
<f:all bean="restaurant" except="featuredImageBytes,featuredImageContentType"/>
<f:all bean="restaurant" except="featuredImageBytes,featuredImageContentType"/>
在显示餐厅页面中添加一个按钮,该按钮将用户带到精选图片编辑页面。
<g:link class="edit" action="editFeaturedImage" resource="${this.restaurant}"><g:message code="restaurant.featuredImageUrl.edit.label" default="Edit Featured Image" /></g:link>
修改 `RestaurantController`。添加 `editFeaturedImage` 控制器操作。
def editFeaturedImage(Long id) {
Restaurant restaurant = restaurantDataService.get(id)
if (!restaurant) {
notFound()
}
[restaurant: restaurant]
}
`editFeaturedImage` GSP 与 `edit` GSP 相同,但它使用 `g:uploadForm` 而不是 `g:form`
<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
<g:hiddenField name="id" value="${this.restaurant?.id}" />
<g:hiddenField name="version" value="${this.restaurant?.version}" />
<input type="file" name="featuredImageFile" />
<fieldset class="buttons">
<input class="save" type="submit" value="${message(code: 'restaurant.featuredImage.upload.label', default: 'Upload')}" />
</fieldset>
</g:uploadForm>
4.3 上传精选图片服务
`uploadFeatureImage` 控制器操作使用之前描述的命令对象来验证上传表单提交。如果它未找到验证错误,则调用 Gorm 数据服务。
def uploadFeaturedImage(FeaturedImageCommand cmd) {
if (cmd == null) {
notFound()
return
}
if (cmd.hasErrors()) {
respond(cmd.errors, model: [restaurant: cmd], view: 'editFeaturedImage')
return
}
Restaurant restaurant = restaurantDataService.update(cmd.id,
cmd.version,
cmd.featuredImageFile.bytes,
cmd.featuredImageFile.contentType)
if (restaurant == null) {
notFound()
return
}
if (restaurant.hasErrors()) {
respond(restaurant.errors, model: [restaurant: restaurant], view: 'editFeaturedImage')
return
}
Locale locale = request.locale
flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), cmd.id, locale)
redirect restaurant
}
完整 GORM 数据服务在以下代码段中显示
package example.grails
import grails.gorm.services.Service
@Service(Restaurant)
interface RestaurantDataService {
Restaurant get(Long id)
List<Restaurant> list(Map args)
Number count()
void delete(Serializable id)
Restaurant update(Serializable id, Long version, String name)
Restaurant update(Serializable id, Long version, byte[] featuredImageBytes, String featuredImageContentType) (1)
Restaurant save(String name)
}
1 | 第一个参数应该是要更新的对象的 ID。如果任何参数与域类上的属性不匹配,则会发生编译错误。 |
4.4 加载字节数组图像
`RestaurantController` 中创建一个操作来呈现餐厅的精选图片
def featuredImage(Long id) {
Restaurant restaurant = restaurantDataService.get(id)
if (!restaurant || restaurant.featuredImageBytes == null) {
notFound()
return
}
render file: restaurant.featuredImageBytes,
contentType: restaurant.featuredImageContentType
}
使用 `g:createLink` GSP 标记来引用 `img` `src` 属性中的前一个控制器操作。
<h1><f:display bean="restaurant" property="name" /></h1>
<g:if test="${this.restaurant.featuredImageBytes}">
<img src="<g:createLink controller="restaurant" action="featuredImage" id="${this.restaurant.id}"/>" width="400"/>
</g:if>
5 兴趣点 - 上传和转让
在本指南的这一部分中,我们已经将 Grails 应用程序与上传的图片分开了。我们使用 MAMP 在端口 8888 处运行本地 Apache 服务器,指向 data 文件夹。
5.1 兴趣点域类
修改 `PointOfInterest` 域类。添加一个属性 `featuredImageUrl`
package example.grails
class PointOfInterest {
String name
String featuredImageUrl (1)
static constraints = {
featuredImageUrl nullable: true
}
}
1 | 使用此属性来保存可以检索图像的 url。 |
5.2 兴趣点视图
我们将稍微修改 GSP 视图,以包括启用上传精选图片的功能。
在兴趣点列表中,表中只显示酒店的名称。查看 Fields 插件 f:table 文档。
<f:table collection="${pointOfInterestList}" properties="['name']"/>
编辑和创建表单不允许用户设置精选图片的 url
<f:all bean="pointOfInterest" except="featuredImageUrl"/>
<f:all bean="pointOfInterest" except="featuredImageUrl"/>
相反,我们添加一个按钮,将用户带到精选图片编辑页面。
<g:link class="edit" action="editFeaturedImage" resource="${this.pointOfInterest}"><g:message code="pointOfInterest.featuredImageUrl.edit.label" default="Edit Featured Image" /></g:link>
修改 `PointOfInterestController`。添加一个名为 `editFeaturedImage` 的控制器操作
def editFeaturedImage(Long id) {
PointOfInterest pointOfInterest = pointOfInterestDataService.get(id)
if (!pointOfInterest) {
notFound()
return
}
[pointOfInterest: pointOfInterest]
}
`editFeaturedImage` GSP 与 `edit` GSP 相同,但它使用 `g:uploadForm` 而不是 `g:form`
<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
<g:hiddenField name="id" value="${this.pointOfInterest?.id}" />
<g:hiddenField name="version" value="${this.pointOfInterest?.version}" />
<input type="file" name="featuredImageFile" />
<fieldset class="buttons">
<input class="save" type="submit" value="${message(code: 'pointOfInterest.featuredImage.upload.label', default: 'Upload')}" />
</fieldset>
</g:uploadForm>
5.3 上传特色图像服务
uploadFeatureImage
控制器操作使用先前描述的命令对象来验证上传表单提交。如果它找不到验证错误,它将调用服务。
def uploadFeaturedImage(FeaturedImageCommand cmd) {
if (cmd.hasErrors()) {
respond(cmd, model: [pointOfInterest: cmd], view: 'editFeaturedImage')
return
}
PointOfInterest pointOfInterest = uploadPointOfInterestFeaturedImageService.uploadFeatureImage(cmd)
if (pointOfInterest == null) {
notFound()
return
}
if (pointOfInterest.hasErrors()) {
respond(pointOfInterest, model: [pointOfInterest: pointOfInterest], view: 'editFeaturedImage')
return
}
Locale locale = request.locale
flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), pointOfInterest.id, locale)
redirect pointOfInterest
}
我们配置将托管上传图像的本地服务器 URL 和我们将用于保存图像的本地文件夹路径。
grails:
guides:
cdnFolder: /Users/sdelamo/Sites/GrailsGuides/UploadFiles
cdnRootUrl: http://localhost:8888
服务使用 transferTo
方法将文件传输到本地文件路径。如果发生错误,该服务将删除它先前传输的文件
package example.grails
import grails.config.Config
import grails.core.support.GrailsConfigurationAware
import groovy.transform.CompileStatic
@SuppressWarnings('GrailsStatelessService')
@CompileStatic
class UploadPointOfInterestFeaturedImageService implements GrailsConfigurationAware {
PointOfInterestDataService pointOfInterestDataService
String cdnFolder
String cdnRootUrl
@Override
void setConfiguration(Config co) {
cdnFolder = co.getRequiredProperty('grails.guides.cdnFolder')
cdnRootUrl = co.getRequiredProperty('grails.guides.cdnRootUrl')
}
@SuppressWarnings('JavaIoPackageAccess')
PointOfInterest uploadFeatureImage(FeaturedImageCommand cmd) {
String filename = cmd.featuredImageFile.originalFilename
String folderPath = "${cdnFolder}/pointOfInterest/${cmd.id}"
File folder = new File(folderPath)
if ( !folder.exists() ) {
folder.mkdirs()
}
String path = "${folderPath}/${filename}"
cmd.featuredImageFile.transferTo(new File(path))
String featuredImageUrl = "${cdnRootUrl}//pointOfInterest/${cmd.id}/${filename}"
PointOfInterest poi = pointOfInterestDataService.updateFeaturedImageUrl(cmd.id, cmd.version, featuredImageUrl)
if ( !poi || poi.hasErrors() ) {
File f = new File(path)
f.delete()
}
poi
}
}
该服务借助 GORM 数据服务来更新 featuredImageUrl
package example.grails
import grails.gorm.services.Service
@SuppressWarnings(['LineLength', 'UnusedVariable', 'SpaceAfterOpeningBrace', 'SpaceBeforeClosingBrace'])
@Service(PointOfInterest)
interface PointOfInterestDataService {
PointOfInterest get(Long id)
List<PointOfInterest> list(Map args)
Number count()
void delete(Serializable id)
PointOfInterest save(String name)
PointOfInterest updateName(Serializable id, Long version, String name)
PointOfInterest updateFeaturedImageUrl(Serializable id, Long version, String featuredImageUrl)
}
6 酒店 - 上传到 AWS S3
对于 Hotel
域类,我们将把文件上传到 Amazon AWS S3。
首先,安装 Grails AWS SDK S3 插件
修改 your build.grade
文件
repositories {
...
..
maven { url 'http://dl.bintray.com/agorapulse/libs' }
}
dependencies {
...
..
.
compile "org.grails.plugins:aws-sdk-s3:$awsSdkS3Version"
}
在 gradle.properties
中定义 awsSdkS3Version
grailsVersion=3.3.6
gormVersion=6.1.10.RELEASE
hibernateCoreVersion=5.1.5.Final
gebVersion=1.1.2
htmlunitDriverVersion=2.47.1
htmlunitVersion=2.18
gradleWrapperVersion=3.5
assetPipelineVersion=2.14.8
awsSdkS3Version=2.2.4
我们将把文件上传到一个已存在的存储桶。
grails:
plugins:
awssdk:
s3:
region: eu-west-1
bucket: grails-guides
6.1 酒店域类
修改 Hotel
域类。添加两个属性 featuredImageKey
和 featuredImageUrl`
package example.grails
class Hotel {
String name
String featuredImageUrl (1)
String featuredImageKey (2)
static constraints = {
featuredImageUrl nullable: true
featuredImageKey nullable: true
}
}
1 | 我们存储特色图像的 AWS S3 url |
2 | 我们存储文件路径。如有必要,则可用于删除文件。 |
6.2 酒店视图
我们将稍微修改视图,以包括可用于上传精选图片的功能。
在酒店列表中,仅在表中显示酒店名称。查看 Fields 插件 f:table 文档。
<f:table collection="${hotelList}" properties="['name']"/>
编辑和创建表单不允许用户设置精选图片的 url
<f:all bean="hotel" except="featuredImageUrl,featuredImageKey"/>
<f:all bean="hotel" except="featuredImageUrl,featuredImageKey"/>
相反,我们添加一个按钮,将用户带到精选图片编辑页面。
<g:link class="edit" action="edit" resource="${this.hotel}"><g:message code="default.button.edit.label" default="Edit" /></g:link>
修改 HotelController
。添加名为 editFeaturedImage
的控制器操作
def editFeaturedImage(Long id) {
Hotel hotel = hotelDataService.get(id)
if (!hotel) {
notFound()
return
}
[hotel: hotel]
}
`editFeaturedImage` GSP 与 `edit` GSP 相同,但它使用 `g:uploadForm` 而不是 `g:form`
<g:uploadForm name="uploadFeaturedImage" action="uploadFeaturedImage">
<g:hiddenField name="id" value="${this.hotel?.id}" />
<g:hiddenField name="version" value="${this.hotel?.version}" />
<input type="file" name="featuredImageFile" />
<fieldset class="buttons">
<input class="save" type="submit" value="${message(code: 'hotel.featuredImage.upload.label', default: 'Upload')}" />
</fieldset>
</g:uploadForm>
6.3 上传图像到 S3
uploadFeatureImage
控制器操作使用先前描述的命令对象来验证上传表单提交。如果它找不到验证错误,它将调用服务。
def uploadFeaturedImage(FeaturedImageCommand cmd) {
if (cmd.hasErrors()) {
respond(cmd.errors, model: [hotel: cmd], view: 'editFeaturedImage')
return
}
def hotel = uploadHotelFeaturedImageService.uploadFeatureImage(cmd)
if (hotel == null) {
notFound()
return
}
if (hotel.hasErrors()) {
respond(hotel.errors, model: [hotel: hotel], view: 'editFeaturedImage')
return
}
Locale locale = request.locale
flash.message = crudMessageService.message(CRUD.UPDATE, domainName(locale), hotel.id, locale)
redirect hotel
}
该服务使用插件提供的 amazonS3Service
将文件上传到 AWS S3。如果发生错误,该服务将删除它先前上传的文件。
package example.grails
import grails.gorm.transactions.Transactional
import grails.plugin.awssdk.s3.AmazonS3Service
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
@CompileStatic
@Slf4j
@Transactional
class UploadHotelFeaturedImageService {
AmazonS3Service amazonS3Service
HotelDataService hotelDataService
Hotel uploadFeatureImage(FeaturedImageCommand cmd) {
String path = "hotel/${cmd.id}/${cmd.featuredImageFile.originalFilename}"
String s3FileUrl = amazonS3Service.storeMultipartFile(path, cmd.featuredImageFile)
Hotel hotel = hotelDataService.update(cmd.id, cmd.version, path, s3FileUrl)
if ( !hotel || hotel.hasErrors() ) {
deleteFileByPath(path)
}
hotel
}
boolean deleteFileByPath(String path) {
boolean result = amazonS3Service.deleteFile(path)
if (!result) {
log.warn 'could not remove file {}', path
}
result
}
}
借助 GORM 数据服务更新域类实例
package example.grails
import grails.gorm.services.Service
@Service(Hotel)
interface HotelDataService {
Hotel get(Long id)
List<Hotel> list(Map args)
Number count()
void delete(Serializable id)
Hotel save(String name)
Hotel update(Serializable id, Long version, String name)
Hotel update(Serializable id, Long version, String featuredImageKey, String featuredImageUrl)
}
6.4 使用 AWS 访问密钥和密钥运行应用程序
如果你不提供凭据,则将使用凭据提供程序链,按以下顺序搜索凭据
环境变量 - AWS_ACCESS_KEY_ID 和 AWS_SECRET_KEY
Java 系统属性 - aws.accessKeyId andaws.secretKey`
通过 Amazon EC2 元数据服务(IAM 角色)传递的实例配置文件凭据
因此,我们可以通过运行应用程序
./gradlew bootRun -Daws.accessKeyId=XXXXX -Daws.secretKey=XXXXX