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