显示导航

如何使用 Grails 4 上传文件

了解如何使用 Grails 4 上传文件;将它们传输到某个文件夹,将它们保存为数据库中的 byte[] 或将它们上传到 AWS S3。

作者: Sergio del Amo

Grails 版本 4.0.1

1 Grails 培训

Grails 培训 - 由 Grails 框架的创建者和积极维护者开发并交付!。

2 入门

在本指南中,您将编写三个不同的域类。此外,您还将编写与其关联的控制器、服务和视图。

我们将探索不同的方式来保存上传的文件;数据库中的 byte[]、本地文件夹或远程服务器 (AWS S3)。

我们将使用命令对象来只允许图像(JPG 或 PNG)上传。

2.1 需要准备的内容

要完成本指南,您需要准备以下内容

  • 一些时间

  • 一个不错的文本编辑器或 IDE

  • 已安装 JDK 1.8 或更高版本,并适当地配置了 JAVA_HOME

2.2 如何完成本指南

要开始,请执行以下操作

或者

Grails 指南存储库包含两个文件夹

  • initial 初始项目。通常是带有附加代码以给你一个开始指导的简单 Grails 应用程序。

  • complete 完成的示例。这是完成指南提供的步骤并将这些更改应用于 initial 文件夹的结果。

要完成指南,请转到 initial 文件夹

  • cd 进入 grails-guides/grails-upload-file/initial

并按照下一部分中的说明进行操作。

如果你 cd 进入 grails-guides/grails-upload-file/complete,则可以直接转到完成的示例

3 编写应用程序

initial 文件夹包含使用 web 配置文件创建的 Grails 4 应用程序。它有几个 Controller、GORM 数据服务、Domian 类和一个用 GSP 构建的简单 CRUD 界面。这是一个用于列出旅游资源的应用程序;酒店、饭店和兴趣点。

在下一部分中,你将为应用程序添加一个功能。每个资源都可以包含一个精选图像。

  • 对于饭店,我们将图片字节直接存储在域类中。

  • 对于兴趣点,我们将文件传输到一个文件夹中。

  • 对于酒店,我们上传文件到 AWS S3。

3.1 精选图像命令对象

我们的每个应用程序实体(酒店、饭店和兴趣点)都将有一个精选图像。

为了封装上传的文件的验证;我们使用了一个 Grails 命令对象

/grails-app/controllers/example/grails/FeaturedImageCommand.groovy
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 4 的默认文件大小是 128000(~128KB)。

我们将允许上传 25MB 的文件。

25 * 1024 * 1024 = 26.214.400 字节

/grails-app/conf/application.yml
grails:
 controllers:
  upload:
    maxFileSize: 26214400
    maxRequestSize: 26214400

4 饭店 - 上传并保存为字节数组

本指南的这部分说明如何将文件上传到 Grails 服务器应用程序并将文件的字节存储在数据库中。

bytearray

4.1 饭店域类

修改 Restaurant 域类。添加两个属性 featuredImageBytesfeaturedImageContentType

/grails-app/domain/example/grails/Restaurant.groovy
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 Plugin f:table 文档。

/grails-app/views/restaurant/index.gsp
<f:table collection="${restaurantList}" properties="['name']"/>

编辑和创建表单不允许用户设置特色图片的内容类型或 byte[]

/grails-app/views/restaurant/create.gsp
<f:all bean="restaurant" except="featuredImageBytes,featuredImageContentType"/>
/grails-app/views/restaurant/edit.gsp
<f:all bean="restaurant" except="featuredImageBytes,featuredImageContentType"/>

在显示餐馆页面中,添加一个按钮,将用户带到特色图片编辑页面。

/grails-app/views/restaurant/show.gsp
<g:link class="edit" action="editFeaturedImage" resource="${this.restaurant}"><g:message code="restaurant.featuredImageUrl.edit.label" default="Edit Featured Image" /></g:link>

修改 RestaurantController。添加 editFeaturedImage 控制器的动作

/grails-app/controllers/example/grails/RestaurantController.groovy
def editFeaturedImage(Long id) {
    Restaurant restaurant = restaurantDataService.get(id)
    if (!restaurant) {
        notFound()
    }
    [restaurant: restaurant]
}

editFeaturedImage GSP 与 edit GSP 相同,但它使用 g:uploadForm 而不是 g:form

/grails-app/views/restaurant/editFeaturedImage.gsp
<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 数据服务

/grails-app/controllers/example/grails/RestaurantController.groovy
    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 数据服务显示在以下代码片段中

/grails-app/services/example/grails/RestaurantDataService.groovy
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 加载 byte 数组图片

RestaurantController 中创建一个动作,用以渲染餐馆的特色图片

/grails-app/controllers/example/grails/RestaurantController.groovy
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 属性中引用之前的控制器动作。

/grails-app/views/restaurant/show.gsp
<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 运行一个指向 data 文件夹的 8888 端口上的本地 apache 服务器。

transfer

5.1 兴趣点域类

修改 PointOfInterest 域类。添加一个属性 featuredImageUrl

/grails-app/domain/example/grails/PointOfInterest.groovy
package example.grails

class PointOfInterest {
    String name
    String featuredImageUrl (1)

    static constraints = {
        featuredImageUrl nullable: true
    }
}
1 使用此属性来保存可以检索图片的 URL。

5.2 兴趣点视图

我们准备对 GSP 视图进行一些修改,以加入支持上传特色图片的功能。

在兴趣点列表中,仅在表格中显示酒店名称。查阅 Fields Plugin f:table 文档。

