kotlin使用spring mvc将接收的字符串生成二维码并响应
kotlin代替java早已有了一定的势头,无论是Intellij IDEA这个如今第一的Java IDE,还是Jetpack Compose Multiplatform这个新兴Android UI都极大的推动了kotlin。此外kotlin的专有生态也在持续发展,像Ktorm,ktor都比较有趣。当然了最重要的还是与以前的scala、groovy一样,作为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
}
然后就是group,version,description这些基础属性的设置。
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 mvc和spring 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)
}
效果展示

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

综合源代码
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
}
}
转载自:https://juejin.cn/post/7238498384914448441