kotlin使用spring mvc将接收的字符串生成二维码并响应

简介: 使用kotlin制作一个基于`spring mvc`的小demo:接收请求中的字符串参数,用`zxing`将字符串生成出一个二维码,再返回给前端。可以通过此例来了解kotlin与spring的搭配

kotlin代替java早已有了一定的势头,无论是Intellij IDEA这个如今第一的Java IDE,还是Jetpack Compose Multiplatform这个新兴Android UI都极大的推动了kotlin。此外kotlin的专有生态也在持续发展,像Ktormktor都比较有趣。当然了最重要的还是与以前的scalagroovy一样,作为JVM平台上的语言能完美与java交互,继承java这边庞大而丰富的生态。

在Java最主要的应用场景——web开发,自然可以用kotlin来尝试尝试的,本文使用kotlin制作一个基于spring mvc的小demo:接收请求中的字符串参数,用zxing将字符串生成出一个二维码,然后返回给前端。

首先从kotlin环境构建说起,如果你熟悉这一点,可以直接跳到图像生成,也可以先跳到最后的效果展示先看看样子。

环境构建

Gradle配置

第一步当然就是写好gradle的配置了,写java时这个语言基本都是选groovy这个默认的,没有额外扩展名,直接就是build.gradle。但现在用kotlin了,自然可以尝试尝试对应的build.gradle.kts,其实也就是一些符号的变化,按照惯例写在前面的自然是引入的插件了:

plugins {
    java
    war
    kotlin("jvm") version "1.8.20"
}

