饭饭's Blog - Java https://ffis.me/Java/ Java学习心得分享 使用雪花算法生成16位全局唯一自增ID https://ffis.me/archives/2263.html 2023-09-16T18:03:00+08:00 背景实际业务中,往往会用数据库自增ID来作为业务对象的唯一ID;但数据日渐增多的互联网行业,分库分表则成了行业的通用解决方案;此时数据库自增ID就不能满足业务需求了,行业内有各种分布式ID生成解决方案,其中雪花ID就是其中使用较多的一种。雪花算法原理1 基本构成雪花算法(Snowflake)是 Twitter 开源的分布式 ID 生成算法,可以基于时间生成全局不重复的、有序的、可自增的 64 Bit 的 ID,适用于分布式系统中的 ID 生成需求。在标准版本中由以下部分组成:符号位(1bit)- 时间戳相对值(41bit)- 数据标志(5bit)- 机器标志(5bit)- 递增序号(12bit)0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000一起来一共64Bit组成,其中:1、最高位是符号位,用于区分是正数还是负数,这里始终为0,我们用不到;2、41位的毫秒级时间戳,41位的长度可以使用69年;3、5位datacenterId和5位workerId,加起来共10位,最多支持32 x 32 = 1024个节点4、最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序号2 生成ID原理标准版雪花算法在实例化时,由于支持的时间范围有限制,所以开始的时候需要指定一个开始时间,并指定数据标识和机器标识;如设定开始时间为 2020-01-01 00:00:00,数据标识设置为:15,机器标识设置为18;此时我们想要生成1个雪花ID1、获取时间戳差值先获取当前时间戳与设定的开始时间的差值当前时间2023-09-16 17:00:00转换为时间戳:1694854800000开始时间2020-01-01 00:00:00转换为时间戳:1577808000000计算时间戳差值:1694854800000 - 1577808000000 = 117046800000转换为二级制:11011010000001000100001101110100000002、获取数据标识和机器标识数据标识:15;机器标识:18分别转换为二级制:数据标识:1111;机器标识:100103、获取毫秒内计数毫秒内计数即是同一毫秒内,获取的第n个雪花ID,假设我们获取的是第1024个雪花ID1024换算为二级制:10000000000则生成的雪花ID为:时间戳差值+数据标识+机器标识+毫秒内计数1101101000000100010000110111010000000 1111 10010 10000000000二进制转为十进制,生成的雪花ID即为:122732465357820928 - 共18位雪花算法优缺点1 优点唯一性:雪花算法生成的ID是全局唯一的,可以在分布式系统中生成不重复的业务ID;有序性:基于时间戳的特性,使得雪花算法生成的ID是顺序增长的;高性能:雪花算法的生成过程是基于位运算实现的,性能好,并且标准的雪花算法每毫秒支持4096个ID生成,满足绝大多数的业务场景;独立性:雪花算法不依赖中央系统或数据库,非常方便在业务中落地和进行横向水平扩展;2 缺点依赖系统时钟:雪花算法依赖系统时钟,如果时钟发生回退,会导致ID生成重复,当然这个都有对应的解决方案;有限的容量:标准的雪花算法支持每毫秒生成4096个ID,如果超过了容量限制,则需要等待下一毫秒才可以生成新的ID;前端精度丢失:标准雪花算法生成的ID在18位和19位之间(时间差超过7.56年,就会达到19位),JavaScript 的 number 类型的数值范围是:2的53次方减1,所以数字位数大于16位且大于 9007199254740991 的数,进制转换会存在精度问题,而雪花ID生成的数值过大,导致超出JavaScript的精度范围,无法直接在前端展示,使用字符串展示雪花ID可避免此问题。定制16位雪花ID1 雪花算法定制既然JavaScript只支持16位一下的数字展示,那我们生成16位以下的雪花ID即可;标准的雪花算法为64Bit,每个位置的设计冗余量都很大,业务中完全可以根据自己的业务形态去定制雪花算法;1、缩短支持时间标准雪花算法为41位时间戳差值,支持使用69年,实际业务中很少有能跑69年的业务,我们可以将时间戳差值修改为39位,39位时间戳支持使用:(2^39)/(1000606024365) = 17.43 年,17年足够大部分业务使用;2、只保留机器标识标准雪花算法支持:5位的数据标识和5位的机器标识即最大支持 32 x 32 = 1024 节点部署实际业务很少有这么大规模的机器部署,一般最多也就10台机器,这里完全可以将5位数据标识去除,只保留5位机器标识,即支持32个节点部署;3、缩短毫秒内计数标准雪花算法同一毫秒支持12位的序列,即每毫秒支持4096个ID生成实际业务也很少能用到这么多ID,我们可以将序列缩短为8位,即每毫秒支持256个ID生成,完全满足大部分业务需要;经过以上定制的雪花算法,最多支持 32 台机器,每台机器每毫秒能够生成最多 256 个 ID,整个集群理论上每秒可以生成 32 1000 256 = 800万 个ID;2 最大支持ID39位时间戳,最大值:549755813887毫秒,约17.43264250022197年5位机器号,支持最大机器数量:328位序列,每毫秒生成:256个ID,每秒生成:256000个ID组成的最大二进制:1111111111111111111111111111111111111111111111111111 共52位最大生成ID:4503599627370495 - 共16位经过定制的雪花算法,完全满足大部分业务使用。源码源码发布于:moyu-framework详见 Github:https://github.com/MoYu-Group/moyu-framework/blob/main/moyu-base/moyu-util/src/main/java/io/github/moyugroup/base/util/UUIDUtil.java 使用HotSwapAgent实现SpringBoot热加载 https://ffis.me/archives/2234.html 2022-02-24T22:32:00+08:00 众所周知,IDEA 自带的热加载只支持方法内的热加载,而使用 HotSwapAgent 不仅支持方法内的热加载,并且可实现新增方法的热加载,甚至是新增类的热加载,可谓是提高开发效率的神器 本文主要介绍在 IDEA 下使用 HotSwapAgent 来进行 SpringBoot 下的热部署;接下来我们开始配置 HotSwapAgent1 安装 DCEVMDCEVM 是个JDK的插件,提供类似 JRebel 的热加载功能,能够在运行时重新定义加载的类,实现“热加载、热插拔、热部署”,而 HotSwapAgent 插件则是实现了 Servlet 程序的热加载功能,并且 DCEVM + HotSwapAgent 开源免费,更适合广大开发者使用。安装 DCEVM 需要和 JDK 版本相对应,你需要先确定自己的 JDK 版本是否是 DCEVM 所支持的版本https://github.com/dcevm/dcevm/releases写此文时,DCEVM 支持的最新 JDK 版本是Java 8u181如果你的JDK版本不被支持,则需要去 Oracle Java Archive 这个页面下载对应版本 JDK 并安装https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html下载 JDK 需要自备 Oracle 的账号下载并安装好对应版本 JDK 后,我们再次来确定 JDK 版本为对应版本此时我们可以下载 DCEVM patch 进行安装,这里我下载的是 DCEVM-8u181-installer-build2.jarhttps://github.com/dcevm/dcevm/releases运行以下命令开始进行安装java -jar DCEVM-8u181-installer-build2.jar这里需要选择安装目录,安装目录默认为 JDK 安装目录,如果没有则需要手动添加,然后点击 Install DCEVM as altjvm 这个按钮安装。2 IntelliJ IDEA 配置在 IDEA 插件库中搜索 HotSwapAgent 进行安装插件然后进行 HotSwapAgent plugin 配置勾选为所有项目启动 HotSwapAgent 插件PS:请手动检查插件目录下的 jar 包是否自动下载成功,如果下载不成功,则手动下载 jar 包替换图中的 Agent installation 路径为Jar包保存路径jar 包下载地址:https://github.com/HotswapProjects/HotswapAgent/releases3 热部署启动在 DEBUG 模式下启动应用程序。如果设置正确完成,您将在控制台中看到 HOTSWAP 代理通知。4 手动触发热部署方式在应用启动的过程中,如果有修改代码,可以手动触发热部署使其生效MacOS: Command+Shift+F9 / Win: Ctrl+Shift+F9手动触发热加载的时候,光标需要将焦点保持在需要热加载的类上面当然你也可以配置自动触发热部署,由于我不喜欢自动触发,所以在此不做过多介绍。5 热加载测试我们使用Spring Initializr快速创建一个 SpringBoot 应用,并且编写一个简单的测试接口:应用使用 DEBUG 模式启动的时候有提示被 HotSwapAgent 代理使用浏览器访问测试接口,正常访问1 修改方法体内代码,手动触发热加载提示1个类被重新加载再次访问接口,方法体内修改的代码已经生效2 新增一个方法,手动触发热加载提示1个类被重新加载,并且类被重新注册访问新增的接口,新增的方法已经生效3 新增一个类,手动触发热加载提示0个类被重新加载,但是新增的Controller2类已经被注册,说明只能重新加载被 Spring 注解标注的类访问新增的类接口,新增的类已经生效6 测试总结经测试,修改方法体内代码,修改方法,并且新增类都是可以支持热加载,但是新增类只支持注解扫描的类,XML配置文件不能进行热加载7 参考文档https://github.com/HotswapProjects/HotswapAgenthttps://github.com/dmitry-zhuravlev/hotswap-agent-intellij-plugin Drone下多工程项目使用Commit日志控制子工程运行 https://ffis.me/archives/2213.html 2021-08-11T23:36:00+08:00 最近拖延症犯了,一个文章标题写了一个月才写了个标题... 前言自从入坑Drone CI/DI以来,我极力推荐在小微项目上使用Drone来完成自动构建,主要是轻量化,安装配置方便 ,只需写一个docker compose文件即可完成Drone的安装配置,只需写一个.drone.yml即可完成接入,极为方便;Drone更深一层探究经过我的不懈努力,经主管同意,最终也在公司项目上使用上了Drone来逐步替代Jenkins进行小项目微服务的自动构建部署 ;但是实际使用中发现一个问题,即SpringBoot工程,通常是一个主工程下包含多个微服务子工程,使用drone不太好控制其中某一个工程的自动构建部署,总不能每次都重新构建整个服务,然后重启所有工程吧,这样效率也太低了 ;//多工程目录结构,本文主要演示SpringBoot多工程项目自动构建部署 demoParant ├── common //公共工程 ├── api //API工程 ├── user //用户工程 └── back //后台工程能想到的最简单实现的方式就是来通过不同的分支来触发不同的构建任务,这样理论上可行,但是实际操作会产生一堆分支,显得极为不整洁,并且正常开发也是开发一个分支,测试一个分支,生产一个分支,太多了操作起来也不方便 ;所以最理想的方式也就是在同一个分支下,通过某种方式来触发不同的构建任务;Drone的启发在Drone CI下有一个默认功能,即在Commit log中输入[CI SKIP]即可跳过本次自动构建,于是我就想能否通过Commit log来控制本次部署具体哪个子工程,这样下来我只要在输入commit log的时候输入需要构建的工程,即可完成对应工程的自动构建部署,并且不影响同项目下的其他工程;初步设想的原理就是在drone执行部署命令时,通过自定义脚本完成工程部署,并将commit log作为参数传入脚本,在脚本中判断commit log中是否指定某些工程的运行的参数,如果不指定则默认运行所有工程 ;例如:我提交commit日志update Admin.java; add admin management interface; [CI API] [CI BACK];这样一来,经过drone自动构建后,只重新部署了API工程和后台工程;具体实现具体实现可参考如下脚本:Drone自动构建脚本参考# drone 自动构建 name: test-project autodeploy kind: pipeline type: docker # drone 构建步骤 steps: # 1. 进行 maven 打包 - name: maven compile pull: if-not-exists image: maven:ibmjava-alpine volumes: # 本机准备挂载到宿主机的 maven构建缓存 - name: cache path: /root/.m2 commands: # 开始打包maven工程 跳过测试步骤 - mvn clean install -Dmaven.test.skip=true - cp api/target/*.jar ./ - cp user/target/*.jar ./ - cp back/target/*.jar ./ # 2. 将打包后的jar包部署到指定服务器 - name: jar deploy pull: if-not-exists image: appleboy/drone-scp settings: host: from_secret: test_host username: from_secret: test_username password: from_secret: test_password port: from_secret: test_port target: /home/drone/data/${DRONE_REPO_NAME} source: ./*.jar when: branch: - master #3. 使用ssh访问测试服务器进行服务部署 - name: test ssh-start pull: if-not-exists image: appleboy/drone-ssh settings: host: from_secret: test_host username: from_secret: test_username password: from_secret: test_password port: from_secret:test_port script: # 运行部署脚本 - cd /data/testProjrct # 这一步是运行部署脚本,并将Comment日志作为参数传入给脚本,部署命令一定要放到drone的最后一条命令,这样在脚本中抛出异常退出后,drone可以捕捉到异常退出,将该次构建标记为构建失败 - ./drone.sh ${DRONE_COMMIT_MESSAGE} - when: branch: - master # 挂载的主机卷,映射到宿主机对应的目录,对应name为steps.volumes.name volumes: # maven构建缓存 - name: cache host: # path: /tmp/cache/.m2 path: /opt/maven # drone执行触发分支 trigger: branch: - master event: - pushJar运行部署脚本参考只做了最简单的实现,脚本有很多可以优化的空间,在这里不多加阐述了#!/bin/bash echo "开始运行Demo服务..." PARAM=$1; echo -e "Commit log: "$PARAM; #----------------------- 基本参数配置 start ----------------------- # JAVA安装目录 JAVA_PATH="/opt/jdk1.8.0_191/bin/java"; # 运行环境配置 JAVA_RUN_ENV="test"; # drone部署的jar包所在路径 DRONE_JAR_PATH="/home/drone/data/demoParant"; # API工程 APP_API_NAME="apiService-0.0.1-SNAPSHOT.jar" APP_API_PATH="/home/demoPatent/apiService" APP_API_PING_URL="http://127.0.0.1:18091/api/open/ping" APP_API_RUN= # 用户端工程 APP_USER_NAME="userService-0.0.1-SNAPSHOT.jar" APP_USER_PATH="/home/demoPatent/userService" APP_USER_RUN= APP_USER_PING_URL="http://127.0.0.1:18071/user/open/ping" # 后台服务工程 APP_BACK_NAME="backService-0.0.1-SNAPSHOT.jar" APP_BACK_PATH="/home/demoPatent/backService" APP_BACK_RUN= APP_BACK_PING_URL="http://127.0.0.1:18081/back/open/ping" # 记录是否有启动失败的服务 APP_START_FAIL= APP_START_RESULT= #----------------------- 基本参数配置 end ----------------------- echo "程序运行环境:$JAVA_RUN_ENV" #----------------------- 定义启动函数 start----------------------- startAPP() { APP_NAME=$1; APP_PATH=$2; PING_URL=$3; # echo "APP_NAME:$APP_NAME" # echo "APP_PATH:$APP_PATH" # echo "PING_URL:$APP_NAME" # 部署新jar包到程序运行目录 if [ -f "$DRONE_JAR_PATH/$APP_NAME" ];then echo "Move the new jar package to the deployment directory ..." mv -f $DRONE_JAR_PATH/$APP_NAME $APP_PATH/$APP_NAME else echo "The drone original jar file not exist, skip move." fi # 获取程序PID getPid() { APP_PID=`ps -ef | grep -v grep | grep $APP_NAME | awk '{print $2}'` } getPid # 启动前检查应用是否启动,如果已经启动则先停止再重新启动 while [ ${APP_PID} ] do echo -e "App ${APP_NAME} is still RUNNING! PID:$APP_PID"; echo "stop ${APP_NAME} ..."; kill -9 ${APP_PID}; sleep 2; getPid done # 运行jar包 echo "start ${APP_NAME} ...." cd ${APP_PATH} nohup ${JAVA_PATH} -server -Xms256m -Xmx512m -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=768m -Xss256k -jar ./${APP_NAME} --spring.profiles.active=$JAVA_RUN_ENV > /dev/null & 2>&1 & # 请求测试接口,判断服务是否正常启动 getHttpCode () { http_code=`curl -Is -m 10 -w %{http_code} -o /dev/null $PING_URL`; } # 检查服务启动状态 sleep 2s; count=0; # 获取接口状态码 getHttpCode while [ $http_code -ne 200 ] do if (($count == 8));then echo "${APP_NAME}: $(expr $count \* 5)秒内未启动,请检查!"; msg="start ${APP_NAME} failed!\n"; echo -e $msg; APP_START_RESULT=$APP_START_RESULT$msg; APP_START_FAIL="YES"; return; fi count=$(($count+1)); echo "${APP_NAME} waiting to start ..."; # sleep 1s; sleep 5s; # 获取接口状态码 getHttpCode echo "http_code --> $http_code"; done getPid msg="start ${APP_NAME} success! PID=$APP_PID\n"; echo -e $msg; APP_START_RESULT=$APP_START_RESULT$msg; } #----------------------- 函数定义结束 end ----------------------- #----------------------- commit log 判断 start----------------------- # 根据commit日志参数判断启动哪个工程 if [[ $PARAM == *'[CI API]'* ]]; then echo -e "Commit参数指定启动API工程...\n"; APP_API_RUN="true" fi if [[ $PARAM == *'[CI USER]'* ]]; then echo -e "Commit参数指定启动用户端工程...\n"; APP_USER_RUN="true" fi if [[ $PARAM == *'[CI BACK]'* ]]; then echo -e "Commit参数指定启动BACK工程...\n"; APP_BACK_RUN="true" fi if [[ -z "$APP_API_RUN" ]] && [[ -z "$APP_USER_RUN" ]] && [[ -z "$APP_BACK_RUN" ]]; then echo -e "Commit未指定参数,准备启动所有工程...\n"; APP_API_RUN="true" APP_USER_RUN="true" APP_BACK_RUN="true" fi #----------------------- commit log 判断结束 ----------------------- # 启动运营端服务 if [ $APP_API_RUN ]; then echo "Run api service ..." startAPP $APP_API_NAME $APP_API_PATH $APP_API_PING_URL fi # 启动用户端服务 if [ $APP_USER_RUN ]; then echo "Run user service" startAPP $APP_USER_NAME $APP_USER_PATH $APP_USER_PING_URL fi # 启动商家端服务 if [ $APP_BACK_RUN ]; then echo "Run back service" startAPP $APP_BACK_NAME $APP_BACK_PATH $APP_BACK_PING_URL fi # 删除drone构建缓存 rm -rf $DRONE_JAR_PATH/*.jar # 打印运行结果 echo "------------ service start status ------------" echo -e $APP_START_RESULT echo "----------------------------------------------" # 如果有启动失败的应用,则退出状态码为1,用于drone标记构建失败 if [ $APP_START_FAIL ]; then exit 1; fi以上脚本在实际生产环境已经稳定运行几个月了,经过如上配置可以很方便的去控制多工程项目下某一个工程的自动构建部署,只需在Commit日志中指定构建参数即可,在实际开发中无疑是解放了双手,能让人更加专注于业务代码实现上(摸鱼) Demo演示最后附上我一个测试环境工程的Drone自动构建日志的Demo演示Commit log未指定参数,默认自动构建并部署所有工程:Commit log指定参数,只自动构建并部署指定工程: 使用docker来构建一个SpringBoot应用 https://ffis.me/archives/1969.html 2020-05-08T23:23:00+08:00 Dcoker是一个开源的应用容器引擎,介绍我就不介绍了,总之就是很牛批,用过的都相见恨晚 平时我们不是应用都是先搭建相关环境,然后把应用启动一通配置好本地环境需要搭建一次,测试环境需要搭建一次,生产环境也需要搭建一次,这样重复的工作就做了几次,效率降低 使用了docker了?我们只需要构建一次镜像,然后不同的环境只需要把镜像拉下来启动就行了,是不是很简单?如果配合CI/DI持续集成,持续部署,那就太爽歪歪了,我们只需要把代码提交到git,剩下打包/编译/测试/部署工作就全自动自己运行了,是不是大大提高了生产力? 废话不多说,今天先研究docker怎么构建SpringBoot,改天再研究CI/DI~1 安装Docker首先docker只支持CentOS7以上的系统,如果系统版本过低请升级系统这里我使用的是CentOS7 x64docker安装教程详见:Docker的基本操作与使用2 准备SpringBoot应用这里我们使用idea来快速构建一个SpringBoot应用简单编写一个HelloController@RestController public class HelloController { @RequestMapping("/hello") public Object hello() { return "hello World!!!"; } }右键启动类运行,浏览器输入 http://localhost:8080/hello 可以看到启动成功3 创建Dockerfile构建文件我们创建docker文件夹,并在里面创建Dockerfile构建文件$ mkdir docker $ cd docker $ vim Dockerfile编写Dockerfile文件# Docker image for springboot file run # VERSION 0.0.1 # Author: Noisky # 基础镜像使用openjdk:8 FROM openjdk:8u181-jdk-alpine # 作者 MAINTAINER Noisky <[email protected]> # VOLUME 指定了临时文件目录为/tmp。 # 其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp VOLUME /tmp # 将jar包添加到容器中并更名为app.jar ADD demo-0.0.1-SNAPSHOT.jar app.jar # 运行参数 ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]4 制作SpringBoot镜像将打包好的jar包放入docker文件夹,使docker文件夹只有Dockerfile和需要打包的jar文件docker安装并启动后,我们来制作SpringBoot镜像cd 进入docker目录运行以下命令来制作镜像,注意最后的.docker build -t springdemo:v1 . -t 参数是指定此镜像的名称如图所示开始制作,最后的.表示当前目录,也就是构建镜像的上下文路径。提示successfully表示构建成功这时候我们可以使用 docker images 来查看构建的镜像了5 从新镜像启动容器docker run -d -p 8188:8180 --name springdemo springdemo:v1 -d 参数是让容器后台运行 -p 是做端口映射,此时将服务器中的8188端口映射到容器中的8080(项目中端口配置的是8080)端口运行成功现在的SpringBoot应用是使用docker构建并管理运行的我们可以使用新端口来访问试试 SpringBoot 配置文件存放位置及读取顺序 https://ffis.me/archives/1893.html 2020-04-02T22:17:00+08:00 因为线上部署的时候和本地往往使用不同的配置文件,所以研究了下SpringBoot配置文件存放位置及读取顺序之前是直接手动指定配置文件启动来启动SpringBoot工程的,后来发现直接按照配置文件的加载顺序优先级放到对应的文件夹下即可进行配置文件的自动覆盖和互补以下内容转载自:https://my.oschina.net/sdlvzg/blog/1612703SpringBoot配置文件可以使用yml格式和properties格式分别的默认命名为:application.yml、application.properties存放目录SpringBoot配置文件默认可以放到以下目录中,可以自动读取到:项目根目录下项目根目录中 config 目录下项目的 resources 目录下项目 resources 目录中 config 目录下读取顺序如果在不同的目录中存在多个配置文件,它的读取顺序是:1、config/application.properties(项目根目录中config目录下)2、config/application.yml3、application.properties(项目根目录下)4、application.yml5、resources/config/application.properties(项目resources目录中config目录下)6、resources/config/application.yml7、resources/application.properties(项目的resources目录下)8、resources/application.yml注:1、如果同一个目录下,有application.yml也有application.properties,默认先读取application.properties。2、如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取到的。3、创建SpringBoot项目时,一般的配置文件放置在“项目的resources目录下” 解决Spring项目打成Jar包后Freemarker找不到模板的问题 https://ffis.me/archives/1730.html 2019-12-11T23:33:00+08:00 昨天改完CheckDomain的bug后,打包jar准备部署到服务器上,本地测试都正常,但是扔到服务器上却发不了邮件了... 经过多次测试发现,freemarker在jar包中无法使用类加载器获取resourse目录下的templates文件 出现的问题代码如下:(本地测试正常,打包jar后无法获取模板)/** * 邮件模板静态化 * * @param mailTemplateModel 模板数据模型 * @return 加上数据后的静态化模板 */ @Override public String getMailHtml(MailTemplateModel mailTemplateModel) { try { //创建配置类 Configuration configuration = new Configuration(Configuration.getVersion()); //设置模板路径 String classpath = this.getClass().getResource("/").getPath(); configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates")); //设置字符集 configuration.setDefaultEncoding("utf-8"); //加载模板 Template template = configuration.getTemplate("mailTemplate.ftl"); //模板静态化并返回 return FreeMarkerTemplateUtils.processTemplateIntoString(template, mailTemplateModel); } catch (Exception e) { e.printStackTrace(); log.error("模板静态化异常", e); return null; } }修改后的代码:(打包jar后正常获取模板)@Autowired FreeMarkerConfigurer freeMarkerConfigurer; /** * 邮件模板静态化 * * @param mailTemplateModel 模板数据模型 * @return 加上数据后的静态化模板 */ @Override public String getMailHtml(MailTemplateModel mailTemplateModel) { try { //创建配置类 Configuration configuration = new Configuration(Configuration.getVersion()); //设置模板路径 //String classpath = this.getClass().getResource("/").getPath(); //configuration.setDirectoryForTemplateLoading(new File(classpath + "/templates")); //设置字符集 configuration.setDefaultEncoding("utf-8"); //加载模板 //Template template = configuration.getTemplate("mailTemplate.ftl"); Template template = freeMarkerConfigurer.getConfiguration().getTemplate("mailTemplate.ftl"); //模板静态化并返回 return FreeMarkerTemplateUtils.processTemplateIntoString(template, mailTemplateModel); } catch (Exception e) { e.printStackTrace(); log.error("模板静态化异常", e); return null; } }PS: .ftl模板文件放在 templates 目录下总结:freemarker无法使用类加载器获取jar包中的resourse目录下的templates文件解决办法:注入FreeMarkerConfigurer配置类,因为freemarker模板的默认目录就在resourse下的templates目录下,使用freeMarkerConfigurer.getConfiguration().getTemplate("mailTemplate.ftl")可直接获取到对应的模板文件 记一次解决RestTemplate无法解析api返回的xml数据问题 https://ffis.me/archives/1714.html 2019-12-06T21:52:00+08:00 最近看上了个本命域名,因为过期即将被注册局删除,于是就自己写了个api接口用来监控这个域名,等可以注册了就直接发邮件通知我 其中用到了阿里云域名的一个api,准备直接使用RestTemplate去请求这个接口,并将结果自动封装为Map集合方便进行进一步处理。等到框架搭好,代码写完,以为万事大吉的时候,程序却给我来了个惊喜: org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/plain;charset=GBK]很明显,上面提示接口返回的是text/plain文本类型数据,RestTemplate解析数据的时候没有找到合适的解析器,就抛了异常。WTF? 我看阿里云的接口明明返回的是xml的数据啊......仔细再定睛一看,类型还真的是文本类型...我嘞个去,还有这种操作? 这可怎么办?当时我没想太多,就直接将返回值保存为String类型,然后使用jackson直接解析成对象了,这样也能进行数据处理了;不过问题虽然暂时解决了,可终究不是好办法,RestTemplate本身就内置了jackson解析器,所以说肯定能直接让RestTemplate解析这个数据的;本着极客精神,今天有空就看了下源码:根据程序提示的异常信息:org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [interface java.util.Map] and content type [text/plain;charset=GBK] at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:123) at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1001) at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:984)先进入HttpMessageConverterExtractor.java中,于是看到如下逻辑HttpMessageConverter<?> messageConverter = (HttpMessageConverter)var4.next(); if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter)messageConverter; if (genericMessageConverter.canRead(this.responseType, (Class)null, contentType)) { if (this.logger.isDebugEnabled()) { ResolvableType resolvableType = ResolvableType.forType(this.responseType); this.logger.debug("Reading to [" + resolvableType + "]"); } return genericMessageConverter.read(this.responseType, (Class)null, responseWrapper); } } if (this.responseClass != null && messageConverter.canRead(this.responseClass, contentType)) { if (this.logger.isDebugEnabled()) { String className = this.responseClass.getName(); this.logger.debug("Reading to [" + className + "] as \"" + contentType + "\""); } return messageConverter.read(this.responseClass, responseWrapper); }可以看到messageConverter.canRead(this.responseClass, contentType)方法是根据contentType来判断这个MessageConverter能不能处理响应的内容,由于我需要处理的类型为xml,所以就进入MappingJackson2XmlHttpMessageConverter类,并终于在它的爸爸类AbstractJackson2HttpMessageConverter的爷爷类AbstractGenericHttpMessageConverter的祖宗类AbstractHttpMessageConverter中的canRead方法中找到了对应的处理逻辑:protected boolean canRead(@Nullable MediaType mediaType) { if (mediaType == null) { return true; } else { Iterator var2 = this.getSupportedMediaTypes().iterator(); MediaType supportedMediaType; do { if (!var2.hasNext()) { return false; } supportedMediaType = (MediaType)var2.next(); } while(!supportedMediaType.includes(mediaType)); return true; } }这里可以看到canRead方法就是根据内置的mediaType列表来判断是否能够解析传入的contentType类型,所以解决方法就是将text/plain文本类数据加入到xml解析器的mediaType列表中,让xml解析器默认支持解析text/plain类型,可以直接在创建RestTemplate的时候实现,具体代码如下:@Bean public RestTemplate restTemplate() { //创建RestTemplate对象,并且使用okHttp客户端 RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory()); //先获取到converter列表 List<HttpMessageConverter<?>> converters = restTemplate.getMessageConverters(); for(HttpMessageConverter<?> converter : converters){ //让jsonConverter支持对text/plain的解析 if(converter instanceof MappingJackson2XmlHttpMessageConverter){ try{ //先将原先支持的MediaType列表拷出 List<MediaType> mediaTypeList = new ArrayList<>(converter.getSupportedMediaTypes()); //加入对text/plain的支持 mediaTypeList.add(MediaType.TEXT_PLAIN); //将已经加入了text/plain的MediaType支持列表设置为其支持的媒体类型列表 ((MappingJackson2XmlHttpMessageConverter) converter).setSupportedMediaTypes(mediaTypeList); }catch(Exception e){ e.printStackTrace(); } } } return restTemplate; }至此,我的RestTemplate就能处理返回的是xml数据,不过类型为text/plain的数据了 Git分布式版本控制工具 https://ffis.me/archives/1614.html 2019-10-27T20:30:04+08:00 1 Git概述1.1 Git历史Git 诞生于一个极富纷争大举创新的年代。Linux 内核开源项目有着为数众多的参与者。 绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。 到 2002 年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper 来管理和维护代码。到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了 Linux 内核社区免费使用 BitKeeper 的权力。 这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds)基于使用 BitKeeper 时的经验教训,开发出自己的版本系统。 他们对新的系统制订了若干目标:速度简单的设计对非线性开发模式的强力支持(允许成千上万个并行开发的分支)完全分布式有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量)1.2 Git与SVN对比SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而开发人员工作的时候,用的都是自己的电脑,所以首先要从中央服务器下载最新的版本,然后开发,开发完后,需要把自己开发的代码提交到中央服务器。集中式版本控制工具缺点:服务器单点故障容错性差Git是分布式版本控制系统(Distributed Version Control System,简称 DVCS) ,分为两种类型的仓库:本地仓库和远程仓库本地仓库:是在开发人员自己电脑上的Git仓库 远程仓库:是在远程服务器上的Git仓库Clone:克隆,就是将远程仓库复制到本地Push:推送,就是将本地仓库代码上传到远程仓库Pull:拉取,就是将远程仓库代码下载到本地仓库1.3 Git工作流程工作流程如下:1.从远程仓库中克隆代码到本地仓库2.从本地仓库中checkout代码然后进行代码修改3.在提交前先将代码提交到暂存区4.提交到本地仓库。本地仓库中保存修改的各个历史版本5.修改完成后,需要和团队成员共享代码时,将代码push到远程仓库1.4 Git下载与安装下载地址: https://git-scm.com/download安装过程详见:https://ffis.me/experience/1441.html2 Git代码托管服务2.1 常用的Git代码托管服务前面已经知道了Git中存在两种类型的仓库,即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢?我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有GitHub、码云、GitLab等。Github( 地址:https://github.com/ )是一个面向开源及私有软件项目的托管平台,因为只支持Git 作为唯一的版本库格式进行托管,故名Github码云(地址: https://gitee.com/ )是国内的一个代码托管平台,由于服务器在国内,所以相比于GitHub,码云速度会更快GitLab (地址: https://about.gitlab.com/ )是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的web服务2.2 在码云注册账号要想使用码云的相关服务,需要注册账号(地址: https://gitee.com/signup )2.3 登录码云并创建Git远程仓库注册完成后就可以使用刚刚注册的邮箱进行登录(地址: https://gitee.com/login )登录成功后就可以创建Git远程仓库创建完成后可以查看仓库信息每个Git远程仓库都会对应一个网络地址,可以点击克隆/下载按钮弹出窗口并点击复制按钮获得这个网络地址当前创建的Git远程仓库对应的地址为:https://gitee.com/noisky/myRepo.git2.4 邀请其他用户成为仓库成员前面已经在码云上创建了自己的远程仓库,目前仓库成员只有自己一个人(身份为管理员)。在企业实际开发中,一个项目往往是由多个人共同开发完成的,为了使多个参与者都有权限操作远程仓库,就需要邀请其他项目参与者成为当前仓库的成员。3 Git常用命令3.1 环境配置当安装Git后首先要做的事情是设置用户名称和email地址。这是非常重要的,因为每次Git提交都会使用该用户信息设置用户信息 git config --global user.name “noisky”git config --global user.email “[email protected]”查看配置信息git config --listgit config user.name通过上面的命令设置的信息会保存在~/.gitconfig(用户目录下)文件中3.2 获取Git仓库要使用Git对我们的代码进行版本控制,首先需要获得Git仓库获取Git仓库通常有两种方式:在本地初始化一个Git仓库从远程仓库克隆3.2.1 在本地初始化一个Git仓库执行步骤如下:1.在电脑的任意位置创建一个空目录(例如repo1)作为我们的本地Git仓库2.进入这个目录中,点击右键打开Git bash窗口3.执行命令git init如果在当前目录中看到.git文件夹(此文件夹为隐藏文件夹)则说明Git仓库创建成功3.2.2 从远程仓库克隆可以通过Git提供的命令从远程仓库进行克隆,将远程仓库克隆到本地命令形式为:git clone 远程Git仓库地址 3.3 工作目录、暂存区以及版本库概念为了更好的学习Git,我们需要了解Git相关的一些概念,这些概念在后面的学习中会经常提到版本库:前面看到的.git隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等工作目录(工作区):包含.git文件夹的目录就是工作目录,主要用于存放开发的代码暂存区:.git文件夹中有很多文件,其中有一个index文件就是暂存区,也可以叫做stage。暂存区是一个临时保存修改文件的地方3.4 Git工作目录下文件的两种状态Git工作目录下的文件存在两种状态:untracked 未跟踪(未被纳入版本控制)tracked 已跟踪(被纳入版本控制)Unmodified 未修改状态Modified 已修改状态Staged 已暂存状态这些文件的状态会随着我们执行Git的命令发生变化3.5 本地仓库操作git status 查看文件状态也可以使用git status –s使输出信息更加简洁git add 将未跟踪的文件加入暂存区将新创建的文件加入暂存区后查看文件状态git reset 将暂存区的文件取消暂存将文件取消暂存后查看文件状态git commit 将暂存区的文件修改提交到本地仓库git rm 删除文件删除文件后查看文件状态上面删除的只是工作区的文件,需要提交到本地仓库将文件添加至忽略列表一般我们总会有些文件无需纳入Git 的管理,也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以在工作目录中创建一个名为 .gitignore 的文件(文件名称固定),列出要忽略的文件模式。下面是一个示例:# no .a files *.a # but do track lib.a, even though you're ignoring .a files above !lib.a # only ignore the TODO file in the current directory, not subdir/TODO /TODO # ignore all files in the build/ directory build/ # ignore doc/notes.txt, but not doc/server/arch.txt doc/*.txt # ignore all .pdf files in the doc/ directory doc/**/*.pdfgit log 查看日志记录3.6 远程仓库操作前面执行的命令操作都是针对的本地仓库,下面介绍远程仓库的一些操作,具体包括:3.6.1 查看远程仓库如果想查看已经配置的远程仓库服务器,可以运行 git remote 命令。 它会列出指定的每一个远程服务器的简写。 如果已经克隆了远程仓库,那么至少应该能看到 origin ,这是 Git 克隆的仓库服务器的默认名字3.6.2 添加远程仓库运行 git remote add 添加一个新的远程 Git 仓库,同时指定一个可以引用的简写3.6.3 从远程仓库克隆如果你想获得一份已经存在了的 Git 仓库的拷贝,这时就要用到 git clone 命令。 Git 克隆的是该 Git 仓库服务器上的几乎所有数据(包括日志信息、历史记录等),而不仅仅是复制工作所需要的文件。 当你执行 git clone 命令的时候,默认配置下远程 Git 仓库中的每一个文件的每一个版本都将被拉取下来。克隆仓库的命令格式是 git clone [url] 3.6.4 移除无效的远程仓库如果因为一些原因想要移除一个远程仓库 ,可以使用 git remote rm注意:此命令只是从本地移除远程仓库的记录,并不会真正影响到远程仓库3.6.5 从远程仓库中抓取与拉取git fetch 是从远程仓库获取最新版本到本地仓库,不会自动mergegit pull 是从远程仓库获取最新版本并merge到本地仓库注意:如果当前本地仓库不是从远程仓库克隆,而是本地创建的仓库,并且仓库中存在文件,此时再从远程仓库拉取文件的时候会报错(fatal: refusing to merge unrelated histories )解决此问题可以在git pull命令后加入参数 --allow-unrelated-histories3.6.6 推送到远程仓库当你想分享你的代码时,可以将其推送到远程仓库。 命令形式:git push remote-name3.7 Git分支几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。Git 的master分支并不是一个特殊分支。 它跟其它分支没有区别。 之所以几乎每一个仓库都有 master 分支,是因为git init 命令默认创建它,并且大多数人都懒得去改动它。下面介绍关于分支的相关命令,具体如下:3.7.1 查看分支\# 列出所有本地分支$ git branch\# 列出所有远程分支$ git branch -r\# 列出所有本地分支和远程分支$ git branch -a3.7.2 创建分支3.7.3 切换分支​3.7.4 推送至远程仓库分支3.7.5 合并分支如将b1分支合并到master分支,先切换到master分支,然后执行git merge b1命令有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没办法合并它们,同时会提示文件冲突。此时需要我们打开冲突的文件并修复冲突内容,最后执行git add命令来标识冲突已解决3.7.5 删除分支如果要删除的分支中进行了一些开发动作,此时执行上面的删除命令并不会删除分支,如果坚持要删除此分支,可以将命令中的-d参数改为-D注:如果要删除远程仓库中的分支,可以使用命令git push origin –d branchName3.8 标签-扩展像其他版本控制系统(VCS)一样,Git 可以给历史中的某一个提交打上标签,以示重要。 比较有代表性的是人们会使用这个功能来标记发布结点(v1.0 、v1.2等)。标签指的是某个分支某个特定时间点的状态。通过标签,可以很方便的切换到标记时的状态。在本节中,我们将学习:列出已有的标签创建新标签将标签推送至远程仓库检出标签删除标签3.8.1 列出已有的标签\# 列出所有tag$ git tag\# 查看tag信息$ git show [tag]3.8.2 创建新标签\# 新建一个tag$ git tag [tagName]3.8.3 将标签推送至远程仓库\# 提交指定tag$ git push [remote] [tag]3.8.4 检出标签\# 新建一个分支,指向某个tag$ git checkout -b [branch] [tag]3.8.5 删除标签\# 删除本地tag$ git tag -d [tag]\# 删除远程tag$ git push origin :refs/tags/[tag]4 在IDEA中使用Git4.1 在IDEA中配置Git安装好IntelliJ IDEA后,如果Git安装在默认路径下,那么idea会自动找到git的位置,如果更改了Git的安装位置则需要手动配置下Git的路径。选择File→Settings打开设置窗口,找到Version Control下的git选项:选择git的安装目录后可以点击“Test”按钮测试是否正确配置4.2 在IDEA中使用Git4.2.1在IDEA中创建工程并将工程添加至Git将项目添加至Git管理后,可以从IDEA的工具栏上看到Git操作的按钮4.2.2 将文件添加到暂存区4.2.3 提交文件4.2.4 将代码推送到远程仓库4.2.5 从远程仓库克隆工程到本地4.2.6 从远程拉取代码4.2.7 版本对比4.2.8 创建分支4.2.9 切换分支4.2.10 分支合并 mybatis03 - 多表查询、简单的SSM整合 https://ffis.me/archives/1586.html 2019-10-21T20:50:24+08:00 1 Mybatis多表查询1.1 一对一查询1.1.1 一对一查询的模型用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户1.1.2一对一查询的语句对应的sql语句:SELECT *,o.id oid FROM orders o, USER u WHERE o.uid = u.id;查询的结果如下:1.1.3 创建Order和User实体public class Order { private int id; private Date ordertime; private double total; //代表当前订单从属于哪一个客户 private User user; //此处省略getter和setter } public class User { private int id; private String username; private String password; private Date birthday; //此处省略getter和setter }1.1.4 创建OrderMapper接口public interface OrderMapper { List<Order> findAll(); }1.1.5 配置OrderMapper.xml<mapper namespace="me.ffis.mapper.OrderMapper"> <resultMap id="orderMap" type="me.ffis.domain.Order"> <result column="uid" property="user.id"></result> <result column="username" property="user.username"></result> <result column="password" property="user.password"></result> <result column="birthday" property="user.birthday"></result> </resultMap> <select id="findAll" resultMap="orderMap"> SELECT *,o.id oid FROM orders o, USER u WHERE o.uid = u.id </select> </mapper>其中还可以配置如下:<resultMap id="orderMap" type="me.ffis.domain.Order"> <result property="id" column="id"></result> <result property="ordertime" column="ordertime"></result> <result property="total" column="total"></result> <association property="user" javaType="me.ffis.domain.User"> <result column="uid" property="id"></result> <result column="username" property="username"></result> <result column="password" property="password"></result> <result column="birthday" property="birthday"></result> </association> </resultMap>1.1.6 测试结果OrderMapper mapper = sqlSession.getMapper(OrderMapper.class); List<Order> all = mapper.findAll(); for(Order order : all){ System.out.println(order); }1.2 一对多查询1.2.1 一对多查询的模型用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单1.2.2 一对多查询的语句对应的sql语句:SELECT *,o.id oid FROM USER u, orders o WHERE u.id = o.uid;查询的结果如下:1.2.3 修改User实体public class Order { private int id; private Date ordertime; private double total; //代表当前订单从属于哪一个客户 private User user; //此处省略getter和setter } public class User { private int id; private String username; private String password; private Date birthday; //代表当前用户具备哪些订单 private List<Order> orderList; //此处省略getter和setter }1.2.4 创建UserMapper接口public interface UserMapper { List<User> findAll(); }1.2.5 配置UserMapper.xml<mapper namespace="me.ffis.mapper.UserMapper"> <resultMap id="userMap" type="me.ffis.domain.User"> <result column="id" property="id"></result> <result column="username" property="username"></result> <result column="password" property="password"></result> <result column="birthday" property="birthday"></result> <collection property="orderList" ofType="me.ffis.domain.Order"> <result column="oid" property="id"></result> <result column="ordertime" property="ordertime"></result> <result column="total" property="total"></result> </collection> </resultMap> <select id="findAll" resultMap="userMap"> select *,o.id oid from user u left join orders o on u.id=o.uid </select> </mapper>1.2.6 测试结果UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> all = mapper.findAll(); for(User user : all){ System.out.println(user.getUsername()); List<Order> orderList = user.getOrderList(); for(Order order : orderList){ System.out.println(order); } System.out.println("----------------------------------"); }1.3 多对多查询1.3.1 多对多查询的模型用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用多对多查询的需求:查询用户同时查询出该用户的所有角色1.3.2 多对多查询的语句对应的sql语句:SELECT u.*,r.*,r.id rid FROM USER u LEFT JOIN sys_user_role sr ON u.id=sr.userid INNER JOIN sys_role r ON sr.roleid=r.id;查询的结果如下:1.3.3 创建Role实体,修改User实体public class User { private int id; private String username; private String password; private Date birthday; //代表当前用户具备哪些订单 private List<Order> orderList; //代表当前用户具备哪些角色 private List<Role> roleList; //此处省略getter和setter } public class Role { private int id; private String rolename; //此处省略getter和setter } 1.3.4 添加UserMapper接口方法List<User> findAllUserAndRole();1.3.5 配置UserMapper.xml<resultMap id="userRoleMap" type="me.ffis.domain.User"> <result column="id" property="id"></result> <result column="username" property="username"></result> <result column="password" property="password"></result> <result column="birthday" property="birthday"></result> <collection property="roleList" ofType="me.ffis.domain.Role"> <result column="rid" property="id"></result> <result column="rolename" property="rolename"></result> </collection> </resultMap> <select id="findAllUserAndRole" resultMap="userRoleMap"> SELECT u.*,r.*,r.id rid FROM USER u LEFT JOIN sys_user_role sr ON u.id=sr.userid INNER JOIN sys_role r ON sr.roleid=r.id; </select>1.3.6 测试结果UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> all = mapper.findAllUserAndRole(); for(User user : all){ System.out.println(user.getUsername()); List<Role> roleList = user.getRoleList(); for(Role role : roleList){ System.out.println(role); } System.out.println("----------------------------------"); }1.4 知识小结MyBatis多表配置方式:一对一配置:使用做配置一对多配置:使用+做配置多对多配置:使用+做配置2 Mybatis的注解开发2.1 MyBatis的常用注解这几年来注解开发越来越流行,Mybatis也可以使用注解开发方式,这样我们就可以减少编写Mapper映射文件了。我们先围绕一些基本的CRUD来学习,再学习复杂映射多表操作。@Insert:实现新增@Update:实现更新@Delete:实现删除@Select:实现查询@Result:实现结果集封装@Results:可以与@Result 一起使用,封装多个结果集@One:实现一对一结果集封装@Many:实现一对多结果集封装2.2 MyBatis的增删改查我们完成简单的user表的增删改查的操作private UserMapper userMapper; @Before public void before() throws IOException { InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(true); userMapper = sqlSession.getMapper(UserMapper.class); } @Test public void testAdd() { User user = new User(); user.setUsername("测试数据"); user.setPassword("123"); user.setBirthday(new Date()); userMapper.add(user); } @Test public void testUpdate() throws IOException { User user = new User(); user.setId(16); user.setUsername("测试数据修改"); user.setPassword("abc"); user.setBirthday(new Date()); userMapper.update(user); } @Test public void testDelete() throws IOException { userMapper.delete(16); } @Test public void testFindById() throws IOException { User user = userMapper.findById(1); System.out.println(user); } @Test public void testFindAll() throws IOException { List<User> all = userMapper.findAll(); for(User user : all){ System.out.println(user); } }修改MyBatis的核心配置文件,我们使用了注解替代的映射文件,所以我们只需要加载使用了注解的Mapper接口即可<mappers> <!--扫描使用注解的类--> <mapper class="me.ffis.mapper.UserMapper"></mapper> </mappers>或者指定扫描包含映射关系的接口所在的包也可以<mappers> <!--扫描使用注解的类所在的包--> <package name="me.ffis.mapper"></package> </mappers>2.3 MyBatis的注解实现复杂映射开发实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置2.4 一对一查询2.4.1 一对一查询的模型用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户2.4.2 一对一查询的语句对应的sql语句:select * from orders; select * from user where id=查询出订单的uid; 查询的结果如下:2.4.3 创建Order和User实体public class Order { private int id; private Date ordertime; private double total; //代表当前订单从属于哪一个客户 private User user; //此处省略getter和setter } public class User { private int id; private String username; private String password; private Date birthday; //此处省略getter和setter }2.4.4 创建OrderMapper接口public interface OrderMapper { List<Order> findAll(); }2.4.5 使用注解配置Mapperpublic interface OrderMapper { @Select("select * from orders") @Results({ @Result(id=true,property = "id",column = "id"), @Result(property = "ordertime",column = "ordertime"), @Result(property = "total",column = "total"), @Result(property = "user",column = "uid", javaType = User.class, one = @One(select = "me.ffis.mapper.UserMapper.findById")) }) List<Order> findAll(); }public interface UserMapper { @Select("select * from user where id=#{id}") User findById(int id); }2.4.6 测试结果@Test public void testSelectOrderAndUser() { List<Order> all = orderMapper.findAll(); for(Order order : all){ System.out.println(order); } }2.5 一对多查询2.5.1 一对多查询的模型用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单2.5.2 一对多查询的语句对应的sql语句:select * from user; select * from orders where uid=查询出用户的id; 查询的结果如下:2.5.3 修改User实体public class Order { private int id; private Date ordertime; private double total; //代表当前订单从属于哪一个客户 private User user; //此处省略getter和setter } public class User { private int id; private String username; private String password; private Date birthday; //代表当前用户具备哪些订单 private List<Order> orderList; //此处省略getter和setter }2.5.4 创建UserMapper接口List<User> findAllUserAndOrder();2.5.5 使用注解配置Mapperpublic interface UserMapper { @Select("select * from user") @Results({ @Result(id = true,property = "id",column = "id"), @Result(property = "username",column = "username"), @Result(property = "password",column = "password"), @Result(property = "birthday",column = "birthday"), @Result(property = "orderList",column = "id", javaType = List.class, many = @Many(select = "me.ffis.mapper.OrderMapper.findByUid")) }) List<User> findAllUserAndOrder(); } public interface OrderMapper { @Select("select * from orders where uid=#{uid}") List<Order> findByUid(int uid); }2.5.6 测试结果List<User> all = userMapper.findAllUserAndOrder(); for(User user : all){ System.out.println(user.getUsername()); List<Order> orderList = user.getOrderList(); for(Order order : orderList){ System.out.println(order); } System.out.println("-----------------------------"); }2.6 多对多查询2.6.1 多对多查询的模型用户表和角色表的关系为,一个用户有多个角色,一个角色被多个用户使用多对多查询的需求:查询用户同时查询出该用户的所有角色2.6.2 多对多查询的语句对应的sql语句:select * from user; select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=用户的id 查询的结果如下:2.6.3 创建Role实体,修改User实体public class User { private int id; private String username; private String password; private Date birthday; //代表当前用户具备哪些订单 private List<Order> orderList; //代表当前用户具备哪些角色 private List<Role> roleList; //此处省略getter和setter } public class Role { private int id; private String rolename; //此处省略getter和setter }2.6.4 添加UserMapper接口方法List<User> findAllUserAndRole();2.6.5 使用注解配置Mapperpublic interface UserMapper { @Select("select * from user") @Results({ @Result(id = true,property = "id",column = "id"), @Result(property = "username",column = "username"), @Result(property = "password",column = "password"), @Result(property = "birthday",column = "birthday"), @Result(property = "roleList",column = "id", javaType = List.class, many = @Many(select = "me.ffis.mapper.RoleMapper.findByUid")) }) List<User> findAllUserAndRole();} public interface RoleMapper { @Select("select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=#{uid}") List<Role> findByUid(int uid); } 2.6.6 测试结果UserMapper mapper = sqlSession.getMapper(UserMapper.class); List<User> all = mapper.findAllUserAndRole(); for(User user : all){ System.out.println(user.getUsername()); List<Role> roleList = user.getRoleList(); for(Role role : roleList){ System.out.println(role); } System.out.println("----------------------------------"); }SSM框架整合1.1 原始方式整合1.准备工作2.创建Maven工程3.导入Maven坐标4.编写实体类public class Account { private int id; private String name; private double money; //省略getter和setter方法 }5.编写Mapper接口public interface AccountMapper { //保存账户数据 void save(Account account); //查询账户数据 List<Account> findAll(); }6.编写Service接口public interface AccountService { void save(Account account); //保存账户数据 List<Account> findAll(); //查询账户数据 }7.编写Service接口实现@Service("accountService") public class AccountServiceImpl implements AccountService { public void save(Account account) { SqlSession sqlSession = MyBatisUtils.openSession(); AccountMapper accountMapper = sqlSession.getMapper(AccountMapper.class); accountMapper.save(account); sqlSession.commit(); sqlSession.close(); } public List<Account> findAll() { SqlSession sqlSession = MyBatisUtils.openSession(); AccountMapper accountMapper = sqlSession.getMapper(AccountMapper.class); return accountMapper.findAll(); } }8.编写Controller@Controller public class AccountController { @Autowired private AccountService accountService; @RequestMapping("/save") @ResponseBody public String save(Account account){ accountService.save(account); return "save success"; } @RequestMapping("/findAll") public ModelAndView findAll(){ ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName("accountList"); modelAndView.addObject("accountList",accountService.findAll()); return modelAndView; } }9.编写添加页面<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h1>保存账户信息表单</h1> <form action="${pageContext.request.contextPath}/save.action" method="post"> 用户名称<input type="text" name="name"><br/> 账户金额<input type="text" name="money"><br/> <input type="submit" value="保存"><br/> </form> </body> </html>10.编写列表页面<table border="1"> <tr> <th>账户id</th> <th>账户名称</th> <th>账户金额</th> </tr> <c:forEach items="${accountList}" var="account"> <tr> <td>${account.id}</td> <td>${account.name}</td> <td>${account.money}</td> </tr> </c:forEach> </table>11.编写相应配置文件(文件参考目录:素材/配置文件)Spring配置文件:applicationContext.xmlSprngMVC配置文件:spring-mvc.xmlMyBatis映射文件:AccountMapper.xmlMyBatis核心文件:sqlMapConfig.xml数据库连接信息文件:jdbc.propertiesWeb.xml文件:web.xml日志文件:log4j.xml12.测试添加账户1.2 Spring整合MyBatis1.整合思路2.将SqlSessionFactory配置到Spring容器中<!--加载jdbc.properties--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置数据源--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driverClass}"/> <property name="jdbcUrl" value="${jdbc.Url}"/> <property name="user" value="${jdbc.user}"/> <property name="password" value="${jdbc.password}"/> </bean> <!--配置MyBatis的SqlSessionFactory--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:sqlMapConfig.xml"/> </bean>3.扫描Mapper,让Spring容器产生Mapper实现类<!--配置Mapper扫描--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="me.ffis.mapper"/> </bean>4.配置声明式事务控制<!--声明式事务配置--> <!--配置平台事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--配置数据源--> <property name="dataSource" ref="dataSource"/> </bean> <!--配置事务增强--> <tx:advice id="myAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> <!--配置aop切面类记录方法执行耗时--> <bean id="timeAspect" class="me.ffis.aop.TimeAspect"/> <!--配置事务的AOP织入--> <aop:config> <aop:pointcut id="implPoint" expression="execution(* me.ffis.service.impl.*.*(..))"/> <aop:advisor advice-ref="myAdvice" pointcut-ref="implPoint"/> <!--配置aop环绕通知记录执行时间--> <aop:aspect ref="timeAspect"> <aop:around method="getTime" pointcut-ref="implPoint"/> </aop:aspect> </aop:config>5.修改Service实现类代码@Service("accountService") public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; public void save(Account account) { accountMapper.save(account); } public List<Account> findAll() { return accountMapper.findAll(); } } mybatis02 - 核心配置文件深入 https://ffis.me/archives/1576.html 2019-10-20T21:03:00+08:00 1 Mybatis的Dao层实现1.1 传统开发方式1.1.1 编写UserDao接口public interface UserDao { List<User> findAll() throws IOException; }1.1.2 编写UserDaoImpl实现public class UserDaoImpl implements UserDao { public List<User> findAll() throws IOException { InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); List<User> userList = sqlSession.selectList("userMapper.findAll"); sqlSession.close(); return userList; } }1.1.3 测试传统方式@Test public void testTraditionDao() throws IOException { UserDao userDao = new UserDaoImpl(); List<User> all = userDao.findAll(); System.out.println(all); }1.2 代理开发方式1.2.1 代理开发方式介绍采用 Mybatis 的代理开发方式实现 DAO 层的开发,这种方式是我们后面进入企业的主流。Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。Mapper 接口开发需要遵循以下规范:1) Mapper.xml文件中的namespace与mapper接口的全限定名相同2) Mapper接口方法名和Mapper.xml中定义的每个statement的id相同3) Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同4) Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同1.2.2 编写UserMapper接口1.2.3 测试代理方式@Test public void testProxyDao() throws IOException { InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); SqlSession sqlSession = sqlSessionFactory.openSession(); //获得MyBatis框架生成的UserMapper接口的实现类 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.findById(1); System.out.println(user); sqlSession.close(); }1.3 知识小结MyBatis的Dao层实现的两种方式:手动对Dao进行实现:传统开发方式代理方式对Dao进行实现: **UserMapper userMapper = sqlSession.getMapper(UserMapper.class);** 2 MyBatis映射文件深入2.1 动态sql语句2.1.1 动态sql语句概述Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化的,此时我们的 SQL 就不能满足要求了。参考的官方文档,描述如下:2.1.2 动态 SQL 之<if>我们根据实体类的不同取值,使用不同的 SQL语句来进行查询。比如在 id如果不为空时可以根据id查询,如果username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。<select id="findByCondition" parameterType="user" resultType="user"> select * from User <where> <if test="id!=0"> and id=#{id} </if> <if test="username!=null"> and username=#{username} </if> </where> </select>当查询条件id和username都存在时,控制台打印的sql语句如下: … … … //获得MyBatis框架生成的UserMapper接口的实现类 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User condition = new User(); condition.setId(1); condition.setUsername("lucy"); User user = userMapper.findByCondition(condition); … … …当查询条件只有id存在时,控制台打印的sql语句如下: … … … //获得MyBatis框架生成的UserMapper接口的实现类 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User condition = new User(); condition.setId(1); User user = userMapper.findByCondition(condition); … … … 2.1.3 动态 SQL 之<foreach>循环执行sql的拼接操作,例如:SELECT * FROM USER WHERE id IN (1,2,5)。<select id="findByIds" parameterType="list" resultType="user"> select * from User <where> <foreach collection="array" open="id in(" close=")" item="id" separator=","> #{id} </foreach> </where> </select>测试代码片段如下://获得MyBatis框架生成的UserMapper接口的实现类 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); int[] ids = new int[]{2,5}; List<User> userList = userMapper.findByIds(ids); System.out.println(userList);foreach标签的属性含义如下:标签用于遍历集合,它的属性:collection:代表要遍历的集合元素,注意编写时不要写#{}open:代表语句的开始部分close:代表结束部分item:代表遍历集合的每个元素,生成的变量名sperator:代表分隔符2.2 SQL片段抽取Sql 中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的<!--抽取sql片段简化编写--> <sql id="selectUser">select * from User</sql> <select id="findById" parameterType="int" resultType="user"> <include refid="selectUser"></include> where id=#{id} </select> <select id="findByIds" parameterType="list" resultType="user"> <include refid="selectUser"></include> <where> <foreach collection="array" open="id in(" close=")" item="id" separator=","> #{id} </foreach> </where> </select>2.3 知识小结MyBatis映射文件配置:<select>:查询 <insert>:插入 <update>:修改 <delete>:删除 <where>:where条件 <if>:if判断 <foreach>:循环 <sql>:sql片段抽取3 MyBatis核心配置文件深入3.1 typeHandlers标签无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器(截取部分)。你可以重写类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。具体做法为:实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 然后可以选择性地将它映射到一个JDBC类型。例如需求:一个Java中的Date数据类型,我想将之存到数据库的时候存成一个1970年至今的毫秒数,取出来时转换成java的Date,即java的Date与数据库的varchar毫秒值之间转换。开发步骤:①定义转换类继承类BaseTypeHandler②覆盖4个未实现的方法,其中setNonNullParameter为java程序设置数据到数据库的回调方法,getNullableResult为查询时 mysql的字符串类型转换成 java的Type类型的方法③在MyBatis核心配置文件中进行注册测试转换是否正确public class MyDateTypeHandler extends BaseTypeHandler<Date> { public void setNonNullParameter(PreparedStatement preparedStatement, int i, Date date, JdbcType type) { preparedStatement.setString(i,date.getTime()+""); } public Date getNullableResult(ResultSet resultSet, String s) throws SQLException { return new Date(resultSet.getLong(s)); } public Date getNullableResult(ResultSet resultSet, int i) throws SQLException { return new Date(resultSet.getLong(i)); } public Date getNullableResult(CallableStatement callableStatement, int i) throws SQLException { return callableStatement.getDate(i); } }<!--注册类型自定义转换器--> <typeHandlers> <typeHandler handler="me.ffis.typeHandlers.MyDateTypeHandler"></typeHandler> </typeHandlers>测试添加操作:user.setBirthday(new Date()); userMapper.add2(user);数据库数据:测试查询操作:3.2 plugins标签MyBatis可以使用第三方的插件来对功能进行扩展,分页助手PageHelper是将分页的复杂操作进行封装,使用简单的方式即可获得分页的相关数据开发步骤:①导入通用PageHelper的坐标②在mybatis核心配置文件中配置PageHelper插件③测试分页数据获取①导入通用PageHelper坐标<!-- 分页助手 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>3.7.5</version> </dependency> <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>0.9.1</version> </dependency>②在mybatis核心配置文件中配置PageHelper插件<!-- 注意:分页助手的插件 配置在通用馆mapper之前 --> <plugin interceptor="com.github.pagehelper.PageHelper"> <!-- 指定方言 --> <property name="dialect" value="mysql"/> </plugin>③测试分页代码实现@Test public void testPageHelper(){ //设置分页参数 PageHelper.startPage(1,2); List<User> select = userMapper2.select(null); for(User user : select){ System.out.println(user); } }获得分页相关的其他参数//其他分页的数据 PageInfo<User> pageInfo = new PageInfo<User>(select); System.out.println("总条数:"+pageInfo.getTotal()); System.out.println("总页数:"+pageInfo.getPages()); System.out.println("当前页:"+pageInfo.getPageNum()); System.out.println("每页显示长度:"+pageInfo.getPageSize()); System.out.println("是否第一页:"+pageInfo.isIsFirstPage()); System.out.println("是否最后一页:"+pageInfo.isIsLastPage());3.3 知识小结MyBatis核心配置文件常用标签:1、properties标签:该标签可以加载外部的properties文件2、typeAliases标签:设置类型别名3、environments标签:数据源环境配置标签4、typeHandlers标签:配置自定义类型处理器5、plugins标签:配置MyBatis的插件