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的阿里云开发者社区。

目录
相关文章
|
22天前
|
JSON 前端开发 Java
Spring MVC——获取参数和响应
本文介绍了如何在Spring框架中通过不同的注解和方法获取URL参数、上传文件、处理cookie和session、以及响应不同类型的数据。具体内容包括使用`@PathVariable`获取URL中的参数,使用`MultipartFile`上传文件,通过`HttpServletRequest`和`@CookieValue`获取cookie,通过`HttpSession`和`@SessionAttribute`获取session,以及如何返回静态页面、HTML代码片段、JSON数据,并设置HTTP状态码和响应头。
41 1
Spring MVC——获取参数和响应
|
25天前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
51 2
|
2月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
1月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
90 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
1月前
|
前端开发 Java
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
文章介绍了如何使用SpringBoot创建简单的后端服务器来处理HTTP请求,包括建立连接、编写Controller处理请求,并返回响应给前端或网址。
50 0
学习SpringMVC,建立连接,请求,响应 SpringBoot初学,如何前后端交互(后端版)?最简单的能通过网址访问的后端服务器代码举例
|
2月前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
3月前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
3月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
3月前
|
前端开发 应用服务中间件 数据库
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
这篇文章通过一个具体的项目案例,详细讲解了如何使用SpringMVC、Thymeleaf、Bootstrap以及RESTful风格接口来实现员工信息的增删改查功能。文章提供了项目结构、配置文件、控制器、数据访问对象、实体类和前端页面的完整源码,并展示了实现效果的截图。项目的目的是锻炼使用RESTful风格的接口开发,虽然数据是假数据并未连接数据库,但提供了一个很好的实践机会。文章最后强调了这一章节主要是为了练习RESTful,其他方面暂不考虑。
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查