如何在Spring Boot后端使用图像(Kotlin)

127 阅读8分钟

在Spring Boot后端使用图像(Kotlin)工作

图片文件是网络的一个相当重要的部分。想象一下一个没有个人照片、社交媒体上的帖子,或者最重要的是没有猫咪gif的世界。能够动态地保存和放置图片是一个相当大的问题,但我们很多人都认为这是理所当然的。

简介

Spring Boot使保存图片和通过REST端点传输图片变得容易。在本指南中,我们将介绍如何在Kotlin中做到这一点。在本教程结束时,你将能够独立完成这项工作,并理解其背后的理论。

前提条件

对于本指南,最好从Spring Boot的一些基础知识开始。这包括创建简单的REST APIsSpring Data和常见的设计模式。我也在尝试一些不同的东西,使用Kotlin作为本指南的示例代码。对Kotlin有基本的了解会有帮助,但作为一个纯Java开发者,你还是应该能够跟上。

在客户端,你应该了解基本的JavaScript、fetch API箭头函数。当然,fetch API也使用了承诺,但如果你对承诺不甚了解,也不会有问题。此外,你应该了解网络客户端和服务器之间通过HTTP的典型流程。

设置我们的项目

为了启动我们的项目,我们当然要从Spring初始化器开始。对于我的项目,我选择了Java 11,尽管我认为Java的版本在这里应该不重要。我将语言设置为Kotlin,将依赖管理器设置为Maven,将打包设置为jar。然后你当然需要配置你的包名、组和工件ID、项目的名称和描述。

在这里,我们选择spring-boot-starter-webspring-boot-starter-data-jpa依赖项。除此之外,你还需要选择数据库的依赖关系。我们不会在application.properties 文件中去配置你的数据库,以允许你使用任何你想要的数据库。只要确保你在application.properties 文件中设置了以下属性,并与你的数据库相匹配。

  • spring.datasource.username
  • spring.datasource.password
  • spring.datasource.driver-class-name
  • spring.datasource.url

然而,在我们的案例中,我们将使用一个SQL数据库。使用无SQL数据库将改变这里的情况,因为我们正在使用SQL数据库的hibernate注释。如果你不知道,hibernate包含在Spring Data中,允许你从类中生成数据库表。

对于客户端,我们将使用vanilla JavaScript。如果你想使用一个框架,同样的概念也应该适用。

第1步:创建一个数据库实体

首先,让我们创建一个将存储图像的实体。作为一个例子,我们将创建一个假的User 实体,它将有一个个人资料图片属性。在本指南中,我们只是将一个样本用户保存到表中,并更新该用户的个人资料图片。实体的代码应该是这样的。

import javax.persistence.*

@Entity
data class User(
    // auto-generate the user’s id
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    var id: Long,

    // the database column is not nullable, has a max length of 30 characters, is called “username”, and must be unique across all entries
    @Column(nullable = false, length = 30, name = "username", unique = true)
    var username: String,
    // the database column is not nullable and has a name of “password”
    @Column(nullable = false, name = "password")
    var password: String,

    // the database column is called "profile_picture" and it has a type of BLOB
    @Lob
    @Column(name="profile_picture")
    var profilePicture: ByteArray

)

请注意,我们将个人资料图片保存为ByteArray ,并且用@Lob 来注解。这就是我们如何告诉hibernate让我们的表把这个字段存储为二进制大对象(BLOB)。这个字节数据将用于一个图像,并可以被转换回一个实际的图像文件。接下来,你需要创建一个匹配的JpaRepository来查询这个表。

@Repository
interface UserRepo : JpaRepository<User, Long>

第2步:创建一个服务和REST端点

现在,让我们创建一个简单的服务来检索和更新一个用户的个人资料图片。

@Service
class UserService(private val userRepo: UserRepo) {

    fun setProfilePicture(id: Long, file: MultipartFile){
        val user : User = userRepo.findById(id).orElseThrow()
        user.profilePicture = file.bytes
        userRepo.save(user)
    }

    fun getProfilePicture(id: Long): ByteArray{
        val user: User = userRepo.findById(id).orElseThrow()
        return user.profilePicture
    }

}

注意setProfilePicture 方法是如何接收一个MultipartFile 对象的。这就是Spring通过REST控制器从客户端发送图片数据的方式。使用该对象,我们通过字节属性获取其字节数据。

现在我们已经创建了这个服务,我们可以开始制作REST端点来展示它。让我们从设置个人资料图片的端点开始。

@RestController
class UserController(private val userService: UserService) {

