赞
踩
掌握什么是serverless和FaaS
学习使用阿里云函数计算(FC)构建多语言的后台服务
使用Spring Boot + 阿里云函数计算 + OSS打造极低成本的表白小程序
距离一年一度的520全民表白日,只!有!一!个!月!了!!!空气中处处弥漫着恋爱的甜(suan)蜜(chou)味!作为程序员,这一次我们来玩点不一样的!自己设计一款520表白小程序,来对他(她)表白吧!
说起当前最火的技术,不得不提的一个概念就是 Serverless。近两年几乎所有人都在说 Serverless,Serverless 作为一种新型的互联网架构,直接或间接推动了云计算的发展,从 AWS Lambda 到阿里云函数计算,Serverless 一路高歌,同时基于 Serverless 的轻量计算开始登录云计算的舞台。
从Serverless名字上可以看出来,其实Serverless这个单词是两个单词的组合Serverless = server + less。Server指的是服务端,而less指的是较少关心,所以组合在一起就是较少关心服务端。
要理解Serverless,我们先来看看Serverfull这种模式下,工作模式到底是怎么样的。
我们以服务端开发中两个角色为例:
做研发的小程,他是个精通JAVA WEB后端技术的程序员,主要负责与产品经理对接,将产品经理提出的产品需求转化成代码,当然他还得负责项目的版本管理以及后续线上bug的修复。
做运维的小魏,他只关心应用的服务端运维事务。他负责部署上线小程的 Web 应用以及日志监控。在用户访问量大的时候,他要给这个应用扩容(多加几台服务器);在用户访问量小的时候,他要给这个应用缩容(把服务器作他用节省成本);在服务器挂了的时候,他还要重启或者换一台服务器。
最开始小魏承诺将运维的事情全包了,小程不用关心任何部署运维相关的事情。小程每次发布新的应用,都会打电话给小魏,让小魏部署上线最新的代码。小魏要管理好迭代版本的发布,分支合并,将应用上线,遇到问题回滚。如果线上出了故障,还要抓取线上的日志发给小程解决。
这个时候小魏就觉得自己像一个工具人,每天都在重复的做一些琐碎的工作,特别是每次出现bug的时候,还要自己登陆到服务器上去查询日志,然后发给小程进行bug的修复。
这个时期研发和运维隔离,服务端运维都交给小服一个人,纯人力处理,也就是 Serverfull。
后来,小魏渐渐发现日常其实有很多事情都是重复性的工作,尤其是发布新版本的时候,与其每次都等小程电话,线上出故障了还要自己抓日志发过去,效率很低,不如干脆自己做一套运维平台,将部署上线和日志抓取的工作让小程自己处理。
运维平台上线后,小魏稍微轻松了一些,但是对于应用的扩容和缩容,还是需要小魏自己定期审查。而小程除了开发的任务,每次发布新版本或解决线上故障,都要自己到运维平台平台上去处理。这个时代就是研发兼运维 DevOps,小程兼任了小魏的部分工作。小魏将部分服务端运维的工作工具化了,自己可以去做更加专业的事情。相对ServerFull时代,看上去小程负责的内容更多了,但实际这些事情本身就应该是小程负责的。本来版本控制、线上故障就应该是小程自己处理的。而且小魏将这部分人力的工作工具化了,更加高效。其实已经有变少(less)的趋势了。那么还能不能再进一步优化呢?
这时,小魏发现资源优化和扩缩容方案也可以利用性能监控 + 流量估算解决。小魏又基于小程的开发流程,运维平台再进一步升级,帮小程做了一套代码自动化发布的流水线:从代码扫描 到测试再到上线。现在的小程连运维平台都不用登陆操作,只要将代码进行提交,剩下的就都由流水线自动化处理发布上线了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zfX660vh-1651140767936)(pic\114.jpg)]
这个时候研发不需要运维了。小魏的服务端运维工作全部自动化了。小程也变回到最初,只需要关心自己的应用业务就可以了。我们不难看出,在服务端运维的发展历史中,对于小程来说,小魏的角色存在感越来越弱,需要小魏参与的事情越来越少,都由自动化工具替代了。这就是“Serverless”。
那么我们怎么来实现serverless呢?这时候我们就需要使用到Faas了,FaaS
是Functions as a Service
的缩写,可以广义的理解为功能服务化,也可以解释为函数服务化。使用FaaS只需要关注业务代码逻辑,无需关注服务器资源,所以FaaS也跟开发者无需关注服务器Serverless密切相关。可以说FaaS提供了一个更加细分和抽象的服务化能力。其实很多的云平台已经提供了Faas相关的功能,今天我们使用的就是阿里云提供的函数计算FC。函数计算FC,它可以让我们随时随地创建、使用、销毁一个函数。函数计算FC需要加载代码进行实例化,然后被触发器 Trigger 或者被其他的函数调用。而我们要做的就是购买函数计算FC的服务,然后上传代码,函数计算服务会自动编译我们的代码并将其发布到服务器上运行。然后我们就可以使用触发器访问服务了。
Serverless 对 Web 服务开发的革命之一,就是极度简化了服务端运维模型,使一个零经验的新手,也能快速搭建一套低成本、高性能的服务端应用。
函数计算是事件驱动的全托管计算服务。使用函数计算,您无需采购与管理服务器等基础设施,只需编写并上传代码。函数计算为您准备好计算资源,弹性地、可靠地运行任务,并提供日志查询、性能监控和报警等功能,借助函数计算,您可以快速构建任何类型的应用和服务,并且只需为任务实际消耗的资源付费。在众多Serverless产品中阿里云函数计算FC更是其中的佼佼者。
3月25日消息,日前权威咨询机构 Forrester 发布 2021 年第一季度 FaaS 平台(Function-As-A-Service Platforms)评估报告,阿里云凭借产品能力全球第一的优势脱颖而出,在八个评测维度中拿到最高分,成为比肩亚马逊的全球 FaaS 领导者。这也是首次有国内科技公司进入 FaaS 领导者象限。
无需采购和管理服务器等基础设施,运维成本低。
您只需专注业务逻辑的开发,使用函数计算支持的开发语言设计、优化、测试、审核以及上传自己的应用代码。
以事件驱动的方式触发应用响应用户请求。与阿里云对象存储OSS、API网关、日志服务和表格存储等服务无缝对接,帮助快速构建应用。
提供日志查询、性能监控和报警等功能快速排查故障。
毫秒级别弹性伸缩,快速实现底层扩容以应对峰值压力。
按需付费,支持毫秒级别收费。只需为实际使用的计算资源付费,适合有明显波峰波谷的用户访问场景。
函数计算支持多种语言开发,支持的语言列表如下:
Node.js | Node.js运行环境 |
---|---|
Python | Python运行环境 |
PHP | PHP运行环境 |
Java | Java运行环境 |
C# | .NET Core运行环境 |
Go | Go Custom Runtime |
Ruby | Ruby Custom Runtime |
PowerShell | PowerShell Custom Runtime |
TypeScript | TypeScript Custom Runtime |
F# | F# Custom Runtime |
C++ | C++ Custom Runtime |
Lua | Lua Custom Runtime |
Dart | Dart Custom Runtime |
其他语言 | Custom Runtime |
流程说明如下:
开发者使用编程语言编写应用和服务。函数计算支持的开发语言请参见开发语言列表。
开发者上传应用到函数计算。
上传途径包括:
函数计算控制台
(https://fc.console.aliyun.com/?spm=a2c4g.11186623.2.9.6f33398eERpRzW)上传。触发函数执行。触发方式包括OSS、API网关、日志服务、表格存储以及函数计算API、SDK等。
动态扩容以响应请求。函数计算可以根据用户请求量自动扩容,该过程对您和您的用户均透明无感知。
根据函数的实际执行时间按量计费。函数执行结束后,可以通过账单来查看执行费用,收费粒度精确到1毫秒。
流程图如下所示:
函数计算每月为您提供一定的免费额度。您的阿里云账户与RAM用户共享每月免费的调用次数和执行时间额度。免费额度不会按月累积,在下一自然月的起始时刻,即1号零点,会清零然后重新计算。具体免费额度如下:
注意 免费额度只能在弹性实例的后付费模式下使用。
以下是以后付费单价为例计算月度费用。
函数执行内存 | 调用次数 | 执行时长 | 网络带宽 | 月度费用 |
---|---|---|---|---|
512 MB | 300万次 | 1秒/次 | 0 | 124.31元 |
128 MB | 3000万次 | 200毫秒/次 | 0 | 77.28元 |
128 MB | 2500万次 | 200毫秒/次 | 0 | 56.80元 |
448 MB | 500万次 | 500毫秒/次 | 0 | 82.04元 |
1024 MB | 250万次 | 1秒/次 | 0 | 234.24元 |
函数计算使用流程图如下所示。
流程说明如下:
登录函数计算控制台。
在顶部菜单栏,选择地域。
在左侧导航栏中,单击服务及函数。在服务列表区域右上角,单击新增服务。
参数说明如下。
参数 | 说明 |
---|---|
服务名称 | 设置服务名称。 |
功能描述 | 设置服务描述信息,便于区分服务,非必选。 |
绑定日志 | 选择是否绑定日志。绑定日志后,您可以查看函数执行日志,方便您进行函数开发及调试。 |
开启链路追踪 | 选择是否开启链路追踪功能。更多信息,请参见链路追踪简介。 |
在服务及函数页面的服务列表中可以查看已创建的服务。
登录函数计算控制台。
在顶部菜单栏,选择地域。
在左侧导航栏,单击服务及函数。
在服务及函数页面,单击目标服务。然后单击服务配置,在服务配置页签,单击修改配置。
注意 删除服务前,请确保您的服务中没有函数、预留的函数实例、版本及别名,否则会导致删除失败。
登录函数计算控制台。
在顶部菜单栏,选择地域。
在左侧导航栏,单击服务及函数。
在服务及函数页面,单击目标服务。然后单击服务配置,在服务配置页签,单击页面右上角的删除服务 。
登录函数计算控制台。
在顶部菜单栏,选择地域。
在左侧导航栏中,单击服务及函数。在服务及函数页面,单击目标服务,然后单击页面右上角的新增函数。
在新建函数页面选择创建的函数类型或函数模板,然后单击配置部署。
本文以创建HTTP函数为例。
参数说明如下所示。
参数 | 是否必填 | 操作 | 示例值 |
---|---|---|---|
函数类型 | 是 | 您选择的函数类型,选择后,无法更改。 | 事件函数 |
所在服务 | 是 | 若已创建服务:在列表中选择已存在的服务。若未创建服务:填写自定义的服务名称,系统将自动为您创建服务。 | Service |
函数名称 | 是 | 填写自定义的函数名称。 | Function |
运行环境 | 是 | 选择您熟悉的语言,例如Python、Java、PHP、Node.js等。函数计算支持的运行环境,请参见函数简介。选择运行环境后,您可以通过以下方式上传您的函数代码:代码包上传:选择后,单击上传代码,上传您的函数代码。文件夹上传:选择后,单击选择文件夹,选择您需要上传的文件夹。OSS上传:选择后,配置Bucket名称和Object名称,即可上传您OSS中的函数代码。使用示例代码:选择后,即可使用函数计算的示例代码。 | Node.JS 12.x |
函数入口 | 是 | 填写函数入口。格式为[文件名].[函数名]。 | index.handler |
高级设置 | |||
函数实例类型 | 是 | 选择适合您的实例类型。弹性实例****性能实例更多信息,请参见实例规格及使用模式。 | 弹性实例 |
函数执行内存 | 是 | 设置函数执行内存。选择输入:单击函数执行内存,在下拉列表中选择所需内存。手动输入:单击手动输入,可自定义函数执行内存。输入的内存必须为64 MB的倍数。 | 512 MB |
超时时间 | 是 | 设置超时时间。默认超时时间为60秒,最长为600秒。说明 超过设置的超时时间,函数将以执行失败结束。 | 60 |
单实例并发度 | 否 | 单个实例能够并发处理的请求数。更多信息,请参见单实例多并发简介。 | Python语言不支持设置实例并发度。 |
层 | 否 | 选择您需要加载的层。更多信息,请参见层概述。 | NodeJS |
登录函数计算控制台。
在顶部菜单栏,选择地域。
在左侧导航栏,单击服务及函数。
点击函数名称,进入函数详情页面:
5.选择代码执行标签,编辑代码:
node代码修改如下:
var getRawBody = require('raw-body'); var getFormBody = require('body/form'); var body = require('body'); /* To enable the initializer feature (https://help.aliyun.com/document_detail/156876.html) please implement the initializer function as below: exports.initializer = (context, callback) => { console.log('initializing'); callback(null, ''); }; */ exports.handler = (req, resp, context) => { resp.send('hello world'); }
6.在代码执行标签页的最后,可以对函数进行测试:
详见https://help.aliyun.com/document_detail/161136.htm?spm=a2c4g.11186623.2.6.6f756fcf9hheNP#section-jni-z2b-zrf
执行以下命令初始化Funcraft工具,配置账号信息。
fun config
根据提示依次配置Account ID(阿里云账号ID)、AccessKey ID、AccessKey Secret、Default Region Name。
如果您的账号是RAM用户,Account ID需要配置为阿里云账号的ID,AccessKey ID、AccessKey Secret为RAM用户的密钥。
完成配置后,Funcraft会将配置保存到用户目录下的.fcli/config.yaml文件中。
执行以下命令初始化项目模板。
fun init -n demo
根据提示选择一个项目模板。
项目模板类型如下:
event-
为前缀的模板是普通的事件函数。http-trigger
为前缀的模板会默认为您创建HTTP触发器。HTTP触发器以Request、Response为入参,帮助您快速搭建Web应用。本示例中,选择http-trigger-spring-boot
的模板,使用Idea加载项目,项目结构如下:
通过 fun build 可以对项目进行编译构建:
fun build
执行效果如下:
创建一个Spring Boot项目,详情请参见Spring Quickstart Guide。
在IDEA中选择File-New Project创建新项目,选择Spring Initializr,在右侧的服务URL中输入:https//start.aliyun.com
选择lombok和spring web两个组件:
删除多余的文件,项目结构如下图所示:
(重要)修改pom文件:
删除pom文件中如下这段
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
添加parent的继承关系:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
</parent>
添加测试接口:
package com.itheima.love520demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author itheima * @version 1.0 * @date 2021/5/13 18:25 */ @RestController public class TestController { @RequestMapping("/test") public String test(){ return "itheima"; } }
执行以下命令进入刚创建的示例项目或您已有的项目。
cd <project-name>
在项目的根目录下执行mvn package -Dmaven.test.skip=true
命令打包。
编译输出结果与以下示例类似。
mvn package -Dmaven.test.skip=true
[INFO] Scanning for projects... [INFO] [INFO] ----------------------< com.example:Spring-Boot >----------------------- [INFO] Building Spring-Boot 0.0.1-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-resources-plugin:3.1.0:resources (default-resources) @ Spring-Boot --- ... ... ... [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] [INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ Spring-Boot --- [INFO] Building jar: /Users/txd123/Desktop/Spring-Boot/target/Spring-Boot-0.0.1-SNAPSHOT.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.2.6.RELEASE:repackage (repackage) @ Spring-Boot --- [INFO] Replacing main artifact with repackaged archive [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 38.850 s [INFO] Finished at: 2020-03-31T15:09:34+08:00 [INFO] ------------------------------------------------------------------------
执行fun deploy -y
命令将项目部署至函数计算。
Funcraft会自动进入部署流程。
fun deploy -y
current folder is not a fun project. Generating template.yml... Generate Fun project successfully! ========= Fun will use 'fun deploy' to deploy your application to Function Compute! ========= using region: cn-qingdao using accountId: ***********3743 using accessKeyId: ***********Ptgk using timeout: 60 Collecting your services information, in order to caculate devlopment changes... Resources Changes(Beta version! Only FC resources changes will be displayed): trigger httpTrigger deploy success function Spring-Boot deploy success service Spring-Boot deploy success Detect 'DomainName:Auto' of custom domain 'Domain' Request a new temporary domain ... The assigned temporary domain is 15639196-XXX.test.functioncompute.com,expired at 2020-04-10 15:19:56, limited by 1000 per day. Waiting for custom domain Domain to be deployed... custom domain Domain deploy success
在控制台找到URL路径中的Path,将其复制下来:
修改application.properties文件,添加访问路径:
server.servlet.context-path=/2016-08-15/proxy/love520demo/love520demo
重新执行打包和部署:
mvn package -Dmaven.test.skip=true
fun deploy -y
如果在页面上调用测试不成功,可能是因为URL中版本被添加了.LATEST的关系,可以使用POSTMAN进行测试
postman测试结果如下:
阿里云对象存储OSS(Object Storage Service)是阿里云提供的海量、安全、低成本、高持久的云存储服务。其数据设计持久性不低于99.9999999999%(12个9),服务可用性(或业务连续性)不低于99.995%。OSS具有与平台无关的RESTful API接口,可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。表白小程序将使用OSS上传二维码和用户自定义的图片。
登录OSS管理控制台。
单击Bucket列表,然后单击创建Bucket。
您也可以单击概览,然后单击右上角的创建Bucket。
在创建Bucket面板,按如下说明配置必要参数。其他参数均可保持默认配置,也可以在Bucket创建完成后单独配置。
参数 | 描述 |
---|---|
Bucket名称 | Bucket的名称。Bucket一旦创建,则无法更改其名称。命名规则如下:Bucket名称必须全局唯一。只能包括小写字母、数字和短划线(-)。必须以小写字母或者数字开头和结尾。长度必须在3~63字节之间。 |
地域 | Bucket的数据中心。Bucket一旦创建,则无法更改其所在地域。如需通过ECS内网访问OSS,请选择与ECS相同的地域。更多信息,请参见OSS访问域名使用规则。 |
同城冗余存储 | OSS同城冗余存储采用多可用区(AZ)机制,将用户的数据以冗余的方式存放在同一地域(Region)的3个可用区。当某个可用区不可用时,仍然能够保障数据的正常访问。启用:开启同城冗余存储,则Bucket内的Object将以同城冗余的方式进行存储。例如,Bucket存储类型为标准存储,则该Bucket内的Object默认为标准存储(同城冗余)。详情请参见同城冗余存储。注意仅华南1(深圳)、华北2(北京)、华东1(杭州)、华东2(上海)、中国(香港)、新加坡地域支持开启同城冗余存储。仅允许创建Bucket时开启同城冗余存储。开启后不支持关闭,请谨慎操作。关闭:默认不开启同城冗余存储,则Bucket内的Object将以本地冗余的方式进行存储。例如,Bucket存储类型为标准存储,则该Bucket内的Object默认为标准存储(本地冗余)。 |
登录OSS管理控制台。
单击左侧导航栏的Bucket列表,然后单击目标Bucket名称。
在文件管理页签,单击上传文件。
在上传文件面板,按如下说明配置各项参数。
参数 | 说明 |
---|---|
上传到 | 设置文件上传到OSS后的存储路径。当前目录:将文件上传到当前目录。指定目录:将文件上传到指定目录,您需要输入目录名称。若输入的目录不存在,OSS将自动创建对应的文件夹并将文件上传到该文件夹中。 |
文件ACL | 选择文件的读写权限。继承Bucket:以Bucket读写权限为准。私有(推荐):只有文件Owner拥有该文件的读写权限,其他用户没有权限操作该文件。公共读:文件Owner拥有该文件的读写权限,其他用户(包括匿名访问者)都可以对文件进行访问,这有可能造成您数据的外泄以及费用激增,请谨慎操作。公共读写:任何用户(包括匿名访问者)都可以对文件进行访问,并且向该文件写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可能会侵害您的合法权益。除特殊场景外,不建议您配置公共读写权限。有关文件ACL的更多信息,请参见Object ACL。 |
待上传文件 | 选择您需要上传的文件或文件夹。您可以单击扫描文件或扫描文件夹选择本地文件或文件夹,或者直接拖拽目标文件或文件夹到待上传文件区域。如果上传文件夹中包含了无需上传的文件,请单击目标文件右侧的移除将其移出文件列表。注意如果上传的文件与存储空间中已有的文件重名,则会覆盖已有文件。使用拖拽方式上传文件夹时,OSS会保留文件夹内的所有文件和子文件夹。文件上传过程中,请勿刷新或关闭页面,否则上传任务会被中断且列表会被清空。 |
单击上传文件。
此时,您可以在上传列表页签查看各个文件的上传进度。上传完成后,您可以在目标路径下查看上传文件的文件名、文件大小以及存储类型等信息。
登录OSS管理控制台。
单击左侧导航栏的Bucket列表,然后单击目标Bucket名称。
单击左侧导航栏的文件管理,下载单个或多个文件。
下载单个文件
方式一:单击目标文件右侧的更多> 下载。
方式二:单击目标文件的文件名或其右侧的详情,在弹出的详情面板中单击下载。
下载多个文件
选中多个文件,选择批量操作 > 下载。通过OSS控制台可一次批量下载最多100个文件。
系统设计核心要点:
为了保证服务部署难度和成本较低,使用了FC函数计算服务(Serverless)。存储层将所有的LOGO图片和表白信息都以文件的方式保存到阿里云OSS对象存储上,既能保证对大量文件保存的支持,也减少了部署数据库的成本。
注:由于小程序以个人身份不支持页面直接跳转,所以扫描二维码跳转小程序无法实现,所以需要单独部署表白信息的展示服务(tomcat),所以需要一台低成本的服务器。如以企业申请小程序,可直接在小程序中开发界面展示表白信息,真正做到访问量小的时候0成本。
本小程序使用uni-app开发,uni-app
是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到iOS、Android、Web(响应式)、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉/淘宝)、快应用等多个平台。
uni-app
在手,做啥都不愁。即使不跨端,uni-app
也是更好的小程序开发框架、更好的App跨平台框架、更方便的H5开发框架。不管领导安排什么样的项目,你都可以快速交付,不需要转换开发思维、不需要更改开发习惯。
请按照3.2.4的方式搭建初始环境
1.添加依赖:
<!--阿里云OSS依赖--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.1.1</version> </dependency> <!--二维码依赖--> <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
2.将资料中的工具类OssUtil和QRCodeKit添加到utils包下。
OSSUtil工具类:
package com.itheima.love520.utils; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.GetObjectRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.*; /** * @author brianxia * @version 1.0 * @date 2021/4/18 23:45 */ @Component public class OssUtil { @Value("${oss.bucketName}") private String bucketName; @Value("${oss.accessKeyId}") private String accessKeyId; @Value("${oss.accessKeySecret}") private String accessKeySecret; @Value("${qr.image.path}") private String qrPath; public OSS createClient() { // Endpoint以杭州为例,其它Region请按实际情况填写。 String endpoint = "https://oss-cn-beijing.aliyuncs.com"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); return ossClient; } /** * 以流的方式保存,用于保存文件 * @param objectName 文件名 * @param stream 流对象 */ public void save(String objectName, InputStream stream) { OSS client = createClient(); // 上传文件到指定的存储空间(bucketName)并将其保存为指定的文件名称(objectName)。 client.putObject(bucketName, objectName, stream); // 关闭OSSClient。 client.shutdown(); } /** * 保存字符串 * @param objectName 文件名 * @param content 字符串 */ public void save(String objectName, String content) { OSS client = createClient(); // 上传文件到指定的存储空间(bucketName)并将其保存为指定的文件名称(objectName)。 client.putObject(bucketName, objectName, new ByteArrayInputStream(content.getBytes())); // 关闭OSSClient。 client.shutdown(); } /** * 保存二维码 * @param objectName 文件名 * @param context 二维码内容 * @param logo 中间的logo图 */ public void saveQr(String objectName, String context,String logo) throws IOException { BufferedImage image = null; if(StringUtils.isEmpty(logo)){ image = QRCodeKit.createQRCode(context); }else{ String path = loadFile(logo); File file = new File(path); image = QRCodeKit.createQRCodeWithLogo(context,file); if(file.exists()){ file.delete(); } } if(image == null){ return; } ByteArrayOutputStream os = new ByteArrayOutputStream(); ImageIO.write(image, "png", os); InputStream qrCode = new ByteArrayInputStream(os.toByteArray()); try{ OSS client = createClient(); // 上传文件到指定的存储空间(bucketName)并将其保存为指定的文件名称(objectName)。 client.putObject(bucketName, objectName, qrCode); // 关闭OSSClient。 client.shutdown(); }finally { if(qrCode != null){ try { qrCode.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 加载文件 * @param objectName 文件名 * @return 文件内容字符串 */ public String loadFile(String objectName) { String path = qrPath + objectName; OSS ossClient = createClient(); // 下载Object到本地文件,并保存到指定的本地路径中。如果指定的本地文件存在会覆盖,不存在则新建。 // 如果未指定本地路径,则下载后的文件默认保存到示例程序所属项目对应本地路径中。 ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File(path)); // 关闭OSSClient。 ossClient.shutdown(); return path; } }
二维码工具类:
package com.itheima.love520.utils; import com.google.zxing.*; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64OutputStream; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.HashMap; import java.util.Map; public class QRCodeKit { public static final String QRCODE_DEFAULT_CHARSET = "UTF-8"; public static final int QRCODE_DEFAULT_HEIGHT = 400; public static final int QRCODE_DEFAULT_WIDTH = 400; public static final int LOGO_DEFAULT_HEIGHT = 100; public static final int LOGO_DEFAULT_WIDTH = 100; private static final int BLACK = 0xFF000000; private static final int WHITE = 0xFFFFFFFF; public static void main(String[] args) throws IOException, NotFoundException{ String data = "http://www.baidu.com"; File logoFile = new File("logo.png"); BufferedImage image = QRCodeKit.createQRCodeWithLogo(data, logoFile); ImageIO.write(image, "png", new File("result7.png")); System.out.println("done"); } /** * Create qrcode with default settings * * @param data * @return */ public static BufferedImage createQRCode(String data) { return createQRCode(data, QRCODE_DEFAULT_WIDTH, QRCODE_DEFAULT_HEIGHT); } /** * Create qrcode with default charset * * @param data * @param width * @param height * @return */ public static BufferedImage createQRCode(String data, int width, int height) { return createQRCode(data, QRCODE_DEFAULT_CHARSET, width, height); } /** * Create qrcode with specified charset * * @param data * @param charset * @param width * @param height * @return */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static BufferedImage createQRCode(String data, String charset, int width, int height) { Map hint = new HashMap(); hint.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hint.put(EncodeHintType.CHARACTER_SET, charset); return createQRCode(data, charset, hint, width, height); } /** * Create qrcode with specified hint * * @param data * @param charset * @param hint * @param width * @param height * @return */ public static BufferedImage createQRCode(String data, String charset, Map<EncodeHintType, ?> hint, int width, int height) { BitMatrix matrix; try { matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset), BarcodeFormat.QR_CODE, width, height, hint); return toBufferedImage(matrix); } catch (WriterException e) { throw new RuntimeException(e.getMessage(), e); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } public static BufferedImage toBufferedImage(BitMatrix matrix) { int width = matrix.getWidth(); int height = matrix.getHeight(); BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE); } } return image; } /** * Create qrcode with default settings and logo * * @param data * @param logoFile * @return */ public static BufferedImage createQRCodeWithLogo(String data, File logoFile) { return createQRCodeWithLogo(data, QRCODE_DEFAULT_WIDTH, QRCODE_DEFAULT_HEIGHT, logoFile); } /** * Create qrcode with default charset and logo * * @param data * @param width * @param height * @param logoFile * @return */ public static BufferedImage createQRCodeWithLogo(String data, int width, int height, File logoFile) { return createQRCodeWithLogo(data, QRCODE_DEFAULT_CHARSET, width, height, logoFile); } /** * Create qrcode with specified charset and logo * * @param data * @param charset * @param width * @param height * @param logoFile * @return */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static BufferedImage createQRCodeWithLogo(String data, String charset, int width, int height, File logoFile) { Map hint = new HashMap(); hint.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); hint.put(EncodeHintType.CHARACTER_SET, charset); return createQRCodeWithLogo(data, charset, hint, width, height, logoFile); } /** * Create qrcode with specified hint and logo * * @param data * @param charset * @param hint * @param width * @param height * @param logoFile * @return */ public static BufferedImage createQRCodeWithLogo(String data, String charset, Map<EncodeHintType, ?> hint, int width, int height, File logoFile) { try { BufferedImage qrcode = createQRCode(data, charset, hint, width, height); BufferedImage logo2 = ImageIO.read(logoFile); BufferedImage logo =new BufferedImage(LOGO_DEFAULT_WIDTH,LOGO_DEFAULT_HEIGHT,BufferedImage.TYPE_INT_RGB); Graphics graphics=logo.getGraphics(); //将原始位图缩小后绘制到bufferedImage对象中 graphics.drawImage(logo2,0,0,LOGO_DEFAULT_WIDTH,LOGO_DEFAULT_HEIGHT,null); int deltaHeight = height - logo.getHeight(); int deltaWidth = width - logo.getWidth(); BufferedImage combined = new BufferedImage(height, width, BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D) combined.getGraphics(); g.drawImage(qrcode, 0, 0, null); g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f)); g.drawImage(logo, (int) Math.round(deltaWidth / 2), (int) Math.round(deltaHeight / 2), null); return combined; } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } /** * Return base64 for image * * @param image * @return */ public static String getImageBase64String(BufferedImage image) { String result = null; try { ByteArrayOutputStream os = new ByteArrayOutputStream(); OutputStream b64 = new Base64OutputStream(os); ImageIO.write(image, "png", b64); result = os.toString("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e.getMessage(), e); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } return result; } /** * Decode the base64Image data to image * * @param base64ImageString * @param file */ public static void convertBase64StringToImage(String base64ImageString, File file) { FileOutputStream os; try { Base64 d = new Base64(); byte[] bs = d.decode(base64ImageString); os = new FileOutputStream(file.getAbsolutePath()); os.write(bs); os.close(); } catch (FileNotFoundException e) { throw new RuntimeException(e.getMessage(), e); } catch (IOException e) { throw new RuntimeException(e.getMessage(), e); } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } }
3.创建resources文件夹,并添加配置文件application.properties:
oss.bucketName=OSS仓库名
oss.accessKeyId=OSS的ak
oss.accessKeySecret=OSS的SK
qr.url=http://www.cloudprogrammer.cn:8090/show/confess
qr.image.path=/tmp/
整体的流程分为两个阶段:
1.表白人通过小程序进行操作,生成二维码。
2.表白对象扫描二维码,展示表白页面。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DsVW8QgW-1651140767946)(pic\520表白小程序业务逻辑流程 .jpg)]
1.小程序的用户点击上传LOGO按钮,将LOGO图片进行上传。后台系统会将此图片存储。
2.小程序的用户编写表白对象、表白内容、选择模板,完成之后点击生成二维码的按钮。
3.后台系统将上述所有的数据保存,并生成最终的访问链接,以二维码的方式返回。
1.表白人将二维码发给表白对象,表白对象扫描二维码。
2.向后端发起请求,后端获取之前保存的表白数据,生成页面。
3.表白对象看到表白的内容。
接口地址:/2016-08-15/proxy/love520demo/love520demo/confess/upload
请求方式:POST
请求数据类型:multipart/form-data
响应数据类型:*/*
接口描述:
生成随机文件名,将文件保存到OSS中,返回文件名。
参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
---|---|---|---|---|---|
file | 图片文件 | formData | true | file |
响应参数:
文件名,是一个随机字符串
响应示例:
5609a150-22cf-4d8f-a8a4-7d673444b617
接口地址:/2016-08-15/proxy/love520demo/love520demo/confess/save
请求方式:POST
请求数据类型:application/json
响应数据类型:*/*
接口描述:
小程序点击生成二维码时,向后端发送请求,保存表白内容
请求示例:
{
"content": "",
"logo": "",
"template": "",
"to": ""
}
请求参数:
参数名称 | 参数说明 | in | 是否必须 | 数据类型 | schema |
---|---|---|---|---|---|
confess | confess | body | true | 表白内容实体定义 | 表白内容实体定义 |
content | 表白内容 | false | string | ||
logo | 二维码中间的logo图片 | false | string | ||
template | 用户选择的模板 | false | string | ||
to | 表白对象 | false | string |
响应参数:
文件名,是一个随机字符串
响应示例:
5609a150-22cf-4d8f-a8a4-7d673444b617
接口地址:/show/confess
请求方式:GET
请求数据类型:*
响应数据类型:*/*
接口描述:
请求参数:
参数名称 | 参数说明 | 是否必须 | 数据类型 | schema |
---|---|---|---|---|
id | 保存表白内容返回的ID | true | String |
响应参数:
展示页面
创建表白实体类(也可以使用工具自动生成 https://www.bejson.com/json2javapojo/new/):
package com.itheima.love520.entity; import lombok.Data; /** * 表白内容实体定义 * @author itheima * @version 1.0 * @date 2021/4/18 23:35 */ @Data public class Confess { //表白对象 private String to; //表白内容 private String content; //二维码中间的logo图片 private String logo; //用户选择的模板 private String template; }
创建接口Controller:
package com.itheima.love520demo.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.itheima.love520demo.entity.Confess; import com.itheima.love520demo.utils.OssUtil; import lombok.extern.java.Log; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.ArrayList; import java.util.Map; import java.util.UUID; /** * @author itheima * @version 1.0 * @date 2021/4/18 23:34 */ @RestController @CrossOrigin("*") @RequestMapping("/confess") @Slf4j public class ConfessController { /** * JSON转换器 */ @Autowired private ObjectMapper objectMapper; /** * OSS上传下载工具类 */ @Autowired private OssUtil ossUtil; /** * 表白的展示页面,已提供 */ @Value("${qr.url}") private String qrUrl; /** * 上传图片接口 * @param file 文件对象 * @return 文件名,是一个随机字符串 */ @PostMapping("/upload") public String uploadFile(MultipartFile file) throws Exception { //生成随机字符串作为文件名 String uuid = UUID.randomUUID().toString(); try { //存储图片 ossUtil.save(uuid,file.getInputStream()); } catch (Exception e) { log.error("二维码图片保存失败",e); } return uuid; } /** * 保存表白内容 * @param confess 表白内容 * @return 文件名,是一个随机字符串 */ @PostMapping("/save") public String saveConfess(@RequestBody Confess confess) throws Exception { //生成随机文件名 String uuid = UUID.randomUUID().toString(); String qrRealUrl = qrUrl + "?id=" + uuid; try { //存储数据 ossUtil.save(uuid,objectMapper.writeValueAsString(confess)); //存储二维码 ossUtil.saveQr(uuid + "qr.jpg",qrRealUrl,confess.getLogo()); } catch (IOException e) { log.error("表白保存失败",e); } return uuid; } }
查询接口请复制资料/初始工程
中的love520-show到本地工作目录中,并使用idea打开工程:
在controller下编写接口:
package com.itheima.love520show.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.itheima.love520show.entity.Confess; import com.itheima.love520show.util.OssUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author itheima * @version 1.0 * @date 2021/4/20 23:58 */ @Controller @RequestMapping("/show") public class ShowController { @Autowired OssUtil ossUtil; @Autowired ObjectMapper mapper; @RequestMapping("/confess") public String confess(HttpServletRequest request,Model model) throws IOException { String id = request.getParameter("id"); String load = ossUtil.load(id); Confess confess = mapper.readValue(load, Confess.class); model.addAttribute("to",confess.getTo()); model.addAttribute("content",confess.getContent()); return "template" + confess.getTemplate(); } }
使用POSTMAN测试接口,上传图片接口:
表白内容保存接口:
运行love520-show项目,访问地址:
http://localhost:8090/show/confess?id=45eaa681-c252-4dce-98b6-c71aa6082db8
显示如下内容,代码编写成功:
将项目中application.properties文件中的展示地址修改为最终服务器的ip地址或者域名:
qr.url=http://www.cloudprogrammer.cn:8090/show/confess
双击maven命令中的package,对当前项目进行打包:
成功之后,在当前目录下执行命令
fun deploy -y
如下图所示,已经显示成功
同5.4.1中,将项目先进行打包,然后将项目上传到服务器中:
执行命令:
nohup java -jar love520-show.jar &
使用HbuilderX打开前端代码,HbuilderX下载地址:https://www.dcloud.io/hbuilderx.html
修改接口地址和OSS地址(129行):
// 修改为程序真实地址
action: 'https://1847961572749689.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/love520-1/love520-1/confess/upload',
saveUrl : 'https://1847961572749689.cn-hangzhou.fc.aliyuncs.com/2016-08-15/proxy/love520-1/love520-1/confess/save',
ossUrl : 'https://itheima-test20210509.oss-cn-beijing.aliyuncs.com/',
点击真机调试,就可以看到画面并运行程序了:
本次课程我们通过使用阿里云的FC函数计算和OSS对象存储打造了一款超低成本的520表白小程序,大家也感受到了Serverless带给我们的独特魅力。
Serverless的优势:
1.超低成本,每个月表白小程序的调用前100万次免费,OSS也有很大的免费使用量。
2.弹性扩容,当用户量增大时阿里云会自动增加服务器以提供支持。
3.降低运维成本,不需要运维人员参与,开发人员上传代码即可发布最新版本。
典型应用场景:
发布小程序注意事项
如果需要将小程序进行发布,还需要在上传及生成二维码时对文本和图片进行内容安全审核,可以调用小程序相应的内容安全代码(详见https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html)。
以下给出示例代码,图片审核:
Map map1 = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=小程序ID&secret=小程序secret", Map.class);
String token = (String) map1.get("access_token");
Boolean aBoolean = PickCheckUtil.checkPic(file,token);
if(!aBoolean){
return "error";
}
文本审核:
String content = confess.getTo() + "," + confess.getContent();
Map map1 = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=小程序ID&secret=小程序secret", Map.class);
String token = (String) map1.get("access_token");
HashMap<Object, Object> param = new HashMap<>();
param.put("content",content);
Map map = restTemplate.postForObject("https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + token, param, Map.class);
Integer errcode = (Integer) map.get("errcode");
if(errcode != 0){
return "error";
}
(https://help.aliyun.com/document_detail/148417.html)
发布小程序注意事项
如果需要将小程序进行发布,还需要在上传及生成二维码时对文本和图片进行内容安全审核,可以调用小程序相应的内容安全代码(详见https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/sec-check/security.imgSecCheck.html)。
以下给出示例代码,图片审核:
Map map1 = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=小程序ID&secret=小程序secret", Map.class);
String token = (String) map1.get("access_token");
Boolean aBoolean = PickCheckUtil.checkPic(file,token);
if(!aBoolean){
return "error";
}
文本审核:
String content = confess.getTo() + "," + confess.getContent();
Map map1 = restTemplate.getForObject("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=小程序ID&secret=小程序secret", Map.class);
String token = (String) map1.get("access_token");
HashMap<Object, Object> param = new HashMap<>();
param.put("content",content);
Map map = restTemplate.postForObject("https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + token, param, Map.class);
Integer errcode = (Integer) map.get("errcode");
if(errcode != 0){
return "error";
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。