/grails-app/views/pointOfInterest/index.gsp
<f:table collection="${pointOfInterestList}" properties="['name']"/>

编辑和创建表单不允许用户设置特色图片的 URL

/grails-app/views/pointOfInterest/create.gsp
<f:all bean="pointOfInterest" except="featuredImageUrl"/>
/grails-app/views/pointOfInterest/edit.gsp
<f:all bean="pointOfInterest" except="featuredImageUrl"/>

相反添加一个按钮,将用户带到特色图片编辑页面。

/grails-app/views/pointOfInterest/show.gsp
<g:link class="edit" action="editFeaturedImage" resource="${this.pointOfInterest}"><g:message code="pointOfInterest.featuredImageUrl.edit.label" default="Edit Featured Image" /></g:link>

修改 PointOfInterestController。添加一个名为 editFeaturedImage 的控制器动作

/grails-app/controllers/example/grails/PointOfInterestController.groovy
def editFeaturedImage(Long id) {
    PointOfInterest pointOfInterest = pointOfInterestDataService.get(id)
    if (!pointOfInterest) {
        notFound()
        return
    }
    [pointOfInterest: pointOfInterest]
}

editFeaturedImage GSP 与 edit GSP 相同,但它使用 g:uploadForm 而不是 g:form

/grails-app/views/pointOfInterest/editFeaturedImage.gsp
<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 控制器操作使用先前描述的命令对象来验证上传表单提交。如果它未找到验证错误,则它调用一项服务。

/grails-app/controllers/example/grails/PointOfInterestController.groovy
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-app/conf/application.yml
grails:
    guides:
        cdnFolder: /Users/sdelamo/Sites/GrailsGuides/UploadFiles
        cdnRootUrl: http://localhost:8888

服务使用 transferTo 方法将文件传输到本地文件路径。如果发生错误,服务将删除它先前传输的文件。

/grails-app/services/example/grails/UploadPointOfInterestFeaturedImageService.groovy
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

/grails-app/services/example/grails/PointOfInterestDataService.groovy
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

s3

首先,安装 Grails AWS SDK S3 插件

修改 build.grade 文件

build.gradle
repositories {
...
..
    maven { url 'http://dl.bintray.com/agorapulse/libs' }
}
dependencies {
...
..
.
    compile "org.grails.plugins:aws-sdk-s3:$awsSdkS3Version"
    compile "com.amazonaws:aws-java-sdk-s3:1.11.375"
}

gradle.properties 中定义 awsSdkS3Version

gradle.properties
grailsVersion=4.0.1
gormVersion=7.0.2.RELEASE
hibernateCoreVersion=5.4.0.Final
gebVersion=3.2
htmlunitDriverVersion=2.47.1
htmlunitVersion=2.35.0
assetPipelineVersion=3.0.11
awsSdkS3Version=2.2.4

我们将文件上传到已存在的存储桶。

/grails-app/conf/application.yml
grails:
    plugins:
        awssdk:
            s3:
                region: eu-west-1
                bucket: grails-guides

6.1 酒店域名类

修改 Hotel 域名类。添加两个属性 featuredImageKeyfeaturedImageUrl

/grails-app/domain/example/grails/Hotel.groovy
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 酒店视图

我们将对视图进行轻微修改以包含启用上传精选图像的功能。

在酒店列表中,仅在表格中显示酒店名称。查看 字段插件 f:table 文档。

/grails-app/views/hotel/index.gsp
<f:table collection="${hotelList}" properties="['name']"/>

编辑和创建表单不允许用户设置特色图片的 URL

/grails-app/views/hotel/create.gsp
<f:all bean="hotel" except="featuredImageUrl,featuredImageKey"/>
/grails-app/views/hotel/edit.gsp
<f:all bean="hotel" except="featuredImageUrl,featuredImageKey"/>

相反添加一个按钮,将用户带到特色图片编辑页面。

/grails-app/views/hotel/show.gsp
<g:link class="edit" action="edit" resource="${this.hotel}"><g:message code="default.button.edit.label" default="Edit" /></g:link>

修改 HotelController。添加一个名为 editFeaturedImage 的控制器操作

/grails-app/controllers/example/grails/HotelController.groovy
def editFeaturedImage(Long id) {
    Hotel hotel = hotelDataService.get(id)
    if (!hotel) {
        notFound()
        return
    }
    [hotel: hotel]
}

editFeaturedImage GSP 与 edit GSP 相同,但它使用 g:uploadForm 而不是 g:form

/grails-app/views/hotel/editFeaturedImage.gsp
<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 控制器操作使用先前描述的命令对象来验证上传表单提交。如果它未找到验证错误,则它调用一项服务。

/grails-app/controllers/example/grails/HotelController.groovy
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。如果发生错误,服务将删除它先前上传的文件。

/grails-app/services/example/grails/UploadHotelFeaturedImageService.groovy
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 数据服务的帮助下更新域名类实例

/grails-app/services/example/grails/HotelDataService.groovy
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 和 aws.secretKey`

  • 通过 Amazon EC2 元数据服务(IAM 角色)传递的实例配置文件凭证

因此,我们可以使用以下命令运行该应用程序

./gradlew bootRun -Daws.accessKeyId=XXXXX -Daws.secretKey=XXXXX

7 您需要关于 Grails 的帮助吗?

Object Computing, Inc.(OCI)赞助了本指南的创建。提供各种咨询和支持服务。

OCI 是 Grails 的家

了解团队