    @PostMapping(value= ["/api/user/{userId}/profile-picture"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
    fun setProfilePicture(@PathVariable("userId") id: Long, @RequestParam file: MultipartFile): ResponseEntity<Void>{
        return try {
                userService.setProfilePicture(id, file)
                ResponseEntity
                    .created(URI("/api/user/${id}/profile-picture"))
                    .build()
            } catch(error: NoSuchElementException){
                ResponseEntity
                    .notFound()
                    .build()
            }
    }

}

对于我们的URL,我们有一个路径变量,用来获取用户的ID以检索他们的数据库条目。这个路径变量在URL中被标记为userId,并作为一个Long 参数传递。在本指南中,我在数据库中硬编码了一个ID为1的用户,我们将改变他的资料图片。

@PostMapping 注解的consumeres参数定义了我们的端点将获取的数据类型。在这种情况下,我们正在消费一个多部分表单数据的媒体类型。在多部分表单数据中,客户端发送的字节数据代表一个表单。表单中的每个字段都由一个特殊的分隔符分隔,该分隔符在请求的内容类型头中指定。所以在我们的例子中,客户端将把文件作为表单数据发送,我们的服务器将把它解析为一个MultipartFile 对象。正如@RequestParam 注解所建议的,我们从请求中检索它作为一个请求参数。

在该方法的主体中,我们首先尝试返回一个ResponseEntity ,其中包含客户端可以从我们的服务器访问个人资料图片的URL。如果我们的服务抛出了一个NoSuchElementException (如果我们不能找到具有该ID的用户),那么我们将返回一个404响应。

现在让我们做一个REST端点,让客户可以访问他们的个人资料图片。

@GetMapping("/api/user/{userId}/profile-picture")
fun getProfilePicture(@PathVariable("userId") id: Long): ResponseEntity<Any>{

  return try {
      val image: ByteArray = userService.getProfilePicture(id)

      ResponseEntity.ok()
        .contentType(MediaType.parseMediaType(MediaType.IMAGE_JPEG_VALUE))
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"${System.currentTimeMillis()}\"")
        .body(image)

      } catch(error: NoSuchElementException){
          ResponseEntity
            .notFound()
            .build()
      }

}

在这里,我们像上次一样根据URL中的id来检索用户。如果用户不存在,即服务抛出一个NoSuchElementException ,那么我们返回一个404响应。否则,我们会得到代表个人资料图片的字节,并尝试将其作为JPEG图片返回给客户端。我们通过创建一个新的ResponseEntity ,状态为ok,内容类型为image/jpeg。这个ResponseEntity 的主体是我们图片的字节数。

最后,我们设置content-disposition头,告诉客户端我们的响应是一个附件。我们还在该头中指定了附件的名称,在本例中就是当前时间。

第3步:创建一个客户端

现在让我们开始创建一个客户端来测试这个Spring Boot应用程序。首先,创建一个简单的HTML页面,用户可以选择一个图片来保存。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <img alt="your-image" id="your-image">
    <input id="image-to-save" type="file" accept="image/*">
    <script src="main.js"></script>
</body>
</html>

注意,我们在页面上有一个图像标签,以后我们会用它来显示后台的图像。这就是为什么它没有一个src 属性;它将在我们的main.js 文件中动态添加。

让我们看看我们将在main.js 文件中做什么来检索和保存图像。在JavaScript文件中,我们将创建一个事件监听器,当文件输入的值发生变化时。这将向服务器发送一个带有图片的POST请求,以便我们可以保存它。当响应返回图像的URL时,我们将在我们的图像元素上显示该图像。

document.getElementById('image-to-save').addEventListener('change', saveImage);

function saveImage(e){

  let file = e.target.files[0];
  const formData = new FormData();
  formData.append('file', file);

  fetch("http://localhost:8080/api/user/1/profile-picture", {
      method: 'POST',
      body: formData
  }).then(data => {
      document.getElementById("your-image").src = data.url + '?t=' + new Date().getTime();
  });

}

首先,我们从我们的事件对象中获取文件并将其存储在一个FormData 对象中。然后,使用fetch API,我们用表单数据向适当的端点发送一个post请求。使用该方法返回的承诺,我们为我们的响应到达时提供一个回调。这个回调为我们的图片设置了src 属性,这样我们就可以在屏幕上显示它。回想一下,我们用于设置个人资料图片的端点会发回客户端可以访问该图片的URL。

这里值得注意的一点是,我们没有手动设置内容类型头。这是因为浏览器会处理将我们的FormData 对象转换为带有随机分隔符的多部分表单数据。这样做之后,浏览器会用适当的值为我们设置头。

请注意,我们在URL的末尾为当前时间添加了一个查询参数。我们这样做的原因是为了迫使浏览器在每次响应回来时都要刷新。这是因为图片总是来自同一个端点*http://localhost:8080/api/user/1/profile-picture。*在我们发送请求后,浏览器不会知道来自该URL的资源发生了变化。在末尾添加查询参数会改变URL,只要响应回来,就会迫使浏览器刷新图像。将这个查询参数放在URL的末尾是完全有效的,即使我们从未在服务器端解析或提及它。

第4步:配置 CORS

如果你用到目前为止的代码来运行这个例子,你应该看到一个错误,看起来像这样。

sample cors error

这是许多人,包括我自己,作为一个新的开发者所面临的一个常见问题。这是因为我们没有配置适当的 CORS(跨源资源共享)设置。默认情况下,我们的浏览器不允许我们从另一个来源(网络服务器)获取资源(即图片)。这就是上面所说的同源策略的意思。值得庆幸的是,Spring让配置CORS变得相当简单。我们只需要配置一个类型为WebMvcConfigurer 的bean。这个Bean将包含一个配置CORS设置的方法。

@Bean
fun cors(): WebMvcConfigurer{

  return object : WebMvcConfigurer{
      override fun addCorsMappings(registry: CorsRegistry) {
        // Allow our client (on localhost:63343) to take resources from our backend
        registry.addMapping("/**").allowedOrigins("http://localhost:63343")
      }
  }

}

这个覆盖所做的是配置我们的服务器,允许我们的客户端从我们后端的任何URL中获取任何资源。这应该设置 "Access-Control-Allow-Origin "响应头,如上图所述。该标头应该有一个客户端的URL值,以表明他们被允许从我们的服务器上获取资源。有了这个小改动,我们的简单应用现在应该可以工作了。

总结

在本指南中,我们学习了在Spring Boot应用程序中处理图像的基本知识。我们首先学习了如何将图像保存到SQL数据库,以及如何通过REST端点传输图像。然后,我们配置了一个客户端来消费我们的REST端点和CORS设置,以实现资源共享。有了这些知识,你应该能够开始在一个全栈应用程序中用图像做一些很酷的事情。