这个差不多就是kotlin的web项目常用的配置,因为多多少少还是可能用到java,打war包的需求基本也都会有,kotlin("jvm") version "1.8.20"则是kotlin的必需,至于这个要追加版本号是因为官方文档就是这样写的。不过这样也会有问题,看似比以前的groovy简单,那如果插件名像maven-publish这样中间带-的怎么引入呢?这种有特殊符号的其实直接用“`”符号包起来就好了,例如下面这样:

plugins {
    kotlin("jvm") version "1.8.20"
    `maven-publish`
    signing
}

然后就是groupversiondescription这些基础属性的设置。

group = "com.lyrieek.gear"
version = "0.0.1"
description = "a demo"

这里我起个包名叫gear,而项目名自然和groovy一样都是在settings.gradle里设置,当然也是要加kts的,文件名应该是settings.gradle.kts

rootProject.name = "Gear Web"

这样就可以将项目名命名为Gear Web了,再然后就是继续在build.gradle.kts里面设置repositories了:

repositories {
    mavenLocal()
    maven { url = uri("https://maven.aliyun.com/repository/google/") }
    maven { url = uri("https://maven.aliyun.com/repository/public/") }
    mavenCentral()
}

众所周知国内一般是需要这个阿里云镜像的,这个设置一般就是这样。然后还可以设置一下JDK的版本,这里我选择用17:

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

然后便是dependencies

dependencies {
    implementation("jakarta.servlet:jakarta.servlet-api:6.0.0")
    implementation("org.springframework:spring-web:6.0.7")
    implementation("org.springframework:spring-webmvc:6.0.7")
    implementation("org.springframework.security:spring-security-web:6.0.2")
    implementation("org.springframework.security:spring-security-config:6.0.2")

    implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
    implementation("com.google.zxing:core:3.5.1")
}

作为web项目,jstl如今可能已经不需要了,但servlet-api还是少不了的。再就是spring mvcspring security基本的几个包。还有现在交互必不可少的jackson,它的annotations就懒得引了,要用也是和java那边一样的。最后就是这个功能的重点,谷歌的zxing,为了轻量这里只需要core部分即可。

类配置

众所周知用xml配置已经过时了,现在又流行回去用配置类了,自然是写个WebConfig先:

import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
import java.text.SimpleDateFormat

@Configuration
@EnableWebMvc
@ComponentScan("com.lyrieek.gear")
open class WebConfig : WebMvcConfigurer {

    override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
        configurer.enable()
    }

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        converters.add(
            MappingJackson2HttpMessageConverter(
                Jackson2ObjectMapperBuilder()
                    .indentOutput(true)
                    .dateFormat(SimpleDateFormat("yyyy-MM-dd")).build()
            )
        )
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
            .allowedOriginPatterns("*")
            .allowCredentials(true)
            .allowedMethods("*")
    }
}

web项目从一开始就要围绕安全来工作,安全配置单独写出来是有必要的,但是不要忘记关掉阻挠前端开发工作的csrf

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
open class WebSecurityConfig {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http.cors().and().csrf().disable().build()
    }
}

虽说框架的xml确实都没了,但服务的src\main\webapp\WEB-INF\web.xml由于历史遗留问题,现阶段还没办法不写,配置还不短:

<web-app>
    <display-name>gear</display-name>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.lyrieek.gear.config</param-value>
    </context-param>

    <servlet>
        <servlet-name>spring</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.lyrieek.gear.config</param-value>
        </init-param>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
        </init-param>
        <init-param>
            <param-name>throwExceptionIfNoHandlerFound</param-name>
            <param-value>true</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

到这也就配置完成了。

图像生成

首先就是写个controller

import jakarta.servlet.http.HttpServletResponse
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.*

@RestController
@RequestMapping("qrcode")
class QRCodeController {

    @Autowired
    private lateinit var server: QRCodeService

    @RequestMapping
    fun generate(@RequestParam("str") str: String, response: HttpServletResponse) {
        server...
    }

}

其中这个Service需要注意,要用lateinit var修饰,基本上所有的Service都是这样注入的。下面的generate()方法接受str参数,还有response准备将未来生成好的BufferedImage响应回去。

下面就是service的部分,这部分是本文的核心,说来也比较简单,首先写好壳子:

@Service
class QRCodeService {

    private val width = 100
    private val height = 100

    fun generate(content: String): BufferedImage {
        ...
    }
}

二维码的宽高是可以提前都定好的,这个不会变,参数就是controller那边的str,返回就返回一个标准的java.awt.image.BufferedImage,然后就开始写这个方法的主体代码,先准备二维码的参数,zxing需要的参数是一个MutableMap<EncodeHintType, Any>

val encodeHints: MutableMap<EncodeHintType, Any> = EnumMap(EncodeHintType::class.java)
encodeHints[EncodeHintType.CHARACTER_SET] = "UTF-8"
encodeHints[EncodeHintType.MARGIN] = 1
val matrix: BitMatrix =
    MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, encodeHints)

一般来说只用的上这两个参数,charset保证中文的编码,margin设置下间距,然后用MultiFormatWriter.encode()就生成二维码的数据矩阵了,接下来就是写入到BufferedImage中:

val image = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY)
val rowPixels = IntArray(width)
var row = BitArray(width)
for (y in 0 until height) {
    row = matrix.getRow(y, row)
    for (x in 0 until width) {
        rowPixels[x] = if (row[x]) 1 else -1
    }
    image.setRGB(0, y, width, 1, rowPixels, 0, width)
}
return image

因为一般是黑白的,所以用BufferedImage.TYPE_BYTE_BINARY色彩模式即可,1就是黑色,-1就是白色。

但如果想生成彩色的二维码,比如说灰底蓝色的二维码,可以改成BufferedImage.TYPE_3BYTE_BGR,然后在填充rowPixels时选择自己想要的颜色:

val image = BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR)
val rowPixels = IntArray(width)
var row = BitArray(width)
for (y in 0 until height) {
    row = matrix.getRow(y, row)
    for (x in 0 until width) {
        rowPixels[x] = if (row[x]) Color.blue.rgb else Color.gray.rgb
    }
    image.setRGB(0, y, width, 1, rowPixels, 0, width)
}
return image

相对应的controller那边只需要响应BufferedImage就好了:

@RequestMapping
fun generate(@RequestParam("str") str: String, response: HttpServletResponse) {
    response.contentType = "image/jpeg"
    ImageIO.write(server.generate(str), "jpg", response.outputStream)
}

效果展示

image.png

选择BufferedImage.TYPE_3BYTE_BGR通道并设置蓝色效果:
image.png

综合源代码

QRCodeController.kt

import jakarta.servlet.http.HttpServletResponse
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.*
import javax.imageio.ImageIO

@RestController
@RequestMapping("qrcode")
class QRCodeController {

    @Autowired
    private lateinit var server: QRCodeService

    @RequestMapping
    fun generate(@RequestParam("str") str: String, response: HttpServletResponse) {
        response.contentType = "image/jpeg"
        ImageIO.write(server.generate(str), "jpg", response.outputStream)
    }

}

QRCodeService.kt

import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.MultiFormatWriter
import com.google.zxing.common.BitArray
import com.google.zxing.common.BitMatrix
import org.springframework.stereotype.Service
import java.awt.image.BufferedImage
import java.util.*

@Service
class QRCodeService {

    private val width = 100
    private val height = 100

    fun generate(content: String): BufferedImage {
        val encodeHints: MutableMap<EncodeHintType, Any> = EnumMap(EncodeHintType::class.java)
        encodeHints[EncodeHintType.CHARACTER_SET] = "UTF-8"
        encodeHints[EncodeHintType.MARGIN] = 1
        val matrix: BitMatrix =
            MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, encodeHints)
        val image = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY)
        val rowPixels = IntArray(width)
        var row = BitArray(width)
        for (y in 0 until height) {
            row = matrix.getRow(y, row)
            for (x in 0 until width) {
                rowPixels[x] = if (row[x]) 1 else -1
            }
            image.setRGB(0, y, width, 1, rowPixels, 0, width)
        }
        return image
    }
}

本文写作于2023年5月29日并发布于lyrieek的掘金,于2023年7月17日进行修订发布于lyrieek的阿里云开发者社区。

目录
相关文章
|
2月前
|
缓存 前端开发 Java
Spring MVC 面试题及答案整理,最新面试题
Spring MVC 面试题及答案整理,最新面试题
97 0
|
2月前
|
XML 前端开发 Java
深入了解Spring MVC工作流程
深入了解Spring MVC工作流程
|
18天前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
23 3
|
18天前
|
存储 前端开发 Java
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
14 1
|
18天前
|
前端开发 Java Spring
数据之桥:深入Spring MVC中传递数据给视图的实用指南
数据之桥:深入Spring MVC中传递数据给视图的实用指南
31 3
|
28天前
|
前端开发 安全 Java
使用Java Web框架:Spring MVC的全面指南
【4月更文挑战第3天】Spring MVC是Spring框架的一部分,用于构建高效、模块化的Web应用。它基于MVC模式,支持多种视图技术。核心概念包括DispatcherServlet(前端控制器)、HandlerMapping(请求映射)、Controller(处理请求)、ViewResolver(视图解析)和ModelAndView(模型和视图容器)。开发流程涉及配置DispatcherServlet、定义Controller、创建View、处理数据、绑定模型和异常处理。
使用Java Web框架:Spring MVC的全面指南
|
2月前
ssm(Spring+Spring mvc+mybatis)Dao层实现类——DeptDaoImpl
ssm(Spring+Spring mvc+mybatis)Dao层实现类——DeptDaoImpl
12 0
|
2月前
ssm(Spring+Spring mvc+mybatis)Dao接口——IDeptDao
ssm(Spring+Spring mvc+mybatis)Dao接口——IDeptDao
9 0
|
2月前
ssm(Spring+Spring mvc+mybatis)实体类——Dept
ssm(Spring+Spring mvc+mybatis)实体类——Dept
14 0
|
2月前
|
Java 关系型数据库 MySQL
ssm(Spring+Spring mvc+mybatis)
ssm(Spring+Spring mvc+mybatis)
14 0