SegmentFault JavaEE教程最新的文章
2019-07-10T18:31:16+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
工作日志,多租户模式下的数据备份和迁移
https://segmentfault.com/a/1190000019723897
2019-07-10T18:31:16+08:00
2019-07-10T18:31:16+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<p>工作日志,多租户模式下的数据备份和迁移</p>
<p>记录和分享一篇工作中遇到的奇难杂症。目前做的项目是多租户模式。一套系统管理多个项目,用户登录不同的项目加载不同的数据。除了一些系统初始化的配置表外,各项目之间数据相互独立。前期选择了共享数据表的隔离方案,为后期的数据迁移挖了一个大坑。这里记录填坑的思路。可能不优雅,仅供参考。</p>
<h2>多租户</h2>
<p>多租户是一种软件架构,在同一台(组)服务器上运行单个实例,能为多个租户提供服务。以实际例子说明,一套能源监控系统,可以为A产业园提供服务,也可以为B产业园提供服务。A的管理员登录能源监控系统只会看到A产业园相关的数据。同样的道理,B产业园也是一样。多住户模式最重要的就是数据之间的独立。其最大的局限性在于对租户定制化开发困难很大。适合通用的业务场景。</p>
<h3>数据隔离方案</h3>
<h4>独立数据库</h4>
<p>顾名思义,一个租户独享一个数据库,其隔离级别最强,数据安全性最高,数据的备份和恢复最方便。对数据独立性要求很高,数据的扩张性要求较多的租户可以考虑使用。或者钱给的多也可以考虑。毕竟该模式下的硬件成本较高。代码成本较低,Hibernate已经提供DATABASE的实现。</p>
<h4>共享数据库、独立 Schema</h4>
<p>多个租户共有一个数据库,每个租户拥有属于自己的Schema(Schema表示数据库对象集合,它包含:表,视图,存储过程,索引等等对象)。其隔离级别较强,数据安全性较高,数据的备份和恢复较为麻烦。数据库出了问题会影响到所有租户。Hibernate也提供SCHEMA的实现。</p>
<h4>共享数据库、共享 Schema、共享数据表</h4>
<p>多个租户共享一个数据库,一个Schema,一张数据表。各租户之间通过字段区分。其隔离级别最低,数据安全性最低,数据的备份和恢复最麻烦(让我哭一分钟😭)。若一张表出现问题会影响到所有租户。其代码工作量也是最多,因为Hibernate(5.0.3版本)并没有支持DISCRIMINATOR模式,目前还只是计划支持。其模式最大的好处就是用最少的服务器支持最多的租户。</p>
<h2>业务场景</h2>
<p>在我们的能源管理的系统中,多个租户就是多个项目。将需要数据独立的数据表通过ProjectID区分。而一些系统初始化的配置表则可以数据共享。怎么用尽可能少的代码来管理每个租户呢?这里提出我个人的思路。</p>
<h3>多租户的实现</h3>
<p>第一步:用户登录时获取当前项目,并保存到上下文中。</p>
<p>第二步:通过EntityListeners注解监听,在实体被创建时将当前项目ID保存到数据库中。</p>
<p>第三步:通过自定义拦截器,拦截需要数据隔离的sql语句,重新拼接查询条件。</p>
<p>将当前项目保存到上下文中,不同的安全框架实现的方法也有所不同,实现的方式也多种多样,这里就不贴出代码。</p>
<p>通过EntityListeners注解可以对实体属性变化的跟踪,它提供了保存前,保存后,更新前,更新后,删除前,删除后等状态,就像是拦截器一样。这里我们可以用到<code>PrePersist</code> 在保存前将项目ID赋值</p>
<pre><code class="kotlin">@MappedSuperclass
@EntityListeners(ProjectIdListener::class)
@Poko
class TenantModel: AuditModel() {
var projectId: String? = null
}</code></pre>
<pre><code class="kotlin">class ProjectIdListener {
@PrePersist
fun setProjectId(resultObj: Any) {
try {
val projectIdProperty = resultObj::class.java.superclass.getDeclaredField("projectId")
if (projectIdProperty.type == String::class.java) {
projectIdProperty.isAccessible = true
projectIdProperty.set(resultObj, ContextUtils.getCurrentProjectId())
} else {
}
} catch (ex: Exception) {
}
}
}</code></pre>
<p>自定义SQL拦截器,通过实现StatementInspector接口,实现inspect方法即可。不同的业务逻辑,实现的逻辑也不一样,这里就不贴代码了。</p>
<p>注意:</p>
<p>一)、以上是kotlin代码,IDEA支持Kotlin和Java代码的互转。</p>
<p>二)、需要数据隔离的实体,继承TenantModel类即可,没有继承的实体默认为数据共享。</p>
<p>三)、ContextUtils是自定义获取上下文的工具类。</p>
<h2>数据备份</h2>
<h4>业务分析</h4>
<p>到了文章的重点。数据的备份目的是数据迁移和数据的还原。友好的备份格式可以为数据迁移减少很多工作量。刚开始觉得这个需求很简单,MySQL的数据备份做过很多次,也很简单。但数据备份不仅仅是数据恢复,还有数据迁移的功能(A项目下的数据备份后,可以导入的B项目下)。这下就有意思了。我们理一理:</p>
<p>一)、数据备份是数据隔离的。A项目数据备份,只能备份A项目下的数据。</p>
<p>二)、备份的数据用于数据恢复。</p>
<p>三)、备份的数据用于数据迁移,之前存在的关联数据要重新绑定关联关系。</p>
<p>四)、数据恢复和迁移过程中,注意重复导入和事务问题。</p>
<p>针对上面的分析,一般都有会三种解决思路:</p>
<p>一)、用MySQL自带的命令导入和导出。</p>
<p>二)、找已经做好的轮子。(如果有,请麻烦告知一下)</p>
<p>三)、自己实现将数据转为JSON数据,再由JSON数据导入的功能。</p>
<p>因为需求三和需求四的特殊性,MySQL自带的命令很难满足,也没有合适的轮子。只能自己实现,这样做也更放心点。</p>
<h4>实现流程</h4>
<p>第一步:确定表的顺序。项目之间数据迁移后,需要重新绑定表的关联关系,优先导入导出没有外键关联的表。</p>
<p>第二步:遍历每张表,将数据转成JSON格式数据一行行写入到文本文件中。</p>
<p>导出数据伪代码:</p>
<pre><code class="kotlin">fun exportSystemData(request: HttpServletRequest, response: HttpServletResponse) {
// 校验权限
checkAuthority("导出系统数据")
// 获取当前项目
val currentProjectId = ContextUtils.getCurrentProjectId()
val systemFilePath = "${attachmentPath}system${File.separator}$currentProjectId"
val file = File(systemFilePath)
if (!file.exists()) {
file.mkdirs()
}
// 获取数据独立的表名(方便查询)和类名的全路径(方便反射)
val moreProjectEntityMap = CommonUtils.getMoreProjectEntity()
moreProjectEntityMap.remove(CommonUtils.toUnderline(SystemLog::class.simpleName))
moreProjectEntityMap.remove(CommonUtils.toUnderline(AlarmRecord::class.simpleName))
// 生成文件
moreProjectEntityMap.forEach { entry ->
var tableFile: FileWriter? = null
try {
tableFile = FileWriter(File(systemFilePath, "${entry.key}.txt"))
dataManagementService.findAll(Class.forName(entry.value)).forEach {
tableFile.write("${JSONObject.toJSONString(it)} \n")
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
tableFile?.let {
it.flush()
it.close()
}
}
}
// 压缩成一个文件
fileUtil.zip(systemFilePath)
file.listFiles().forEach { it.delete() }
fileUtil.downloadAttachment("$systemFilePath.zip", response)
}</code></pre>
<h2>数据迁移</h2>
<h4>业务分析</h4>
<p>备份后的数据有两个用途。第一是数据还原;最重要的是数据迁移。将A项目中的配置导入到B项目中,可以提高用户的效率。数据还原最简单,这里重点介绍数据迁移的思路(可能不太合理)</p>
<p>数据迁移最麻烦的就是新创建后的数据如何重新绑定主外表的关系。其次就是如果导入过程中失败,事务的处理问题。为了处理这两个问题,我选择新增一张表维护新旧ID的迁移记录。每次导入成功后就在表中保存数据。这样可以避免重复导入的情况。也为新数据重新绑定主外关系做准备。</p>
<h4>实现步骤</h4>
<p>第一步:解压上传后的文件,并按照指定的排序顺序读取解压后的文件。</p>
<p>第二步:一行行读取数据,通过反射将JSON格式字符串转为对象。遍历对象的值将旧ID根据数据迁移记录替换成迁移后的新ID。</p>
<p>第三步:检擦数据迁移记录表中是否已经存在迁移记录,若没有则插入数据并记录日志。</p>
<p>第四步:若数据迁移记录表中已经存在记录,则更新数据。</p>
<p>第五步:读取第二行数据,重复执行。</p>
<p>数据恢复伪代码</p>
<pre><code class="kotlin">fun importSystemData(file: MultipartFile, request: HttpServletRequest) {
checkAuthority("导入系统数据")
val currentProjectId = ContextUtils.getCurrentProjectId()
val systemFilePath = "${attachmentPath}system"
val tempFile = File(systemFilePath, file.originalFilename)
val fileOutputStream = FileOutputStream(tempFile)
fileOutputStream.write(file.bytes)
fileOutputStream.close()
// 获取排序后迁移表
val moreProjectEntityMap = CommonUtils.getMoreProjectEntity()
moreProjectEntityMap.remove(CommonUtils.toUnderline(SystemLog::class.simpleName))
val files: MutableMap<String, File> = mutableMapOf()
fileUtil.unzip(tempFile.absoluteFile, systemFilePath, "").forEach {
files[it!!.nameWithoutExtension] = it
}
val dataTransferHistories = dataTransferHistoryRepository.findByProjectId(currentProjectId).toMutableList()
try {
moreProjectEntityMap.keys.forEach { fileName ->
val tableFile = files.getOrDefault(fileName, null) ?: return@forEach
val entity = Class.forName(moreProjectEntityMap[fileName])
tableFile.forEachLine { dataStr ->
val data = JSONObject.parseObject(dataStr, entity)
// 获取对象所有属性
val fieldMap = CommonUtils.getEntityAllField(data)
// 获取数据迁移的旧ID
val id = fieldMap["id"]!!.get(data) as String
val dataTransferHistory = dataTransferHistories.find { it.oldId == id }
// 重新绑定迁移数据后的id
handleEntityData(data, fieldMap, moreProjectEntityMap.values.toList(), dataTransferHistories)
fieldMap["projectId"]!!.set(data, currentProjectId)
if (null == dataTransferHistory || null == dataManagementService.getByIdElseNull(dataTransferHistory.newId, entity)) {
val saved = dataManagementService.create(data, entity)
// 绑定旧ID和新ID的关系
val savedId = CommonUtils.getEntityAllField(saved)["id"]!!.get(saved) as String
if (null == dataTransferHistory) {
dataTransferHistories.add(DataTransferHistory(id, savedId, currentProjectId, fileName))
}
} else {
fieldMap["id"]!!.set(data, dataTransferHistory.newId)
dataManagementService.update(data, entity)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
throw IllegalArgumentException("数据导入失败")
} finally {
tempFile.delete()
files.values.forEach { it.delete() }
recordDataTransferHistory(dataTransferHistories)
}
}
// 记录数据迁移
private fun recordDataTransferHistory(dataTransferHistories: MutableList<DataTransferHistory>) {
dataTransferHistoryRepository.saveAll(dataTransferHistories)
}
// 重新绑定主外关系表
fun handleEntityData(sourceClass: Any, fieldMap: MutableMap<String, Field>, classPaths: List<String>, dataTransferHistories: MutableList<DataTransferHistory>) {
val currentProjectId = ContextUtils.getCurrentProjectId()
fieldMap.values.forEach { field ->
val classPath = field.type.toString().split(" ").last()
// 一对多或多对多关系
if (classPath == "java.util.List") {
val listValue = field.get(sourceClass) as List<*>
listValue.forEach { listObj ->
listObj?.let { changeOldRelId4NewData(it, dataTransferHistories, currentProjectId) }
}
}
// 一对一或多对一关系
if (classPaths.contains(classPath)) {
val value = field.get(sourceClass)?: return@forEach
changeOldRelId4NewData(value, dataTransferHistories, currentProjectId)
}
// 字符串ID关联
if (classPath == "java.lang.String" && null != field.get(sourceClass)) {
var oldId = field.get(sourceClass).toString()
dataTransferHistories.forEach {
oldId = oldId.replace(it.oldId, it.newId)
}
field.set(sourceClass, oldId)
}
}
}
fun changeOldRelId4NewData(data: Any, dataTransferHistories: MutableList<DataTransferHistory>, currentProjectId: String) {
val fieldMap = CommonUtils.getEntityAllField(data)
fieldMap.values.forEach { field ->
if (field.type.toString().contains("java.lang.String") && null != field.get(data)) {
var oldId = field.get(data).toString()
dataTransferHistories.forEach {
oldId = oldId.replace(it.oldId, it.newId)
}
field.set(data, oldId)
}
}
fieldMap["projectId"]!!.set(data, currentProjectId)
}</code></pre>
<pre><code class="kotlin">/**
* 数据迁移记录表
*/
@Entity
@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["oldId", "projectId"])])
data class DataTransferHistory (
var oldId: String = "",
var newId: String = "",
var projectId: String = "",
var tableName: String = "",
var createTime: Instant = Instant.now(),
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
var id: String = ""
)</code></pre>
<p>到这里就结束了,以上思路仅供参考。</p>
<h3>小结</h3>
<p>一)、数据备份需要项目独立<br>二)、通过项目ID 区分备份的数据是用来数据还原还是数据迁移<br>三)、数据迁移过程中需要考虑数据重复导入的问题<br>四)、数据迁移过程中需要重新绑定主外键的关联<br>五)、第三和第四点可以通过记录数据迁移表做辅助<br>六)、数据迁移过程尽量避免删除操作。避免对其他项目造成影响。</p>
工作日志,跨域和缓存的冲突问题
https://segmentfault.com/a/1190000019553385
2019-06-22T13:43:05+08:00
2019-06-22T13:43:05+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<p>记录和分享一篇工作中遇到的奇难杂症。一个前后端分离的项目,前端件图片上传到服务器上,存在跨域的问题。后端将图片返回给前端,并希望前端能对图片进行缓存。这是一个很常见的跨越和缓存的问题。可偏偏就能擦出意想不到的火花(据说和前端使用的框架有关)。</p>
<h2>跨域问题</h2>
<p>首先要解决跨域的问题。方法很简单,重写<code>addCorsMappings</code>方法即可。前端反馈跨域的问题虽然解决,但是静态资源返回的响应头是<code>Cache-Control: no-cache</code> ,导致资源文件加载速度较慢。</p>
<p>处理跨域的代码</p>
<pre><code class="kotlin">override fun addCorsMappings(registry: CorsRegistry) {
super.addCorsMappings(registry)
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("POST","GET","DELETE","PUT")
.allowedOrigins("*")
.maxAge(3600)
}</code></pre>
<p>处理后的响应头</p>
<pre><code class="html">Access-Control-Allow-Headers: authorization
Access-Control-Allow-Methods: POST,GET,DELETE,PUT
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 3600
Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
Cache-Control: no-cache, no-store, max-age=0, must-revalidate</code></pre>
<h2>静态资源配置</h2>
<p>然后再处理静态资源缓存的问题。方法也很简单,在资源映射的方法上加上<code>.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS))</code> 代码 。前端反馈缓存的问题虽然解决,但是静态资源跨域的问题又双叒叕出现了。</p>
<p>处理静态资源代码</p>
<pre><code class="kotlin">override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/attachment/**")
.addResourceLocations("file:$attachmentPath")
.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS))
}</code></pre>
<p>经过反复的调试后发现,是<code>setCacheControl</code>方法导致静态资源的跨域配置失效。至于什么原因,看了源码,翻了资料都没有找到(可能是我找的不够认真)。更让人匪夷所思的是,火狐浏览器竟然是可以正常使用的。这让我排查问题的方向更乱了。但我也不能甩锅给浏览器啊!就在我快要下定决心甩锅给浏览器的时候,再次验证了“船到桥头自然直”的真谛。我抱着试一试的心态加了一个拦截器!</p>
<h2>解决方法</h2>
<p>到现在我还是不能很好地接受这个解决方法,我相信更好、更优雅地解决方法。目前的解决思路:既然返回的图片存在跨域和缓存的问题,那是否可以自定义拦截器,针对图片地址添加跨域和缓存的响应头呢?</p>
<p>拦截器代码</p>
<pre><code class="kotlin">import org.springframework.stereotype.Component
import javax.servlet.*
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.io.IOException
@Component
class CorsCacheFilter : Filter {
@Throws(IOException::class, ServletException::class)
override fun doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) {
val response = res as HttpServletResponse
val request = req as HttpServletRequest
if (request.requestURL.contains("/attachment/")) {
response.addHeader("Access-Control-Allow-Origin", "*")
response.addHeader("Access-Control-Allow-Credentials", "true")
response.addHeader("Access-Control-Allow-Methods", "GET")
response.addHeader("Access-Control-Allow-Headers", "*")
response.addHeader("Access-Control-Max-Age", "3600")
response.addHeader("Cache-Control", "max-age=86400")
}
chain.doFilter(req, res)
}
override fun init(filterConfig: FilterConfig) {}
override fun destroy() {}
}</code></pre>
<p>MVC配置代码</p>
<pre><code class="kotlin">import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration
class WebMvcConfig : WebMvcConfigurer {
@Value("\${great.baos.attachment.path}")
val attachmentPath: String = ""
override fun addCorsMappings(registry: CorsRegistry) {
super.addCorsMappings(registry)
registry.addMapping("/**")
.allowedHeaders("*")
.allowedMethods("POST","GET","DELETE","PUT")
.allowedOrigins("*")
.maxAge(3600)
}
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/attachment/**")
.addResourceLocations("file:$attachmentPath")
}
}</code></pre>
<p>处理后的响应头</p>
<pre><code class="html">Accept-Ranges: bytes
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 3600
Cache-Control: max-age=86400</code></pre>
<p>注意:</p>
<p>一)、拦截器只能针对图片路径下的请求做处理</p>
<p>二)、<code>addResourceHandlers </code>方法不能再设置<code>setCacheControl</code></p>
<p>到这里,处理跨域和缓存冲突问题的其中一种解决方法就结束了。如果你也遇到了这样的问题,可以考虑try一try。</p>
初识Kotlin之集合
https://segmentfault.com/a/1190000019224149
2019-05-18T23:05:22+08:00
2019-05-18T23:05:22+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<p>Kotlin的集合是让我为之心动的地方,丰富的高阶函数帮助我们高效开发。今天介绍Kotlin的基础集合用法、获取集合元素的函数、过滤元素的函数、元素排序的函数、元素统计的函数、集合元素映射的函数、集合的交差并补集的函数。还有一些工作中的经验。</p>
<h2>先睹为快</h2>
<h3>批量更新、创建、删除功能</h3>
<p>需求:前端有一个二维表格,希望后端提供一个支持批量更新、创建、删除功能的接口。且对部分字段的值有特殊要求。</p>
<p>分析:这样的需求并不少见,如工厂车间的能耗统计。统计的是每个车间,每台设备的能耗值。这些值是可以被用户手动维护的。且这些值都是有取值范围。</p>
<p>1)、特殊字段拦截:如果是一条数据的操作,可以通过注解对字段进行校验。但是批量操作,要考虑事务回滚带来的没必要的开销。可以考虑用代码进行特殊字段的过滤。</p>
<p>2)、区分创建、更新和删除:一个接口完成三个操作,必须要清楚哪些数据。我们可以通过是否有id来区分更新和创建。通过旧数据和新数据求差集区分删除。</p>
<p>下面是一段伪代码,为了方便演示集合的函数,一些方法都放在一起介绍。</p>
<pre><code class="kotlin">@Transactional
fun modifyEquipmentEnergyValue(equipmentEnergyValues: List<EquipmentEnergyValue>): OperateStatus {
// 通过上下文获取当前登录的用户,从而获取其权限
val currentUser = ContextUtils.getCurrentUser()
// 一个用户关联多个角色,一个角色绑定多个权限,所有一定会有重复的权限存在
// 通过flatMap 方法获取所有权限,在通过.toSet()方法去重
val authorities = currentUser.roles?.flatMap { it.authorities.orEmpty() }.toSet()
// 先判断是否针对所有设备都有权限,避免没必要的事务回滚
// 通过find 方法找出没有权限设备
equipmentEnergyValues.find { !authorities.contains(it.equipment.id) }?.let {
throw AuthenticationException("您没有权限$it设备能耗,请联系工程人员")
}
// 先判断是否存在重复数据或者不合理数据,避免没必要的事务回滚
// 设备名称不能重复,用map映射出一个新集合,原集合不受影响
val equipmenNameSize = equipmentEnergyValues.map { it.equipment.name }.toSet().size
if (equipmenNameSize != equipmentEnergyValues.size) {
throw IllegalArgumentException("设备不能重复修改")
}
// 通过 maxBy 方法找出值最大的一项
if (equipmentEnergyValues.maxBy { it.value }.value >= 1000) {
throw IllegalArgumentException("设备能耗值不符合规范")
}
// 旧数据和新数据求差集,找出需要清空的数据(或者设为零)
val oldEquipmentEnergyValues = equipmentRepository.findByLocationAndDate(xxx,xxx)
oldEquipmentEnergyValues.subtract(equipmentEnergyValues).forEach {
// 删除
}
// 更新数据时考虑null值覆盖的问题
equipmentEnergyValues.forEach {
// 通过id判断是更新还是创建,用BeanUtils.copyProperties做复制时需要注意null的问题
}
return OperateStatus()
}</code></pre>
<p>既然写了接口逻辑,顺势谈谈我对接口的肤浅理解(听了许多小白后端和前端的矛盾)。</p>
<p>1)、首先接口是可以独立完成业务逻辑。调用者并不需要关系业务逻辑,只需按照给定的参数发送请求,就可以获取想要的结果。</p>
<p>2)、其次接口是有较强的健壮能力。后端的业务逻辑不能因为调用者的错误请求,而报出500的错误,至少也是已知的业务错误。</p>
<p>3)、最后接口应该尽量避免级联删数据功能。所有的删除操作尽可能甩锅给用户。</p>
<p>随着前端功能越来越强大,前后端在处理接口的问题上矛盾也是越来越多。一些后端处理的逻辑都开始交给前端(部分前端开始膨胀了,部分后端开始偷懒了)。这导致一些分工的不明确,甚至一些本该由后端处理的逻辑也交给了前端。似乎在他们眼里,后端就是数据的crud。好在这样的后端大多都比较年轻,也许后期会成长起来。只能心疼一下前端。</p>
<p>对于我而言,不会把主动权交给前端。提供一个健壮、优质的接口是对自己的要求。给小白一些建议:<strong>对接口负责,就是对自己负责,也是对其他同事负责。</strong></p>
<h2>集合简介</h2>
<p>和Java集合不同的是,Kotlin的集合分可变和不可变两种集合。同时也支持两种集合相互切换。</p>
<h4>List集合</h4>
<pre><code class="kotlin">// 声明并初始化不可变List集合
val list: List<Any> = listOf<Any>(1, "2", 3)
// 声明并初始化可变MutableList集合
val mutableList: MutableList<Any> = mutableListOf<Any>(4, "5", 6)
mutableList.add("7")
list.map { print("$it \t") }
mutableList.map { print("$it \t") }</code></pre>
<h4>Set集合</h4>
<pre><code class="kotlin">// 声明并初始化不可变Set集合
val set: Set<Any> = setOf<Any>(1, "2", 3, "3")
// 声明并初始化可变MutableSet集合
val mutableSet: MutableSet<Any> = mutableSetOf<Any>(4, "5", 6)
mutableSet.add(6)
set.map { print("$it \t") }
mutableSet.map { print("$it \t") }</code></pre>
<h4>Map集合</h4>
<pre><code class="kotlin">// 声明并初始化不可变Map集合
val map: Map<String, Any> = mapOf("k1" to "v1" , "k2" to 3)
// 声明并初始化可变MutableMap集合
val mutableMap: MutableMap<String, Any> = mutableMapOf("k1" to "v1" , "k1" to 3)
map.map { println("key : ${it.key} \t value : ${it.value}") }
mutableMap.map { println("key : ${it.key} \t value : ${it.value}") }</code></pre>
<h2>集合高阶函数</h2>
<h3>获取集合元素</h3>
<p>用Java语言开发时,我们通常用循环遍历集合的每个元素。有时候也会通过下标直接获取指定元素。此时原则上时需要我们先考虑集合元素的长度,以避免下标越界的异常问题。但往往我们会抱着侥幸的心态直接通过<code>get(index)</code>方法获取元素。一般情况下我们会在黑盒自测中发现越界问题(有部分朋友从不黑盒,直接白盒测试,并反问:测试的工作难道不就是发现问题?)。即便是在运行中出现越界问题,也可以甩锅给数据库。但不管怎么样,因为越界导致系统不稳定是不合理的。</p>
<p>用Kotlin语言开发时,我们会发现有很多带有"Or"字样的方法。比如我常用的<code>getOrElse</code>,<code>firstOrNull</code> 等方法。分别表示:通过下标如果没有获取到值,则返回自定的值。和获取集合的第一个元素,若集合为空则返回null。正因为Kotlin提供了很多类似<code>getOrElse</code>,<code>firstOrNull</code> 的方法。很大程度上提高了我们的开发效率,和减少了一些低级错误发生的概率。接下来我们学习一下Kotlin具体有哪些获取集合元素的方法(single方法没怎么用过)</p>
<h4>常用函数</h4>
<ul>
<li>
<code>get(index)</code> : List的函数,通过下标获取指定元素。若找不到值(下标越界),会抛出<code>IndexOutOfBoundsException</code>异常</li>
<li>
<code>getOrElse(index, {...})</code> : List的扩展函数,通过下标获取指定元素。找不到值则返回默认值</li>
<li>
<code>getOrNull(index)</code> : List的扩展函数,通过下标获取指定元素。找不到值则返回null</li>
<li>
<code>elementAtOrElse(index, {...})</code> : Iterable接口的扩展函数,功能同<code>getOrElse</code> 方法</li>
<li>
<code>elementAtOrNull(index)</code> : Iterable接口的扩展函数,功能同<code>getOrNull</code> 方法</li>
<li><strong>注意get方法是List独有,其他集合可以用element方法。</strong></li>
<li>
<code>first()</code> : 获取集合第一个元素。若没有返回值,则抛出<code>NoSuchElementException</code>异常</li>
<li>
<code>first{}</code> : 获取集合中指定元素的第一个元素。若没有返回值,则抛出<code>NoSuchElementException</code>异常</li>
<li>
<code>firstOrNull()</code> : 获取集合第一个元素。若没有返回值,返回null</li>
<li>
<code>firstOrNull{}</code> : 获取集合指定元素的第一个元素。若没有返回值,返回null</li>
<li><strong>看到这里,是不是有点明白Kotlin获取元素的规则:如果没有则怎么样</strong></li>
<li>
<code>last()</code> : 与<code>first()</code>相反</li>
<li>
<code>last{}</code> : 与<code>first{}</code>相反</li>
<li>
<code>lastOrNull{}</code> : 与<code>firstOrNull()</code>相反</li>
<li>
<code>lastOrNull()</code> : 与<code>firstOrNull{}</code>相反</li>
<li>
<code>indexOfFirst{...}</code> : 返回集合中第一个满足条件元素的下标</li>
<li>
<code>indexOfLast{...}</code> : 返回集合中最后一个满足条件元素的下标</li>
<li><strong>咋也不知道single方法设计的初衷,咋也不敢问</strong></li>
<li>
<code>single()</code> : Returns the single element, or throws an exception if the collection is empty or has more than one element. <a>官方api文档地址</a>
</li>
<li>
<code>single{}</code> : 按照条件返回单个元素,若集合为空或者有多个元素满足条件,则报错</li>
<li>
<code>singleOrNull()</code> : 返回单个元素,若集合为空或者有多个元素,则返回null</li>
<li>
<code>singleOrNull{}</code> : 按照条件返回单个元素,若集合为空或者有多个元素满足条件,则返回null</li>
</ul>
<h4>使用建议</h4>
<p><strong>在使用获取元素的方法时,推荐方法名中带有"Or"字样的方法</strong>,可以减少很多不必要的报错。</p>
<p><strong>List集合通过下标获取元素可以用get,getOrElse,getOrNull函数</strong>,但其他集合没有这些方法。</p>
<p><strong>笔者单方面认为single函数和数据库的唯一约束的功能有点类似</strong>,在使用Kotlin的过程中,你会发现它有很多和数据库类似的功能。</p>
<h4>基础用法</h4>
<pre><code class="kotlin">val list: MutableList<Int> = mutableListOf(1,2,3,4,5)
println("getOrElse : ${list.getOrElse(10,{ 20 })}")
println("getOrNull : ${list.getOrNull(10)}")
println("firstOrNull : ${list.firstOrNull()}")
println("firstOrNull : ${list.firstOrNull { it > 3 }}")
println("indexOfFirst : ${list.indexOfFirst { it > 3 }}")
println("indexOfLast : ${list.indexOfLast { it > 3 }}")
-----------------------------------------------------
getOrElse : 20
getOrNull : null
firstOrNull : 1
firstOrNull : 4
indexOfFirst : 3
indexOfLast : 4</code></pre>
<h3>集合元素排序</h3>
<p>用Java语言开发时,给对象集合做排序是常有的业务逻辑。(Java8之后的写法不太了解)按照我之前工作中排序的代码其实也并不复杂,十行代码基本可以搞定一个排序逻辑。注意是一个,一个。业务中存在大量的排序需求,这种代码会反复出现。对于我这种佛系程序员兼CV高手而言,早已经习以为常了。但自从用了Kotlin的<code>sortedBy</code>方法后。突然觉得Kotlin用起来倍儿爽!</p>
<p>用Java7开发了几年,Java8只接触了一点皮毛,现在Java12都已经出来了。经常看到一些文章为了突出某个语言的强大,而去踩其他语言。我只想问:who are you?每个语言都有自己独特的一面.神仙打架,我们负责吃瓜就好。就懂点皮毛的人,瞎掺和啥?</p>
<pre><code class="java">Collections.sort(list,new Comparator () {
@Override
public int compare(Object o1, Object o2) {
return o1.compareTo(e2);
}
});</code></pre>
<p>用Kotlin语言开发时,我们不需要重复写类似上面的排序代码,Kotlin已经帮我们封装好了,只需要我们写需要排序的字段即可。其底层也是通过Java 的Collections.sort实现的。所有我们就放心大胆的用吧。</p>
<pre><code class="kotlin">public inline fun <T, R : Comparable<R>> MutableList<T>.sortBy(crossinline selector: (T) -> R?): Unit {
if (size > 1) sortWith(compareBy(selector))
}
@kotlin.jvm.JvmVersion
public fun <T> MutableList<T>.sortWith(comparator: Comparator<in T>): Unit {
if (size > 1) java.util.Collections.sort(this, comparator)
}</code></pre>
<h4>常用函数</h4>
<ul>
<li>
<code>sortedBy{}</code> : 根据条件给集合升序,常用与给对象集合的某个字段排序,并返回排序后的集合,原集合顺序不变</li>
<li>
<code>reversed()</code> : 集合反序。与降序不同,反序指的是和初始化的顺序相反</li>
<li>
<code>sorted()</code> : 自然升序,常用于给普通集合排序</li>
<li>
<code>sortedDescending()</code> : 自然降序</li>
<li>
<code>sortedByDescending{}</code> : 根据条件给集合降序</li>
<li>
<strong>ed结尾的排序方法,是不会对原集合进行修改,而是返回一个排序后的新集合。没有以ed结尾的方法恰恰相反</strong> ---来自一个不严谨的总结</li>
<li>
<code>sortBy{}</code> : 根据条件给原集合升序,常用与给对象集合的某个字段排序</li>
<li>
<code>sortByDescending{}</code> : 根据条件给原集合降序</li>
<li>
<code>reverse()</code> : 原集合反序</li>
</ul>
<h4>使用建议</h4>
<p><strong>千万不要把反序理解成了倒序</strong>,前车之鉴</p>
<p><strong>sortBy方法是对原集合做排序操作,而sortedBy方法是返回一个排序后的新集合</strong>,原集合排序没有变</p>
<p><strong>kotlin排序方法中可以用and,or 组装多个条件</strong>,但效果并不理想</p>
<h4>基础用法</h4>
<pre><code class="kotlin">data class Person(
var name: String = "",
var age: Int = 0,
var salary: Double = 0.0
)
val persons = mutableListOf(Person("n1", 20, 2000.0),
Person("n2", 24, 4000.0),
Person("n3", 28, 6000.0),
Person("n4", 26, 8000.0),
Person("n5", 34, 7000.0),
Person("n6", 44, 5000.0))
persons.sortedBy { it.age }.map { println(it) }
persons.map { it.age }.sorted()
persons.sortBy { it.age }
persons.reversed()</code></pre>
<h3>过滤元素</h3>
<p>Java8也提供了Map和Filter函数用于转换和过滤对象,使开发变得更轻松,遥想当年在for循环里面加if语句。慢慢成了过去式。集合遍历之前先filter一下,已经成了我开发过程中不可或缺的一步。虽然 <code>filter</code> 函数相对于Kotlin的 <code>getOrNull</code> 和 <code>sortedBy</code> 函数,并没有给人一种眼前一亮的感觉。但它提高了代码的可读性和美观性。</p>
<h4>常用函数</h4>
<ul>
<li>
<code>filter{...}</code> : 过滤不满足条件的元素,返回只满足条件元素列表,不影响原集合</li>
<li>
<code>filterNot{...}</code> : 和<code>filter{}</code>函数的功能相反</li>
<li>
<code>filterNotNull()</code> : 过滤掉集合中为null的元素</li>
<li>
<code>filterIndexed{...}</code> : 在<code>filter{}</code>函数上多了一个下标功能,可以通过索引进一步过滤</li>
<li><strong>Kotlin的函数是见名知意,非常好用,上手也快,弄明白一个方法,其他方法都没大的问题</strong></li>
<li>
<code>distinct()</code> : 去除重复元素,返回元素的顺序和原集合顺序一致</li>
<li>
<code>distinctBy{...}</code> : 根据操作元素后的结果去去重,去除的是操作前的元素</li>
<li>
<code>take(num)</code> : 返回集合中前num个元素组成的集合</li>
<li>
<code>takeWhile{...}</code> : 从第一个元素开始遍历集合,当出现第一个不满足条件元素时退出循环。返回所有满足条件的元素集合</li>
<li>
<code>takeLast(num)</code> : 和<code>take</code> 函数相反,返回集合中后num个元素组成的集合</li>
<li>
<code>takeLastWhile{...}</code> : 从最后一个元素开始遍历集合,当出现第一个不满足条件元素时退出循环。返回所有满足条件的元素集合</li>
<li><strong>不要被这么多方法吓到,学了take函数的用法,takeLast、drop、dropLast的用法都可以猜到</strong></li>
<li>
<code>drop(num)</code> : 过滤集合中前num个元素</li>
<li>
<code>dropWhile{...}</code> : 和执行<code>takeWhile{...}</code>函数后得到的结果相反</li>
<li>
<code>dropLast(num)</code> : 过滤集合中后num个元素</li>
<li>
<code>dropLastWhile{...}</code> : 和执行<code>takeLastWhile{...}</code>函数后得到的结果相反</li>
<li>
<code>slice(...)</code> : 过滤掉所有不满足执行下标的元素。参数是下标集合或者是下标区间。</li>
</ul>
<h4>使用建议</h4>
<p><strong>以上Filter、Distinct、Take、Drop、Slice方法都返回一个处理后的新集合,不影响原集合</strong>。</p>
<p>Kotlin提供了丰富的函数供我们使用,同时也吓退了很多朋友,别怕!<strong>Kotlin的函数都是买一送一的,学会一个,不愁另一个</strong>。</p>
<h4>基础用法</h4>
<pre><code class="kotlin">val list = listOf(-3,-2,1,3,5,3,7,2,10,9)
println("filter : ${list.filter { it > 1 }}")
println("filterIndexed : ${list.filterIndexed { index, result ->
index % 2 == 0 && result > 5
}}")
println("take : ${list.take(5)}")
println("takeWhile : ${list.takeWhile { it < 5 }}")
println("drop : ${list.drop(5)}")
println("distinct : ${list.distinct()}")
println("distinctBy : ${list.distinctBy { it % 2 }}")
println("slice : ${list.slice(IntRange(1,5))}")
-----------------------------------------------------
filter : [3, 5, 3, 7, 2, 10, 9]
filterIndexed : [7, 10]
take : [-3, -2, 1, 3, 5]
takeWhile : [-3, -2, 1, 3]
drop : [3, 7, 2, 10, 9]
distinct : [-3, -2, 1, 3, 5, 7, 2, 10, 9]
distinctBy : [-3, -2, 1]
slice : [-2, 1, 3, 5, 3]</code></pre>
<h3>统计元素</h3>
<p>在用Java8和Kotlin之前。和排序一样,在实现求最大值、平均值、求和等操作时,都要写很多冗余的代码。现在好了,Kotlin已经封装了这些方法。朋友们,<strong>千万不要过于依赖这些方法</strong>。有些一条sql能解决的问题,就不要把统计的逻辑留给代码完成。这里的方法更适合在业务处理过程中,对一些简单集合的统计处理。如果是统计报表的功能,就不要有什么歪心思了。分享一篇关于统计的文章:<a href="https://link.segmentfault.com/?enc=KjdIzzs1EOS1JpD8GCXhfA%3D%3D.IPzfziq3pRyWtLz6mn6RmeMxicu8Ao6jNHof%2BRs8wlxGip%2BBsSGZlKcYd9LvUygr" rel="nofollow">常见的统计解决方案</a></p>
<h4>常用函数</h4>
<ul>
<li>
<code>max()</code> : 获取集合中最大的元素,若为空元素集合,则返回null</li>
<li>
<code>maxBy{...}</code> : 获取方法处理后返回结果最大值对应那个元素的初始值,如果没有则返回null</li>
<li>
<code>min()</code> : 获取集合中最小的元素,若为空元素集合,则返回null</li>
<li>
<code>minBy{...}</code> : 获取方法处理后返回结果最小值对应那个元素的初始值,如果没有则返回null</li>
<li>
<code>sum()</code> : 对集合原元素数据进行累加,返回值类型是Int</li>
<li>
<code>sumBy{...}</code> : 根据元素运算操作后的结果进行累加,返回值类型是Int</li>
<li>
<code>sumByDouble{...}</code> : 和<code>sumBy{}</code>相似,但返回值类型是Double</li>
<li>
<code>average()</code> : 对集合求平均数</li>
<li>
<code>reduce{...}</code> : 从集合中的第一个元素到最后一个元素的累计操作</li>
<li>
<code>reduceIndexed{...}</code> : 在<code>reduce{}</code>函数基础上多了一个下标功能</li>
<li>
<code>reduceRight{...}</code> : 与<code>reduce{...}</code> 相反,该方法是从最后一个元素开始</li>
<li>
<code>reduceRightIndexed{...}</code> : 在<code>reduceRight{}</code>函数基础上多了一个下标功能</li>
<li>
<code>fold{...}</code> : 和<code>reduce{}</code>类似,但是<code>fold{}</code>有一个初始值</li>
<li>
<code>foldIndexed{...}</code> : 和<code>reduceIndexed{}</code>类似,但是<code>foldIndexed{}</code>有一个初始值</li>
<li>
<code>foldRight{...}</code> : 和<code>reduceRight{}</code>类似,但是<code>foldRight{}</code>有一个初始值</li>
<li>
<code>foldRightIndexed{...}</code> : 和<code>reduceRightIndexed{}</code>类似,但是<code>foldRightIndexed{}</code>有一个初始值</li>
<li>
<code>any{...}</code> : 判断集合中是否存在满足条件的元素</li>
<li>
<code>all{...}</code> : 判断集合中的所有元素是否都满足条件</li>
<li>
<code>none{...}</code> : 和<code>all{...}</code>函数的作用相反</li>
</ul>
<h4>使用建议</h4>
<p><strong>不能过于依赖Kotlin的统计方法,这些方法更适合一些业务逻辑上的简单统计处理</strong>,不适合数据统计功能。</p>
<p><strong>注意sum函数返回结果是Int类型</strong>,如果是Double则需要用sumByDouble方法。</p>
<h4>基础用法</h4>
<pre><code class="kotlin">val persons = mutableListOf(Person("n1", 20, 2000.0),
Person("n2", 24, 4000.0),
Person("n3", 28, 6000.0),
Person("n4", 26, 8000.0),
Person("n5", 34, 7000.0),
Person("n6", 44, 5000.0))
println("maxBy : ${persons.maxBy { it.age }}")
println("sumByDouble : ${persons.sumByDouble { it.salary }}")
println("average : ${persons.map { it.salary }.average()}")
println("any : ${persons.any { it.salary < 1000 }}")
-----------------------------------------------------
maxBy : Person(name=n6, age=44, salary=5000.0)
sumByDouble : 32000.0
average : 5333.333333333333
any : false</code></pre>
<h3>元素映射</h3>
<p>Kotlin提供了一个遍历集合的forEach方法,也提供了对集合每个元素都进行指定操作并返回一个新集合的map方法。map方法是可以遍历集合,但如果误将其认为遍历集合的方法,同样会将mapNotNull方法误以为成遍历非null元素的方法。</p>
<h4>常用方法</h4>
<ul>
<li>
<code>map{...}</code> : 把每个元素按照特定的方法进行转换,并返回一个新的集合</li>
<li>
<code>mapNotNull{...}</code> : 同<code>map{}</code>相同,过滤掉转换之后为null的元素</li>
<li>
<code>mapIndexed{index,result}</code> : 在<code>map{}</code> 函数上多了一个下标功能</li>
<li>
<code>mapIndexedNotNull{index,result}</code> : 在<code>mapNotNull{}</code>函数上多了一个下标功能</li>
<li>
<code>flatMap{...}</code> : 根据条件合并两个集合,组成一个新的集合</li>
<li>
<code>groupBy{...}</code> : 分组。即根据条件把集合拆分为为一个<code>Map<K,List<T>></code>类型的集合</li>
</ul>
<h4>使用建议</h4>
<p><strong>map方法不是集合遍历,集合遍历的方法是forEach</strong>。</p>
<p><strong>mapNotNull方法不是遍历集合不为null的方法,而是过滤转换后为null的元素</strong>。</p>
<p><strong>调用string.split()函数,无论用forEach还是map,即使没有内容还是会遍历一次</strong>。</p>
<h4>基础用法</h4>
<pre><code class="kotlin">val list = listOf(-3,-2,1,3,5,3,7,2,10,9)
list.map { it + 1 }.forEach { print("$it \t") }
list.mapIndexedNotNull { index, value ->
if (index % 2 == 0) value else null
}.forEach { print("$it \t") }
println("flatMap : ${list.flatMap { listOf(it, it + 1,"n$it") }}")
println("groupBy : ${list.groupBy { if (it % 2 == 0) "偶数" else "奇数" }}")</code></pre>
<h3>集合的交差并补操作</h3>
<p>对集合的求交差集是一个常用的方法。比如前端需要将更新,创建,删除的逻辑用一个接口完成。我们可以通过旧数据与新数据求差集找出需要删除的数据。通过新数据和旧数据求差集找出需要创建的数据。通过求交集找出需要更新的数据。</p>
<ul>
<li>
<code>intersect(...)</code> : 返回一个集合,其中包含此集合和指定集合所包含的所有元素,交集</li>
<li>
<code>subtract(...)</code> : 返回一个集合,其中包含此数组包含但未包含在指定集合中的所有元素,差集</li>
<li>
<code>union(...)</code> : 返回包含两个集合中所有不同元素的集合,并集</li>
<li>
<code>minus(...)</code> : 返回包含原始集合的所有元素的列表,但给定的数组中包含的元素除外,补集</li>
</ul>
<h4>基础用法</h4>
<pre><code class="kotlin">val list1 = mutableListOf(1,2,3,4,5)
val list2 = mutableListOf(4,5,6,7)
println("intersect : ${list1.intersect(list2)}")
println("subtract : ${list1.subtract(list2)}")
println("union : ${list1.union(list2)}")
println("minus : ${list1.minus(list2)}")
-----------------------------------------------------
intersect : [4, 5]
subtract : [1, 2, 3]
union : [1, 2, 3, 4, 5, 6, 7]
minus : [1, 2, 3]</code></pre>
<p>官网地址:<a href="https://link.segmentfault.com/?enc=k%2FASh50GI49ftBM2OOSyLA%3D%3D.SaUFW5TZru0N7ABm8%2Bmo582yw1Bc7PLesLuiXygD6RzOmRfa4AdKofEJp2esJRix5tpwFgyPxCJ%2Fm67UhYgZpbNV%2FmiyUieMYQG9gSa0QHw%3D" rel="nofollow">https://kotlinlang.org/api/la...</a></p>
<p>到这里文章就结束了。如果用好集合的高阶函数,可以让我们的开发效率有明显的提高,bug的数量也会锐减。文章还有一部分内容没有介绍。我在工作用中集合就用MutableList、MutableSet、MutableMap,可Java中还有ArrayList,LinkedList,HashMap,HashSet等集合Kotlin中也有这些。一直都没有好好研究,这个坑先挖好,后来再补上。</p>
初识Kotlin之函数
https://segmentfault.com/a/1190000019091330
2019-05-06T23:09:01+08:00
2019-05-06T23:09:01+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<p>本章通过介绍Kotlin的基本函数,默认参数函数,参数不定长函数,尾递归函数,高阶函数,Lamdba表达式。来对Kotlin函数做进一步了解。将上一篇的Kotlin变量的知识得以运用。<a href="https://link.segmentfault.com/?enc=RD14T%2Fw3gxQR7SytkHYJrA%3D%3D.ap80m4%2BenfjTsJGCBkpeNzZYzYvQl5T5Yv5oVB8TicCdaAId%2FFc0tolLKJ6daLaehNdR1doeO4ym4%2BAQZuCg3w%3D%3D" rel="nofollow">Kotlin变量</a></p>
<h3>Kotlin函数简介</h3>
<p>Kotlin中是通过关键字fun声明函数。和变量一样,返回值类型放在名称后面,并用":"冒号分开。Kotlin函数默认修饰符public,且可以在文件顶层声明。其格式如下</p>
<pre><code class="kotlin">fun 函数名(变量): 返回值类型 {
}</code></pre>
<h3>Kotlin常见函数</h3>
<h4>基础函数</h4>
<pre><code class="kotlin">fun getValue(v: Int): Int {
return v
}</code></pre>
<p>当函数不需要返回任何值时,可以将返回值类型定义成Unit,也可以不显式返回。</p>
<pre><code class="kotlin">fun setValue(v: Int) {
}</code></pre>
<h4>参数默认值函数</h4>
<p>函数的参数可以有默认值,当函数调用者不给默认参数赋值时,函数体就使用参数的默认值。这样可以减少很多方法重载的代码量。</p>
<pre><code class="kotlin">fun setValue(x: Int, y: Int = 10): Int {
retunr x + y
}
setValue(10) -----> 20
setValue(10, 20) -----> 30</code></pre>
<p>参数默认值函数固然好用。但是由于每个人的编程习惯和编程水平的不同。项目中出现下面的代码的概率还不低。通过程序打印的结果可以看出,输出的结果并不是我们预期的21.2,而且10。说明编译器是调用的是第一个函数。</p>
<pre><code class="kotlin">fun main(args: Array<String>) {
println(setValue(10)) -----> 10
}
fun setValue(x: Int) = x
fun setValue(x: Int, y: Int = 10, z: Double = 1.2) = x + y + z</code></pre>
<p>还一个语法问题,子类继承父类的参数默认值函数后,是不允许重写的函数为其参数指定默认值。好在这种情况编译器会提示错误。</p>
<pre><code class="kotlin">open class FatherClass {
open fun setValue(x: Int, y: Int = 10, z: Double = 1.2) = x + y + z
}
class SunClass: FatherClass() {
// An overriding function is not allowed to specify default values for its paramete
override fun setValue(x: Int, y: Int, z: Double) = x + y + z
}</code></pre>
<h4>单表达式函数</h4>
<p>若函数体只是单个表达式时,可以省略花括号并用"=" 指定代码体。了解一下即可,至少遇到了不要惊讶。</p>
<pre><code class="kotlin">fun setValue(x: Int, y: Int) = x + y</code></pre>
<h4>不定长参数函数</h4>
<p>有很多场景函数的变量的个数是不确定。Java是通过三个点"..."表示不定个数的参数。而Kotlin需要通过关键字vararg定义参数,表示函数的参数个数不确定。</p>
<pre><code class="kotlin">fun mathPlus(vararg arguments: Any): Any {
var result: BigDecimal = BigDecimal.ZERO
arguments.map {
result = result.plus(BigDecimal(it.toString()))
}
return result
}
mathPlus(1,2,3,4.5) ------> 10.5</code></pre>
<h4>尾递归函数</h4>
<p>Kotlin支持尾递归的编程风格。<strong>允许一些算法可以通过循环而不是递归解决问题,避免堆栈溢出导致的系统不稳定</strong>。Kotlin还提供了尾递归优化的关键字tailrec。但要符合 tailrec 修饰符的条件,需要函数必须将其自身调用作为它执行的最后一个操作。我们用求阶乘的代码演示尾递归。</p>
<pre><code class="kotlin">// 尾递归,可以保证堆栈不溢出,但是还要考虑数据类型的取值范围
tailrec fun fibolaAlgorithm(num: Int, result: Int): Int {
println("剩余递归次数 : $num \t 计算结果: $result")
return if (num == 0) {
1
} else {
fibolaAlgorithm(num - 1, result + num)
}
}</code></pre>
<h4>高阶函数</h4>
<p>高阶函数是Kotlin的一大亮点,高阶函数是可以将函数用作参数或返回值的函数。下面代码中,forEach是函数,println也是一个方法,通过双冒号将函数作为一个参数传递。这种用法在Kotlin中非常常见。</p>
<pre><code class="kotlin">// 函数作为参数
fun paramFun() {
val list = listOf(1, 2)
list.forEach(::println)
}
// 函数作为返回值
fun returnFun(): (Int, Int) -> Int {
return { j, i -> j + i }
}
println(returnFun().invoke(1,2))</code></pre>
<h4>闭包函数</h4>
<p>闭包就是能够读取其他函数内部变量的函数。当我们的程序希望读取到函数的内部变量,或者希望被访问的变量保存在内存中。就需要用到闭包。这下这段代码算是比较典型的闭包函数。</p>
<pre><code class="kotlin">fun closureMethod(i: Int): () -> Int {
var memoryValue = 1
return fun(): Int {
return i + memoryValue++
}
}
val closure = closureMethod(0)
println(closure()) ------> 1
println(closure()) ------> 2</code></pre>
<h3>Kotlin Lamdba表达式</h3>
<p>Lambda表达式的本质其实是匿名函数,底层还是通过匿名函数来实现。Lambda的出现确实是减少了代码量,同时代码变得更加简洁明了。</p>
<p>Lamdba语法结构</p>
<pre><code class="kotlin">val/var 变量名: (参数类型,参数类型,...) -> 返回值类型 = { 参数1,参数2,... -> 代码块 }</code></pre>
<p>在这个基础上,Kotlin还支持智能推导模式,让代码更简单,让读者更摸不清头脑,新手看这种代码一定觉得怪怪的。注意:<strong>实参并没有用括号括起来,而是通过箭头将实参和代码块区分开</strong>。</p>
<pre><code class="kotlin">// 无参: val/var 变量名: () -> 返回值类型 = { 代码块 },
val a:() -> Int = { 10 }
// 有参: val/var 变量名: (变量类型...) -> 返回值类型 = { 参数1,参数2, ... -> 操作参数的代码 }
val b: (Int, Int) -> Int = {x, y -> x + y }
// 推导: val/var 变量名 = { 参数1: 类型, 参数2: 类型, ... -> 操作参数的代码 }
val c = { x: Int, y: Int -> x + y }
println(c(1,2)) ------> 3</code></pre>
<p>Lamdba和集合可以擦出爱情的火花,下一章介绍Kotlin集合函数API(filter,map,groupBy,maxBy...)时,你就知道Lamdba有多么强大了。</p>
<h3>Kotlin 扩展函数</h3>
<p>扩展函数指的是在已有类中添加新的方法,且不会对原类做修改。</p>
<pre><code class="kotlin">fun receiverType.funName(params): returnType{
/*代码块*/
}</code></pre>
<ul>
<li>receiverType:扩展函数的接收者,也就是函数扩展的对象</li>
<li>returnType: 扩展函数的返回值类型</li>
<li>funName:扩展函数的名称</li>
<li>params:扩展函数的参数,可以为NULL</li>
</ul>
<pre><code class="kotlin">fun Int.extensionFun(i: Int): Int {
return this + i
}
println(10.extensionFun(20)) ------> 30</code></pre>
<p>因为扩展函数是可以让程序员自己添加的,出现函数重名的情况非常常见。所以,如果遇到重名的情况。可以在导入包时,通过 as 关键字进行改名。注意:<strong>改名后不能再用原来的函数名</strong>。</p>
<pre><code class="kotlin">import com.kotlin.demo.extensionFun as aliasITDragon
fun main(args: Array<String>) {
println(1.aliasITDragon(2)) ------> 3
}</code></pre>
<p>如果扩展函数只有一个变量,我们可以使用中缀符号( infix 关键字)修饰函数,位于fun关键字之前。</p>
<pre><code class="kotlin">infix fun Int.extensionFun(i: Int): Int {
return this + i
}
println(10 extensionFun 20) ------> 30
println(10.extensionFun(20)) ------> 30</code></pre>
<p>文章到这里就介绍了,Kotlin提供的扩展函数,Lamdba表达式提高了我们的开发效率。值得我们去深入学习。</p>
编程和英语一起学,每日一词
https://segmentfault.com/a/1190000018621843
2019-03-22T22:15:59+08:00
2019-03-22T22:15:59+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>编程和英语一起学,每日一词</h2>
<h3>苦衷</h3>
<p>笔者可以坚持每天花一两个小时学习技术,并坚持了快两年了。但学习英语,坚持五天都做不到。笔者曾经在跨境电商公司工作过,看到同事用流利的英语和外国友人交流时,羡慕与崇拜。我开始督促自己也要学好英语。什么杂七杂八的APP下了一大堆。报名了一些看起来高大上的课程。可一个都没有坚持下来。</p>
<h3>反思</h3>
<p>很多人告诉我,英语是最简单的。只要每天坚持读它就可以了。嗯,好的,说的很简单,说的很轻松,为啥我就不能坚持地学英语了。我反思了很久,我觉得每个人坚持的方式不一样。我既然可以做到每天坚持学习技术,为啥不把英语夹杂到技术中呢?于是我打算通过学习英语单词的方式温故编程语法。也许温故还有新知,技术还可以更上一层楼,岂不是两全其美。</p>
<h3>每日一词</h3>
<p>我若断更,说明我还是太菜。微信上每日一词,博客上有时间同步更新。</p>
<p><a href="https://link.segmentfault.com/?enc=3GKt%2FTzpbqUgbUfB71sfBg%3D%3D.Djb%2BtlK7siNqZ8uj%2FSFwNPoqyiDVbpMIJTTWLloTq88AshsrE3ipMv9EO7VNL0IwP5w4LIoKSIF2%2F30ehI3A%2F28DotdFxeFF9BGeGVySmqfLB6ZQMMM77stv3%2FaQJhr3urrVkunkzmwhjqAh1EusVvz%2FiLMtIE4Tg0Fb2dsuX3MVEulquySAWph8uB6OLeHQbAwYfVvbE5SKCiFjF%2BBZY9%2FIJUG%2BaMNAcTtlW9th5OrNe1VDY%2BpaQP%2BqqNCV6EsLLn75U9%2Bu84ztkR5dd9q5E9q7NlPEptKBiCm1x860yKdcdDnXZONJMZadHEC5phbetWwtJcYYG0aSIXaCsME29A%3D%3D" rel="nofollow">Class</a> , <a href="https://link.segmentfault.com/?enc=qI0r4OqW12fiiLYbAIHchQ%3D%3D.VkftnqSJw2QfMOgIDnKHsnq57Sc328z9mflOz3mpR7tnWtJIMrEQnwGobYdddZ0JpVJThML72fM3EOu3l18l0H5efjKzOkCI59Mu%2BwoD7Tkxb80qwLSAqFWtpueD6ai0zTBOsnlDJpezI1MTErVxvnPRbmo183Xl359SAVVfdMDyvrx2%2Fp583IegqNnIqbCsp12zwfwk4Yic5M2LdjRaInkyDDHEMOnMGFsQbzYSjoZagl72VEZppQOS5PLCJqSVrTkHNnYdt%2BLzKxAsaG4MNR3Ra9eX3G5G5h8sJfJXE9mQnJoKOLOF3Si9RaABgoG%2FSHg7XMUCeLWn%2BrtRraJ5bA%3D%3D" rel="nofollow">Constructor</a> , <a href="https://link.segmentfault.com/?enc=sSiAhw8di9r5zBq0ucXhZg%3D%3D.dOqMZK%2Fa3GWOzq08pjFmMLo4jkslOz%2F%2FoAZfx6UIEqkD%2BetWVNJGmDBL0EGI7Cr0aJzEj89o2AB15uXLE%2FfqXLHEIqXMfMiQl5qmHhoxsElq3mKE4SJtktv7dXdkUFCwiSrik0I5ItUUPW9uWYtE1H6b0DyHOxSD6dkFzVskqpPm5msRlIgH57q9oyIGY6BJNpvoWLODW8XJAnSlGYP%2FLXLAbimTCbZH79PFOUrWgG3i%2BOwROAlqJAVI4Lj%2FmpdHrmxEu00IPlRMeFngXaq5N%2BAiDCEONgW0Edxvrf4QbDaYgFdhIxscxNNaDRSUCoF4snBqJS52FQ%2Bp67r4tbesiA%3D%3D" rel="nofollow">New</a> , <a href="https://link.segmentfault.com/?enc=awfYRE%2B5dyoNfEQo%2BSGw2A%3D%3D.PlxATS%2FBTepNWUbt3JM5Mxvk3cRP2GLRb%2F7ZXVZKW6201UczyZeD%2BjR%2Bq3pcRjzyEdNeuDEFQdd3d3wrw%2FvE8wrB%2FK4VQdv29YZTHnBVxG30Y63YrivFIpL6M%2Fv5DeSkLLbl65YcvaaDTAfNI7%2B9UdsajD2R2mguSl0obSrE6U%2BJdxXmUgdxOSZb3sNbxeSNTauCLcgqdwHfcTcQBDFDbXuGKAShAcg%2F0bi9lqeTqZNc9HvwdoCZhZ6kW3PZILidUkDo2XV57hYtmWC4iIIsMoGQbNiQSobkVsEFcYoAvlxbgq%2F5EbQG%2FE3xy1tylIwZ%2BPrzB9SRIjVJX7CSP7Fb0w%3D%3D" rel="nofollow">Extend</a> , <a href="https://link.segmentfault.com/?enc=KwsKZJKy1B5B3lhNyWhtow%3D%3D.0jrjOJCHU0GRjLZQf1mDA8GIRUBraauQzRHRy5PfkinwfowZCzg2tX2vMHowPGlsyjWhGk%2BJteF0EDvWEyhL%2BeOpiU9Jzurij2y03vZJ99sVUkt7HNBx617dDOSENIH8EE%2BGHg32hXXsB6HbiRUR4jfxUOulfUXY%2FyeeDtYN8TI8Gq%2Fk8Zrhjx13mFsVOCTGML6d0v6M0XAxUXPaWW8Fr%2BBX33BFvTmdqAuQ981zqFTaUZHszkAL0r8euuLDVKVeeFIo8Gbun5vIKfN5ATL1lQPcBjBcpNxTZ8hfv0rbkEuI1wAsNmDC9a%2BYOqBF6UJbE3ccF4oOy1fVmD3zYCgj7A%3D%3D" rel="nofollow">Private</a> , <a href="https://link.segmentfault.com/?enc=QZggJr7u0MIXDSj68OS8Ng%3D%3D.modhKLnpAtLNfHOl63C%2FsC1k7gKsJmquAh8Xo6KGL%2B32ccZNexyR7cwMxHbcIf%2FQk%2BLRUO%2BCA1Zm0LxlII99o5A2dGkxkfHvxkzBFi8%2FLfAJZULrwub7D2GMlNdh3PbfqDUwKrWn5D9vL1G0h9pZFKnwkYXlQoF0CizBCRyauG%2B2OpfVt54tmf3I3rO9GWuR7d2kC2JIYdsLjVdT1iEkcFP8UcnK9amUmQh6PalTDsR2TNrCGQQGiOV855uLniTm6w%2BW7PujEyQdbUtVZbWaWpwievlMK%2Fc1FJ%2F%2F4ZlegpUIUSyyLvutWM1S7o9ACA2a7fec3TWVvo5wAXatae%2F4aw%3D%3D" rel="nofollow">Function</a> , <a href="https://link.segmentfault.com/?enc=tt%2FLhfiCQ0Rh4j9y2osZag%3D%3D.ojYQt6QoSVDCexWkgKGpg8dadHfzUqOa1hZ1wz2VsYAtYvAFHI0TXUvW8OM7Kl2mE1pMGtjR47tkztFp1V7RqQ3sAwUNdXDAdBzNa1hrmbja%2BAOw5XUwHbw7XQOkbqS3Q7gPCjEXkGUzMPdeV5noB6AN2Iqx5nudMa4shNoCqbOJZ%2FcN4J%2FbguY7fWFaA5I4KqPhVCuJ9sFAfZGLIE3YqpLE%2BCofvzeq7QKke17DVA4ytamS7uAe6OajtDAEFtWqKXTWDRp6bew8DDHJtZNLmN9hdmQdJrjuVjACevLydCgbOV%2FHp%2Bs2anwEr445b9Ei7%2B9WiiWOgeLgGQ7yKFo1iA%3D%3D" rel="nofollow">Catch</a> , <a href="https://link.segmentfault.com/?enc=oT5oFAo6gP0CgcN%2Boa9zpw%3D%3D.XCGUn2APvEu88wtBys7ArB%2FXiGEcovHTRRnwTTKXzEhgUU4yBW8N%2B263DfJpEMHMeuXivALH2zsQggWDC0HNAQSFZ8bHNtuXiPVz280iOOki2InA%2F4UVQmdA5%2BZPtHSzNnEJtC14vTeoMfMwlbjXAiDBAjDPJR3Yly64SriwjG0zfrnf%2FeM4OE1W3qFyxKU3YpylophkIYzlyxQ2lRWgbP14uJpDg%2BPneupdfcm%2F0YTwu4jsSUhP7l%2FG2%2BXMFEk%2B8YqU%2FeYJdBNvTIKQH%2FSkFCnDkw6WLmSMflb6rcbxqt8mD4YF0VFKzQVvmkmWKa%2FBAmFEA7OapM%2BYxXyVFCYMNX5n4vrv8DjoQ1hb5fuOQe4%3D" rel="nofollow">Final</a> , <a href="https://link.segmentfault.com/?enc=qrLuntcoDiD3Ob32lByebg%3D%3D.EmawwxyJAzXRNMt7%2Brh%2FT5Z92EgBaBiSZHKChnC0ufh6CvDl8L38Lk2S%2BywG%2B2qmWmQ4bm5xguQS3c0rXczjIn9W4CIoqLaR3qgSEbJhChIm5FaoHDhimxV4IW1xiI8M%2FjInlE0%2F7sAWCj1caQfsxqFCOPEq0mpOo1iyOWaU6Urrg0Q2qzUMHIbYcIvHCgNQfgvQXZz1%2Fz3yPXnUMXrXlhTeMwMxW2RXXb07BMKAbfyGsDvZ4G21Wft7F8eGJkilathW1TQLRNGVYpghM7lZKPxx6wNYMg3qj%2BnZHtR0bHJeQvVuQVfZlvqLgMlr2qQMcBRCtkyT4Xys5%2BukOjHUzZlAippOuIoFw65cP78BenE%3D" rel="nofollow">Finally</a> , <a href="https://link.segmentfault.com/?enc=iKN7J%2FXdzEre3nMpbKNb7Q%3D%3D.0k%2Br39gOwlPhyxRtghZ04YIzx1%2FWUOzEa7RUHVBaZ9fg8K1QI80lypPvbOq2AYka4pk%2B0cTRP%2FkI75aCKMmRDzivpK6MfVrZNAaZhGiOblkIshoZ%2FR%2BfbIfZSWlCKsLB4P4ZaauWLDZpy%2Ft2%2FPh6PDKkXU2%2FKK%2FWugn%2FKmuNUH7z4rYCGXip0qRoMrE1vRSTZumVa4eVWUnUD4PeSoMSKuI93MyAxWATtAJTJOkY%2FInumBjinojoFNZGAbG2O8b%2BW%2BKG%2BdgOnjN%2Bstsd1yGNBMhU7wFZtK7cdR1072Etw2Vn0zZEtFSK1ZNjoYvYG0UHs%2FAfX1cXNKACFCm%2Btu13pRQF4dNhMR5dwMNvDRgX2Hw%3D" rel="nofollow">Exception</a> , <a href="https://link.segmentfault.com/?enc=2NGcxx4lka6g02HylFwxLQ%3D%3D.Nl09EVApwVCot7okJenMnM%2FICEghcnmrBsGHAaquKMVU6AgrZVKr6HHFNgA7afoC9PnE%2BiYNkMJMM5Da%2FqupY0YSD1YQ45XneCggPM9%2BQcJHQc3OrhzokA4tVx2vhhpZ0plNUPGtA%2FFvWjitMAVZuUek09gfZgtWwaww1yRtgCimQSjxWZQUXXSzvDL26kU9c87WeGOhwrpItz%2Bs%2BbEOqBvByz28Q0kE16pL8P2R7%2FQWmCO0LaaBiUtoVUo3m%2BtEu%2FnG1cKHN0UiuzqfvAHhbJ%2BcLN5xI65Ui18ysY482EWcRaGqWi0OesrQD6q2%2BTm%2FPP1WBrS%2BrRWpFwCA5%2Fc2dRu2bTqf59%2Fry6oCqFDiF4Y%3D" rel="nofollow">Error</a> , <a href="https://link.segmentfault.com/?enc=DvZnK9TXdQbksNqXw9RbRQ%3D%3D.9lsrFmNSK2Ed3fKYMUps87uQZUfhFNj0QFKkmuCeygQi84eXTLPIlXJALTjFlU7VQ%2BFgihhLtBtfopy1l0VfVpdxl9EBVuDoDKcqteHC155he8TZ9ColtPhX0LlEvlu9plSrfqz16HhtYPVEKfAbli4Jncph8PurC023p7uQYg01ARao0uowB73u7LJxZ8h1fnSHOXUuDCYKiFNXnkoE%2BfYOFomqqifjpJ2XLJUPC%2FeImlUVKn0novCbajEWrDv%2BGCD6uPjhKB2WXyyNd9EtROdHc2xqH%2BpZg2%2FMgqhSYjh6HhVuPpQia6d9zs9oNN4arwEBVY4%2FZD3jjEK6zQYd8coTNqoyaqiJPnh8AQ5yjwU%3D" rel="nofollow">Throwable</a> , <a href="https://link.segmentfault.com/?enc=%2FTbQE5%2FPvDz0rwxHHLKCLw%3D%3D.iCKQdIuV6WH7Iu7NUeO%2F5zIQi22SzVhTTkAxlKLwlZH%2BBSFbZUZZL5LOUyu2LSwxtp3VJizo38VCrSmcYEjUchMeQJjIvt68B%2FrRoE3otcZEWZ8xS9uQ1xVKctiCCZV4E44SguO1XpiQG%2BkAqbPez%2BUklrP5bbNsYHSRTr7ODXyQr45DARtQLZ4Sb%2BkthBvRxmKcJFOjDrNW%2FLL16Pc%2FTY2Fq%2BFFTNEdVddRaEYVvNunO5hSfsMstJdPH2Og%2BaGVP9VMCOOGtKT9K37brtyjX03EbOQjs%2F4hD6NQ89zSd8bcMfDP53GywsMyYBICYHF%2F7ruB%2BYa4lM2WHpVGE5WYQIBFJWPSxPnAM%2BlpYYxpiGw%3D" rel="nofollow">Throw</a> , <a href="https://link.segmentfault.com/?enc=C0IxTgzcgTeN6KcwFQQN0g%3D%3D.HKk1Frh0plUWZbjMj6I%2BZQknInxqdeRQJzR9zHlficBsbRcghPZ6NLWSYKE2155q85yZajpkPrtIHl7RpUwXWppCMAsDFZugHoI0vy8ezqpNT0tPZmoRlpao%2FE1MEvIqbaW%2BEYPZ5PxQcFKkmU9uWj9vZT14AJ3lIKFC8Wb2%2BYY4kJQWwBFkA5DtlXlU61QY3%2F2vuoqkP7NjnhiM6nRfAO2OEW8GAT5fSrENuli4RCnCTVgsb5UqRAcwRtNmp%2FgK6a5XvqWdGtTDSARNBMwy7B0qgPWmVfpJs2129%2BVDi47YcPs9nIzaNjG6%2BTis%2B%2B%2BysIEuYr6iTa%2FEuMTfbFSKhqIGpwDd7QpQcp9IxUnPBIE%3D" rel="nofollow">Static</a> , <a href="https://link.segmentfault.com/?enc=pCfSDO0EdnRYxBkRiux3jQ%3D%3D.em2ymh14L%2F3WMr0KtzgHx313f9FLfwhQ6dXfgc1GD2qylnCtKSTXquvbdzE1ccqct5Ni87wFo%2FiNoDAmQamN4tptgiVvWq8t5vnZCRRdU2cac1%2BAjroD8MvvaKljCaQgLKpSZ3ungqnysm5Zt8uB6tcSVvxrgeqUzeHp%2Fw%2BvZHVbOz25R1Hu6%2Fx95SFVQT1zwVFOHLH48bjcfvTGd9hriC70awrucDKuMVfL37SNHoAhtfqs%2F8%2FOFm%2B3UZbHzJVRzPTFSyEiG4ZXJMNI6NMIySMtzT5TFCuJFW5Pa1L%2FWGkmbjYYALF%2B1KVp6QlxVFw7NBJABbBDA7mDjTHgysxAbw%3D%3D" rel="nofollow">Abstract</a></p>
<p><img src="/img/remote/1460000018621846?w=258&h=258" alt="" title=""></p>
SpringBoot注册Windows服务和启动报错的原因
https://segmentfault.com/a/1190000018480141
2019-03-12T21:18:34+08:00
2019-03-12T21:18:34+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>SpringBoot注册Windows服务和启动报错的原因</h2>
<p>Windows系统启动Java程序会弹出黑窗口。黑窗口有几点不好。首先它不美观;其次容易误点导致程序关闭;但最让我匪夷所思的是:将鼠标光标选中黑窗口日志信息,程序竟然不会继续执行,日志也不会继续输出。从而导致页面一直处于请求状态。回车后程序才能正常执行。同时客户希望我们能部署在Windows系统上并且做到开机自动启动。针对以上需求将系统程序注册成Windows服务变得尤为重要。</p>
<p>针对于SpringBoot程序,目前主流的方法是采用winsw,简单方便。可是在开发过程中,针对不同的系统,启动服务可能会出现意想不到的结果。同样的配置方法,在win10可以成功注册并启动服务。而在windows server 2012 却启动失败。这里分享我的经验。</p>
<h3>注册windows服务</h3>
<h4>制作流程</h4>
<p>winsw是⼀款可以将可执⾏程序安装成Windows Service的开源⼩⼯具,<a href="https://link.segmentfault.com/?enc=vD7gqXI9kLAct3zTCR3%2BLA%3D%3D.1%2BMS95%2BxVWkK2D9V3ianHZmNWUfZG%2F38jDmfKoLztoE3yGMwLptg%2FRhmsW0AB6yU" rel="nofollow">官⽹地址</a>, <a href="https://link.segmentfault.com/?enc=LucII3ggT5CcRYU9bcEYFA%3D%3D.ARPzmDn%2B5XQ7SeRfZy%2FwyhGrcUf1Ydan76FSI%2BBdEovePIfWliSKwFbguqqhFCWE" rel="nofollow">下载地址</a><br>制作步骤:<br>第一步:将springboot项目打包成MyServer.jar</p>
<p>第二步:将下载的WinSW.NET2.exe 改名为MyServer.exe</p>
<p>第三步:将下载的sample-minimal.xml 改名为MyServer.xml</p>
<p>第四步:注册和启动服务</p>
<p>这里重点介绍 sample-minimal.xml 文件</p>
<pre><code class="xml"><service>
<!-- Windows 服务唯一标识ID-->
<id>My Server</id>
<!-- Windows 服务名称-->
<name>My Server</name>
<!-- Windows 服务描述-->
<description>This service is a service cratead from a minimal configuration</description>
<!-- 启动的可执行文件的路径,如果已经配置环境变量,则不必写全路径(则其实是一个坑) -->
<executable>java</executable>
<arguments> -jar MyServer.jar --spring.datasource.url=jdbc:mysql://localhost:3306/database </arguments>
<!-- 日志路径,若目录不存在,则默认为配置文件所在的同一目录-->
<logpath>ServerPath\log\dashboard\</logpath>
<!-- 日志模式,默认为append追加模型,rotate为旋转模式-->
<logmode>rotate</logmode>
</service></code></pre>
<p><strong>executable</strong>:启动可执行文件的全路径,如果配置环境变量,则可以简写,所有这里填写Java</p>
<p><strong>arguments</strong>:命令执行的参数</p>
<p><strong>logpath</strong>:配置日志路径</p>
<p><strong>logmode</strong>:日志输出模式,默认为append,<a href="https://link.segmentfault.com/?enc=WDWyBNJ%2FKchMxmK1LUKEOw%3D%3D.b5fcjp2H3zNe9iKu28jJqYMQTYTpnpPUr1KPS6nnwEZ28RSND4m6Fx4lz0tG2mIolaUIfIWCmZlyfCh4fR6Jv6Mc%2Fv%2FCKcESPYvJjWL57l4%3D" rel="nofollow">官方文档</a></p>
<ul>
<li>append (追加模式)其特点是将日志文件全部输出在一个文件中,这个文件可能会越来越大。</li>
<li>rotate(旋转模式,推荐)当日志文件大小达到10兆(默认值),winsw会将日志重新输出到另外一份日志文件,最多保留8个(默认值)。</li>
<li>reset(重置模式)每次重启服务都会重置日志文件。</li>
<li>none(忽略模式)几乎不会生成日志文件。</li>
</ul>
<h4>winsw常用命令</h4>
<ul>
<li>MyServer.exe install:安装服务</li>
<li>MyServer.exe uninstall:删除服务</li>
<li>MyServer.exe start:启动服务</li>
<li>MyServer.exe stop:停⽌服务</li>
<li>MyServer.exe restart:重启服务</li>
<li>MyServer.exe status:输出当前服务的状态</li>
</ul>
<p>MyServer.exe 是WinSW.NET2.exe文件。在win10系统上一次成功,没有多余的烦恼。可生活哪有这么容易,在windows server 2012 r2的系统上启动失败。有错误不可怕,可怕的是不会找错误日志。</p>
<p><img src="https://segmentfault.com/img/bVRgO5?w=401&h=156" alt="" title=""></p>
<h3>启动windows服务失败</h3>
<p>服务启动成功后自动关闭,配置的日志文件也没有生成。尝试用cmd执行java -jar的命令,服务可以正常启动。但可具体是什么错误却不得而知。其实Windows服务是有日志管理的。选择:控制面板---管理工具---事件查看器---window日志---应用程序---找出对应服务的日志。如下:</p>
<pre><code>Service cannot be started.
System.ComponentModel.Win32Exception: The system cannot find the file specified
at System.Diagnostics.Process.StartWithCreateProcess(ProcessStartInfo startInfo)
at winsw.WrapperService.StartProcess(Process processToStart, String arguments, String executable)
at winsw.WrapperService.OnStart(String[] _)
at System.ServiceProcess.ServiceBase.ServiceQueuedMainCallback(Object state)</code></pre>
<p>提示很清楚,系统没有找到指定文件,而在winsw的xml文件中就已经配置了executable,并且配置了环境变量。那为什么还提示文件没有找到?抱着试一试的心态,将java改为了全路径。重新注册服务并启动,结果服务启动成功了。一肚子的火不知道往那撒。</p>
<p>为了避免这种事情再次发生,决定将executable的内容设置成Java的全路径,于是简单写了一个bat文件。</p>
<pre><code class="bat">@echo off
# 获取java环境变量
set JAVA_HOME=%JAVA_HOME%
echo %JAVA_HOME%
# 替换java路径
setlocal enabledelayedexpansion
set file=%cd%\MyServer.xml
set file_tmp=%cd%\MyServer_tmp.xml
set source=JAVAHOME
set replaced=%JAVA_HOME%\bin\java
for /f "delims=" %%i in (%file%) do (
set str=%%i
set "str=!str:%source%=%replaced%!"
echo !str!>>%file_tmp%
)
move "%file_tmp%" "%file%"
# 注册并启动服务
MyServer.exe uninstall
MyServer.exe install
MyServer.exe start
EXIT
</code></pre>
从Docker 到Jenkins 到Ansible的部署经验
https://segmentfault.com/a/1190000017128237
2018-11-24T15:10:42+08:00
2018-11-24T15:10:42+08:00
itdragon
https://segmentfault.com/u/itdragon
6
<h2>从Docker 到Jenkins 到Ansible的部署经验</h2>
<p>工作中,除了开发功能,还负责系统的部署工作。我从频繁的部署工作中,逐渐找到了一些偷懒的方法。从传统的Java -jar命令启动服务,到通过Docker 容器构建部署服务,再后来通过自动化部署工具Jenkins来完成部署,最后再结合Ansible完成远程部署。一步步的进步极大的减少部署工作,提高了工作效率(增加了许多划水时间)。</p>
<h3>Docker</h3>
<h4>简介</h4>
<blockquote>Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的<a href="https://link.segmentfault.com/?enc=lbkWRfgiEVVYe6kphveaDw%3D%3D.jGyVqh9z0A%2FkTUcUWlDiSyNut7%2BIkzB2XH7u35WFDuTUDZQjkxP1v%2Bcjs97%2F3LMb" rel="nofollow">Linux</a>机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。</blockquote>
<p>Docker给我的印象很深,没有什么环境是docker pull 解决不了的,</p>
<h4>常用命令</h4>
<pre><code>docker ps , docker ps 默认显示运行中的容器,-a 显示所有,-l显示近期创建的容器
docker start xxx , 启动xxx容器
docker restart xxx , 重启xxx容器
docker run xxx , 创建并运行xxx容器
docker build -t xxx . ,使用 Dockerfile 创建镜像
docker stop xxx , 关闭容器
docker rm xxx , 删除容器
docker images , 查看所有镜像
docker rmi xxx , 删除xxx镜像
docker exec -it xxx sh , 进入xxx容器中,用quit退出
docker logs -f xxx --tail 500 , 查看xxx容器的日志,显示最后500行,常用命令
docker inspect xxxx , 查看容器配置信息
docker-compose -f app.yml up -d , 按照app.yml文件配置以debug形式启动
docker-compose -f app.yml down , 按照app.yml文件配置形式关闭
</code></pre>
<h4>使用场景</h4>
<p>第一步:在gradle项目加入docker插件,即在gradle.build 文件中加入以下代码。需要注意的有插件的版本,项目打包后的名称,Dockerfile文件目录</p>
<pre><code>dependencies {
classpath("se.transmode.gradle:gradle-docker:1.2")
}
apply plugin: 'docker'
task buildDocker(type: Docker, dependsOn: build) {
push = false
applicationName = "项目名"
dockerfile = file('src/main/docker/Dockerfile文件目录')
doFirst {
copy {
from jar
into stageDir
}
}
}</code></pre>
<p>第二步:创建Dockerfile文件,文件目录要和第一步中设置的保持一致。需要配置jdk镜像和基本的启动参数</p>
<pre><code>FROM frolvlad/alpine-oraclejdk8:slim
VOLUME /tmp
ADD 项目jar名称.jar app.jar
RUN sh -c 'touch /app.jar'
ENV JAVA_OPTS=""
ENV PORT="6666"
ENV DB_CONNECTION="jdbc:mysql://ip:port/database"
ENV DB_USER="user"
ENV DB_PASSWORD="password"
ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar --spring.datasource.url=$DB_CONNECTION --spring.datasource.usernam=$DB_USER --spring.datasource.password=$DB_PASSWORD --port=$PORT"]</code></pre>
<p>第三步:将jar拷贝到服务器上,然后执行编译,运行的docker命令</p>
<p>一)、通过gradle的bootJar,将项目打包。同时需要把引入的第三方jar也要一起打入到项目jar中。</p>
<p>二)、Windows系统中可以通过Xftp将jar和Dockerfile文件拷贝同一个目录下。Linux系统可以通过scp命令上传文件。</p>
<p>三)、执行docker ps,查看当前运行的容器,执行docker stop和docker rm 关闭和删除之前旧版本的容器</p>
<p>四)、找到jar的目录,并在当前目录下,执行 docker build -t 镜像名称 . 的命令编译项目,注意后面的点不要漏了。</p>
<p>五)、编译成功后执行 docker run --name 容器名 -v /tmp:/tmp -p 对外开发的端口:项目启动的端口 镜像名:latest 。启动容器</p>
<p>六)、执行docker ps,查看容器启动是否正常启动。同时执行docker logs -f 容器名 --tail 500,查看容器启动日志,检查是否有异常</p>
<p>七)、最后浏览器访问一下,已确保部署成功。</p>
<p>全称大概需要几分钟的时间,虽然不算麻烦。可次数多了,就很麻烦了。有没有什么好的工具帮助我们完成这一系列操作呢?答案是肯定的。</p>
<h3>Jenkins</h3>
<h4>简介</h4>
<blockquote>The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project.</blockquote>
<p>Jenkins 的logo是一个管家的形象,很贴切。对它的理解比较肤浅。他通过管理Git上的项目,来确保每次打包的jar都是最新的。同时在构建成功后执行我们输入的shell命令,来达到自动化部署的工作。</p>
<h4>使用场景</h4>
<p>第一步:创建一个负责编译的Jenkins项目,</p>
<p>在Jenkins控制台页面,点击页面左上角的“新建”按钮。再输入项目名后,可以选择创建一个空项目,也可以在页面最下面选择copy from 其他项目。不管如何创建,我们需要Jenkins管理项目的源码,构建和构建后的操作。</p>
<p><img src="/img/remote/1460000017128240?w=947&h=609" alt="" title=""><br><img src="/img/remote/1460000017128241" alt="" title=""></p>
<p>第二步:创建一个负责运行的Jenkins项目</p>
<p>以同样的方式创建项目,在构建触发器上,选择第一步创建的项目,构建的Shell命令是先删除之前的容器,然后在重新运行容器。若之前的容器不存在,则会构建失败。所以第一次构建的时候把第一行命令删掉。解决方案傻乎乎的,只是因为没有花时间去处理。</p>
<p><img src="/img/remote/1460000017128242" alt="" title=""><br><img src="/img/remote/1460000017128243" alt="" title=""></p>
<p>第三步:选择编译项目,点击立即构建,当第一个项目构成成功后,会自动触发运行项目。等待两个项目都成功后,就可以访问浏览器,检查功能。</p>
<p>有了Jenkins,一切变得轻松很多。但他也有一个较大的弊端,就是使用前必须要先安装。特别是在客户的服务器上,也许别人就只跑这一个服务,你给别人整了一个Jenkins,似乎有点大材小用了。有没有好的解决方法?答案是肯定的。</p>
<h3>Ansible</h3>
<h4>简介</h4>
<blockquote>Ansible is an IT automation tool. It can configure systems, deploy software, and orchestrate more advanced IT tasks such as continuous deployments or zero downtime rolling updates.</blockquote>
<p>从接触到使用Ansible大概有一天的时间,对它的理解也是比较肤浅。我单纯的认为,他可以帮助我们在服务器之间传输文件,同时还可以执行一些shell命令。抱着这样的想法,我们可以通过Jenkins完成自动化编译,再通过Ansible传输资源文件到部署的环境中,同时执行启动Shell命令。</p>
<h4>使用场景</h4>
<p>第一步:修改Jenkins运行项目的构建Shell,将之前的docker run改成</p>
<pre><code>ansible-playbook ansible命令文件路径/app.yaml</code></pre>
<p>第二步:创建Ansible脚本文件app.yaml,目录和第一步中设置的保存一致,模版大致如下</p>
<pre><code>- hosts: '需要部署的远程服务ip'
tasks:
- name: "关闭旧版本的容器"
shell: docker stop xxx
ignore_errors: true
- name: "删除旧版本的容器"
shell: docker rm xxx
ignore_errors: true
- name: "删除之前的旧文件"
shell: rm -rf /旧文件路径/*
- name: "传输Dockerfile文件"
copy:
src=/文件目录/Dockerfile
dest=/远程服务指定目录
- name: "传输Jar文件"
copy:
src=/jar目录/xxx.jar
dest=/远程服务指定目录
- name: "构建docker 镜像"
shell: chdir=/jar所在目录 nohup docker build -t 镜像名 .
- name: "启动容器"
shell: nohup docker run --name 容器名 -v /挂载路径/:/挂载路径/ -p 对外端口:服务端口 -d 镜像名:latest
</code></pre>
<p>第三步:在Jenkins上构建编译项目。</p>
<h3>前后端项目的部署</h3>
<p>到这里,三种部署的流程就完成了。如果你熟悉Docker的方式构建,再用Jenkins和Ansible的时候,就会简单很多。我在实际开发中,项目是前后端分离的。公司做了两个方案,</p>
<p>第一种:前后端分开部署,即Jenkins上有四个项目。前端和后端各两个项目。这样的好处就是前后端互不影响。不会因为对方的错误而从新编译。缺点也是有的,很难保证对方部署的环境是最新的。</p>
<p>第二种:把前后端放在一个项目中,一次构建完成两个项目的打包部署。缺点是构建慢,优点就是保证两端的代码都是最新的,适合发布到预发布环境和正式环境。</p>
<p>那么,针对前后端一起部署的需求,Jenkins和Ansible同样也需要简单的修改。其思路就是Jenkins负责编译项目,将资源文件压缩,再通过Ansible上传到其他服务器上。执行解压,构建,启动的命令。</p>
<p>看起来视乎很简单,但有一个坑希望你们跨过去。前端打包需要npm或者其他工具,但是你的服务器上没有安装。此时请务必通过Jenkins控制台,或者用Jenkins帐号登录服务器安装这些工具。笔者就是通过root帐号登录服务器安装的npm,通过Jenkins编译时提示没有权限。</p>
常见的统计解决方案
https://segmentfault.com/a/1190000015778391
2018-07-26T16:07:08+08:00
2018-07-26T16:07:08+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<p>最近用MySQL做统计的需求比较多,这里整理一些常用的场景方便后期查阅,同时也是抛砖引玉的过程。其中包括<strong>普通的分组统计</strong>,<strong>连续的每日统计</strong>,<strong>区间范围统计</strong>。</p>
<p>技术:MySQL, SpringDataJpa, Kotlin<br>说明:文章前半部分是场景分析,后半部分是语法分析<br>要点:GROUP BY, UNION, DATE_FORMAT, 流程控制函数</p>
<h2>普通分组统计</h2>
<h3>场景一:根据订单状态统计订单数量。</h3>
<p>一个很常见,也很简单的统计需求。其中状态字段是订单实体的一个属性。参考代码:(Kotlin语法)</p>
<pre><code class="java">@Query("SELECT status, COUNT(id) FROM Order GROUP BY status")
fun summaryOrderByStatus(): Array<Array<String>>?</code></pre>
<h3>场景二:根据订单中商品类目统计订单数量和金额。</h3>
<p>比场景一稍微麻烦了一点,商品字段是订单实体的一个属性,而类目字段才是商品实体的一个属性。参考代码:(Kotlin语法)</p>
<pre><code class="java">@Query("SELECT commodity.category, COUNT(id), SUM(finalPrice) FROM Order GROUP BY commodity.category")
fun summaryOrderByCommodityCategory(): Array<Array<String>>?</code></pre>
<h3>小结:</h3>
<p>一)、分组统计少不了GROUP BY语句,如果需要加查询条件,请在其前面添加 WHERE 语句。<br>二)、统计数量用COUNT,统计总和用SUM函数,有GROUP BY的地方,少不了这些聚合函数。<br>三)、统计返回的结果是字符串类型的二维数组。<br>四)、以内嵌属性分组,如果是SpringDataJpa框架,则可以直接通过"实体类.属性名"的方式。</p>
<h2>每日统计</h2>
<p>在做每日,每周,每月统计时,遇到返回日期不是连续的情况。原因是数据库中没有值,而我们理想状态应该是:如果没有值则默认为零,使其数据是连续的日期。</p>
<h3>场景三:统计结果日期可能不连续</h3>
<p>如果数据库中某个时间段没有值,那统计出来的结果会缺这段时间。参考代码:(sql语句)</p>
<pre><code class="sql">-- 统计每日
SELECT DATE_FORMAT(create_date,'%Y-%m-%d') as days, COUNT(id) count FROM order GROUP BY days;
-- 统计每周
SELECT DATE_FORMAT(create_date,'%Y-%u') as weeks, COUNT(id) count FROM order GROUP BY weeks;
-- 统计每月
SELECT DATE_FORMAT(create_date,'%Y-%m') as months, COUNT(id) count FROM order GROUP BY months;</code></pre>
<h3>场景四:统计结果日期连续</h3>
<p>要让日期连续,又要代码优雅。说实话,困扰了我很久,一直没有找到很好的解决方法,虽然目前这个方法很挫。但可以解决问题。毕竟抓到老鼠的都是好猫。如果各位有好的建议,望赐教!</p>
<p>解决思路:<br>第一步:创建一张date_summary辅助表,字段只需要有date和count(默认值为零)。<br>第二步:先向date_summary表插入10年内的数据。<br>第三步:通过UNION ALL 联合查询,将空缺的日期补上。</p>
<p>第二步参考代码(Kotlin语法)</p>
<pre><code class="java">val startDate = Calendar.getInstance()
startDate.set(2018, 6, 1)
val startTIme = startDate.timeInMillis
val endDate = Calendar.getInstance()
endDate.set(2028, 11, 30)
val endTime = endDate.timeInMillis
val oneDay = 1000 * 60 * 60 * 24L
var time = startTIme
val dates: MutableList<DateSummary> = arrayListOf()
while (time<=endTime) {
val dateSummary = DateSummary()
dateSummary.date = SimpleDateFormat("yyyy-MM-dd").format(Date(time))
dateSummary.count = 0
dates.add(dateSummary)
time += oneDay
}
dateSummaryRepository.saveAll(dates)</code></pre>
<p>第三步统计每日的SQL语句</p>
<pre><code class="sql">SELECT
summary.oneDay,
summary.count
FROM
(
SELECT
DATE_FORMAT( created_date, '%Y-%m-%d' ) oneDay,
COUNT(id) count
FROM
service_order
WHERE created_date BETWEEN "2018-06-01" and "2018-08-01"
GROUP BY oneDay
UNION ALL
(
SELECT
DATE_FORMAT( date, '%Y-%m-%d' ) templateDay,
count
FROM
date_summary
WHERE date BETWEEN "2018-06-01" and "2018-08-01"
GROUP BY
templateDay
)
) summary
GROUP BY
summary.oneDay
ORDER BY
summary.oneDay ASC</code></pre>
<h3>小结:</h3>
<p>一)、MySQL的DATE_FORMAT(date,format) 函数用于以不同的格式显示日期/时间数据,文章后面会详细介绍<br>二)、MySQL的UNION 操作符用于合并两个或多个SELECT语句的结果集,文章后面会详细介绍</p>
<h2>区间范围统计</h2>
<p>这是一个较为常见的需求,比如按照年龄段统计人员分布情况,甚至要求分别统计男女人数分布情况。</p>
<h3>场景五:根据小区年龄段统计人数</h3>
<p>只根据年龄范围统计,没有其他限制条件,使用SUM只需要加一。</p>
<pre><code class="sql">SELECT INTERVAL(age,10,20,30,40,50,60,70,80,90) AS ageRatio, SUM(1) AS count FROM user GROUP BY ageRatio</code></pre>
<h3>场景六:根据小区年龄段统计男女人数</h3>
<p>在场景五的基础上多了一个区分性别,用流程控制函数来设置SUM加一的情况。</p>
<pre><code class="sql">SELECT INTERVAL(age,10,20,30,40,50,60,70,80,90) AS ageRatio,
SUM(CASE WHEN sex=1 THEN 1 ELSE 0 END) AS male,
SUM(CASE WHEN sex=0 THEN 1 ELSE 0 END) AS female FROM user GROUP BY ageRatio</code></pre>
<h3>小结:</h3>
<p>一)、通过区间统计需要使用MySQL的INTERVAL函数,第一个参数是需要比较的字段,后面是比较的区间,值必须从小到大<br>二)、区间统计的结果也是二维数组,注意返回的结果可能不是连续的(这里的不连续可以用代码解决,毕竟区间数量较少)。第一个参数返回的是区间的下标,从0开始。<br>三)、当age的值在区间范围内就SUM加一,也可以通过流程控制函数(CASE WHEN THEN ELSE END)来判断是加一还是加零</p>
<h2>MySQL知识点</h2>
<p>知道现在都是快餐文化,大家都很忙,很少有时间去揣摩各语法的特点。所以先把常用的场景写在前面,语法知识写在后面。</p>
<h3>GROUP BY 分组</h3>
<p>一)、分组一般与聚合函数一起使用如SUM,COUNT等<br>二)、GROUP BY 在WHERE 语句之后</p>
<h3>DATE_FORMAT 时间格式化</h3>
<p>一)、用来修改时间的格式<br>二)、语法格式: DATE_FORMAT(date,format) date必须是合格的时间参数,format是输出时间格式<br>三)、常见的format格式有:</p>
<ul>
<li>%Y: 4位数的年,</li>
<li>%y: 2位数的年,</li>
<li>%m: 2位数的月(00~12),</li>
<li>%M: 英文单词的月,</li>
<li>%d: 2位数的日(00~31),</li>
<li>%u: 周,星期一是一周的第一条,</li>
<li>更多可以访问<a href="https://link.segmentfault.com/?enc=OgCTn%2F5UdSaFzIN0gWXbKw%3D%3D.neIVVZ6y3jL6ssbKyiKt2fNPOsyiAY1AOsNzftmKZepoAQ7tXOSOPg8RkLeWaESvoCHHY3yRcqR7UkHmnXRprw%3D%3D" rel="nofollow">w3school</a>
</li>
</ul>
<h3>UNION 联合结果</h3>
<p>一)、UNION可以合并、联合,将多次查询结果合并成一个结果,通过查询结果合并解决了统计不连续的情况。<br>二)、多条查询语句的列数必须一致,各列的顺序最好一致。场景四中,两条sql都只查询了date和count,且顺序保持一致。<br>三)、union 去重,union all包含重复项</p>
<h3>INTERVAL 比较间距</h3>
<p>一)、INTERVAL()函数是比较列表(N, arg1, arg2, arg3...argN)中的N值。<br>二)、INTERVAL()函数如果N<arg1则返回0,如果N<arg2则返回1,如果N<arg3则返回2,如果N为NULL,它将返回-1。<br>三)、列表值必须是arg1 < arg2 < arg3 的形式才能正常工作。</p>
<h3>流程控制函数</h3>
<p>一)、case when then else end 是流程控制函数中的一种,还有一种是if函数<br>二)、使用语法:</p>
<pre><code class="sql">case
when 条件1 then 值1
when 条件2 then 值2
...
else 值n
end</code></pre>
<p>文章到这里就结束了。如果文章对你有帮助,可以点个"推荐",也可以"关注"我,获得更多丰富的知识。若文中有什么不对或者不严谨的地方,请指正。</p>
windows一键部署java项目
https://segmentfault.com/a/1190000015573114
2018-07-09T13:30:58+08:00
2018-07-09T13:30:58+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<p>windows一键部署java项目</p>
<p>因为公司需求,要在windows的环境上做一键部署启动java项目,同时还要支持从安装界面动态修改配置文件的IP地址。就像安装软件一样将jdk,tomcat,mysql,influxdb,nginx安装并配置到系统上,顺便还要初始化一下数据。花了一周的时间,这里记录我的踩坑日志。</p>
<h2>准备工作</h2>
<p>磨刀不误砍柴工,选择好工具可以事半功倍。<br>一)、Inno Setup,一款为Windows程序提供的免费安装程序,通过它可以将需要的文件压缩打包成exe安装程序,然后像安装程序一样解压到另外一个环境中。<a href="https://link.segmentfault.com/?enc=SVxzjXAdni8jZHvANEwrhQ%3D%3D.ck6ZEcfExysGsnIEFh9VgLZR39RHGl9yyhZKstbMjrEziKqTUA0NRJcNhuABMO0L" rel="nofollow">官网地址</a><br>二)、虚拟机,两个作用:第一可以避免玩坏自己的电脑,第二可以保证每次测试安装的环境都是干净的系统,减少一些不必要的麻烦。<br>三)、JDK1.8,MySQL5.7,Tomcat8,Nginx,InfluxDB等,这是需要压缩的文件资源。<br>四)、Windows Server 2012 R2,你值得拥有,用2008安装MySQL会很不顺。</p>
<h2>Inno Setup基础使用</h2>
<p>Inno Setup的模版几乎一样,如果需要自定义界面,可以在[Code]中添加代码。比如我需要在安装的过程中添加一个有输入框的自定义界面,将输入的值替换配置文件中的指定内容,安装成功后在桌面生成快捷键。<br>对于bat脚本语言很薄弱的我来说这里有两个难点,但依葫芦画瓢还是可以做出来的。<br>一)、Inno Setup的函数,在[Code]代码块中,可以自定义很多功能来实现自定义的开发,具体可以参考<a href="https://link.segmentfault.com/?enc=bADpoed6%2B3byEIIQ%2FYc5TA%3D%3D.sVg%2BpkTWmRS2t3cNkjvx0hPrxeKRi3eIlGoogiGUbisuwNprOQXXdUnYn4jwglEB" rel="nofollow">在线的文档</a><br>二)、需要用bat脚本配置jdk环境变量,安装部署Tomcat,安装部署MySQL。</p>
<pre><code class="xml">#define MyAppName "自定义程序名称"
#define MyAppVersion "V1.0"
#define MyAppPublisher "自定义程序出版商"
#define MyAppURL "http://www.xxxx.com/"
; 基本配置
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
; 单独标识,可以通过innosetup-QSP-5.6.1.exe 工具自动生成
AppId={{0167D65D-549A-4BA3-B88A-4814EC5A1D35}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
; 默认安装路径
DefaultDirName=C:\Program Files\ITDragon\
DefaultGroupName={#MyAppName}
; 软件名称
OutputBaseFilename=ITDragon
; 软件图标
SetupIconFile=C:\Users\Long\Desktop\ok\ITDragon\itdragon.ico
; 压缩方式
Compression=lzma
; yes 可以使文件更小
SolidCompression=yes
; 必需有管理员权限才能安装
PrivilegesRequired=admin
; 安装密码
;Password=itdragon
; 开启加密,可能还需要一个dll文件
;Encryption=yes
; 语言配置
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
; 安装文件
[Files]
; 安装部署的源文件路径
Source: "C:\Users\Long\Desktop\ITDragon\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
; 快捷键
[Icons]
Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"
Name: "{commondesktop}\快捷HTTP地址"; Filename: http://localhost
Name: "{commondesktop}\Mysql数据初始化脚本"; Filename: "{app}\mysql\init-data.bat"
Name: "{commondesktop}\安装须知"; Filename: "{app}\安装须知.doc"
; 程序安装成功后执行脚本
[Run]
Filename: "{app}\tomcat\init-jdk.bat";
Filename: "{app}\tomcat\bin\init-tomcat.bat";
Filename: "{app}\mysql\bin\init-mysql.bat";
; 在安装的时候输入IP地址,动态将localhost修改为输入值
[Code]
var
myPage:TwizardPage;//自定义窗口
ed1:TEdit;//自定义输入框
Lbl1: TNewStaticText;//自定义标题
//初始化引导窗口
procedure InitializeWizard();
begin
myPage:=CreateCustomPage(wpWelcome, '配置服务IP地址', '请输入正确的IP地址,已确保服务的正常使用');
Lbl1 := TNewStaticText.Create(myPage);
Lbl1.Left := ScaleX(5);
Lbl1.Top := ScaleY(5);
Lbl1.Width := ScaleX(250);
Lbl1.Height := ScaleY(50);
Lbl1.Caption := 'IP地址输入框标题';
Lbl1.Parent := myPage.Surface;
ed1:=TEdit.Create(myPage);
ed1.Width:=ScaleX(410);
ed1.Top := ScaleY(25);
ed1.Text :='127.0.0.1';
ed1.Parent:=myPage.Surface;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
fileName:String;
svArray: TArrayOfString;
nLines,i:Integer;
begin
//复制文件后执行
if CurStep = ssPostinstall then
begin
fileName := ExpandConstant('{app}\nginx\html\main.bundle.js');
LoadStringsFromFile(fileName, svArray);//读取文件
nLines := GetArrayLength(svArray);
for i := 0 to nLines - 1 do
if (0 < Pos('localhost', svArray[i])) then//查找目标
StringChange(svArray[i], 'localhost', ed1.Text);
SaveStringsToUTF8File(fileName, svArray, false);
end;
end;</code></pre>
<p>注意:<br>一)、如果你不需要自定义函数,[Code]代码块都可以删掉<br>二)、点击Inno Setup上的compile按钮开始编译,编译成功后会在一个Output目录夹生成exe文件,这个Output目录一般和iss文件在同一层。</p>
<h2>配置JDK环境变量</h2>
<p>在做这个需求的时候,看了几篇文章,发现他们都把jdk放在Tomcat目录中,第一次做的时候也傻乎乎的放在Tomcat目录中,其实没必要。而且目前主流的springboot项目都是内嵌tomcat。新建一个bat脚本用来运行java程序<code>java -jar xxx.jar </code>安装成功后在桌面生成一个快捷键,让用户双击启动服务。如果你有跟合理的方法,可以告诉我!!!<br>init-jdk.bat,内容来源网络,修改时需要目录层级关系:</p>
<pre><code class="xml">@echo off
echo
cd ..
echo "%~dp0"
echo "%cd%"
set jdkpath=%cd%\tomcat\bin\java\jdk
echo %jdkpath%
setx JAVA_HOME "%jdkpath%" -m
setx CLASSPATH ".;%%JAVA_HOME%%\lib\tools.jar;%%JAVA_HOME%%\lib\dt.jar" -m
echo %Path%
echo %Path%|find /i "%java_home%" && set IsNull=true || set IsNull=false
echo %IsNull%
if not %IsNull%==true (
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v Path /t REG_SZ /d "%Path%;%%JAVA_HOME%%\bin;%%JAVA_HOME%%\jre\bin" /f
setx Path "%%JAVA_HOME%%\bin;%Path%"
)
exit</code></pre>
<h2>Tomcat安装部署</h2>
<p>需要将init-tomcat.bat文件放在tomcat/bin目录下,有博客说要修改service.bat文件,我没有修改依然可以正常启动。在看别人的博客的时候,遇到不明白的地方可以先试着跳过去。我就傻乎乎的下了一个tomcat6,对比两者有什么区别......</p>
<pre><code class="xml">echo -------tomcat begin--------
call "%~dp0%service.bat" install tomcat8
echo -------tomcat install end------------------
sc config tomcat8 start= auto
net start tomcat8
exit</code></pre>
<h2>MySQL安装部署</h2>
<p>MySQL安装需要注意两点:第一设置数据库初始密码,第二设置数据库编码格式,<br>一)、初始化数据库的时候不要生成密码,方便后期修改,命令<code>mysqld.exe --initialize-insecure --user=mysql --console</code><br>二)、数据库的编码格式要统一为utf8,网上很多方法都是在my.ini文件中配置编码格式,可MySQL5.7没有该文件,没有就创建一个。MySQL5.6如果手动创建my.ini文件可能在启动服务时有问题。<br>init-mysql.bat,内容来源网络,稍作修改。</p>
<pre><code class="xml">cd /d %~dp0
cd ..
set inipath=%cd%\my.ini
cd bin
"%cd%\mysqld.exe" -install mysql --defaults-file="%inipath%"
"%cd%\mysqld.exe" --initialize-insecure --user=mysql --console
net start mysql
sc config mysql start=auto
net stop mysql
net start mysql
echo 安装完毕
"%cd%\mysqladmin.exe" -u root password root
echo 修改密码完毕
cd ..
"%cd%\bin\mysql.exe" -uroot -proot < "%cd%\sqlfile\initMysql.sql"
echo 数据库初始化完成
pause;</code></pre>
<p>initMysql.sql</p>
<pre><code class="sql">create database IF NOT EXISTS itdragon_data character set utf8;
set global character_set_database=utf8;
set global character_set_server=utf8;</code></pre>
<h2>遇到的坑</h2>
<p>用了Inno Setup工具,一键部署配置web项目变的很简单,只需要将部署的资源压缩成exe文件,然后点击exe文件待安装成功后执行自动运行bat文件初始化配置即可。可我依然话了很长的时间。原因有几点:<br>一)、开始安装的环境是Windows Server 2008,安装MySQL5.7失败,原因是不支持MySQL5.7提供的高级读写锁。后来换成了MySQL5.6安装成功<br>二)、MySQL5.6启动服务失败1067,网上也有很多解决方法,没有一个成功。无奈换了WIndows Server 2012 r2,结果一次成功。<br>三)、对Inno Setup函数使用不熟,资源文件太大,每次编译调试要等待半小时。</p>
<p>文章到这里就结束了,InfluxDB和Nginx就更简单了,一样的逻辑。希望大家把时间用到正确的地方。如果觉得不错可以点个"推荐"</p>
<p>参考文章:<br><a href="https://link.segmentfault.com/?enc=gmxeG2nMADdQOFbRM0%2BB4A%3D%3D.z6K3vJaaGBm3r6aobtHuYlAsnEtYv0vEexd9EMlMB1mYRuwxTNsK2GNPdI6bslJ%2FoEGDwr3e4RAFsrAms0B14g%3D%3D" rel="nofollow">https://blog.csdn.net/liuhaom...</a></p>
<p><a href="https://link.segmentfault.com/?enc=%2BtH7mLaeQeI2Ypr9iBmDdA%3D%3D.qLhYwGMxkd%2Bmz2pk6UizeM%2FWQr4NYaIrpvnbnDJeiHLxJHQ1e2d%2FK1DMdPbRdchfZOAcZFlHOC1DENMe0Vwy6Q%3D%3D" rel="nofollow">https://blog.csdn.net/dj0721/...</a></p>
Thymeleaf3语法详解和实战
https://segmentfault.com/a/1190000014359882
2018-04-13T11:02:38+08:00
2018-04-13T11:02:38+08:00
itdragon
https://segmentfault.com/u/itdragon
10
<h2>Thymeleaf3语法详解</h2>
<p>Thymeleaf是Spring boot推荐使用的模版引擎,除此之外常见的还有Freemarker和Jsp。Jsp应该是我们最早接触的模版引擎。而Freemarker工作中也很常见(<a href="https://link.segmentfault.com/?enc=hp5a6dV3gVwjZ788%2B%2FZKOA%3D%3D.f0McwX5f%2BBa5IGWMnuwRRfFt0wrlHhnjMsUSWJsIdjlwCcueVWP3OJmHdVm%2BKbmH" rel="nofollow">Freemarker教程</a>)。今天我们从三个方面学习Thymeleaf的语法:有常见的TH属性,四种标准表达式用法,在SpringBoot中的应用。还在等什么,一起来学吧!</p>
<p>技术:Thymeleaf,SpringBoot</p>
<p>说明:为突出Thymeleaf3的语法知识,文章只提出与Thymeleaf有关的代码,完整代码请异步github,喜欢的朋友可以点个star,蟹蟹!</p>
<p>源码:<a href="https://link.segmentfault.com/?enc=aYIcqPRkrW5X5R2XYlcSVQ%3D%3D.aXP4uEp7xwmQTlfjClaz8nTByIb6ZJvoyS%2BdlDkVccOibM1WpLNql5BJJo5mk1%2BkwHDZytr6YHulKp9mpQpRjUqkUD1GJUB4TrAxP6a2igToZiCiqBdeKm7HM17ZcUgJ" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>好文推荐: <a href="https://link.segmentfault.com/?enc=3zHmqFCNYLlH7NHyIrrcPA%3D%3D.5IDI4lS%2FE0BkYr9TkoI56dJtX7qZFXy87zWOsRU3w%2BJBgX%2BJF6OzUILSyF0wdvCl" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<p>文章目录结构:<br><img src="/img/remote/1460000014359887?w=689&h=449" alt="文章目录结构" title="文章目录结构"></p>
<h3>一、th属性</h3>
<h4>常用th属性解读</h4>
<p>html有的属性,Thymeleaf基本都有,而常用的属性大概有七八个。其中th属性执行的优先级从1~8,数字越低优先级越高。</p>
<p>一、<strong>th:text</strong> :设置当前元素的文本内容,相同功能的还有<strong>th:utext</strong>,两者的区别在于前者不会转义html标签,后者会。优先级不高:order=7</p>
<p>二、<strong>th:value</strong>:设置当前元素的value值,类似修改指定属性的还有<strong>th:src</strong>,<strong>th:href</strong>。优先级不高:order=6</p>
<p>三、<strong>th:each</strong>:遍历循环元素,和th:text或th:value一起使用。注意该属性修饰的标签位置,详细往后看。优先级很高:order=2</p>
<p>四、<strong>th:if</strong>:条件判断,类似的还有<strong>th:unless</strong>,<strong>th:switch</strong>,<strong>th:case</strong>。优先级较高:order=3</p>
<p>五、<strong>th:insert</strong>:代码块引入,类似的还有<strong>th:replace</strong>,<strong>th:include</strong>,三者的区别较大,若使用不恰当会破坏html结构,常用于公共代码块提取的场景。优先级最高:order=1</p>
<p>六、<strong>th:fragment</strong>:定义代码块,方便被th:insert引用。优先级最低:order=8</p>
<p>七、<strong>th:object</strong>:声明变量,一般和*{}一起配合使用,达到偷懒的效果。优先级一般:order=4</p>
<p>八、<strong>th:attr</strong>:修改任意属性,实际开发中用的较少,因为有丰富的其他th属性帮忙,类似的还有th:attrappend,th:attrprepend。优先级一般:order=5</p>
<h4>常用th属性使用</h4>
<p>使用Thymeleaf属性需要注意点以下五点:</p>
<p>一、若要使用Thymeleaf语法,首先要声明名称空间: <code>xmlns:th="http://www.thymeleaf.org"</code></p>
<p>二、设置文本内容 th:text,设置input的值 th:value,循环输出 th:each,条件判断 th:if,插入代码块 th:insert,定义代码块 th:fragment,声明变量 th:object</p>
<p>三、th:each 的用法需要格外注意,打个比方:如果你要循环一个div中的p标签,则th:each属性必须放在p标签上。若你将th:each属性放在div上,则循环的是将整个div。</p>
<p>四、变量表达式中提供了很多的内置方法,该内置方法是用#开头,请不要与#{}消息表达式弄混。</p>
<p>五、th:insert,th:replace,th:include 三种插入代码块的效果相似,但区别很大。</p>
<pre><code class="html"><!DOCTYPE html>
<!--名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Thymeleaf 语法</title>
</head>
<body>
<h2>ITDragon Thymeleaf 语法</h2>
<!--th:text 设置当前元素的文本内容,常用,优先级不高-->
<p th:text="${thText}" />
<p th:utext="${thUText}" />
<!--th:value 设置当前元素的value值,常用,优先级仅比th:text高-->
<input type="text" th:value="${thValue}" />
<!--th:each 遍历列表,常用,优先级很高,仅此于代码块的插入-->
<!--th:each 修饰在div上,则div层重复出现,若只想p标签遍历,则修饰在p标签上-->
<div th:each="message : ${thEach}"> <!-- 遍历整个div-p,不推荐-->
<p th:text="${message}" />
</div>
<div> <!--只遍历p,推荐使用-->
<p th:text="${message}" th:each="message : ${thEach}" />
</div>
<!--th:if 条件判断,类似的有th:switch,th:case,优先级仅次于th:each, 其中#strings是变量表达式的内置方法-->
<p th:text="${thIf}" th:if="${not #strings.isEmpty(thIf)}"></p>
<!--th:insert 把代码块插入当前div中,优先级最高,类似的有th:replace,th:include,~{} :代码块表达式 -->
<div th:insert="~{grammar/common::thCommon}"></div>
<!--th:object 声明变量,和*{} 一起使用-->
<div th:object="${thObject}">
<p>ID: <span th:text="*{id}" /></p><!--th:text="${thObject.id}"-->
<p>TH: <span th:text="*{thName}" /></p><!--${thObject.thName}-->
<p>DE: <span th:text="*{desc}" /></p><!--${thObject.desc}-->
</div>
</body>
</html></code></pre>
<p>后台给负责给变量赋值,和跳转页面。</p>
<pre><code class="java">import com.itdragon.entities.ThObject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Controller
public class ThymeleafController {
@RequestMapping("thymeleaf")
public String thymeleaf(ModelMap map) {
map.put("thText", "th:text 设置文本内容 <b>加粗</b>");
map.put("thUText", "th:utext 设置文本内容 <b>加粗</b>");
map.put("thValue", "thValue 设置当前元素的value值");
map.put("thEach", Arrays.asList("th:each", "遍历列表"));
map.put("thIf", "msg is not null");
map.put("thObject", new ThObject(1L, "th:object", "用来偷懒的th属性"));
return "grammar/thymeleaf";
}
}</code></pre>
<h3>二、标准表达式语法</h3>
<p><code>${...}</code> 变量表达式,Variable Expressions</p>
<p><code>@{...}</code> 链接表达式,Link URL Expressions</p>
<p><code>#{...}</code> 消息表达式,Message Expressions</p>
<p><code>~{...}</code> 代码块表达式,Fragment Expressions</p>
<p><code>*{...}</code> 选择变量表达式,Selection Variable Expressions</p>
<p>变量表达式使用频率最高,其功能也是非常的丰富。所以我们先从简单的代码块表达式开始,然后是消息表达式,再是链接表达式,最后是变量表达式,随带介绍选择变量表达式。</p>
<h4>~{...} 代码块表达式</h4>
<h5>支持两种语法结构</h5>
<p>推荐:<code>~{templatename::fragmentname}</code></p>
<p>支持:<code>~{templatename::#id}</code></p>
<p>templatename:模版名,Thymeleaf会根据模版名解析完整路径:/resources/templates/templatename.html,要注意文件的路径。</p>
<p>fragmentname:片段名,Thymeleaf通过th:fragment声明定义代码块,即:<code>th:fragment="fragmentname"</code></p>
<p>id:HTML的id选择器,使用时要在前面加上#号,不支持class选择器。</p>
<h5>代码块表达式的使用</h5>
<p>代码块表达式需要配合th属性(th:insert,th:replace,th:include)一起使用。</p>
<p><strong>th:insert</strong>:将代码块片段整个插入到使用了th:insert的HTML标签中,</p>
<p><strong>th:replace</strong>:将代码块片段整个替换使用了th:replace的HTML标签中,</p>
<p><strong>th:include</strong>:将代码块片段包含的内容插入到使用了th:include的HTML标签中,</p>
<p>用一个官方例子来区分三者的不同,第三部分会通过实战再次用到该知识。</p>
<pre><code class="html"><!--th:fragment定义代码块标识-->
<footer th:fragment="copy">
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
<!--三种不同的引入方式-->
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
<!--th:insert是在div中插入代码块,即多了一层div-->
<div>
<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
</div>
<!--th:replace是将代码块代替当前div,其html结构和之前一致-->
<footer>
&copy; 2011 The Good Thymes Virtual Grocery
</footer>
<!--th:include是将代码块footer的内容插入到div中,即少了一层footer-->
<div>
&copy; 2011 The Good Thymes Virtual Grocery
</div></code></pre>
<h4>{...} 消息表达式</h4>
<p>消息表达式一般用于国际化的场景。结构:<code>th:text="#{msg}"</code> 。会在第三部分的实战详细介绍。</p>
<h4>@{...} 链接表达式</h4>
<h5>链接表达式好处</h5>
<p>不管是静态资源的引用,form表单的请求,凡是链接都可以用<code>@{...}</code> 。这样可以动态获取项目路径,即便项目名变了,依然可以正常访问</p>
<pre><code class="properties">#修改项目名,链接表达式会自动修改路径,避免资源文件找不到
server.context-path=/itdragon</code></pre>
<h5>链接表达式结构</h5>
<p>无参:<code>@{/xxx}</code></p>
<p>有参:<code>@{/xxx(k1=v1,k2=v2)}</code> 对应url结构:<code>xxx?k1=v1&k2=v2</code></p>
<p>引入本地资源:<code>@{/项目本地的资源路径}</code></p>
<p>引入外部资源:<code>@{/webjars/资源在jar包中的路径}</code></p>
<p>列举:第三部分的实战引用会详细使用该表达式</p>
<pre><code class="html"><link th:href="@{/webjars/bootstrap/4.0.0/css/bootstrap.css}" rel="stylesheet">
<link th:href="@{/main/css/itdragon.css}" rel="stylesheet">
<form class="form-login" th:action="@{/user/login}" th:method="post" >
<a class="btn btn-sm" th:href="@{/login.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/login.html(l='en_US')}">English</a></code></pre>
<h4>${...}变量表达式</h4>
<p>变量表达式有丰富的内置方法,使其更强大,更方便。</p>
<h5>变量表达式功能</h5>
<p>一、可以获取对象的属性和方法</p>
<p>二、可以使用ctx,vars,locale,request,response,session,servletContext内置对象</p>
<p>三、可以使用dates,numbers,strings,objects,arrays,lists,sets,maps等内置方法(重点介绍)</p>
<h5>常用的内置对象</h5>
<p>一、<strong>ctx</strong> :上下文对象。</p>
<p>二、<strong>vars</strong> :上下文变量。</p>
<p>三、<strong>locale</strong>:上下文的语言环境。</p>
<p>四、<strong>request</strong>:(仅在web上下文)的 HttpServletRequest 对象。</p>
<p>五、<strong>response</strong>:(仅在web上下文)的 HttpServletResponse 对象。</p>
<p>六、<strong>session</strong>:(仅在web上下文)的 HttpSession 对象。</p>
<p>七、<strong>servletContext</strong>:(仅在web上下文)的 ServletContext 对象</p>
<p>这里以常用的Session举例,用户刊登成功后,会把用户信息放在Session中,Thymeleaf通过内置对象将值从session中获取。</p>
<pre><code class="java">// java 代码将用户名放在session中
session.setAttribute("userinfo",username);
// Thymeleaf通过内置对象直接获取
th:text="${session.userinfo}"</code></pre>
<h5>常用的内置方法</h5>
<p>一、<strong>strings</strong>:字符串格式化方法,常用的Java方法它都有。比如:equals,equalsIgnoreCase,length,trim,toUpperCase,toLowerCase,indexOf,substring,replace,startsWith,endsWith,contains,containsIgnoreCase等</p>
<p>二、<strong>numbers</strong>:数值格式化方法,常用的方法有:formatDecimal等</p>
<p>三、<strong>bools</strong>:布尔方法,常用的方法有:isTrue,isFalse等</p>
<p>四、<strong>arrays</strong>:数组方法,常用的方法有:toArray,length,isEmpty,contains,containsAll等</p>
<p>五、<strong>lists</strong>,<strong>sets</strong>:集合方法,常用的方法有:toList,size,isEmpty,contains,containsAll,sort等</p>
<p>六、<strong>maps</strong>:对象方法,常用的方法有:size,isEmpty,containsKey,containsValue等</p>
<p>七、<strong>dates</strong>:日期方法,常用的方法有:format,year,month,hour,createNow等</p>
<p>文章底部提供了对应的官网链接</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ITDragon Thymeleaf 内置方法</title>
</head>
<body>
<h2>ITDragon Thymeleaf 内置方法</h2>
<h3>#strings </h3>
<div th:if="${not #strings.isEmpty(itdragonStr)}" >
<p>Old Str : <span th:text="${itdragonStr}"/></p>
<p>toUpperCase : <span th:text="${#strings.toUpperCase(itdragonStr)}"/></p>
<p>toLowerCase : <span th:text="${#strings.toLowerCase(itdragonStr)}"/></p>
<p>equals : <span th:text="${#strings.equals(itdragonStr, 'itdragonblog')}"/></p>
<p>equalsIgnoreCase : <span th:text="${#strings.equalsIgnoreCase(itdragonStr, 'itdragonblog')}"/></p>
<p>indexOf : <span th:text="${#strings.indexOf(itdragonStr, 'r')}"/></p>
<p>substring : <span th:text="${#strings.substring(itdragonStr, 2, 8)}"/></p>
<p>replace : <span th:text="${#strings.replace(itdragonStr, 'it', 'IT')}"/></p>
<p>startsWith : <span th:text="${#strings.startsWith(itdragonStr, 'it')}"/></p>
<p>contains : <span th:text="${#strings.contains(itdragonStr, 'IT')}"/></p>
</div>
<h3>#numbers </h3>
<div>
<p>formatDecimal 整数部分随意,小数点后保留两位,四舍五入: <span th:text="${#numbers.formatDecimal(itdragonNum, 0, 2)}"/></p>
<p>formatDecimal 整数部分保留五位数,小数点后保留两位,四舍五入: <span th:text="${#numbers.formatDecimal(itdragonNum, 5, 2)}"/></p>
</div>
<h3>#bools </h3>
<div th:if="${#bools.isTrue(itdragonBool)}">
<p th:text="${itdragonBool}"></p>
</div>
<h3>#arrays </h3>
<div th:if="${not #arrays.isEmpty(itdragonArray)}">
<p>length : <span th:text="${#arrays.length(itdragonArray)}"/></p>
<p>contains : <span th:text="${#arrays.contains(itdragonArray, 5)}"/></p>
<p>containsAll : <span th:text="${#arrays.containsAll(itdragonArray, itdragonArray)}"/></p>
</div>
<h3>#lists </h3>
<div th:if="${not #lists.isEmpty(itdragonList)}">
<p>size : <span th:text="${#lists.size(itdragonList)}"/></p>
<p>contains : <span th:text="${#lists.contains(itdragonList, 0)}"/></p>
<p>sort : <span th:text="${#lists.sort(itdragonList)}"/></p>
</div>
<h3>#maps </h3>
<div th:if="${not #maps.isEmpty(itdragonMap)}">
<p>size : <span th:text="${#maps.size(itdragonMap)}"/></p>
<p>containsKey : <span th:text="${#maps.containsKey(itdragonMap, 'thName')}"/></p>
<p>containsValue : <span th:text="${#maps.containsValue(itdragonMap, '#maps')}"/></p>
</div>
<h3>#dates </h3>
<div>
<p>format : <span th:text="${#dates.format(itdragonDate)}"/></p>
<p>custom format : <span th:text="${#dates.format(itdragonDate, 'yyyy-MM-dd HH:mm:ss')}"/></p>
<p>day : <span th:text="${#dates.day(itdragonDate)}"/></p>
<p>month : <span th:text="${#dates.month(itdragonDate)}"/></p>
<p>monthName : <span th:text="${#dates.monthName(itdragonDate)}"/></p>
<p>year : <span th:text="${#dates.year(itdragonDate)}"/></p>
<p>dayOfWeekName : <span th:text="${#dates.dayOfWeekName(itdragonDate)}"/></p>
<p>hour : <span th:text="${#dates.hour(itdragonDate)}"/></p>
<p>minute : <span th:text="${#dates.minute(itdragonDate)}"/></p>
<p>second : <span th:text="${#dates.second(itdragonDate)}"/></p>
<p>createNow : <span th:text="${#dates.createNow()}"/></p>
</div>
</body>
</html></code></pre>
<p>后台给负责给变量赋值,和跳转页面。</p>
<pre><code class="java">@RequestMapping("varexpressions")
public String varexpressions(ModelMap map) {
map.put("itdragonStr", "itdragonBlog");
map.put("itdragonBool", true);
map.put("itdragonArray", new Integer[]{1,2,3,4});
map.put("itdragonList", Arrays.asList(1,3,2,4,0));
Map itdragonMap = new HashMap();
itdragonMap.put("thName", "${#...}");
itdragonMap.put("desc", "变量表达式内置方法");
map.put("itdragonMap", itdragonMap);
map.put("itdragonDate", new Date());
map.put("itdragonNum", 888.888D);
return "grammar/varexpressions";
}</code></pre>
<h3>三、Thymeleaf在SpringBoot应用</h3>
<p>Thymeleaf是Spring Boot 官方推荐使用的模版引擎,这也意味着用Thymeleaf比其他模版引擎更简单。</p>
<p>开发步骤:</p>
<p>第一步:引入Thymeleaf依赖。</p>
<p>第二步: 提取公共页面,提高代码的重用性,统一页面风格。</p>
<p>第三步:页面显示和国际化功能</p>
<h4>引入Thymeleaf</h4>
<p>pom.xml 引入Thymeleaf的依赖,并确定其版本</p>
<pre><code class="xml"><properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
</properties>
<dependencies>
<dependency> <!--引入模版引擎thymeleaf-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies></code></pre>
<h4>提取公共页面</h4>
<p>为了统一页面风格,提高页面的复用率,我们一般都会提取公共页面。之前在文章中介绍了SiteMesh的使用,今天用Thymeleaf来实现。</p>
<p>统一规范引入的资源文件</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="common-head">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, width=device-width, user-scalable=no" />
<title>ITDragon系统</title>
<link type="image/x-icon" href="images/favicon.ico" rel="shortcut icon">
<link th:href="@{/sb-admin-1.0.4/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/sb-admin-1.0.4/css/sb-admin.css}" rel="stylesheet">
<link th:href="@{/sb-admin-1.0.4/css/plugins/morris.css}" rel="stylesheet">
<link th:href="@{/sb-admin-1.0.4/font-awesome/css/font-awesome.min.css}" rel="stylesheet">
<script th:src="@{/sb-admin-1.0.4/js/jquery.js}"></script>
<script th:src="@{/sb-admin-1.0.4/js/bootstrap.min.js}"></script>
<script th:src="@{/sb-admin-1.0.4/js/plugins/morris/raphael.min.js}"></script>
<script th:src="@{/sb-admin-1.0.4/js/plugins/morris/morris.min.js}"></script>
<script th:src="@{/sb-admin-1.0.4/js/plugins/morris/morris-data.js}"></script>
</head>
</html></code></pre>
<p>统一左侧菜单栏</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<header id="header" th:fragment="common-header">
<!-- Navigation -->
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/dashboard">ITDragon sb-admin-1.0.4</a>
</div>
<!--和知识点没关系的代码,这里就补贴出来了,完整代码请异步github-->
<!-- Sidebar Menu Items - These collapse to the responsive navigation menu on small screens -->
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav side-nav itdragon-nav">
<li th:class="${activeUrl=='dashboard'?'nav-link active':'nav-link'}">
<a th:href="@{/dashboard}"><i class="fa fa-fw fa-dashboard"></i> Dashboard</a>
</li>
<li th:class="${activeUrl=='employees'?'nav-link active':'nav-link'}">
<a th:href="@{/employees}"><i class="fa fa-fw fa-bar-chart-o"></i> Employees</a>
</li>
</ul>
</div>
<!-- /.navbar-collapse -->
</nav>
</header>
</body>
</html></code></pre>
<p>以上代码用到了三个知识点:</p>
<p>一、使用链接表达式引入本地的资源文件,若引入第三方外部资源文件,可以通过webjars(将资源文件打成jar包放在项目中)方式引入。</p>
<p>二、使用th:fragment标识需要被引用的代码块,也可以用id选择器但不推荐。</p>
<p>三、activeUrl变量是通过代码块表达式在其他页面传入的变量,这也是代码块表达式强大的一个功能。</p>
<h4>页面显示和国际化</h4>
<p>Spring Boot 实现国际化步骤:</p>
<p>第一步:准备好国际化文件,至少三分(系统默认,中文,英文)</p>
<p>第二步:在Spring Boot全局配置文件中,指定国际化文件路径,</p>
<p>第三步:自定义Locale Resolver</p>
<p>第四步:在页面上使用消息表达式输出国际化内容</p>
<p>这里只贴出第四步的代码,前三步以及完整代码请异步github</p>
<pre><code class="html"><!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{commons/default::common-head}">
<meta name="viewport" content="initial-scale=1.0, width=device-width, user-scalable=no" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1" />
<meta http-equiv="X-UA-Compatible" content="IE=8" />
<title>员工列表</title>
</head>
<body>
<div id="wrapper">
<header th:replace="~{commons/header::common-header(activeUrl='employees')}"></header>
<div id="page-wrapper">
<div class="container-fluid">
<!-- Page Heading -->
<div class="row">
<div class="col-lg-12">
<h1 class="page-header" th:text="#{employees}"></h1>
<ol class="breadcrumb">
<li><i class="fa fa-dashboard"></i> <a th:href="@{/dashboard}" th:text="#{dashboard}"></a>
</li>
<li class="active"><i class="fa fa-table"></i> <span th:text="#{employees}"></span></li>
</ol>
</div>
</div>
<div class="row" th:if="${not #strings.isEmpty(message)}">
<div class="col-lg-12">
<div class="alert alert-info alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<i class="fa fa-info-circle"></i> <strong th:text="${message}"></strong>
</div>
</div>
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<h2 th:text="#{employees}"></h2>
<div class="table-responsive">
<a class="pull-right btn" th:href="@{/employee}" th:text="#{add.employees}"></a>
<table class="table table-striped table-sm">
<thead>
<tr>
<th th:text="#{id}"></th>
<th th:text="#{last.name}"></th>
<th th:text="#{email}"></th>
<th th:text="#{gender}"></th>
<th th:text="#{department}"></th>
<th th:text="#{position}"></th>
<th th:text="#{birth}"></th>
<th th:text="#{operation}"></th>
</tr>
</thead>
<tbody>
<tr th:each="emp:${employees}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender}==0?#{female}:#{male}"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${emp.department.position}"></td>
<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
<td>
<a class="btn btn-sm btn-success" th:href="@{/employee/}+${emp.id}" th:text="#{edit}"></a>
<a class="btn btn-sm btn-danger deleteBtn" th:href="@{/employee/}+${emp.id}" th:text="#{delete}"></a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- /.row -->
</div>
</div>
</div>
</body>
</html></code></pre>
<p>以上代码用到了知识点:</p>
<p>一、页面通过代码块表达式引入公共代码块,并传入参数给公共代码块文件。</p>
<p>二、页面使用消息表达式输出国际化内容。</p>
<p>三、使用th:if属性条件判断,使用内置方法#strings.isEmpty 判断参数是否为空,使用内置方法#dates.format 格式化参数</p>
<p>四、使用th:each属性循环遍历,注意该属性修改在tr标签上。并使用th:text属性给td标签赋值</p>
<h3>四、总结</h3>
<p><strong>一、Thymeleaf 是Spring Boot 官方推荐的Java模版引擎框架,其文件扩展名为.html</strong></p>
<p><strong>二、Thymeleaf 几乎支持所有的html属性,用于赋值的th:text和th:value,用于循环遍历的th:each,用于条件判断的th:if</strong></p>
<p><strong>三、Thymeleaf 提供四种标准的表达式,有丰富内置方法的${},用于国际化的#{},用于代码插入的~{},用于处理链接的@{}</strong></p>
<p><strong>四、一定要注意循环遍历的th:each和代码插入的th:insert用法,尽量避免破坏html结构的细节问题</strong></p>
<p>参考文献:</p>
<p><a href="https://link.segmentfault.com/?enc=rNctu5%2B7tiHlzKfkYoPpVA%3D%3D.rdBGKRjXuunxwb%2FmT%2BvCtKtFBxi%2Fqfhp7l6sHUg3WRxNf4u1ZLiDzKLaMtZcIrg7529Coj0ltQJzgo6tZsypxRy08131qO9g140WsONGa5uE%2BdEyhQZlDN7mWokINCswRtYAhP%2BbkwVruwqxK%2Bteog%3D%3D" rel="nofollow">https://www.thymeleaf.org/doc...</a></p>
<p><a href="https://link.segmentfault.com/?enc=J7aiRgZqsbnBZvs00PArzw%3D%3D.4KChoNqlPZ1uD%2FyCdkHFTk%2Fp%2FD40lnISxUQvDfyaZx4TyK6kMZcR6SoF3ELnANXULpXCqTQXSq93JiNNEtnQhLuSH99yrYJnOea%2B8Lfk1MWa7i4Pwwlhy0AznCA7OQmWXc0pWFP%2FVzgh6mYVPRw%2BDQ%3D%3D" rel="nofollow">https://www.thymeleaf.org/doc...</a></p>
<p>文章到这里就结束了。如果文章对你有帮助,可以点个"推荐",也可以"关注"我,获得更多丰富的知识。</p>
<p>这里是博客文章目录一栏表中的部分内容,如果有感兴趣的内容可以点击右边的链接: <a href="https://link.segmentfault.com/?enc=vrpHWBqAyhaM4r8LuPqNzg%3D%3D.uartrDK01sO4ePZSSJJDo52wJ%2Bnsh3ORWxxr7xlfMeWDVBnRADU32ZC5bh2v4PcM" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<p><img src="/img/remote/1460000014206903?w=958&h=595" alt="文章目录" title="文章目录"></p>
Spring Boot配置文件详解
https://segmentfault.com/a/1190000014206897
2018-04-06T09:02:42+08:00
2018-04-06T09:02:42+08:00
itdragon
https://segmentfault.com/u/itdragon
4
<h2>Spring Boot配置文件详解</h2>
<p>Spring Boot提供了两种常用的配置文件,分别是properties文件和yml文件。他们的作用都是修改Spring Boot自动配置的默认值。相对于properties文件而言,yml文件更年轻,也有很多的坑。可谓成也萧何败也萧何,yml通过空格来确定层级关系,是配置文件结构更清晰,但也会因为微不足道的空格而破坏了层级关系。本章重点介绍yml的语法和从配置文件中取值。还在等什么,赶快来学习吧!</p>
<p>技术:yaml、properties语法,ConfigurationProperties和Value注解的使用,配置文件占位符的使用</p>
<p>说明:本章重点介绍yaml的语法和ConfigurationProperties注解的使用,测试代码和完整代码请移步github,喜欢的朋友可以点个star。</p>
<p>源码:<a href="https://link.segmentfault.com/?enc=VNBSWZNBbkgpOj140Q3Tvg%3D%3D.d5w14gn9SPFNeMOhqUQiuzqVHkwi94fFrY5pwSTkvHgDbZG6dqeHcaLLKhV1c3HjXwIQF4z8EtPQDf%2Bz4%2FYnfMhG%2F4b17e906NRf7pGQclY%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>文章目录结构:<br><img src="/img/remote/1460000014206902?w=918&h=488" alt="" title=""></p>
<h3>一、YAML简介</h3>
<blockquote>yml是YAML(YAML Ain't Markup Language)语言的文件,以数据为中心,比json、xml等更适合做配置文件</blockquote>
<p>yml和xml相比,少了一些结构化的代码,使数据更直接,一目了然。</p>
<p>yml和json呢?没有谁好谁坏,合适才是最好的。yml的语法比json优雅,注释更标准,适合做配置文件。json作为一种机器交换格式比yml强,更适合做api调用的数据交换。</p>
<h4>一)YAML语法</h4>
<p>以空格的缩进程度来控制层级关系。空格的个数并不重要,只要左边空格对齐则视为同一个层级。注意不能用tab代替空格。且大小写敏感。支持字面值,对象,数组三种数据结构,也支持复合结构。</p>
<p><strong>字面值</strong>:字符串,布尔类型,数值,日期。字符串默认不加引号,单引号会转义特殊字符。日期格式支持yyyy/MM/dd HH:mm:ss</p>
<p><strong>对象</strong>:由键值对组成,形如 <strong>key:(空格)value</strong> 的数据组成。冒号后面的空格是必须要有的,每组键值对占用一行,且缩进的程度要一致,也可以使用行内写法:{k1: v1, ....kn: vn}</p>
<p><strong>数组</strong>:由形如 <strong>-(空格)value</strong> 的数据组成。短横线后面的空格是必须要有的,每组数据占用一行,且缩进的程度要一致,也可以使用行内写法: [1,2,...n]</p>
<p><strong>复合结构</strong>:上面三种数据结构任意组合</p>
<h4>二)YAML的运用</h4>
<p>创建一个Spring Boot 的全局配置文件 application.yml,配置属性参数。主要有字符串,带特殊字符的字符串,布尔类型,数值,集合,行内集合,行内对象,集合对象这几种常用的数据格式。</p>
<pre><code class="yaml">yaml:
str: 字符串可以不加引号
specialStr: "双引号直接输出\n特殊字符"
specialStr2: '单引号可以转义\n特殊字符'
flag: false
num: 666
Dnum: 88.88
list:
- one
- two
- two
set: [1,2,2,3]
map: {k1: v1, k2: v2}
positions:
- name: ITDragon
salary: 15000.00
- name: ITDragonBlog
salary: 18888.88</code></pre>
<p>创建实体类YamlEntity.java 获取配置文件中的属性值,通过注解@ConfigurationProperties获取配置文件中的指定值并注入到实体类中。其具体的测试方法和获取值的原理,请继续往后看!</p>
<pre><code class="java">import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* YAML 语法实体类
* 切记点:
* 一、冒号后面加空格,即 key:(空格)value
* 二、每行参数左边空格数量决定了该参数的层级,不可乱输入。
*/
@Component
@ConfigurationProperties(prefix = "yaml")
public class YamlEntity {
// 字面值,字符串,布尔,数值
private String str; // 普通字符串
private String specialStr; // 转义特殊字符串
private String specialStr2;// 输出特殊字符串
private Boolean flag; // 布尔类型
private Integer num; // 整数
private Double dNum; // 小数
// 数组,List和Set,两种写法: 第一种:-空格value,每个值占一行,需缩进对齐;第二种:[1,2,...n] 行内写法
private List<Object> list; // list可重复集合
private Set<Object> set; // set不可重复集合
// Map和实体类,两种写法:第一种:key空格value,每个值占一行,需缩进对齐;第二种:{key: value,....} 行内写法
private Map<String, Object> map; // Map K-V
private List<Position> positions; // 复合结构,集合对象
// 省略getter,setter,toString方法
}</code></pre>
<h4>三)YML小结</h4>
<p><strong>一、字符串可以不加引号,若加双引号则输出特殊字符,若不加或加单引号则转义特殊字符;</strong></p>
<p><strong>二、数组类型,短横线后面要有空格;对象类型,冒号后面要有空格;</strong></p>
<p><strong>三、YAML是以空格缩进的程度来控制层级关系,但不能用tab键代替空格,大小写敏感;</strong></p>
<p><strong>四、如何让一个程序员崩溃?在yml文件中加几个空格!(〃>皿<)</strong></p>
<h3>二、Properties简介</h3>
<p>properties文件大家经常用,这里就简单介绍一下。其语法结构形如:key=value。注意中文乱码问题,需要转码成ASCII。具体如下所示:</p>
<pre><code class="prop">userinfo.account=itdragonBlog
userinfo.age=25
userinfo.active=true
userinfo.created-date=2018/03/31 16:54:30
userinfo.map.k1=v1
userinfo.map.k2=v2
userinfo.list=one,two,three
userinfo.position.name=Java架构师
userinfo.position.salary=19999.99</code></pre>
<p>从配置文件中取值注入到实体类中,和YAML是一样的。</p>
<pre><code class="java">import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 用户信息
* @ConfigurationProperties : 被修饰类中的所有属性会和配置文件中的指定值(该值通过prefix找到)进行绑定
*/
@Component
@ConfigurationProperties(prefix = "userinfo")
public class UserInfo {
private String account;
private Integer age;
private Boolean active;
private Date createdDate;
private Map<String, Object> map;
private List<Object> list;
private Position position;
// 省略getter,setter,toString方法
}</code></pre>
<h3>三、配置文件取值</h3>
<p>Spring Boot通过ConfigurationProperties注解从配置文件中获取属性。从上面的例子可以看出ConfigurationProperties注解可以通过设置prefix指定需要批量导入的数据。支持获取字面值,集合,Map,对象等复杂数据。ConfigurationProperties注解还有其他特么呢?它和Spring的Value注解又有什么区别呢?带着这些问题,我们继续往下看。(๑•̀ㅂ•́)و✧</p>
<h4>一)ConfigurationProperties和Value优缺点</h4>
<p>ConfigurationProperties注解的优缺点</p>
<p>一、可以从配置文件中批量注入属性;</p>
<p>二、支持获取复杂的数据类型;</p>
<p>三、对属性名匹配的要求较低,比如user-name,user_name,userName,USER_NAME都可以取值;</p>
<p>四、支持JAVA的JSR303数据校验;</p>
<p>五、缺点是不支持强大的SpEL表达式;</p>
<p>Value注解的优缺点正好相反,它只能一个个配置注入值;不支持数组、集合等复杂的数据类型;不支持数据校验;对属性名匹配有严格的要求。最大的特点是支持SpEL表达式,使其拥有更丰富的功能。</p>
<h4>二)@ConfigurationProperties详解</h4>
<p>第一步:导入依赖。若要使用ConfigurationProperties注解,需要导入依赖 spring-boot-configuration-processor;</p>
<p>第二步:配置数据。在application.yml配置文件中,配置属性参数,其前缀为itdragon,参数有字面值和数组,用来判断是否支持获取复杂属性的能力;</p>
<p>第三步:匹配数据。在类上添加注解ConfigurationProperties,并设置prefix属性值为itdragon。并把该类添加到Spring的IOC容器中。</p>
<p>第四步:校验数据。添加数据校验Validated注解,开启数据校验,测试其是否支持数据校验的功能;</p>
<p>第五步:测试ConfigurationProperties注解是否支持SpEL表达式;</p>
<p>导入依赖:pom.xml 添加 spring-boot-configuration-processor依赖</p>
<pre><code class="xml"><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency></code></pre>
<p>配置数据:application.yml 配置属性参数,nick-name是用来判断匹配属性的松散性,若换成nick_name依然可以获取值。</p>
<pre><code class="yaml">itdragon:
nick-name: ITDragonBlog
email: 1234567890@qq.com
iphone: 1234567890
abilities: [java, sql, html]
created_date: 2018/03/31 15:27:30</code></pre>
<p>匹配和校验数据:</p>
<pre><code class="java">import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Email;
import java.util.Date;
import java.util.List;
/**
* ConfigurationProperties 注解语法类
* 第一步:导入依赖 spring-boot-configuration-processor;
* 第二步:把ConfigurationProperties注解修饰的类添加到Spring的IOC容器中;
* 第三步:设置prefix属性,指定需要注入属性的前缀;
* 第四步:添加数据校验注解,开启数据校验;
*
* 注意点:
* 一、nickName和createdDate在yml配置文件中,对应参数分别是中划线和下划线,用于测试其对属性名匹配的松散性
* 二、email和iphone 测试其支持JSR303数据校验
* 三、abilities 测试其支持复杂的数据结构
*/
@Component
@ConfigurationProperties(prefix = "itdragon")
@Validated
public class ConfigurationPropertiesEntity {
private String nickName; // 解析成功,支持松散匹配属性
private String email;
// @Email // 解析失败,数据校验成功:BindValidationException: Binding validation errors on itdragon
private String iphone;
private List<String> abilities;
private Date createdDate; // 解析成功,支持松散匹配属性
// @ConfigurationProperties("#{(1+2-3)/4*5}")
private String operator; // 语法报错,不支持SpEL表达式:not applicable to field
// 省略getter,setter,toString方法
}</code></pre>
<h4>三)@Value详解</h4>
<p><a href="https://link.segmentfault.com/?enc=LxMz49m3wkNVGbNSttIFYQ%3D%3D.%2F0tsJoeO77pVKyU%2FKl3QsNo3NLE7pyNUPzCyyrpeA%2BS6HQSW3%2BMXKHlyx8zsYTjs" rel="nofollow">上一篇博客</a>已经介绍过Value注解的使用,这里只简单说明。</p>
<p>第一步:在属性上添加Value注解,通过${}设置参数从配置文件中注入值;</p>
<p>第二步:修改<code>${itdragon.ceatred_date}</code>中的参数值,改为<code>${itdragon.ceatredDate}</code>测试是否能解析成功;</p>
<p>第三步:添加数据校验Validated注解,开启数据校验,测试其是否支持数据校验的功能;</p>
<p>第四步:测试Value注解是否支持SpEL表达式;</p>
<pre><code class="java">import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Email;
import java.util.Date;
import java.util.List;
/**
* Value 注解语法类
* 第一步:在属性上添加注解Value注入参数
* 第二步:把Value注解修饰的类添加到Spring的IOC容器中;
* 第三步:添加数据校验注解,检查是否支持数据校验;
*
* 注意点:
* 一、nickName和createdDate在yml配置文件中,对应参数分别是中划线和下划线,用于测试其对属性名匹配的松散性
* 二、email和iphone 测试其支持JSR303数据校验
* 三、abilities 测试其支持复杂的数据结构
*
* 结论:
* 一、createDate取值必须和yml配置文件中的参数保持一致,
* 二、既是在iphone上添加邮箱验证注解依然可以通过测试,
* 三、不支持复杂的数据结构,提示错误和第一条相同:IllegalArgumentException: Could not resolve placeholder 'itdragon.abilities' in value "${itdragon.abilities}"
*/
@Component
@Validated
public class ValueEntity {
@Value("${itdragon.nick-name}")
private String nickName;
@Value("${itdragon.email}")
private String email;
@Email
@Value("${itdragon.iphone}") // 解析成功,并不支持数据校验
private String iphone;
// @Value("${itdragon.abilities}") // 解析错误,并不支持复杂的数据结构
private List<String> abilities;
// @Value("${itdragon.ceatredDate}") // 解析错误,并不支持松散匹配属性,必须严格一致
private Date createdDate;
// Value注解的强大一面:支持SpEL表达式
@Value("#{(1+2-3)/4*5}") // 算术运算
private String operator;
@Value("#{1>2 || 2 <= 3}") // 关系运算
private Boolean comparison;
@Value("#{systemProperties['java.version']}") // 系统配置:os.name
private String systemProperties;
@Value("#{T(java.lang.Math).abs(-18)}") // 表达式
private String mapExpression;
// 省略getter,setter,toString方法
}</code></pre>
<h4>四)配置文件取值小结</h4>
<p><strong>一、ConfigurationProperties注解支持批量注入,而Value注解适合单个注入;</strong></p>
<p><strong>二、ConfigurationProperties注解支持数据校验,而Value注解不支持;</strong></p>
<p><strong>三、ConfigurationProperties注解支持松散匹配属性,而Value注解必须严格匹配属性;</strong></p>
<p><strong>四、ConfigurationProperties不支持强大的SpEL表达式,而Value支持;</strong></p>
<h3>四、配置文件占位符</h3>
<p>占位符和随机数比较简单,这里就直接贴出代码。需要注意的是:</p>
<p>一、占位符的值必须是完整路径</p>
<p>二、占位符设置默认值,冒号后面不能有空格</p>
<pre><code class="yaml">ran: # 这里的prefix不能是random,
ran-value: ${random.value}
ran-int: ${random.int}
ran-long: ${random.long}
ran-int-num: ${random.int(10)}
ran-int-range: ${random.int[10,20]}
ran-placeholder: placeholder_${ran.ran-value:此处不能有空格,且key为完整路径}</code></pre>
<pre><code class="java">import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 随机数和占位符语法类
*/
@Component
@ConfigurationProperties(prefix = "ran")
public class RandomEntity {
private String ranValue; // 随机生成一个字符串
private Integer ranInt; // 随机生成一个整数
private Long ranLong; // 随机生成一个长整数
private Integer ranIntNum; // 在指定范围内随机生成一个整数
private Integer ranIntRange;// 在指定区间内随机生成一个整数
private String ranPlaceholder;// 占位符
// 省略getter,setter,toString方法e
}</code></pre>
<p>测试代码:</p>
<pre><code class="java">@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootYmlApplicationTests {
@Autowired
private UserInfo userInfo;
@Autowired
private YamlEntity yamlEntity;
@Autowired
private ConfigurationPropertiesEntity configurationPropertiesEntity;
@Autowired
private ValueEntity valueEntity;
@Autowired
private RandomEntity randomEntity;
@Test
public void contextLoads() {
// System.out.println("YAML Grammar : " + yamlEntity);
// System.out.println("UserInfo : " + userInfo);
// System.out.println("ConfigurationProperties Grammar : " + configurationPropertiesEntity);
// System.out.println("Value Grammar : " + valueEntity);
System.out.println("Random Grammar : " + randomEntity);
}
}</code></pre>
<h3>五、总结</h3>
<p><strong>一、Spring Boot 支持两种格式的配置文件,其中YAML的数据结构比properties更清晰。</strong></p>
<p><strong>二、YAML 是专门用来写配置文件的语言,非常简洁和强大。</strong></p>
<p><strong>三、YAML 对空格的要求很严格,且不能用Tab键代替。</strong></p>
<p><strong>四、YAML 通过空格缩进的程度确定层级,冒号后面有空格,短横线后面有空格。</strong></p>
<p><strong>五、ConfigurationProperties注解适合批量注入配置文件中的属性,Value注解适合获取配置文件中的某一项。</strong></p>
<p><strong>六、ConfigurationProperties注解支持数据校验和获取复杂的数据,Value注解支持SpEL表达式。</strong></p>
<p>文章到这里就结束了。如果文章对你有帮助,可以点个"<strong>推荐</strong>",也可以"<strong>关注</strong>"我,获得更多丰富的知识。</p>
<p>这里是博客文章目录一栏表中的部分内容,如果有感兴趣的内容可以点击右边的链接: <a href="https://link.segmentfault.com/?enc=5M8U1LPRgh7Ygfvil9nXmw%3D%3D.uSMlPQVmmiMez%2BdVEk5dids4FHakd3tQpKLOUw2pJceaVSn6XE9W4DA2UU7V7tPV" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<p><img src="/img/remote/1460000014206903?w=958&h=595" alt="文章目录" title="文章目录"></p>
Java编程配置思路详解
https://segmentfault.com/a/1190000014080358
2018-03-30T09:18:30+08:00
2018-03-30T09:18:30+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Java编程配置思路详解</h2>
<p>SpringBoot虽然提供了很多优秀的starter帮助我们快速开发,可实际生产环境的特殊性,我们依然需要对默认整合配置做自定义操作,提高程序的可控性,虽然你配的不一定比官方提供的starter好。上周因为工作和装修的事情,导致博客没有正常更新,害怕停更会让人懒惰起来,挤了一点时间写了一篇内容比较简单的文章。后面闲谈一下我是如何从装修小白到入门的经历。</p>
<p><strong>技术</strong>:Configuration,ComponentScan,PropertySource,EnableTransactionManagement,Bean,Value<br><strong>说明</strong>:文中只贴出了配置代码,完整的测试代码在github上。<br><strong>源码</strong>:<a href="https://link.segmentfault.com/?enc=hKHJpI8iUlw35WzV0Cl16g%3D%3D.Ada75I%2FQvRZjQnWPUsWqNnmb2WkiG6qUPTig%2FZUgGTN0Co21gtQu3ummmUVKMEXLqIrxITmxi03atI9C1lA6mtYV3GBF1GKSwwZrRz4AW1FOs1P6pzAVS7hBm8BETvLP" rel="nofollow">https://github.com/ITDragonBl...</a><br>文章目录结构:</p>
<p><img src="/img/remote/1460000014080363" alt="文章目录结构" title="文章目录结构"></p>
<h3>一、Java编程配置</h3>
<p>在Spring4.x之前,应用的基本配置中一般使用xml配置的方式,而在业务逻辑中建议使用注解的方式。可在Spring4.x以后,官方便开始推荐使用Java的编程配置来代替xml配置,这又是为什么?这两种配置又有什么优缺点呢?</p>
<h4>Java编程配置和xml配置</h4>
<p>xml配置优点:对于我们这些老一辈的程序员来说(┬_┬),xml非常亲切,使用简单,容易扩展,修改应用配置参数不需要重新编译。</p>
<p>xml配置缺点:配置文件的读取和解析需要耗时,xml配置文件内容太多会显得很臃肿,不方便管理。</p>
<p>Java编程配置优点:相对于xml配置而言,其结构更清晰,可读性更高,同时也节省了解析xml耗时。</p>
<p>Java编程配置缺点:修改应用配置参数需要重新编译。其实并不是一个大的问题,实际生成环境中,应用配置完成后一般都不会也不敢去随意修改。</p>
<p>两者各有千秋,考虑到Spring4.x和SpringBoot都在推荐使用Java编程配置的方式,那我们也应该顺应时代潮流,你可以不用,但你应该要懂!</p>
<h4>Java编程配置步骤</h4>
<p>第一步:创建配置类,在类名上添加注解Configuration,告知Spring这是一个配置类,其作用类似xml文件</p>
<p>第二步:加载外部配置文件,在类名上添加注解PropertySource,指定properties文件的读取路径</p>
<p>第三步:获取应用配置属性值,在属性变量上添加注解Value,通过${}表达式获取配置文件中参数</p>
<p>第四步:依赖注入,在方法上添加Bean注解,也可以用FactoryBean</p>
<p>第一步和第四步的语法,文章第二部分会详细介绍。第二步和第三步的语法,文章第三部分会详细介绍。</p>
<pre><code class="java">import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.mchange.v2.c3p0.ComboPooledDataSource;
/**
* Spring 配置类
* 配置数据源,事务管理,bean,自动扫描包
* @author itdragon
*/
@Configuration // 声明该类为配置类
@PropertySource({"classpath:propertySource.properties"}) // 引入外部文件
@ComponentScan("com.itdragon") // 配置自动扫描包的路径
@EnableTransactionManagement // 开启基于注解的事务管理功能
public class ApplicationContextConfig {
@Value("${DATA_USER}")
private String DATA_USER;
@Value("${DATA_PAWD}")
private String DATA_PAWD;
@Value("${DATA_DRIVER}")
private String DATA_DRIVER;
@Value("${DATA_JDBC_URL}")
private String DATA_JDBC_URL;
@Bean // 数据源
public DataSource dataSource() throws Exception{
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(DATA_USER);
dataSource.setPassword(DATA_PAWD);
dataSource.setDriverClass(DATA_DRIVER);
dataSource.setJdbcUrl(DATA_JDBC_URL);
return dataSource;
}
@Bean // jdbc模板
public JdbcTemplate jdbcTemplate() throws Exception{
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource());
return jdbcTemplate;
}
@Bean // 事务管理
public PlatformTransactionManager transactionManager() throws Exception{
return new DataSourceTransactionManager(dataSource());
}
}</code></pre>
<p>事务类</p>
<pre><code class="java">import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.itdragon.dao.ITDragonDao;
@Service
public class ITDragonServer {
@Autowired(required=false)
private ITDragonDao itdragonDao;
public List<Map<String,Object>> findAll() {
return itdragonDao.findAll();
}
@Transactional
public void updateNameById(String name, Long id) {
itdragonDao.updateUserNameById(name, id);
System.out.println(0/0); // 事务异常
}
}</code></pre>
<p>完整代码,请异步github</p>
<h3>二、组件注入</h3>
<p>Bean注解类似xml文件中的<code><bean></code>标签,其中被Bean注解修饰的方法名对应<code><bean></code>标签中的id,也可以通过Bean注解的value属性设置id的值。在SpringBoot底层代码中被大量使用。</p>
<p>若希望容器启动后创建对象,并在使用后从容器中直接获取,则什么都不需要做,因为Spring默认是单实例,即容器启动后创建对象,并保存到容器中,使用时再从容器中获取。</p>
<p>若希望容器启动后不创建对象,而是在使用时再创建,继而保存到容器中,下次使用再从容器中获取,可以通过懒加载的方式实现,即使用Lazy注解修饰。</p>
<p>若希望容器启动后不创建对象,而是在每次使用时创建,则采用多实例的方式,即使用Scope注解,参数的值为prototype,即@Scope("prototype") 。</p>
<p>若希望容器启动后根据条件选择需要注入的Bean,可以使用注解Conditional判断,SpringBoot的底层打量使用了该注解。</p>
<pre><code class="java">import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Controller;
import com.itdragon.entity.ITDragonEntity;
import com.itdragon.server.ITDragonServer;
/**
* 知识点二:配置自动扫描包路径
* 一、注解ComponentScan的value值设置自动扫描包的路径
* 二、注解ComponentScan的excludeFilters值设置扫描排除的规则
* 1)、通过注解@Filter设置排除的类型,type=ANNOTATION表示按照注解包含排除。classes是具体的注解,如Controller,Server,Repository
* 三、注解ComponentScan的includeFilters值设置扫描加入的规则
* 1)、通过设置useDefaultFilters=false关闭Spring默认扫描全部的功能,使includeFilters生效
*
* 知识点三:@Filter常用的拦截类型
* 一、FilterType.ANNOTATION:按照注解
* 二、FilterType.ASSIGNABLE_TYPE:按照给定的类型,包括其子类和实现类
* 三、FilterType.CUSTOM:使用自定义规则
*
* 第一个ComponentScan注解表示在指定包下不扫描通过Controller注解修饰的类和ITDragonServer类及其子类和实现类
* 第二个ComponentScan注解表示在指定包下只扫描通过Controller注解修饰的类
* 第三个ComponentScan注解表示在指定包下根据自定义拦截规则,不扫描满足规则的类
*/
@Configuration
@ComponentScan(value="com.itdragon",excludeFilters={@Filter(type=FilterType.ANNOTATION,classes={Controller.class}),
@Filter(type=FilterType.ASSIGNABLE_TYPE,classes={ITDragonServer.class})})
//@ComponentScan(value="com.itdragon",includeFilters={@Filter(type=FilterType.ANNOTATION,classes={Controller.class})},useDefaultFilters=false)
//@ComponentScan(value="com.itdragon",excludeFilters={@Filter(type=FilterType.CUSTOM,classes={ITDragonCustomTypeFilter.class})})
public class ITDragonConfig {
/**
* 知识点一:配置bean
* 一、注解Bean的value值表示bean的id
* 二、注解Bean的value值未设置,则方法名表示bean的id
*/
@Bean(value="itdragonBean")
public ITDragonEntity itdragonEntity() { //方法名很重要,类似xml的id名,也可以通过@bean的value值重定义
return new ITDragonEntity("itdragon", "configuration-password", 25);
}
/**
* 知识点四:Scope属性
* @Scope,调整作用域,默认单实例
* singleton:单实例:ioc容器启动后创建对象放到ioc容器中,需要是从容器中获取。
* prototype:多实例:ioc容器启动后每次获取对象时都要创建对象。
* request:同一次请求创建一个实例
* session:同一个session创建一个实例
*
* 知识点五:懒加载
* 针对单实例而言,在容器启动后不创建对象,在第一次使用Bean时创建对象。可以理解为单实例的一种补充。
*
*/
// @Scope("prototype")
@Lazy
@Bean
public ITDragonEntity scopeTopicBean() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^Create Bean");
return new ITDragonEntity("scopeBean", "singleton-prototype-request-session", 25);
}
/**
* 知识点六:Conditional条件判断
* 满足条件才会注册bean,可以修饰在类上,管理整个类下的组件注入。
*/
@Bean
@Conditional({ITDragonCustomCondition.class})
public ITDragonEntity conditionalBean() {
return new ITDragonEntity("conditionalBean", "Conditional-Condition-CustomCondition", 25);
}
/**
* 知识点七:FactoryBean工厂Bean
* FactoryBean默认通过调用getObject创建的对象,通过调用isSingleton设置单实例和多实例。
*/
@Bean
public ITDragonFactoryBean itdragonFactoryBean() {
return new ITDragonFactoryBean();
}
}</code></pre>
<p>自定义的条件判断组件注入类</p>
<pre><code class="java">import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* 自定义的条件判断组件注入
* @author itdragon
*
*/
public class ITDragonCustomCondition implements Condition{
/**
* 判断注册的bean中是否含有指定的bean
*/
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获取bean的注册类
BeanDefinitionRegistry registry = context.getRegistry();
return registry.containsBeanDefinition("scopeTopicBean"); // 有则加载conditionalBean
}
}</code></pre>
<p>自定义Bean的工厂类</p>
<pre><code class="java">import org.springframework.beans.factory.FactoryBean;
import com.itdragon.entity.ITDragonEntity;
/**
* 自定义Bean的工厂类
* @author itdragon
*
*/
public class ITDragonFactoryBean implements FactoryBean<ITDragonEntity>{
public ITDragonEntity getObject() throws Exception {
System.out.println("^^^^^^^^^^^^^^^^^^^^^FactoryBean Create Bean");
return new ITDragonEntity(); // 创建对象并返回到容器中
}
public Class<?> getObjectType() {
return ITDragonEntity.class;
}
public boolean isSingleton() {
return false; // 设置多实例,true则为单例
}
}</code></pre>
<h3>三、属性赋值</h3>
<p>属性赋值步骤:</p>
<p>第一步:通过注解PropertySource引入外部文件。可以引入多个,若担心文件不存在,可以通过参数ignoreResourceNotFound设置忽略</p>
<p>第二步:通过注解Value从外部文件中获取值,一般采用${}格式,还支持#{} SpEL表达式,也可以直接传字符串。如果想接收一些复杂的值,比如集合,可以考虑使用注解ConfigurationProperties,后续会详细介绍两者的优缺点。</p>
<pre><code class="java">import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import com.itdragon.entity.ITDragonEntity;
/**
* 知识点一: 引入外部文件,并从文件中获取值
* @PropertySource 引入外部文件,支持多个,如果文件不存在会报错,可以通过设置参数ignoreResourceNotFound=true忽略
* @Value 从外部文件中获取值,支持spel表达式,#{},${},string
* @author itdragon
*/
@Configuration
@PropertySource(value={"classpath:propertySource.properties","classpth:xxx.properties"},ignoreResourceNotFound=true)
public class ITDragonConfigValue {
@Value("${ACCOUNT}") // 从配置文件获取数据
private String ACCOUNT;
@Value("${PASSWORD}")
private String PASSWORD;
@Value("${AGE}")
private Integer AGE;
@Value("ITDragon") // 普通赋值
private String str;
@Value("#{(1+2-3)/4*5}") // 算术运算
private String operator;
@Value("#{1>2 || 2 <= 3}") // 关系运算
private Boolean comparison;
@Value("#{systemProperties['java.version']}") // 系统配置:os.name
private String systemProperties;
@Value("#{T(java.lang.Math).abs(-18)}") // 表达式
private String mapExpression;
@Bean("valueBean")
public ITDragonEntity itdragonEntity() {
System.out.println("^^^^^^^^^^^^^^^^^^^^ str : " + str);
System.out.println("^^^^^^^^^^^^^^^^^^^^ operator : " + operator);
System.out.println("^^^^^^^^^^^^^^^^^^^^ comparison : " + comparison);
System.out.println("^^^^^^^^^^^^^^^^^^^^ systemProperties : " + systemProperties);
System.out.println("^^^^^^^^^^^^^^^^^^^^ mapExpression : " + mapExpression);
return new ITDragonEntity(ACCOUNT, PASSWORD, AGE);
}
}</code></pre>
<h3>四、闲谈学习</h3>
<p>这里并不是介绍如何学习一门技术,而是论养成一个学习习惯的重要性。大一时期,因为担心找不到工作而报了一个线上培训机构。经常被洗脑,其中一句话而我印象深刻 ---- "让优秀成为一种习惯"。听得我热血沸腾。花了五六千大洋报名,后来才发现网上有免费的。。。。<strong>个人不建议参加培训</strong>。</p>
<p>这钱花的还算值得,"让优秀成为一种习惯",这句话对我的影响很大,从大学到工作,每当遇到陌生的知识,并没有选择逃避它。而是抽时间从网上找资料,去学习,整理,实践直到弄懂它。可我万万没有想到,这种学习的精神竟然用到了装修上。。。。<strong>可能学习已经是我的一个习惯了吧</strong>!</p>
<p>开始,我是一个装修小白,不知道什么是全包、半包、清包;不知道什么是硬装、软装;也不知道装修的流程;不知道水电线、橱柜、洁具的品牌选择,不知道挂机、柜机、风管机、中央空调的优缺点;不知道封阳台的利弊;更不知道一个装修效果图要七八千。面对这些未知的领域,我寸步难行。我清楚的知道:如果你不懂,你就是砧板上的鱼肉,任人宰割。</p>
<p>现在,我通过在百度,知乎,兔巴兔等平台上找答案,并把内容用Markdown的格式整理!我都被自己吓到了。不仅如此,我还在抢设计师的饭碗,自己动手设计效果图。在制作效果图的过程中,发现了很多不合理的设想。庆幸问自己设计了一套效果图,不然又有很多无用功,耗时,耗力,耗钱。爸妈和女票就是客户,而我就一直处于改!改!改!的阶段。体验了一把前端工程师的辛酸。</p>
<p><strong>我是谁?我在哪?我在做什么?</strong></p>
<p><strong>我是一名程序员,我在学习的道路上,我在做能提高自己的事情!</strong></p>
消息中间件企业级应用
https://segmentfault.com/a/1190000013754936
2018-03-15T14:34:10+08:00
2018-03-15T14:34:10+08:00
itdragon
https://segmentfault.com/u/itdragon
4
<h2>消息中间件企业级应用</h2>
<p>众所周知,消息中间件是大型分布式系统中不可或缺的重要组件。它使用简单,却解决了不少难题,比如异步处理,系统藕合,流量削锋,分布式事务管理等。实现了一个高性能,高可用,高扩展的系统。本章通过介绍<strong>消息中间件的应用场景</strong>,<strong>消息中间件的传输模式</strong>,<strong>ActiveMQ快速入门</strong> 三个方面来对消息中间件进行入门介绍。还在等什么,赶快来学习吧!</p>
<p>说明:消息中间件非常强大,值得我们认真去学习和使用。完整代码请异步github。<br>技术:消息中间件的应用场景,通信模式,ActiveMQ。<br>源码:<a href="https://link.segmentfault.com/?enc=UY60jBeLItZ4Jj8SOTTVRQ%3D%3D.5pIMOsJoIqjlWFMrPEp6bU0dRiU8jdbIZ%2BkJoshnaA9oI5KeCBu4bS%2Frkooei6gqGYZ0aiGuSS4V4UbjHrmmRQ%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a><br>文章目录结构:<br><img src="/img/remote/1460000013754939?w=533&h=426" alt="文章目录结构" title="文章目录结构"></p>
<h3>消息中间件应用场景</h3>
<h4>异步处理</h4>
<p><strong>异步处理</strong>:调用者发起请求后,调用者不会立刻得到结果,也无需等待结果,继续执行其他业务逻辑。提高了效率但存在异步请求失败的隐患,适用于非核心业务逻辑处理。<br><strong>同步处理</strong>:调用者发起请求后,调用者必须等待直到返回结果,再根据返回的结果执行其他业务逻辑。效率虽然没有异步处理高,但能保证业务逻辑可控性,适用于核心业务逻辑处理。</p>
<p>举一个比较常见的应用场景:为了确保注册用户的真实性,一般在注册成功后会发送验证邮件或者验证码短信,只有认证成功的用户才能正常使用平台功能。<br>如下图所示:同步处理和异步处理的比较。</p>
<p><img src="/img/remote/1460000013754940?w=673&h=266" alt="异步处理" title="异步处理"></p>
<p>用消息中间件实现异步处理的好处:<br>一、在传统的系统架构,用户从注册到跳转成功页面,中间需要等待邮件发送的业务逻辑耗时。这不仅影响系统响应时间,降低了CPU吞吐量,同时还影响了用户的体验。<br>二、通过消息中间件将邮件发送的业务逻辑异步处理,用户注册成功后发送数据到消息中间件,再跳转成功页面,邮件发送的逻辑再由订阅该消息中间件的其他系统负责处理,<br>三、消息中间件的读写速度非常的快,其中的耗时可以忽略不计。通过消息中间件可以处理更多的请求。</p>
<p><strong>小结:正确使用消息中间件将非核心业务逻辑功能异步处理,可以提高系统的响应效率,提高了CPU的吞吐量,改善用户的体验。</strong></p>
<h4>系统藕合和事务的最终一致性</h4>
<p>分布式系统是若干个独立的计算机(系统)集合。每个计算机负责自己的模块,实现系统的解耦,也避免单点故障对整个系统的影响。每个系统还可以做一个集群,进一步降低故障的发生概率。<br>在这样的分布式系统中,消息中间件又扮演着什么样的角色呢?</p>
<p>举一个比较常见的应用场景:订单系统下单成功后,需要调用仓库系统接口,选择最优的发货仓库和更新商品库存。若因为某种原因在调用仓库系统接口失败,会直接影响到下单流程。<br>如下图所示:感受一下消息中间件扮演的重要角色。</p>
<p><img src="/img/remote/1460000013754941?w=589&h=307" alt="系统解耦" title="系统解耦"></p>
<p>用消息中间件实现系统藕合的好处:<br>一、消息中间件可以让各系统之间耦合性降低,不会因为其他系统的异常影响到自身业务逻辑。各尽其职,订单系统只需负责将订单数据持久化到数据库中,仓库系统只需负责更新库存,不会因为仓库系统的原因从而影响到下单的流程。<br>二、各位看官是否发现了一个问题,下单和库存减少本应该是一个事务。因为分布式的原因很难保证事务的强一致性。这里通过消息中间件实现事务的最终一致性效果(后续会详细介绍)。</p>
<p><strong>小结:事务的一致性固然重要,没有库存会导致下单失败是一个理论上很正常的逻辑。但实际业务中并非如此,我们完全可以利用发货期通过采购或者借库的方式来增加库存。这样无疑可以增加销量,还是可以保证事务的最终一致性。</strong></p>
<h4>流量削锋</h4>
<p>流量削锋也称限流。在秒杀,抢购的活动中,为了不影响整个系统的正常使用,一般会通过消息中间件做限流,避免流量突增压垮系统,前端页面可以提示"排队等待",即便用户体验很差,也不能让系统垮掉。</p>
<p><img src="/img/remote/1460000013754942?w=584&h=141" alt="流量削锋" title="流量削锋"></p>
<p><strong>小结:限流可以在流量突增的情况下保障系统的稳定。系统宕机会被同行抓住笑柄。</strong></p>
<h3>消息中间件的传输模式</h3>
<p>消息中间件除了支持对点对和发布订阅两种模式外,在实际开发中还有一种双向应答模式被广泛使用。</p>
<h4>点对点(p2p)模式</h4>
<p>点对点(p2p)模式有三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。发送者将消息发送到一个特定的队列中,等待接收者从队列中获取消息消耗。<br>P2P的三个特点:<br>一、每个消息只能被一个接收者消费,且消息被消费后默认从队列中删掉(也可以通过其他签收机制重复消费)。<br>二、发送者和接收者之间没有依赖性,生产者发送消息和消费者接收消息并不要求同时运行。<br>三、接收者在成功接收消息之后需向队列发送接收成功的确认消息。</p>
<p><img src="/img/remote/1460000013754943?w=509&h=97" alt="p2p模式" title="p2p模式"></p>
<h4>发布订阅(Pub/Sub)模式</h4>
<p>发布订阅(Pub/Sub)模式也有三个角色:主题(Topic),发布者(Publisher),订阅者(Subscriber)。发布者将消息发送到主题队列中,系统再将这些消息传递给订阅者。<br>Pub/Sub的特点:<br>一、每个消息可以被多个订阅者消费。<br>二、发布者和订阅者之间存在依赖性。订阅者必须先订阅主题后才能接收到信息,在订阅前发布的消息,订阅者是接收不到的。<br>三、非持久化订阅:如果订阅者不在线,此时发布的消息订阅者是也接收不到,即便订阅者重新上线也接收不到。<br>四、持久化订阅:订阅者订阅主题后,即便订阅者不在线,此时发布的消息可以在订阅者重新上线后接收到的。</p>
<p><img src="/img/remote/1460000013754944?w=452&h=156" alt="Pub/Sub模式" title="Pub/Sub模式"></p>
<h4>双向应答模式</h4>
<p>双向应答模式并不是消息中间件提供的一种通信模式,它是由于实际生成环境的需要,在原有的基础上做了改良。即消息的发送者也是消息的接收者。消息的接收者也是消息的发送者。如下图所示</p>
<p><img src="/img/remote/1460000013754945?w=532&h=198" alt="双向应当模式" title="双向应当模式"></p>
<h3>ActiveMQ 入门</h3>
<p>ActiveMQ是Apache出品,简单好用,能力强大,可以处理大部分的业务的开源消息总线。同时也支持JMS规范。</p>
<blockquote>JMS(JAVA Message Service,java消息服务)API是一个消息服务的标准或者说是规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。</blockquote>
<h4>ActiveMQ 安装</h4>
<p>ActiveMQ 的安装很简单,三个步骤:<br>第一步:下载,官网下载地址:<a href="https://link.segmentfault.com/?enc=YR0%2FuB4qPJp0vK0AdrwlAA%3D%3D.Bd%2BqfdiXtOwjpjczUo9PsaP6v7tPJ8TvJ51FDZPlrF%2BdXW3wYyg%2F1hUMhIAirf3q" rel="nofollow">http://activemq.apache.org/do...</a>。<br>第二步:运行,压缩包解压后,在bin目录下根据电脑系统位数找到对应的wrapper.exe程序,双击运行。<br>第三步:访问,浏览器访问<a href="https://link.segmentfault.com/?enc=scVnJ3YkRWKAVRIr4rEVZQ%3D%3D.TimxtxOeRET%2FROiCImbjboAgzvl%2F4pWUDzA8upE3DSg%3D" rel="nofollow">http://localhost</a>:8161/admin,账号密码都是admin。</p>
<h4>ActiveMQ 工作流程</h4>
<p>我们通过简单的P2P模式来了解ActiveMQ的工作流程。<br>生产者发送消息给MQ主要步骤:<br>第一步:创建连接工厂实例<br>第二步:创建连接并启动<br>第三步:获取操作消息的接口<br>第四步:创建队列,即Queue或者Topic<br>第五步:创建消息发送者<br>第六步:发送消息,关闭资源</p>
<pre><code class="java">import java.util.Random;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
/**
* 消息队列生产者
* @author itdragon
*/
public class ITDragonProducer {
private static final String QUEUE_NAME = "ITDragon.Queue";
public static void main(String[] args) {
// ConnectionFactory: 连接工厂,JMS 用它创建连接
ConnectionFactory connectionFactory = null;
// Connection: 客户端和JMS系统之间建立的链接
Connection connection = null;
// Session: 一个发送或接收消息的线程 ,操作消息的接口
Session session = null;
// Destination: 消息的目的地,消息发送给谁
Destination destination = null;
// MessageProducer: 消息生产者
MessageProducer producer = null;
try {
// step1 构造ConnectionFactory实例对象,需要填入 用户名, 密码 以及要连接的地址,默认端口为"tcp://localhost:61616"
connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD, ActiveMQConnection.DEFAULT_BROKER_URL);
// step2 连接工厂创建连接对象
connection = connectionFactory.createConnection();
// step3 启动
connection.start();
// step4 获取操作连接
/**
* 第一个参数:是否设置事务 true or false。 如果设置了true,第二个参数忽略,并且需要commit()才执行
* 第二个参数:acknowledge模式
* AUTO_ACKNOWLEDGE:自动确认,客户端发送和接收消息不需要做额外的工作。不管消息是否被正常处理。 默认
* CLIENT_ACKNOWLEDGE:客户端确认。客户端接收到消息后,必须手动调用acknowledge方法,jms服务器才会删除消息。
* DUPS_OK_ACKNOWLEDGE:允许重复的确认模式。
*/
session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);
// step5 创建一个队列到目的地
destination = session.createQueue(QUEUE_NAME);
// step6 在目的地创建一个生产者
producer = session.createProducer(destination);
// step7 生产者设置不持久化,若要设置持久化则使用 PERSISTENT
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
// step8 生产者发送信息,具体的业务逻辑
sendMessage(session, producer);
session.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != connection) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void sendMessage(Session session, MessageProducer producer) throws Exception {
for(int i = 0; i < 5; i++) {
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
TextMessage message = session.createTextMessage(expression);
// 发送消息到目的地方
producer.send(message);
System.out.println("Queue Sender ---------> " + expression);
}
}
}</code></pre>
<p>消费者从MQ中获取数据消费步骤和上面类似,只是将发送消息改成了接收消息。</p>
<pre><code class="java">import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;
import com.itdragon.utils.ITDragonUtil;
/**
* 消息队列消费者
* @author itdragon
*/
public class ITDragonConsumer {
private static final String QUEUE_NAME = "ITDragon.Queue"; // 要和Sender一致
public static void main(String[] args) {
ConnectionFactory connectionFactory = null;
Connection connection = null;
Session session = null;
Destination destination = null;
// MessageConsumer: 信息消费者
MessageConsumer consumer = null;
try {
connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD, ActiveMQConnection.DEFAULT_BROKER_URL);
connection = connectionFactory.createConnection();
connection.start();
session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);
destination = session.createQueue(QUEUE_NAME);
consumer = session.createConsumer(destination);
// 不断地接收信息,直到没有为止
while (true) {
TextMessage message = (TextMessage) consumer.receive();
if (null != message) {
System.out.print(ITDragonUtil.cal(message.getText()));
} else {
break;
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != connection) {
connection.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}</code></pre>
<h4>SpringBoot 整合ActiveMQ使用</h4>
<p>SpringBoot可以帮助我们快速搭建项目,减少Spring整合第三方配置的精力。SpringBoot整合ActiveMQ也是非常简单,只需要简单的两个步骤:<br>第一步,在pom.xml文件中添加依赖使其支持ActiveMQ<br>第二步,在application.properties文件中配置连接ActiveMQ参数</p>
<p>pom.xml是Maven项目的核心配置文件</p>
<pre><code class="xml"><dependency> <!-- 支持ActiveMQ依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency> <!-- 支持使用mq连接池 -->
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
</dependency> </code></pre>
<p>application.properties是SpringBoot项目的核心参数配置文件</p>
<pre><code class="xml">spring.activemq.user=admin
spring.activemq.password=admin
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.in-memory=true
spring.activemq.pool.enabled=true</code></pre>
<p><code>spring.activemq.in-memory</code> 默认值为true,表示无需安装ActiveMQ的服务器,直接使用内存。<br><code>spring.activemq.pool.enabled</code> 表示通过连接池的方式连接。</p>
<h5>springboot-activemq-producer</h5>
<p>springboot-activemq-producer 项目模拟生产者所在的系统,支持同时发送点对点模式和发布订阅模式。<br>两个核心文件:一个是消息发送类,一个是队列Bean管理配置类。<br>三种主要模式:一个是对点对模式,队列名为"queue.name";一个是发布订阅模式,主题名为"topic.name";最后一个是双向应答模式,队列名为"response.name" 。</p>
<pre><code class="java">import java.util.Random;
import javax.jms.Queue;
import javax.jms.Topic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.core.JmsMessagingTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
/**
* 消息队列生产者
* @author itdragon
*/
@Service
@EnableScheduling
public class ITDragonProducer {
@Autowired
private JmsMessagingTemplate jmsTemplate;
@Autowired
private Queue queue;
@Autowired
private Topic topic;
@Autowired
private Queue responseQueue;
/**
* 点对点(p2p)模式测试
* 包含三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。
* 发送者将消息发送到一个特定的队列,队列保留着消息,直到接收者从队列中获取消息。
*/
@Scheduled(fixedDelay = 5000)
public void testP2PMQ(){
for(int i = 0; i < 5; i++) {
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
jmsTemplate.convertAndSend(this.queue, expression);
System.out.println("Queue Sender ---------> " + expression);
}
}
/**
* 订阅/发布(Pub/Sub)模拟测试
* 包含三个角色:主题(Topic),发布者(Publisher),订阅者(Subscriber) 。
* 多个发布者将消息发送到Topic,系统将这些消息传递给多个订阅者。
*/
@Scheduled(fixedDelay = 5000)
public void testPubSubMQ() {
for (int i = 0; i < 5; i++) {
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
jmsTemplate.convertAndSend(this.topic, expression);
System.out.println("Topic Sender ---------> " + expression);
}
}
/**
* 双向应答模式测试
* P2P和Pub/Sub是MQ默认提供的两种模式,而双向应答模式则是在原有的基础上做了改进。发送者既是接收者,接收者也是发送者。
*/
@Scheduled(fixedDelay = 5000)
public void testReceiveResponseMQ(){
for (int i = 0; i < 5; i++) {
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
jmsTemplate.convertAndSend(this.responseQueue, expression);
}
}
// 接收P2P模式,消费者返回的数据
@JmsListener(destination = "out.queue")
public void receiveResponse(String message) {
System.out.println("Producer Response Receiver ---------> " + message);
}
}</code></pre>
<pre><code class="java">import javax.jms.Queue;
import javax.jms.Topic;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* bean配置管理类
* @author itdragon
*/
@Configuration
public class ActiveMQBeansConfig {
@Bean // 定义一个名字为queue.name的点对点列队
public Queue queue() {
return new ActiveMQQueue("queue.name");
}
@Bean // 定义一个名字为topic.name的主题队列
public Topic topic() {
return new ActiveMQTopic("topic.name");
}
@Bean // 定义一个名字为response.name的双向应答队列
public Queue responseQueue() {
return new ActiveMQQueue("response.name");
}
}</code></pre>
<h5>springboot-activemq-consumer</h5>
<p>springboot-activemq-consumer 模拟消费者所在的服务器,主要负责监听队列消息。<br>两个核心文件:一个是消息接收类,一个是兼容点对点模式和发布订阅模式的链接工厂配置类。</p>
<pre><code class="java">import org.springframework.jms.annotation.JmsListener;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Service;
import com.itdragon.utils.ITDragonUtil;
/**
* 消息队列消费者
* @author itdragon
*/
@Service
public class ITDragonConsumer {
// 1. 监听名字为"queue.name"的点对点队列
@JmsListener(destination = "queue.name", containerFactory="queueListenerFactory")
public void receiveQueue(String text) {
System.out.println("Queue Receiver : " + text + " \t 处理结果 : " + ITDragonUtil.cal(text));
}
// 2. 监听名字为"topic.name"的发布订阅队列
@JmsListener(destination = "topic.name", containerFactory="topicListenerFactory")
public void receiveTopicOne(String text) {
System.out.println(Thread.currentThread().getName() + " No.1 Topic Receiver : " + text + " \t 处理结果 : " + ITDragonUtil.cal(text));
}
// 2. 监听名字为"topic.name"的发布订阅队列
@JmsListener(destination = "topic.name", containerFactory="topicListenerFactory")
public void receiveTopicTwo(String text) {
System.out.println(Thread.currentThread().getName() +" No.2 Topic Receiver : " + text + " \t 处理结果 : " + ITDragonUtil.cal(text));
}
// 3. 监听名字为"response.name"的接收应答(双向)队列
@JmsListener(destination = "response.name")
@SendTo("out.queue")
public String receiveResponse(String text) {
System.out.println("Response Receiver : " + text + " \t 正在返回数据......");
return ITDragonUtil.cal(text).toString();
}
}</code></pre>
<pre><code class="java">import java.util.concurrent.Executors;
import javax.jms.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
@Configuration
@EnableJms
public class JmsConfig {
@Bean // 开启pub/Sub模式
public JmsListenerContainerFactory<?> topicListenerFactory(ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setPubSubDomain(true);
factory.setConnectionFactory(connectionFactory);
return factory;
}
@Bean // JMS默认开启P2P模式
public JmsListenerContainerFactory<?> queueListenerFactory(ConnectionFactory connectionFactory) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setPubSubDomain(false);
factory.setConnectionFactory(connectionFactory);
return factory;
}
} </code></pre>
<h3>总结</h3>
<p>1) <strong>消息中间件可以解决异步处理,系统解耦,流量削锋,分布式系统事务管理等问题。</strong></p>
<p>2) <strong>消息中间件默认支持点对点模式和发布订阅模式,实际工作中还可以使用双向应当模式。</strong></p>
<p>3) <strong>ActiveMQ是Apache出品,简单好用,功能强大,可以处理大部分的业务的开源消息总线。</strong></p>
<p>到这里 消息中间件企业级应用使用 的文章就写完了。如果文章对你有帮助,可以点个"推荐",也可以"关注"我,获得更多丰富的知识。后续博客计划是:RocketMQ和Kafka的使用,Zookeeper和相关集群的搭建。若文中有什么不对或者不严谨的地方,请指正。</p>
双刃剑MongoDB的学习和避坑
https://segmentfault.com/a/1190000013589617
2018-03-07T21:53:16+08:00
2018-03-07T21:53:16+08:00
itdragon
https://segmentfault.com/u/itdragon
3
<h2>双刃剑MongoDB的学习和避坑</h2>
<p>MongoDB 是一把双刃剑,它对数据结构的要求并不高。数据通过key-value的形式存储,而value的值可以是字符串,也可以是文档。所以我们在使用的过程中非常方便。正是这种方便给我们埋下了一颗颗地雷。当内嵌的文档太深,或者内嵌文档有相同的属性名。你会被炸得很惨。本章节通过 MongoDB简介,Shell编程,SpringBoot整合MongoDB,工作中注意事项,四个方面介绍MongoDB的使用。让你轻松入门,轻松避坑。还在等什么,赶快来学习吧!</p>
<p>技术:MongoDB,SpringBoot,SpringDataMongoDB<br>说明:本章重点介绍MongoDB的使用,对非关系型数据库的介绍会比较简单。完整代码和相关sql请移步github,ths!<br>源码:<a href="https://link.segmentfault.com/?enc=j9e7FGfQ%2BUPLhjBlUuNvTQ%3D%3D.8eRYWrXyA8PTLqjxAMTdniyU7PJV0F8rPaAGSE1u0otnvygagqety9ENWWL%2B1ysJtaHwknH%2BkLkgCpNHA5tzeA%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>MongoDB 简介</h3>
<p>MongoDB 是非关系型数据库中,最接近关系型数据库的,文档型数据库。它支持的查询功能非常强大。<br>MongoDB 是为快速开发互联网Web应用而设计的数据库系统。他的数据模型是面向文档的,这种文档是一种类似于JSON的结构,准确来说是一种支持二进制的BSON(Binary JSON)结构。</p>
<h4>非关系性数据库</h4>
<p>非关系性数据库 也被称为 NoSQL(Not only sql),主要有四大类:键值存储数据库、列存储数据库、文档型数据库、图形数据库。之前介绍的Redis属于键值存储数据库。</p>
<h4>关系与非关系型数据库</h4>
<p>关系型数据库的优点:<br>1 支持事务处理,事务特性:原子性、一致性、隔离性、持久性。<br>2 数据结构清晰,便于理解,可读性高。<br>3 使用方便,有标准的sql语法。</p>
<p>关系型数据库的缺点:<br>1 读写性能相对较差,为保证事务的一致性,需要一定的开销。在高并发下表现的尤为突出。<br>2 表结构固定,不易于表后期的扩展,所以前期对表的设计要求较高。</p>
<p>非关系型数据库的优点:<br>1 读写性能高,没有保障数据的一致性。<br>2 表结构灵活,表结构并不是固定的,通过key-value存储数据,value又可以存储其他格式的数据。</p>
<p>两者的优缺点其实是向反的,一件事物不会凭空出现,都是在原有的基础上做了补充和优化,两者的侧重点各有不同。就像MySQL保障了数据的一致性,却影响了读写的性能。MongoDB放弃数据的强一致性,保障了读写的效率。在合适的场景使用合适的数据库,是需要我们考虑的。<br>1 对于需要高度事务特性的系统,比如和钱有关的,银行系统,金融系统。我们要考虑使用关系型数据库,确保数据的一致性和持久性。<br>2 对于那些数据并不是很重要,访问量又很大的系统,比如电商平台的商品信息。我们可以使用非关系型数据库来做缓存,充分提高了系统查询的性能。</p>
<p>这里对银行和金融我想抱怨两句:<br>第一:投资理财千万不要选择小平台金融公司,收益再高都是虚假的,多半都是圈钱跑路的,钱的教训。<br>第二:某些银行APP显示的金额不是实时的。16年某生银行卡转入40万,但在我的总资产界面并没有转入的金额,吓得我一身冷汗。颤抖着双手给客服打了几个电话才知道,某生银行APP的总资产界面数据是统计前一天的。直到第二天,金额才显示正确。从此我再也没有用某生的银行卡。某商的信用卡也是一样,还了钱金额并没有减下来。不知道现在有没有改。</p>
<p>有在银行工作的朋友,能否告诉我这样设计的原因是啥?难道用户体验不重要?还是要体现客服的价值?反正,这锅我们程序员不背。</p>
<h3>Mongodb Shell 编程</h3>
<h4>查询数据</h4>
<p>Mongodb的查询功能十分强大,有find() 和 findOne()。支持的查询条件有:$lt、$lte、$gt、$gte、$ne、$or、$in、$nin、$not、$exists、$and、正则表达式等。</p>
<ul>
<li>db.collection.find() 根据查询条件返回所有文档</li>
<li>db.collection.findOne() 根据查询条件返回第一个文档</li>
</ul>
<p>查询建议:<br>1 查询所有数据,建议使用分页查询。<br>2 查询key建议用引号,对象的属性可以省略引号,内嵌的对象属性不能省略。比如下面的name可以省略,但address.province则不能。<br>3 尽量少用$or, $in 查询,效率很低。</p>
<pre><code class="sql">// 查询所有(不推荐,一般使用分页查询)
db.itdragonuser.find();
{"_id":ObjectId("5a9bbefa2f3fdfdf540a1be7"),"name":"ITDragon","age":25,"address":{"province":"广东省","city":"深圳"},"ability":["JAVA"]}
// 等于查询
db.itdragonuser.find({"name":"ITDragon"});
// 模糊查询
db.itdragonuser.find({"name":/ITDragon/});
// 或者查询
db.itdragonuser.find({$or:[{"address.province":"湖北"},{"address.province":"湖南"}]});
// 包含查询(包含了JAVA或者HTML)
db.itdragonuser.find({"ability":{$in:["JAVA","HTML"]}});
// 不包含查询(JAVA和HTML都不包含)
db.itdragonuser.find({"ability":{$nin:["JAVA","HTML"]}});
// 范围查询$gt , $lt , $gte , $lte , $ne
db.itdragonuser.find({"age":{$gt:25}});
// 正则表达式查询(查询以WeiXin结尾的数据)
db.itdragonuser.find({"name":/WeiXin$/});
// 按照条件统计数据
db.itdragonuser.count({"name":/ITDragon/});
// 过滤重复内容(打印不重复的name值)
db.itdragonuser.distinct("name");
// sort:排序(1表示升序 -1表示降序),skip:跳过指定数量,limit:每页查询数量
db.itdragonuser.find().sort({"age":1}).skip(2).limit(3);
// 字段投影,(0表示不显示,1表示显示)
db.itdragonuser.find({},{_id:0,name:1,address:1,aliblity:1});</code></pre>
<h4>插入数据</h4>
<p>插入数据比较简单,insert() 可以向集合插入一个或多个文档,而insertOne() 和 insertMany() 细化了insert() 方法,语法是一样的,命名规则上更清晰。</p>
<ul>
<li>db.collection.insert() 可以向集合中插入一个或多个文档</li>
<li>db.collection.insertOne() 向集合中插入一个文档</li>
<li>db.collection.insertMany()向集合中插入多个文档</li>
</ul>
<p>插入建议:<br>1 插入数据不能破坏原有的数据结构,造成不必要的麻烦。<br>2 批量插入数据,尽量一次执行多个文档,而不是多个文档执行多次方法。</p>
<pre><code class="sql">// 插入一条数据,类型有字符串,数字,对象,集合
db.itdragonuser.insert({"name":"ITDragon","age":24,"address":{"province":"广东","city":"深圳"},"ability":["JAVA","HTML"]})
// 插入多条数据
db.itdragonuser.insert([
{"name":"ITDragon","age":24,"address":{"province":"广东","city":"深圳"},"ability":["JAVA","HTML"]},
{"name":"ITDragonGit","age":24,"address":{"province":"湖北","city":"武汉"},"ability":["JAVA","HTML","GIT"]}
])</code></pre>
<h4>更新数据</h4>
<p>更新数据时,需要确保value的数据结构,是字符串,是集合,还是对象,不能破坏原有的数据结构。尽量使用修改器来帮忙完成操作。</p>
<ul>
<li>db.collection.update() 可以修改、替换集合中的一个或多个文档,默认修改第一个,若要修改多个,则需要使用multi:true</li>
<li>db.collection.updateOne() 修改集合中的一个文档</li>
<li>db.collection.updateMany() 修改集合中的多个文档</li>
<li>db.collection.replaceOne() 替换集合中的一个文档</li>
</ul>
<p>常用的修改器:<br><strong>$inc</strong> : 数值类型属性自增<br><strong>$set</strong> : 用来修改文档中的指定属性<br><strong>$unset</strong> : 用来删除文档的指定属性<br><strong>$push</strong> : 向数组属性添加数据<br><strong>$addToSet</strong> : 向数组添加不重复的数据</p>
<p>更新建议:<br>1 更新数据不能破坏原有的数据结构。<br>2 正确使用修改器完成更新操作。</p>
<pre><code class="sql">// 更新字符串属性
db.itdragonuser.update({"name":"ITDragonGit"},{$set:{"name":"ITDragon"}});
// 更新对象属性
db.itdragonuser.update({"name":"ITDragon"},{$set:{"address.province":"广东省"}});
// 更新集合属性
db.itdragonuser.update({"name":"ITDragon"},{$push:{"ability":"MONGODB"}});
// 批量更新属性
db.itdragonuser.updateMany({"name":"ITDragon"},{$set:{"age":25}});
// 批量更新属性,加参数multi:true
db.itdragonuser.update({"name":"ITDragon"},{$set:{"age":25}},{multi:true});</code></pre>
<h4>删除数据</h4>
<p>删除数据是一个非常谨慎的操作,实际开发中不会物理删除数据,只是逻辑删除。方便数据恢复和大数据分析。这里只简单介绍。</p>
<ul>
<li>db.collection.remove() 删除集合中的一个或多个文档(默认删除多个)</li>
<li>db.collection.deleteOne() 删除集合中的一个文档</li>
<li>db.collection.deleteMany() 删除集合中的多个文档</li>
</ul>
<h3>SpringBoot MongoDB 整合</h3>
<p>如果你觉得Spring整合MongoDB略显麻烦,那SpringBoot整合MongoDB就是你的福音。SpringBoot旨在零配置,只需简单的两个步骤即可。<br>第一步:在pom.xml文件中添加<code>spring-boot-starter-data-mongodb</code>。</p>
<pre><code class="xml"><dependency> <!-- 添加对mongodb的支持 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency></code></pre>
<p>第二步:在application.properties文件中配置MongoDB数据库链接地址。 <br>链接MongoDB数据库地址规则:<code>spring.data.mongodb.uri=mongodb://account:password@ip:port/database</code><br>其中 account和password方便是链接数据库的账号和密码。而database是需要链接的数据库地址</p>
<pre><code class="xml"># 没有账号密码可以简写
spring.data.mongodb.uri=mongodb://localhost:27017/itdragonstu</code></pre>
<h3>Spring Data MongoDB 编程</h3>
<p>Spring Data给我们提供了MongoTemplate类,极大的方便了我们的工作,但是若每个实体类都基于MongoTemplate重写一套CRUD的实现类,似乎显得有些笨重。于是我们可以将其简单的封装一下。步骤如下</p>
<p>第一步:创建用户实体类,其数据库表名就是类名首字母小写。<br>第二步:封装MongoTemplate类,实现增删改查,分页,排序,主键自增等常用功能。<br>第三步:创建封装类的Bean管理类,针对不同的实体类,需要配置不同的bean。<br>第四步:创建测试类,测试:注册,更新,分页,排序,查询用户功能。</p>
<h4>创建用户实体类</h4>
<p>用户实体类有五个字段,除了主键ID,其他四个分别代表四个常用的类型(字符串,数字,对象,集合)。为了简化开发,实体类建议不实用@Document注解重命名User在MongoDB数据库中的表名。<br>省略get/set方法和toString方法</p>
<pre><code class="java">import java.io.Serializable;
import java.util.ArrayList;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
/**
* 用户实体类
* @author itdragon
*/
//@Document(collection = "itdragon_user") 如果为了代码的通用性,建议不要使用
public class User implements Serializable{
private static final long serialVersionUID = 1L;
@Id
private Long id;
private String name;
private Integer age;
private Address address;
private ArrayList ability;
}
public class Address{
private Long id;
private String province;
private String city;
}</code></pre>
<h4>封装MongoTemplate类</h4>
<p>SpringData提供的MongoTemplate类,极大的方便我们操作MongoDB数据库。可是它的很多方法都涉及到了Class,和CollectionName。针对不同的实体类,我们需要重复写不同的方法。这里,我们进一步封装,实现代码的高可用。<br>实现的思路大致:将Class作为一个参数,在初始化MongoTemplate的封装类时赋值。这里有一个约束条件是:CollectionName是Class类名的首字母小写。</p>
<pre><code class="java">import java.util.List;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Repository;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
@Repository
@SuppressWarnings({"unchecked", "rawtypes"})
public class ITDragonMongoHelper {
@Autowired(required = false)
private MongoTemplate mongoTemplate;
private Class entityClass; // 实体类
private String collectionName; // 数据库表名
private String orderAscField; // 升序字段
private String orderDescField; // 降序字段
private static final String ID = "id";
private static final String MONGODB_ID = "_id";
public ITDragonMongoHelper() {
}
public ITDragonMongoHelper(Class entityClass) {
this.entityClass = entityClass;
this.collectionName = _getCollectionName();
}
public ITDragonMongoHelper(Class entityClass, String collectionName) {
this.entityClass = entityClass;
this.collectionName = collectionName;
}
/**
* @Title save
* @Description 通过Map创建实体类
* @param object Map,无需自带ID
* @return
*/
public Boolean save(Map<String, Object> requestArgs) {
try {
Object object = getEntityClass().newInstance();
if (null == requestArgs.get(ID)) {
requestArgs.put(ID, getNextId());
}
BeanUtils.populate(object, requestArgs);
saveOrUpdate(object);
} catch (Exception e) {
e.printStackTrace();
return Boolean.valueOf(false);
}
return Boolean.valueOf(true);
}
/**
* @Title save
* @Description 通过对象创建实体类
* @param object 实体类,需自带ID
* @return
*/
public Boolean saveOrUpdate(Object object) {
try {
this.mongoTemplate.save(object, this.collectionName);
} catch (Exception e) {
e.printStackTrace();
return Boolean.valueOf(false);
}
return Boolean.valueOf(true);
}
/**
* @Title update
* @Description 通过Map更新实体类具体字段,可以减少更新出错字段,执行的销率更高,需严格要求数据结构的正确性
* @param requestArgs Map,需自带ID, 形如:{id: idValue, name: nameValue, ....}
* @return
*/
public Boolean update(Map<String, Object> requestArgs) {
Object id = requestArgs.get(ID);
if (null == id) {
return Boolean.valueOf(false);
}
try {
Update updateObj = new Update();
requestArgs.remove(ID);
for (String key : requestArgs.keySet()) {
updateObj.set(key, requestArgs.get(key));
}
findAndModify(Criteria.where(ID).is(id), updateObj);
} catch (Exception e) {
e.printStackTrace();
return Boolean.valueOf(false);
}
return Boolean.valueOf(true);
}
/**
* @Title find
* @Description 根据查询条件返回所有数据,不推荐
* @param criteria 查询条件
* @return
*/
public List find(Criteria criteria) {
Query query = new Query(criteria);
_sort(query);
return this.mongoTemplate.find(query, this.entityClass, this.collectionName);
}
/**
* @Title find
* @Description 根据查询条件返回指定数量数据
* @param criteria 查询条件
* @param pageSize 查询数量
* @return
*/
public List find(Criteria criteria, Integer pageSize) {
Query query = new Query(criteria).limit(pageSize.intValue());
_sort(query);
return this.mongoTemplate.find(query, this.entityClass, this.collectionName);
}
/**
* @Title find
* @Description 根据查询条件分页返回指定数量数据
* @param criteria 查询条件
* @param pageSize 查询数量
* @param pageNumber 当前页数
* @return
*/
public List find(Criteria criteria, Integer pageSize, Integer pageNumber) {
Query query = new Query(criteria).skip((pageNumber.intValue() - 1) * pageSize.intValue()).limit(pageSize.intValue());
_sort(query);
return this.mongoTemplate.find(query, this.entityClass, this.collectionName);
}
public Object findAndModify(Criteria criteria, Update update) {
// 第一个参数是查询条件,第二个参数是需要更新的字段,第三个参数是需要更新的对象,第四个参数是MongoDB数据库中的表名
return this.mongoTemplate.findAndModify(new Query(criteria), update, this.entityClass, this.collectionName);
}
/**
* @Title findById
* @Description 通过ID查询数据
* @param id 实体类ID
* @return
*/
public Object findById(Object id) {
return this.mongoTemplate.findById(id, this.entityClass, this.collectionName);
}
/**
* @Title findOne
* @Description 通过查询条件返回一条数据
* @param id 实体类ID
* @return
*/
public Object findOne(Criteria criteria) {
Query query = new Query(criteria).limit(1);
_sort(query);
return this.mongoTemplate.findOne(query, this.entityClass, this.collectionName);
}
// id自增长
public String getNextId() {
return getNextId(getCollectionName());
}
public String getNextId(String seq_name) {
String sequence_collection = "seq";
String sequence_field = "seq";
DBCollection seq = this.mongoTemplate.getCollection(sequence_collection);
DBObject query = new BasicDBObject();
query.put(MONGODB_ID, seq_name);
DBObject change = new BasicDBObject(sequence_field, Integer.valueOf(1));
DBObject update = new BasicDBObject("$inc", change);
DBObject res = seq.findAndModify(query, new BasicDBObject(), new BasicDBObject(), false, update, true, true);
return res.get(sequence_field).toString();
}
private void _sort(Query query) {
if (null != this.orderAscField) {
String[] fields = this.orderAscField.split(",");
for (String field : fields) {
if (ID.equals(field)) {
field = MONGODB_ID;
}
query.with(new Sort(Sort.Direction.ASC, new String[] { field }));
}
} else {
if (null == this.orderDescField) {
return;
}
String[] fields = this.orderDescField.split(",");
for (String field : fields) {
if (ID.equals(field)) {
field = MONGODB_ID;
}
query.with(new Sort(Sort.Direction.DESC, new String[] { field }));
}
}
}
// 获取Mongodb数据库中的表名,若表名不是实体类首字母小写,则会影响后续操作
private String _getCollectionName() {
String className = this.entityClass.getName();
Integer lastIndex = Integer.valueOf(className.lastIndexOf("."));
className = className.substring(lastIndex.intValue() + 1);
return StringUtils.uncapitalize(className);
}
public Class getEntityClass() {
return entityClass;
}
public void setEntityClass(Class entityClass) {
this.entityClass = entityClass;
}
public String getCollectionName() {
return collectionName;
}
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
public String getOrderAscField() {
return orderAscField;
}
public void setOrderAscField(String orderAscField) {
this.orderAscField = orderAscField;
}
public String getOrderDescField() {
return orderDescField;
}
public void setOrderDescField(String orderDescField) {
this.orderDescField = orderDescField;
}
}</code></pre>
<h4>创建封装类的Bean管理类</h4>
<p>这里用Bean注解修饰的方法名和测试类中ITDragonMongoHelper 的变量名要保持一致。这样才能具体知道是哪个实体类的数据操作。</p>
<pre><code class="java">import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.itdragon.pojo.User;
import com.itdragon.repository.ITDragonMongoHelper;
/**
* ITDragonMongoHelper的bean配置管理类
* @author itdragon
*/
@Configuration
public class MongodbBeansConfig {
@Bean // 该方法名很重要
public ITDragonMongoHelper userMongoHelper() {
return new ITDragonMongoHelper(User.class);
}
}</code></pre>
<h4>MongoDB的测试类</h4>
<p>主要测试MongoDB保存数据,更新字符串,更新数值,更新对象(文档),更新集合,分页查询几个常用方法。</p>
<pre><code class="java">import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.test.context.junit4.SpringRunner;
import com.itdragon.StartApplication;
import com.itdragon.pojo.Address;
import com.itdragon.pojo.User;
import com.itdragon.repository.ITDragonMongoHelper;
/**
* @RunWith 它是一个运行器
* @RunWith(SpringRunner.class) 表示让测试运行于Spring测试环境,不用启动spring容器即可使用Spring环境
* @SpringBootTest(classes=StartApplication.class) 表示将StartApplication.class纳入到测试环境中,若不加这个则提示bean找不到。
* @author itdragon
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes=StartApplication.class)
public class SpringbootStudyApplicationTests {
@Autowired
private ITDragonMongoHelper userMongoHelper; // 命名规则:需和MongodbBeansConfig配置Bean的方法名一致
@Test
public void createUser() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^createUser");
for (int i = 0; i < 25; i++) { // 插入25条数据
User user = new User();
user.setId(Long.valueOf(userMongoHelper.getNextId(User.class.getName())));
user.setAge(25 + i);
user.setName("itdragon-" + i);
Address address = new Address();
address.setId(Long.valueOf(userMongoHelper.getNextId(Address.class.getName())));
address.setProvince("湖北省");
address.setCity("武汉市");
user.setAddress(address);
ArrayList<String> ability = new ArrayList<>();
ability.add("Java");
user.setAbility(ability);
userMongoHelper.saveOrUpdate(user);
System.out.println("user : " + user.toString());
}
}
@Test
public void updateUserName() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^updateUserName");
Map<String, Object> updateMap = new HashMap<>();
// 查询name为itdragon-1的数据,将name修改为ITDragonBlog
User user = (User) userMongoHelper.findOne(Criteria.where("name").is("itdragon-1"));
if (null == user) {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^User non-existent");
return ;
}
updateMap.put("id", user.getId());
updateMap.put("name", "ITDragonBlog");
userMongoHelper.update(updateMap);
}
@Test
public void updateUserAddress() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^updateUserAddress");
Map<String, Object> updateMap = new HashMap<>();
User user = (User) userMongoHelper.findOne(Criteria.where("name").is("itdragon-3"));
if (null == user) {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^User non-existent");
return ;
}
Address address = new Address();
address.setId(Long.valueOf(userMongoHelper.getNextId(Address.class.getName())));
address.setProvince("湖南省");
address.setCity("长沙");
updateMap.put("id", user.getId());
updateMap.put("address", address);
userMongoHelper.update(updateMap);
}
@Test
public void updateUserAbility() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^updateUserAbility");
Map<String, Object> updateMap = new HashMap<>();
User user = (User) userMongoHelper.findOne(Criteria.where("name").is("itdragon-4"));
if (null == user) {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^User non-existent");;
return ;
}
ArrayList<String> abilitys = user.getAbility();
abilitys.add("APP");
updateMap.put("id", user.getId());
updateMap.put("ability", abilitys);
userMongoHelper.update(updateMap);
}
@Test
public void findUserPage() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^^findUserPage");
userMongoHelper.setOrderAscField("age"); // 排序
Integer pageSize = 5; // 每页页数
Integer pageNumber = 1; // 当前页
List<User> users = userMongoHelper.find(Criteria.where("age").gt(25), pageSize, pageNumber); // 查询age大于25的数据
for (User user : users) {
System.out.println("user : " + user.toString());
}
}
}</code></pre>
<h3>MongoDB开发注意事项</h3>
<p>MongoDB对表结构要求不严,方便了我们的开发,同时也提高了犯错率,特别是公司来了新同事,这颗地雷随时都会爆炸。<br>第一点: MongoDB通过key获取value的值。而这个value可以是内嵌的其他文档。因为没有主外键的概念,使用起来非常方便。若嵌套的文档太深,在更新数据是,需要注意不能覆盖原来的值。比如User表中的ability是一个集合,若传一个字符串,依然可以更新成功,但已经破坏了数据结构。这是很多新手容易犯的错。</p>
<p>第二点: 内嵌的文档属性名最好不要重名。举个例子,如果User表中的address对象,也有一个name的属性。那么在后续写代码的过程中,极容易混淆。导致数据更新异常。</p>
<p>第三点: 表的设计尽量做到扁平化,单表设计能有效提高数据库的查询销率。</p>
<p>第四点: 使用Mongoose约束数据结构,当数据结构不一致时操作失败。</p>
<p>前两点足以让一些老辈程序员抓狂,让新来的程序员懵圈。这也是很多开发人员喜欢又讨厌MongoDB的原因。</p>
<h3>总结</h3>
<p>1 MongoDB是最接近关系型数据的非关系型数据库中的文档型数据库。</p>
<p>2 MongoDB支持非常丰富的查询语句,功能强大,但容易犯错。</p>
<p>3 MongoDB表结构的设计需谨慎,尽量减少嵌套层数,各嵌套的文档属性名尽量避免相同。</p>
<h3>参考文档</h3>
<p>MongoDB官方文档: <a href="https://link.segmentfault.com/?enc=ri569RLiNsFhyHn2h0wzNw%3D%3D.zVXL8LVil615HlyhKmS5VS%2Fl0ZZSMSbrreWxXkeqSVQ%3D" rel="nofollow">https://docs.mongodb.com</a></p>
<p>双刃剑MongoDB的学习和避坑到这里就结束了,感谢大家的阅读,欢迎点评。如果你觉得不错,可以"<strong>推荐</strong>"一下。也可以"<strong>关注</strong>"我,获得更多丰富的知识。</p>
Shiro 核心功能案例讲解 基于SpringBoot 有源码
https://segmentfault.com/a/1190000013449167
2018-02-28T21:06:33+08:00
2018-02-28T21:06:33+08:00
itdragon
https://segmentfault.com/u/itdragon
9
<h2>Shiro 核心功能案例讲解 基于SpringBoot 有源码</h2>
<p>从实战中学习Shiro的用法。本章使用SpringBoot快速搭建项目。整合SiteMesh框架布局页面。整合Shiro框架实现用身份认证,授权,数据加密功能。通过本章内容,你将学会用户权限的分配规则,SpringBoot整合Shiro的配置,Shiro自定义Realm的创建,Shiro标签式授权和注解式授权的使用场景,等实战技能,还在等什么,快来学习吧!</p>
<p>技术:SpringBoot,Shiro,SiteMesh,Spring,SpringDataJpa,SpringMVC,Bootstrap-sb-admin-1.0.4<br>说明:前端使用的是Bootstrap-sb-admin模版。注意文章贴出的代码可能不完整,请以github上源码为主,谢谢!<br>源码:<a href="https://link.segmentfault.com/?enc=eakKkoTybGhrIKlyp38IfA%3D%3D.QcPZT4Zf2Ty0CSMGM1wTeTIPZ1r0viUUp%2F4ff7C4t7JR0AwLwwZLXui%2FaObkhQWZ2vvfwZSJYBTAaBRspkeIWw%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a> 喜欢的朋友可以鼓励(star)下。<br>效果图:</p>
<p><img src="/img/remote/1460000013449172?w=1214&h=527" alt="" title=""></p>
<h3>Shiro 功能介绍</h3>
<p>四个核心:登录认证,权限验证,会话管理,数据加密。<br>六个支持:支持WEB开发,支持缓存,支持线程并发验证,支持测试,支持用户切换,支持"记住我"功能。</p>
<p>• <strong>Authentication</strong> :身份认证,也可以理解为登录,验证用户身份。<br>• <strong>Authorization</strong> :权限验证,也可以理解为授权,验证用户是否拥有某个权限;即判断用户是否能进行什么操作。<br>• <strong>Session Manager</strong> :会话管理,用户登录后就是一次会话,在退出前,用户的所有信息都在会话中。<br>• <strong>Cryptography</strong> :数据加密,保护数据的安全性,常见的有密码的加盐加密。<br>• <strong>Web Support</strong> :支持Web开发。<br>• <strong>Caching</strong> :缓存,Shiro将用户信息、拥有的角色/权限数据缓存,以提高程序效率。<br>• <strong>Concurrency</strong> :支持多线程应用的并发验证,即在一个线程中开启另一个线程,Shiro能把权限自动传播过去。<br>• <strong>Testing</strong> :提供测试支持。<br>• <strong>Run As</strong> :允许一个用户以另一个用户的身份进行访问;前提是两个用户运行切换身份。<br>• <strong>Remember Me</strong> :记住我,常见的功能,即登录一次后,在指定时间内免登录。</p>
<p><img src="/img/remote/1460000013449173" alt="Shiro 功能介绍" title="Shiro 功能介绍"></p>
<h3>Shiro 架构介绍</h3>
<p>三个角色:当前用户 Subject,安全管理器 SecurityManager,权限配置域 Realm。</p>
<p>• <strong>Subject</strong> :代表当前用户,提供了很多方法,如login和logout。Subject 只是一个门面,与Subject的所有交互都会委托给SecurityManager,SecurityManager才是真正的执行者;<br>• <strong>SecurityManager</strong> :安全管理器;Shiro的核心,它负责与Shiro的其他组件进行交互,即所有与安全有关的操作都会与SecurityManager 交互;且管理着所有的 Subject;<br>• <strong>Realm</strong> :Shiro 从 Realm 获取安全数据(如用户、角色、权限),SecurityManager 要验证用户身份,必需要从 Realm 获取相应的用户信息,判断用户身份是否合法,判断用户角色或权限是否授权。</p>
<h3>SpringBoot 整合SiteMesh</h3>
<blockquote>SiteMesh 是一个网页布局和修饰的框架,利用它可以将网页的内容和页面结构分离,以达到页面结构共享的目的。</blockquote>
<p>SiteMesh 统一了页面的风格,减少了重复代码,提高了页面的复用率,是一款值得我们去学习的框架(也有很多坑)。当然,今天的主角是Shiro,这里只介绍它的基本用法。<br>SpringBoot 整合SiteMesh只需二个步骤:<br>第一步:配置拦截器FIlter,并在web中注册bean。<br>第二步:创建装饰页面,引入常用的css和js文件,统一系统样式。</p>
<h4>配置拦截器FIlter</h4>
<p>指定拦截的URL请求路径,指定装饰页面的文件全路径,指定不需要拦截的URL请求路径。这里拦截所有请求到装饰页面,只有登录页面和静态资源不拦截。</p>
<pre><code class="java">import org.sitemesh.builder.SiteMeshFilterBuilder;
import org.sitemesh.config.ConfigurableSiteMeshFilter;
/**
* 配置SiteMesh拦截器FIlter,指定装饰页面和不需要拦截的路径
* @author itdragon
*/
public class WebSiteMeshFilter extends ConfigurableSiteMeshFilter{
@Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
builder.addDecoratorPath("/*", "/WEB-INF/layouts/default.jsp") // 配置装饰页面
.addExcludedPath("/static/*") // 静态资源不拦截
.addExcludedPath("/login**"); // 登录页面不拦截
}
}</code></pre>
<pre><code class="java">import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* web.xml 配置
* @author itdragon
*/
@Configuration
public class WebConfig {
@Bean // 配置siteMesh3
public FilterRegistrationBean siteMeshFilter(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
WebSiteMeshFilter siteMeshFilter = new WebSiteMeshFilter();
filterRegistrationBean.setFilter(siteMeshFilter);
return filterRegistrationBean;
}
}</code></pre>
<h4>创建装饰页面</h4>
<p>SiteMesh语法<br><code><sitemesh:write property='title'/></code> : 被修饰页面title的内容会在这里显示。<br><code><sitemesh:write property='head'/></code> : 被修饰页面head的内容会在这里显示,除了title。<br><code><sitemesh:write property='body'/></code> : 被修饰页面body的内容会在这里显示。<br>需要注意的是:SiteMesh的jar有OpenSymphony(最新版是2009年)和Apache(最新版是2015年),两者用法是有差异的。笔者选择的是Apache版本的jar。</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, width=device-width, user-scalable=no" />
<title>ITDragon系统-<sitemesh:write property='title'/></title>
<link type="image/x-icon" href="images/favicon.ico" rel="shortcut icon">
<c:set var="ctx" value="${pageContext.request.contextPath}" />
<link href="${ctx}/static/sb-admin-1.0.4/css/bootstrap.min.css" rel="stylesheet">
<link href="${ctx}/static/sb-admin-1.0.4/css/sb-admin.css" rel="stylesheet">
<sitemesh:write property='head'/>
</head>
<body>
<div id="wrapper">
<%@ include file="/WEB-INF/layouts/header.jsp"%>
<div class='mainBody'>
<sitemesh:write property='body'/>
</div>
</div>
<script src="${ctx}/static/sb-admin-1.0.4/js/jquery.js"></script>
<script src="${ctx}/static/sb-admin-1.0.4/js/bootstrap.min.js"></script>
</body>
</html></code></pre>
<h3>SpringBoot 整合Shiro</h3>
<p>这是本章的核心知识点,SpringBoot 整合Shiro 有三个步骤:<br>第一步:创建实体类:用户,角色,权限。确定三者关系,以方便Realm的授权工作。<br>第二步:创建自定义安全数据源Realm:负责用户登录认证,用户操作授权。<br>第三步:创建Spring整合Shiro配置类:配置拦截规则,生命周期,安全管理器,安全数据源,等。</p>
<h4>创建实体类</h4>
<p>实体类:User,SysRole,SysPermission。<br>权限设计思路:<br>1). 角色表确定系统菜单资源,权限表确定菜单操作资源。<br>2). 用户主要通过角色来获取权限,且一个用户可以拥有多个角色(不推荐,但必须支持该功能)。<br>3). 一个角色可以拥有多个权限,同时也可以有用多个用户。<br>4). 一个权限可以被多个角色使用。<br>5). 工作都是从易到难,我们可以先从“一个用户拥有一个角色,一个角色拥有多个权限”开始。<br>有了上面的分析,三个实体类代码如下,省略了get/set方法。</p>
<pre><code class="java">import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
/**
* 用户实体类
* @author itdragon
*/
@Table(name="itdragon_user_shiro")
@Entity
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id; // 自增长主键,默认ID为1的账号为超级管理员
private String account; // 登录的账号
private String userName; // 注册的昵称
@Transient
private String plainPassword; // 登录时的密码,不持久化到数据库
private String password; // 加密后的密码
private String salt; // 用于加密的盐
private String iphone; // 手机号
private String email; // 邮箱
private String platform; // 用户来自的平台
private String createdDate; // 用户注册时间
private String updatedDate; // 用户最后一次登录时间
@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(name = "SysUserRole", joinColumns = { @JoinColumn(name = "uid") }, inverseJoinColumns ={@JoinColumn(name = "roleId") })
private List<SysRole> roleList; // 一个用户拥有多个角色
private Integer status; // 用户状态,0表示用户已删除
}</code></pre>
<pre><code class="java">import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
/**
* 角色表,决定用户可以访问的页面
* @author itdragon
*/
@Table(name="itdragon_sysrole")
@Entity
public class SysRole {
@Id
@GeneratedValue
private Integer id;
private String role; // 角色
private String description; // 角色描述
private Boolean available = Boolean.FALSE; // 默认不可用
//角色 -- 权限关系:多对多关系; 取出这条数据时,把它关联的数据也同时取出放入内存中
@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
private List<SysPermission> permissions;
// 用户 - 角色关系:多对多关系;
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
private List<User> users;
}</code></pre>
<pre><code class="java">import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
/**
* 权限表,决定用户的具体操作
* @author itdragon
*/
@Table(name = "itdragon_syspermission")
@Entity
public class SysPermission {
@Id
@GeneratedValue
private Integer id;
private String name; // 名称
private String url; // 资源路径
private String permission; // 权限字符串 如:employees:create,employees:update,employees:delete
private Boolean available = Boolean.FALSE; // 默认不可用
@ManyToMany
@JoinTable(name = "SysRolePermission", joinColumns = { @JoinColumn(name = "permissionId") }, inverseJoinColumns = {@JoinColumn(name = "roleId") })
private List<SysRole> roles;
}</code></pre>
<h4>创建自定义安全数据源Realm</h4>
<p>Shiro 从 Realm 获取安全数据(如用户、角色、权限),SecurityManager 身份认证和权限认证都是从Realm中获取相应的用户信息,然后做比较判断是否有身份登录,是否有权限操作。<br>Shiro 支持多个Realm。同时也有不同的认证策略:<br>• <strong>FirstSuccessfulStrategy</strong> : 只要有一个Realm成功就返回,后面的忽略;<br>• <strong>AtLeastOneSuccessfulStrategy</strong> : 只要有一个Realm成功就通过,返回所有认证成功的信息,默认;<br>• <strong>AllSuccessfulStrategy</strong> : 必须所有Realm都成功才算通过</p>
<pre><code class="java">import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.itdragon.pojo.SysPermission;
import com.itdragon.pojo.SysRole;
import com.itdragon.pojo.User;
import com.itdragon.service.UserService;
/**
* 自定义安全数据Realm,重点
* @author itdragon
*/
public class ITDragonShiroRealm extends AuthorizingRealm {
private static final transient Logger log = LoggerFactory.getLogger(ITDragonShiroRealm.class);
@Autowired
private UserService userService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("^^^^^^^^^^^^^^^^^^^^ ITDragon 配置当前用户权限");
String username = (String) principals.getPrimaryPrincipal();
User user = userService.findByAccount(username);
if(null == user){
return null;
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for (SysRole role : user.getRoleList()) {
authorizationInfo.addRole(role.getRole()); // 添加角色
for (SysPermission permission : role.getPermissions()) {
authorizationInfo.addStringPermission(permission.getPermission()); // 添加具体权限
}
}
return authorizationInfo;
}
/**
* 身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
log.info("^^^^^^^^^^^^^^^^^^^^ ITDragon 认证用户身份信息");
String username = (String) token.getPrincipal(); // 获取用户登录账号
User userInfo = userService.findByAccount(username); // 通过账号查加密后的密码和盐,这里一般从缓存读取
if(null == userInfo){
return null;
}
// 1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
// 2). credentials: 加密后的密码.
Object credentials = userInfo.getPassword();
// 3). realmName: 当前 realm 对象的唯一名字. 调用父类的 getName() 方法
String realmName = getName();
// 4). credentialsSalt: 盐值. 注意类型是ByteSource
ByteSource credentialsSalt = ByteSource.Util.bytes(userInfo.getSalt());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}</code></pre>
<h4>创建Spring整合Shiro配置类</h4>
<p>第一步:配置Shiro拦截器,指定URL请求的权限。首先静态资源和登录请求匿名访问,然后是用户登出操作,最后是所有请求都需身份认证。Shiro拦截器优先级是从上到下,切勿将/**=authc,放在前面。<br>第二步:配置Shiro生命周期处理器,<br>第三步:配置自定义Realm,负责身份认证和授权。<br>第四步:配置安全管理器SecurityManager,Shiro的核心。</p>
<pre><code class="java">import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
/**
* Shiro 配置,重点
* @author itdragon
*/
@Configuration
public class ShiroSpringConfig {
private static final transient Logger log = LoggerFactory.getLogger(ShiroSpringConfig.class);
/**
* 配置拦截器
*
* 定义拦截URL权限,优先级从上到下
* 1). anon : 匿名访问,无需登录
* 2). authc : 登录后才能访问
* 3). logout: 登出
* 4). roles : 角色过滤器
*
* URL 匹配风格
* 1). ?:匹配一个字符,如 /admin? 将匹配 /admin1,但不匹配 /admin 或 /admin/;
* 2). *:匹配零个或多个字符串,如 /admin* 将匹配 /admin 或/admin123,但不匹配 /admin/1;
* 2). **:匹配路径中的零个或多个路径,如 /admin/** 将匹配 /admin/a 或 /admin/a/b
*
* 配置身份验证成功,失败的跳转路径
*/
@Bean
public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) {
log.info("^^^^^^^^^^^^^^^^^^^^ ITDragon 配置Shiro拦截工厂");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/static/**", "anon"); // 静态资源匿名访问
filterChainDefinitionMap.put("/employees/login", "anon");// 登录匿名访问
filterChainDefinitionMap.put("/logout", "logout"); // 用户退出,只需配置logout即可实现该功能
filterChainDefinitionMap.put("/**", "authc"); // 其他路径均需要身份认证,一般位于最下面,优先级最低
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setLoginUrl("/login"); // 登录的路径
shiroFilterFactoryBean.setSuccessUrl("/dashboard"); // 登录成功后跳转的路径
shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 验证失败后跳转的路径
return shiroFilterFactoryBean;
}
/**
* 配置Shiro生命周期处理器
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 自动创建代理类,若不添加,Shiro的注解可能不会生效。
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启Shiro的注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/**
* 配置加密匹配,使用MD5的方式,进行1024次加密
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
/**
* 自定义Realm,可以多个
*/
@Bean
public ITDragonShiroRealm itDragonShiroRealm() {
ITDragonShiroRealm itDragonShiroRealm = new ITDragonShiroRealm();
itDragonShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return itDragonShiroRealm;
}
/**
* SecurityManager 安全管理器;Shiro的核心
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(itDragonShiroRealm());
return securityManager;
}
}</code></pre>
<h3>实现业务逻辑</h3>
<p>系统有四个菜单:控制面板 Dashboard,员工管理 Employees,权限管理 Permissions,角色管理 Roles 。<br>系统有三个角色:超级管理员 admin, 经理 manager, 普通员工 staff 。<br>业务的逻辑要求:<br>1) admin角色可以访问所有菜单,manager角色除了Roles菜单外都可以访问,staff角色只能访问Dashboard和Employees菜单 。<br>2) admin角色拥有删除用户信息的权限,其他两个角色没有权限。</p>
<p>实现业务逻辑步骤:<br>第一步:模拟数据,创建用户,角色,权限数据。<br>第二步:左侧菜单权限配置,需要用到Shiro的标签式授权。<br>第三步:在删除用户的Controller层方法上配置操作权限,需要用到Shiro的注解式授权。<br>第四步:权限验证失败统一处理。</p>
<h4>配置数据</h4>
<p>sql文件路径:<a href="https://link.segmentfault.com/?enc=ueUW4cMABOMZ7DGYK1Ug2Q%3D%3D.%2FIrM3N4UXGXXxJAQVnRAoVGawglUqw09T8fz3oki3fIrKExW%2BK9cU3b0EO%2FCIims6Wu3lzKtoLlO1kzej68WMWMv0pVrE0dTiBOdpLiQdWI%3D" rel="nofollow">https://github.com/ITDragonBl...</a><br>建议先执行sql文件,再启动项目。<br>用户密码通常采用加盐加密的方式,笔者采用MD5的加密方式,以UUID作为盐,进行1024次加密。代码如下:</p>
<pre><code class="java">import java.util.UUID;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
import com.itdragon.pojo.User;
/**
* 工具类
* @author itdragon
*/
public class ItdragonUtils {
private static final String ALGORITHM_NAME = "MD5";
private static final Integer HASH_ITERATIONS = 1024;
public static void entryptPassword(User user) {
String salt = UUID.randomUUID().toString();
String temPassword = user.getPlainPassword();
Object md5Password = new SimpleHash(ALGORITHM_NAME, temPassword, ByteSource.Util.bytes(salt), HASH_ITERATIONS);
user.setSalt(salt);
user.setPassword(md5Password.toString());
}
}</code></pre>
<h4>左侧菜单权限配置</h4>
<p>系统使用了SiteMesh框架,左侧菜单页面属于修饰页面的一部分。只需要在一个文件中添加shiro的标签,就可以在整个系统生效,耦合性很低。<br><code><shiro:guest></code> : 允许游客访问的代码块<br><code><shiro:user></code> : 允许已经验证或者通过"记住我"登录的用户才能访问的代码块。<br><code><shiro:authenticated></code> : 只有通过登录操作认证身份,而并非通过"记住我"登录的用户才能访问的代码块。<br><code><shiro:notAuthenticated></code> : 未登录的用户显示的代码块。<br><code><shiro:principal></code> : 显示当前登录的用户信息。<br><code><shiro:hasRole name="admin"></code> : 只有拥有admin角色的用户才能访问的代码块。<br><code><shiro:hasAnyRoles name="admin,manager"></code> : 只有拥有admin或者manager角色的用户才能访问的代码块。<br><code><shiro:lacksRole name="admin"></code> : 没有admin角色的用户显示的代码块<br><code><shiro:hasPermission name="admin:delete"></code> : 只有拥有"admin:delete"权限的用户才能访问的代码块。<br><code><shiro:lacksPermission name="admin:delete"></code> : 没有"admin:delete"权限的用户显示的代码块。</p>
<pre><code class="jsp"><div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav side-nav itdragon-nav">
<li class="active">
<a href="/dashboard"><i class="fa fa-fw fa-dashboard"></i> Dashboard</a>
</li>
<li>
<a href="/employees"><i class="fa fa-fw fa-bar-chart-o"></i> Employees</a>
</li>
<!-- 只有角色为admin或manager的用户才有权限访问 -->
<shiro:hasAnyRoles name="admin,manager">
<li>
<a href="/permission"><i class="fa fa-fw fa-table"></i> Permissions</a>
</li>
</shiro:hasAnyRoles>
<!-- 只有角色为admin的用户才有权限访问 -->
<shiro:hasRole name="admin">
<li>
<a href="/roles"><i class="fa fa-fw fa-file"></i> Roles</a>
</li>
</shiro:hasRole>
</ul>
</div></code></pre>
<h4>在操作上添加权限</h4>
<p>Shiro常见的权限注解有:<br><strong>@RequiresAuthentication</strong> : 表示当前 Subject 已经认证登录的用户才能调用的代码块。<br><strong>@RequiresUser</strong> : 表示当前 Subject 已经身份验证或通过记住我登录的。<br><strong>@RequiresGuest</strong> : 表示当前 Subject 没有身份验证,即是游客身份。<br><strong>@RequiresRoles(value={"admin", "user"}, logical=Logical.AND)</strong> : 表示当前 Subject 需要角色 admin和user<br><strong>@RequiresPermissions (value={"user:update", "user:delete"}, logical= Logical.OR)</strong> : 表示当前 Subject 需要权限 user:update或user:delete。<br>这里值得注意的是:如果你的注解没有生效,很可能没有配置Shiro注解开启的问题。</p>
<pre><code class="java">@RequestMapping(value = "delete/{id}")
@RequiresPermissions(value={"employees:delete"})
public String delete(@PathVariable("id") Long id, RedirectAttributes redirectAttributes) {
userService.deleteUser(id);
redirectAttributes.addFlashAttribute("message", "删除用户成功");
return "redirect:/employees";
}</code></pre>
<h4>权限验证失败统一处理</h4>
<p>Shiro提供权限验证失败跳转页面的功能,但这个逻辑是不友好的。我们需要统一处理权限验证失败,并返回执行失败的页面。</p>
<pre><code class="java">import org.apache.shiro.web.util.WebUtils;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* 异常统一处理
* @author itdragon
*/
@ControllerAdvice
public class ExceptionController {
@ExceptionHandler(org.apache.shiro.authz.AuthorizationException.class)
public String handleException(RedirectAttributes redirectAttributes, Exception exception, HttpServletRequest request) {
redirectAttributes.addFlashAttribute("message", "抱歉!您没有权限执行这个操作,请联系管理员!");
String url = WebUtils.getRequestUri(request);
return "redirect:/" + url.split("/")[1]; // 请求的规则 : /page/operate
}
}</code></pre>
<h3>Shiro和SpringSecurity</h3>
<p>1) Shiro使用更简单,更容易上手。<br>2) Spring Security功能更强大,和Spring无缝整合,但学习门槛比Shiro高。<br>3) 我的建议是两个都可以学习,谁知道公司下一秒会选择什么框架。。。</p>
<h3>总结</h3>
<p>1) Shiro 四个核心功能:身份认证,授权,数据加密,Seesion管理。<br>2) Shiro 三个重要角色:Subject,SecurityManager,Realm。<br>3) Shiro 五个常见开发:自定义Realm,配置拦截器,标签式授权控制菜单,注解式授权控制操作,权限不够异常统一处理。<br>4) 项目搭建推荐从拦截器开始,然后再是身份认证,角色权限认证,操作权限认证。<br>5) Shiro 其他知识后续介绍。</p>
<p>到这里Shiro 核心功能案例讲解 基于SpringBoot 的文章就写完了,一个基本的系统也搭完了。还有很多缺陷和建议,不吝赐教!如果文章对你有帮助,可以点个"推荐",也可以"关注"我,获得更多丰富的知识。</p>
<h3>其他知识查考文献</h3>
<p>Shiro 权限注解 :<a href="https://link.segmentfault.com/?enc=pE7l24ckER5OjUk7dcLBHg%3D%3D.iC0ieXrw4mYrn5ghyi%2BGlL6fkK4dTTjvBMYQauQ37d3QMU72%2BO3j1T7%2BvtqdA0ouXqT7yO5RkypXRewvWW8YVA%3D%3D" rel="nofollow">http://blog.csdn.net/w_strong...</a></p>
<p>Spring @ControllerAdvice注解 : <a href="https://link.segmentfault.com/?enc=7LMh3icieH4I6EqwW%2Fnbvg%3D%3D.Yz19D0CtC4z7MAFWWG4bz1JJ5OsIeMd5RlfeDfGDv5Y1LlAqg5%2Fmfej3WF%2FCgHGgn59Gwt%2BO9QgO4AtFcaFNQg%3D%3D" rel="nofollow">http://blog.csdn.net/jackfrue...</a></p>
<p>bootstrap 模块页面 : <a href="https://link.segmentfault.com/?enc=N%2BlfAa88nORxeHW3%2FQou2Q%3D%3D.JbDKusxsYuXJ8nbdfioUL9uC4oyQWoNfgYsI8iC1gdQfMJk7oNMpx%2B9Ck%2BH12%2FZ2o30dERtI%2Br2otDXCNv5W9g%3D%3D" rel="nofollow">https://startbootstrap.com/te...</a></p>
Java 常用List集合使用场景分析
https://segmentfault.com/a/1190000013262798
2018-02-11T16:10:32+08:00
2018-02-11T16:10:32+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Java 常用List集合使用场景分析</h2>
<p>过年前的最后一篇,本章通过介绍ArrayList,LinkedList,Vector,CopyOnWriteArrayList 底层实现原理和四个集合的区别。让你清楚明白,为什么工作中会常用ArrayList和CopyOnWriteArrayList?了解底层实现原理,我们可以学习到很多代码设计的思路,开阔自己的思维。本章通俗易懂,还在等什么,快来学习吧!</p>
<p>知识图解:</p>
<p><img src="/img/remote/1460000013262803?w=753&h=1061" alt="JavaList集合图解" title="JavaList集合图解"></p>
<p>技术:ArrayList,LinkedList,Vector,CopyOnWriteArrayList<br>说明:本章基于jdk1.8,github上有ArrayList,LinkedList的简单源码代码<br>源码:<a href="https://link.segmentfault.com/?enc=YGt8eXE1PeIlsvPipvgl5A%3D%3D.eGNLxPXAFxbN43cf9h5aju7%2BF%2FudNBP4QQtc1e7GJVNy3IWU9avAkRzvOc%2B8%2BcdWheVd9CSY6VTlFBu9t%2Fx0WQHUCkz9g%2B61dXbwH4q5EDo%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>知识预览</h3>
<p><strong>ArrayList</strong> : 基于数组实现的非线程安全的集合。查询元素快,插入,删除中间元素慢。<br><strong>LinkedList</strong> : 基于链表实现的非线程安全的集合。查询元素慢,插入,删除中间元素快。<br><strong>Vector</strong> : 基于数组实现的线程安全的集合。线程同步(方法被synchronized修饰),性能比ArrayList差。<br><strong>CopyOnWriteArrayList</strong> : 基于数组实现的线程安全的写时复制集合。线程安全(ReentrantLock加锁),性能比Vector高,适合读多写少的场景。</p>
<h4>ArrayList 和 LinkedList 读写快慢的本质</h4>
<p>ArrayList : 查询数据快,是因为数组可以通过<strong>下标</strong>直接找到元素。 写数据慢有两个原因:一是<strong>数组复制</strong>过程需要时间,二是<strong>扩容</strong>需要实例化新数组也需要时间。<br>LinkedList : 查询数据慢,是因为链表需要<strong>遍历</strong>每个元素直到找到为止。 写数据快有一个原因:除了实例化对象需要时间外,只需要<strong>修改指针</strong>即可完成添加和删除元素。<br>本章会通过源码分析,验证上面的说法。</p>
<p>注:这里的块和慢是相对的。并不是LinkedList的插入和删除就一定比ArrayList快。明白其快慢的本质:<strong>ArrayList快在定位,慢在数组复制。LinkedList慢在定位,快在指针修改</strong>。</p>
<h3>ArrayList</h3>
<p>ArrayList 是基于<strong>动态数组</strong>实现的<strong>非线程安全</strong>的集合。当底层数组满的情况下还在继续添加的元素时,ArrayList则会执行扩容机制扩大其数组长度。ArrayList查询速度非常快,使得它在实际开发中被广泛使用。美中不足的是插入和删除元素较慢,同时它并不是线程安全的。<br>我们可以从源码中找到答案</p>
<pre><code class="java">// 查询元素
public E get(int index) {
rangeCheck(index); // 检查是否越界
return elementData(index);
}
// 顺序添加元素
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 扩容机制
elementData[size++] = e;
return true;
}
// 从数组中间添加元素
public void add(int index, E element) {
rangeCheckForAdd(index); // 数组下标越界检查
ensureCapacityInternal(size + 1); // 扩容机制
System.arraycopy(elementData, index, elementData, index + 1, size - index); // 复制数组
elementData[index] = element; // 替换元素
size++;
}
// 从数组中删除元素
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
}</code></pre>
<p>从源码中可以得知,<br>ArrayList在执行查询操作时:<br>第一步:先判断下标是否越界。<br>第二步:然后在直接通过下标从数组中返回元素。</p>
<p>ArrayList在执行顺序添加操作时:<br>第一步:通过扩容机制判断原数组是否还有空间,若没有则重新实例化一个空间更大的新数组,把旧数组的数据拷贝到新数组中。<br>第二步:在新数组的最后一位元素添加值。</p>
<p>ArrayList在执行中间插入操作时:<br>第一步:先判断下标是否越界。<br>第二步:扩容。<br>第三步:若插入的下标为i,则通过复制数组的方式将i后面的所有元素,往后移一位。<br>第四步:新数据替换下标为i的旧元素。<br>删除也是一样:只是数组往前移了一位,最后一个元素设置为null,等待JVM垃圾回收。</p>
<p>从上面的源码分析,我们可以得到一个结论和一个疑问。<br>结论是:ArrayList快在下标定位,慢在数组复制。<br>疑问是:能否将每次扩容的长度设置大点,减少扩容的次数,从而提高效率?其实每次扩容的长度大小是很有讲究的。若扩容的长度太大,会造成大量的闲置空间;若扩容的长度太小,会造成频发的扩容(数组复制),效率更低。</p>
<h3>LinkedList</h3>
<p>LinkedList 是基于双向链表实现的非线程安全的集合,它是一个链表结构,不能像数组一样随机访问,必须是每个元素依次遍历直到找到元素为止。其结构的特殊性导致它查询数据慢。<br>我们可以从源码中找到答案</p>
<pre><code class="java">// 查询元素
public E get(int index) {
checkElementIndex(index); // 检查是否越界
return node(index).item;
}
Node<E> node(int index) {
if (index < (size >> 1)) { // 类似二分法
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// 插入元素
public void add(int index, E element) {
checkPositionIndex(index); // 检查是否越界
if (index == size) // 在链表末尾添加
linkLast(element);
else // 在链表中间添加
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}</code></pre>
<p>从源码中可以得知,<br>LinkedList在执行查询操作时:<br>第一步:先判断元素是靠近头部,还是靠近尾部。<br>第二步:若靠近头部,则从头部开始依次查询判断。和ArrayList的<code>elementData(index)</code>相比当然是慢了很多。</p>
<p>LinkedList在插入元素的思路:<br>第一步:判断插入元素的位置是链表的尾部,还是中间。<br>第二步:若在链表尾部添加元素,直接将尾节点的下一个指针指向新增节点。<br>第三步:若在链表中间添加元素,先判断插入的位置是否为首节点,是则将首节点的上一个指针指向新增节点。否则先获取当前节点的上一个节点(简称A),并将A节点的下一个指针指向新增节点,然后新增节点的下一个指针指向当前节点。</p>
<h3>Vector</h3>
<p>Vector 的数据结构和使用方法与ArrayList差不多。最大的不同就是Vector是线程安全的。从下面的源码可以看出,几乎所有的对数据操作的方法都被synchronized关键字修饰。synchronized是线程同步的,当一个线程已经获得Vector对象的锁时,其他线程必须等待直到该锁被释放。从这里就可以得知Vector的性能要比ArrayList低。<br>若想要一个高性能,又是线程安全的ArrayList,可以使用<code>Collections.synchronizedList(list);</code>方法或者使用CopyOnWriteArrayList集合</p>
<pre><code class="java">public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized boolean removeElement(Object obj) {
modCount++;
int i = indexOf(obj);
if (i >= 0) {
removeElementAt(i);
return true;
}
return false;
}</code></pre>
<h3>CopyOnWriteArrayList</h3>
<p>在这里我们先简单了解一下CopyOnWrite容器。它是一个写时复制的容器。当我们往一个容器添加元素的时候,不是直接往当前容器添加,而是先将当前容器进行copy一份,复制出一个新的容器,然后对新容器里面操作元素,最后将原容器的引用指向新的容器。所以CopyOnWrite容器是一种读写分离的思想,读和写不同的容器。<br>应用场景:适合高并发的读操作(读多写少)。若写的操作非常多,会频繁复制容器,从而影响性能。</p>
<p>CopyOnWriteArrayList 写时复制的集合,在执行写操作(如:add,set,remove等)时,都会将原数组拷贝一份,然后在新数组上做修改操作。最后集合的引用指向新数组。<br>CopyOnWriteArrayList 和Vector都是线程安全的,不同的是:前者使用ReentrantLock类,后者使用synchronized关键字。ReentrantLock提供了更多的锁投票机制,在锁竞争的情况下能表现更佳的性能。就是它让JVM能更快的调度线程,才有更多的时间去执行线程。这就是为什么CopyOnWriteArrayList的性能在大并发量的情况下优于Vector的原因。</p>
<pre><code class="java">private E get(Object[] a, int index) {
return (E) a[index];
}
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
......
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1, newElements, index, len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}</code></pre>
<h3>总结</h3>
<p>看到这里,如果面试官问你ArrayList和LinkedList有什么区别时<br>如果你回答:ArrayList查询快,写数据慢;LinkedList查询慢,写数据快。面试官只能算你勉强合格。<br>如果你回答:ArrayList查询快是因为底层是由数组实现,通过下标定位数据快。写数据慢是因为复制数组耗时。LinkedList底层是双向链表,查询数据依次遍历慢。写数据只需修改指针引用。<br>如果你继续回答:ArrayList和LinkedList都不是线程安全的,小并发量的情况下可以使用Vector,若并发量很多,且读多写少可以考虑使用CopyOnWriteArrayList。<br>因为CopyOnWriteArrayList底层使用ReentrantLock锁,比使用synchronized关键字的Vector能更好的处理锁竞争的问题。<br>面试官会认为你是一个基础扎实,内功深厚的人才!!!</p>
<p>到这里Java 常用List集合使用场景分析就结束了。过年前的最后一篇博客,有点浮躁,可能身在职场,心在老家!最后祝大家新年快乐!!!狗年吉祥!!!大富大贵!!!可能都没人看博客了 ⊙﹏⊙‖∣ 哈哈哈哈(ಡωಡ)hiahiahia</p>
Netty 编解码技术 数据通信和心跳监控案例
https://segmentfault.com/a/1190000013122610
2018-02-04T08:20:22+08:00
2018-02-04T08:20:22+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Netty 编解码技术 数据通信和心跳监控案例</h2>
<p>多台服务器之间在进行跨进程服务调用时,需要使用特定的编解码技术,对需要进行网络传输的对象做编码和解码操作,以便完成远程调用。Netty提供了完善,易扩展,易使用的编解码技术。本章除了介绍Marshalling的使用,还会基于编解码技术实现数据通信和心跳检测案例。通过本章,你将学到Java序列化的优缺点,主流编解码框架的特点,模拟特殊长连接通信,心跳监控案例。还在等什么,丰满的知识等你来拿!</p>
<p>技术:编解码,数据通信,心跳监控<br>说明:github上有完整代码,部分文字描述<strong>摘录《Netty权威指南》</strong><br>源码:<a href="https://link.segmentfault.com/?enc=e4MBpOwTXhyIUh24VHS0Rw%3D%3D.%2Bcfj9AcSbldOMzX24Tim1dBCuuAVr0OyqjMjWgQRrgTk2WkpOik9McCvNxJmwRGm0Qe9dKsDi337AEHaym0nC4A7sQzBFjzXfMkp8Zz5twA%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>编解码</h3>
<p>Netty 的一大亮点就是<strong>使用简单</strong>,将常用的功能和API进行了很好的封装,编解码也不例外。针对编解码功能,Netty提供了<strong>通用的编解码框架</strong>和<strong>常用的编解码类库</strong>,方便用户扩展和使用。从而降低用户的工作量和开发门槛。在io.netty.handler.codec目录下找到很多预置的编解码功能.<br>其实在<a href="https://link.segmentfault.com/?enc=Ejd5Y814lmthlVd9CRfllA%3D%3D.xM6bnIq%2BWhk6Zap9xu4bNJxWi6Jabsk2gMAWyni0SaUBaUi87dkrPoTfZrERJwFz" rel="nofollow">上一章的知识点中</a>,就已经使用了Netty的编解码技术,如:DelimiterBasedFrameDecoder,FixedLengthFrameDecoder,StringDecoder</p>
<h4>什么是编解码技术</h4>
<p><strong>编码(Encode)</strong>也称序列化(serialization),将对象序列化为字节数组,用于网络传输、数据持久化等用途。<br><strong>解码(Decode)</strong>也称反序列化(deserialization),把从网络、磁盘等读取的字节数组还原成原始对象,以方便后续的业务逻辑操作。</p>
<h4>主流编解码框架</h4>
<h5>Java序列化</h5>
<p>Java序列化<strong>使用简单</strong>,<strong>开发难度低</strong>。只需要实现java.io.Serializable接口并生成序列化ID,这个类就能够通过java.io.ObjectInput序列化和java.io.ObjectOutput反序列化。<br>但它也有存在很多缺点 : <br>1 无法跨语言(java的序列化是java语言内部的私有协议,其他语言并不支持),<br>2 序列化后码流太大(采用二进制编解码技术要比java原生的序列化技术强),<br>3 序列化性能太低</p>
<h5>JBoss的Marshalling</h5>
<p>JBoss的Marshalling是一个Java对象的序列化API包,<strong>修正</strong>了JDK自带序列化包的很多问题,又<strong>兼容</strong>java.io.Serializable接口;同时可通过工厂类进行参数和特性地配置。<br>1) 可插拔的类解析器,提供更加便捷的类加载定制策略,通过一个接口即可实现定制;<br>2) 可插拔的对象替换技术,不需要通过继承的方式;<br>3) 可插拔的预定义类缓存表,可以减小序列化的字节数组长度,提升常用类型的对象序列化性能;<br>4) 无须实现java.io.Serializable接口,即可实现Java序列化;<br>5) 通过缓存技术提升对象的序列化性能。<br>6) 使用范围小,通用性较差。</p>
<h5>Google的Protocol Buffers</h5>
<p>Protocol Buffers由谷歌开源而来。将数据结构以 .proto 文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。<br>1) 结构化数据存储格式(XML,JSON等);<br>2) 高效的编解码性能;<br>3) 平台无关、扩展性好;<br>4) 官方支持Java、C++和Python三种语言。</p>
<h5>MessagePack框架</h5>
<p>MessagePack是一个高效的二进制序列化格式。和JSON一样跨语言交换数据。但是它比JSON更快、更小(It's like JSON.but fast and small)。<br>1) 高效的编解码性能;<br>2) 跨语言;<br>3) 序列化后码流小;</p>
<p>Marshalling 配置工厂</p>
<pre><code class="java">package com.itdragon.marshalling;
import io.netty.handler.codec.marshalling.DefaultMarshallerProvider;
import io.netty.handler.codec.marshalling.DefaultUnmarshallerProvider;
import io.netty.handler.codec.marshalling.MarshallerProvider;
import io.netty.handler.codec.marshalling.MarshallingDecoder;
import io.netty.handler.codec.marshalling.MarshallingEncoder;
import io.netty.handler.codec.marshalling.UnmarshallerProvider;
import org.jboss.marshalling.MarshallerFactory;
import org.jboss.marshalling.Marshalling;
import org.jboss.marshalling.MarshallingConfiguration;
public final class ITDragonMarshallerFactory {
private static final String NAME = "serial"; // serial表示创建的是 Java序列化工厂对象.由jboss-marshalling-serial提供
private static final Integer VERSION = 5;
private static final Integer MAX_OBJECT_SIZE = 1024 * 1024 * 1; // 单个对象最大长度
/**
* 创建Jboss Marshalling 解码器MarshallingDecoder
*/
public static MarshallingDecoder buildMarshallingDecoder() {
// step1 通过工具类 Marshalling,获取Marshalling实例对象,参数serial 标识创建的是java序列化工厂对象
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory(NAME);
// step2 初始化Marshalling配置
final MarshallingConfiguration configuration = new MarshallingConfiguration();
// step3 设置Marshalling版本号
configuration.setVersion(VERSION);
// step4 初始化生产者
UnmarshallerProvider provider = new DefaultUnmarshallerProvider(marshallerFactory, configuration);
// step5 通过生产者和单个消息序列化后最大长度构建 Netty的MarshallingDecoder
MarshallingDecoder decoder = new MarshallingDecoder(provider, MAX_OBJECT_SIZE);
return decoder;
}
/**
* 创建Jboss Marshalling 编码器MarshallingEncoder
*/
public static MarshallingEncoder builMarshallingEncoder() {
final MarshallerFactory marshallerFactory = Marshalling.getProvidedMarshallerFactory(NAME);
final MarshallingConfiguration configuration = new MarshallingConfiguration();
configuration.setVersion(VERSION);
MarshallerProvider provider = new DefaultMarshallerProvider(marshallerFactory, configuration);
MarshallingEncoder encoder = new MarshallingEncoder(provider);
return encoder;
}
}</code></pre>
<h3>数据通信</h3>
<p>一个网络应用最重要的工作莫过于数据的传输。两台机器之间如何建立连接才能提高服务器利用率,减轻服务器的压力。这都是我们值得去考虑的问题。</p>
<h4>常见的三种通信模式</h4>
<p>1) <strong>长连接</strong>:服务器和客户端的通道一直处于开启状态。合适服务器性能好,客户端数量少的场景。<br>2) <strong>短连接</strong>:只有在发送数据时建立连接,数据发送完后断开连接。一般将数据保存在本地,根据某种逻辑一次性批量提交。适合对实时性不高的应用场景。<br>3) <strong>特殊长连接</strong>:它拥有长连接的特性。当在服务器指定时间内,若没有任何通信,连接就会断开。若客户端再次向服务端发送请求,则需重新建立连接。主要为减小服务端资源占用。<br>本章重点介特殊长连接。它的设计思想在很多场景中都有,比如QQ的离开状态,电脑的休眠状态。既保证了用户的正常使用,又减轻了服务器的压力。是实际开发中比较常用的通信模式。<br>它有三个情况:<br><strong>一、服务器和客户端的通道一直处于开启状态。</strong><br><strong>二、指定时间内没有通信则断开连接。</strong><br><strong>三、客户端重新发起请求,则重新建立连接。</strong></p>
<p>结合上面的Marshalling 配置工厂,模拟特殊长连接的通信场景。<br>客户端代码,辅助启动类Bootstrap,配置编解码事件,超时事件,自定义事件。客户端发送请求分两中情况,通信连接时请求和连接断开后请求。<a href="https://link.segmentfault.com/?enc=SyahQMNpLACU1Kbm9xYLdw%3D%3D.MywDg3gjN36Nkazg5q1p0wQnuIfq%2FfauT7FwEI02MT%2BxRaY4BvTFbexxuQrDCftn" rel="nofollow">上一章有详细的配置说明</a></p>
<pre><code class="java">package com.itdragon.marshalling;
import java.io.File;
import java.io.FileInputStream;
import java.util.concurrent.TimeUnit;
import com.itdragon.utils.ITDragonUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;
public class ITDragonClient {
private static final Integer PORT = 8888;
private static final String HOST = "127.0.0.1";
private EventLoopGroup group = null;
private Bootstrap bootstrap = null;
private ChannelFuture future = null;
private static class SingletonHolder {
static final ITDragonClient instance = new ITDragonClient();
}
public static ITDragonClient getInstance(){
return SingletonHolder.instance;
}
public ITDragonClient() {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
try {
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(ITDragonMarshallerFactory.buildMarshallingDecoder()); // 配置编码器
socketChannel.pipeline().addLast(ITDragonMarshallerFactory.builMarshallingEncoder()); // 配置解码器
socketChannel.pipeline().addLast(new ReadTimeoutHandler(5)); // 表示5秒内没有连接后断开
socketChannel.pipeline().addLast(new ITDragonClientHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 1024);
} catch (Exception e) {
e.printStackTrace();
}
}
public void connect(){
try {
future = bootstrap.connect(HOST, PORT).sync();
System.out.println("连接远程服务器......");
} catch (Exception e) {
e.printStackTrace();
}
}
public ChannelFuture getChannelFuture(){
if(this.future == null || !this.future.channel().isActive()){
this.connect();
}
return this.future;
}
/**
* 特殊长连接:
* 1. 服务器和客户端的通道一直处于开启状态,
* 2. 在服务器指定时间内,没有任何通信,则断开,
* 3. 客户端再次向服务端发送请求则重新建立连接,
* 4. 从而减小服务端资源占用压力。
*/
public static void main(String[] args) {
final ITDragonClient client = ITDragonClient.getInstance();
try {
ChannelFuture future = client.getChannelFuture();
// 1. 服务器和客户端的通道一直处于开启状态,
for(Long i = 1L; i <= 3L; i++ ){
ITDragonReqData reqData = new ITDragonReqData();
reqData.setId(i);
reqData.setName("ITDragon-" + i);
reqData.setRequestMsg("NO." + i + " Request");
future.channel().writeAndFlush(reqData);
TimeUnit.SECONDS.sleep(2); // 2秒请求一次,服务器是5秒内没有请求则会断开连接
}
// 2. 在服务器指定时间内,没有任何通信,则断开,
Thread.sleep(6000);
// 3. 客户端再次向服务端发送请求则重新建立连接,
new Thread(new Runnable() {
public void run() {
try {
System.out.println("唤醒......");
ChannelFuture cf = client.getChannelFuture();
System.out.println("连接是否活跃 : " + cf.channel().isActive());
System.out.println("连接是否打开 : " + cf.channel().isOpen());
ITDragonReqData reqData = new ITDragonReqData();
reqData.setId(4L);
reqData.setName("ITDragon-picture");
reqData.setRequestMsg("断开的通道被唤醒了!!!!");
// 路径path自定义
String path = System.getProperty("user.dir") + File.separatorChar +
"sources" + File.separatorChar + "itdragon.jpg";
File file = new File(path);
FileInputStream inputStream = new FileInputStream(file);
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
reqData.setAttachment(ITDragonUtil.gzip(data));
cf.channel().writeAndFlush(reqData);
cf.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
future.channel().closeFuture().sync();
System.out.println("断开连接,主线程结束.....");
} catch (Exception e) {
e.printStackTrace();
}
}
}</code></pre>
<p>客户端自定义事务代码,负责将服务器返回的数据打印出来</p>
<pre><code class="java">package com.itdragon.marshalling;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ITDragonClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Netty Client active ^^^^^^");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
ITDragonRespData responseData = (ITDragonRespData) msg;
System.out.println("Netty Client : " + responseData.toString());
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}</code></pre>
<p>服务器代码,辅助启动类ServerBootstrap,配置日志打印事件,编解码事件,超时控制事件,自定义事件。</p>
<pre><code class="java">package com.itdragon.marshalling;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.ReadTimeoutHandler;
public class ITDragonServer {
private static final Integer PORT = 8888;
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(ITDragonMarshallerFactory.buildMarshallingDecoder()); // 配置解码器
socketChannel.pipeline().addLast(ITDragonMarshallerFactory.builMarshallingEncoder()); // 配置编码器
socketChannel.pipeline().addLast(new ReadTimeoutHandler(5)); // 传入的参数单位是秒,表示5秒内没有连接后断开
socketChannel.pipeline().addLast(new ITDragonServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.bind(PORT).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}</code></pre>
<p>服务器自定义事件代码,负责接收客户端传输的数据,若有附件则下载到receive目录下(这里只是简单的下载逻辑)。</p>
<pre><code class="java">package com.itdragon.marshalling;
import java.io.File;
import java.io.FileOutputStream;
import com.itdragon.utils.ITDragonUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ITDragonServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Netty Server active ......");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
// 获取客户端传来的数据
ITDragonReqData requestData = (ITDragonReqData) msg;
System.out.println("Netty Server : " + requestData.toString());
// 处理数据并返回给客户端
ITDragonRespData responseData = new ITDragonRespData();
responseData.setId(requestData.getId());
responseData.setName(requestData.getName() + "-SUCCESS!");
responseData.setResponseMsg(requestData.getRequestMsg() + "-SUCCESS!");
// 如果有附件则保存附件
if (null != requestData.getAttachment()) {
byte[] attachment = ITDragonUtil.ungzip(requestData.getAttachment());
String path = System.getProperty("user.dir") + File.separatorChar + "receive" +
File.separatorChar + System.currentTimeMillis() + ".jpg";
FileOutputStream outputStream = new FileOutputStream(path);
outputStream.write(attachment);
outputStream.close();
responseData.setResponseMsg("file upload success , file path is : " + path);
}
ctx.writeAndFlush(responseData);
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}</code></pre>
<p>ITDragonReqData 和 ITDragonRespData 实体类的代码就不贴出来了,github上有源码。</p>
<h3>心跳监控案例</h3>
<p>在分布式,集群系统架构中,我们需要定时获取各机器的资源使用情况和服务器之间是否保持正常连接状态。以便能在最短的时间内避免和处理问题。类比集群中的哨兵模式。</p>
<h4>获取本机数据</h4>
<p>可以通过第三方sigar.jar的帮助,获取主机的运行时信息,包括操作系统、CPU使用情况、内存使用情况、硬盘使用情况以及网卡、网络信息。使用很简单,根据自己电脑的系统选择对应的dll文件,然后拷贝到C:WindowsSystem32 目录下即可。比如windows7 64位操作系统,则需要sigar-amd64-winnt.dll文件。<br>下载路径:<a href="https://link.segmentfault.com/?enc=qz6mGNu6r2cW0I%2FMxBNShg%3D%3D.toZcBXrjFhIt1Mda13mjIzFz7F97yfx9vgPxmrRZB6xofiZgoy9lddzuK6opChKJ" rel="nofollow">https://pan.baidu.com/s/1jJSaucI</a> 密码: 48d2 </p>
<p>ITDragonClient.java,ITDragonCoreParam.java,ITDragonRequestInfo.java,ITDragonServer.java,ITDragonSigarUtil.java,pom.xml 的代码这里就不贴出来了,github上面有完整的源码。</p>
<p>客户端自定义事件代码,负责发送认证信息,定时向服务器发送cpu信息和内存信息。</p>
<pre><code class="java">package com.itdragon.monitoring;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.hyperic.sigar.CpuPerc;
import org.hyperic.sigar.Mem;
import org.hyperic.sigar.Sigar;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ITDragonClientHandler extends ChannelInboundHandlerAdapter{
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private ScheduledFuture<?> heartBeat;
private InetAddress addr ; //主动向服务器发送认证信息
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Client 连接一开通就开始验证.....");
addr = InetAddress.getLocalHost();
String ip = addr.getHostAddress();
System.out.println("ip : " + ip);
String key = ITDragonCoreParam.SALT_KEY.getValue(); // 假装进行了很复杂的加盐加密
// 按照Server端的格式,传递令牌
String auth = ip + "," + key;
ctx.writeAndFlush(auth);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
if(msg instanceof String){
String result = (String) msg;
if(ITDragonCoreParam.AUTH_SUCCESS.getValue().equals(result)){
// 验证成功,每隔10秒,主动发送心跳消息
this.heartBeat = this.scheduler.scheduleWithFixedDelay(new HeartBeatTask(ctx), 0, 10, TimeUnit.SECONDS);
System.out.println(msg);
}
else {
System.out.println(msg);
}
}
} finally {
ReferenceCountUtil.release(msg);
}
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
if (heartBeat != null) {
heartBeat.cancel(true);
heartBeat = null;
}
ctx.fireExceptionCaught(cause);
}
}
class HeartBeatTask implements Runnable {
private final ChannelHandlerContext ctx;
public HeartBeatTask(final ChannelHandlerContext ctx) {
this.ctx = ctx;
}
public void run() {
try {
// 采用sigar 获取本机数据,放入实体类中
ITDragonRequestInfo info = new ITDragonRequestInfo();
info.setIp(InetAddress.getLocalHost().getHostAddress()); // ip
Sigar sigar = new Sigar();
CpuPerc cpuPerc = sigar.getCpuPerc();
HashMap<String, Object> cpuPercMap = new HashMap<String, Object>();
cpuPercMap.put(ITDragonCoreParam.COMBINED.getValue(), cpuPerc.getCombined());
cpuPercMap.put(ITDragonCoreParam.USER.getValue(), cpuPerc.getUser());
cpuPercMap.put(ITDragonCoreParam.SYS.getValue(), cpuPerc.getSys());
cpuPercMap.put(ITDragonCoreParam.WAIT.getValue(), cpuPerc.getWait());
cpuPercMap.put(ITDragonCoreParam.IDLE.getValue(), cpuPerc.getIdle());
Mem mem = sigar.getMem();
HashMap<String, Object> memoryMap = new HashMap<String, Object>();
memoryMap.put(ITDragonCoreParam.TOTAL.getValue(), mem.getTotal() / 1024L);
memoryMap.put(ITDragonCoreParam.USED.getValue(), mem.getUsed() / 1024L);
memoryMap.put(ITDragonCoreParam.FREE.getValue(), mem.getFree() / 1024L);
info.setCpuPercMap(cpuPercMap);
info.setMemoryMap(memoryMap);
ctx.writeAndFlush(info);
} catch (Exception e) {
e.printStackTrace();
}
}
} </code></pre>
<p>服务器自定义事件代码,负责接收客户端传输的数据,验证令牌是否失效,打印客户端传来的数据。</p>
<pre><code class="java">package com.itdragon.monitoring;
import java.util.HashMap;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class ITDragonServerHandler extends ChannelInboundHandlerAdapter {
// 令牌验证的map,key为ip地址,value为密钥
private static HashMap<String, String> authMap = new HashMap<String, String>();
// 模拟数据库查询
static {
authMap.put("xxx.xxx.x.x", "xxx");
authMap.put(ITDragonCoreParam.CLIENT_HOST.getValue(), ITDragonCoreParam.SALT_KEY.getValue());
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("Netty Server Monitoring.......");
}
// 模拟api请求前的验证
private boolean auth(ChannelHandlerContext ctx, Object msg) {
System.out.println("令牌验证...............");
String[] ret = ((String) msg).split(",");
String clientIp = ret[0]; // 客户端ip地址
String saltKey = ret[1]; // 数据库保存的客户端密钥
String auth = authMap.get(clientIp); // 客户端传来的密钥
if (null != auth && auth.equals(saltKey)) {
ctx.writeAndFlush(ITDragonCoreParam.AUTH_SUCCESS.getValue());
return true;
} else {
ctx.writeAndFlush(ITDragonCoreParam.AUTH_ERROR.getValue()).addListener(ChannelFutureListener.CLOSE);
return false;
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果传来的消息是字符串,则先验证
if (msg instanceof String) {
auth(ctx, msg);
} else if (msg instanceof ITDragonRequestInfo) {
ITDragonRequestInfo info = (ITDragonRequestInfo) msg;
System.out.println("--------------------------------------------");
System.out.println("当前主机ip为: " + info.getIp());
HashMap<String, Object> cpu = info.getCpuPercMap();
System.out.println("cpu 总使用率: " + cpu.get(ITDragonCoreParam.COMBINED.getValue()));
System.out.println("cpu 用户使用率: " + cpu.get(ITDragonCoreParam.USER.getValue()));
System.out.println("cpu 系统使用率: " + cpu.get(ITDragonCoreParam.SYS.getValue()));
System.out.println("cpu 等待率: " + cpu.get(ITDragonCoreParam.WAIT.getValue()));
System.out.println("cpu 空闲率: " + cpu.get(ITDragonCoreParam.IDLE.getValue()));
HashMap<String, Object> memory = info.getMemoryMap();
System.out.println("内存总量: " + memory.get(ITDragonCoreParam.TOTAL.getValue()));
System.out.println("当前内存使用量: " + memory.get(ITDragonCoreParam.USED.getValue()));
System.out.println("当前内存剩余量: " + memory.get(ITDragonCoreParam.FREE.getValue()));
System.out.println("--------------------------------------------");
ctx.writeAndFlush("info received!");
} else {
ctx.writeAndFlush("connect failure!").addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}</code></pre>
<h3>总结</h3>
<p>1 <strong>Netty的编解码功能很好的解决了Java序列化 无法跨语言,序列化后码流太大,序列化性能太低等问题</strong><br>2 <strong>JBoss的Marshalling是一个Java对象的序列化API包,修正了JDK自带的序列化包的很多问题,又兼容java.io.Serializable接口,缺点使用范围小。</strong><br>3 <strong>特殊长连接可以减小服务端资源占用压力,是一种比较常用的数据通信方式。</strong><br>4 <strong>Netty可以用做心跳监测,定时获取被监听机器的数据信息。</strong></p>
<h3>推荐文档</h3>
<p>Netty 能做什么?学Netty有什么用?<br><a href="https://link.segmentfault.com/?enc=JK0cFyJ9ocaKWRdfP%2FzoXg%3D%3D.OX3oz9z%2BEGXv4ZeueengU9p0QdLgZHSUdK7Xs1KQBVAX%2BfkscPJTudwDF4p6geD1" rel="nofollow">https://www.zhihu.com/questio...</a><br><a href="https://link.segmentfault.com/?enc=sxJj4W%2FY3hNf8suAM2uxhQ%3D%3D.%2F8fOc%2BHjiByN7siCJ6zTJ4HPxS6RUKrurzT6KkC1o4rbBnRt6FKQ5Mzch227YWjTV%2BVDxXyra0aIlR3KBvUuxg%3D%3D" rel="nofollow">http://blog.csdn.net/broadvie...</a><br>Marshalling : <br><a href="https://link.segmentfault.com/?enc=RiVRSfxZkg9B71i22asfmw%3D%3D.zXERSjvcb719t8xQvvfOrMaQzfLt%2Byib%2BVbxVVnLWanwQ9eB0%2Fj233s%2Be8Jjn6Wu" rel="nofollow">http://jbossmarshalling.jboss...</a></p>
<p>Netty 编解码数据通信和心跳监控案例到这里就结束了,感谢大家的阅读,欢迎点评。如果你觉得不错,可以"<strong>推荐</strong>"一下。也可以"<strong>关注</strong>"我,获得更多丰富的知识。</p>
Netty 拆包粘包和服务启动流程分析
https://segmentfault.com/a/1190000013039327
2018-01-29T20:15:26+08:00
2018-01-29T20:15:26+08:00
itdragon
https://segmentfault.com/u/itdragon
4
<h2>Netty 拆包粘包和服务启动流程分析</h2>
<p>通过本章学习,笔者希望你能掌握EventLoopGroup的工作流程,ServerBootstrap的启动流程,ChannelPipeline是如何操作管理Channel。只有清楚这些,才能更好的了解和使用Netty。还在等什么,快来学习吧!</p>
<p>知识结构图:</p>
<p><img src="/img/remote/1460000013039330?w=1241&h=726" alt="Netty" title="Netty"></p>
<p>技术:Netty,拆包粘包,服务启动流程<br>说明:若你对NIO有一定的了解,对于本章知识来说有很大的帮助!<a href="https://link.segmentfault.com/?enc=d7%2FutrMneapsgoPjePvH4Q%3D%3D.CUeJQv54hmONSm5BUzw684Uiy6Qm3y28PVY5nnn0JPXL7bWdeza54zSK7zhbGmEy" rel="nofollow">NIO教程</a><br>源码:<a href="https://link.segmentfault.com/?enc=qSJs4eZm02rSG6T%2Bt1G%2Fuw%3D%3D.%2Bm5ngrN7OxXJ8ZX%2BB8s6XcaLjASERAKDCGYow6BtE5iG79bqtiOSEw4NnaJnDQW6OTwGi0DynFeoLZXpTTa0dZZeIli%2Bdb964FIQhHWOd38%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>Netty 重要组件</h3>
<p>这里让你清楚了解 ChannelPipeline,ChannelHandlerContext,ChannelHandler,Channel 四者之间的关系。<br>这里让你清楚了解 NioEventLoopGroup,NioEventLoop,Channel 三者之间的关系。<br>这里让你清楚了解 ServerBootstrap,Channel 两者之间的关系。<br>看懂了这块的理论知识,后面Netty拆包粘包的代码就非常的简单。</p>
<h4>Channel</h4>
<p><strong>Channel</strong> : Netty最核心的接口。NIO通讯模式中通过Channel进行Socket套接字的读,写和同时读写操作。<br><strong>ChannelHandler</strong> : 因为直接使用Channel会比较麻烦,所以在Netty编程中通过ChannelHandler间接操作Channel,从而简化开发。<br><strong>ChannelPipeline</strong> : 可以理解为一个管理ChandlerHandler的链表。对Channel进行操作时,Pipeline负责从尾部依次调用每一个Handler进行处理。每个Channel都有一个属于自己的ChannelPipeline。<br><strong>ChannelHandlerContext</strong> : ChannelPipeline通过ChannelHandlerContext间接管理每个ChannelHandler。</p>
<p>如下图所示,结合代码,在服务器初始化和客户端创建连接的过程中加了四个Handler,分别是日志事务,字符串分割解码器,接受参数转字符串解码器,处理任务的Handler。<br><img src="/img/remote/1460000013039331?w=902&h=194" alt="" title=""></p>
<h4>NioEventLoopGroup</h4>
<p><strong>EventLoopGroup</strong> : 本质是个线程池,继承了ScheduledExecutorService 定时任务线程池。<br><strong>NioEventLoopGroup</strong> : 是用来处理NIO通信模式的线程池。每个线程池有N个NioEventLoop来处理Channel事件,每一个NioEventLoop负责处理N个Channel。<br><strong>NioEventLoop</strong> : 负责不停地轮询IO事件,处理IO事件和执行任务,类比多路复用器,细化分三件事。<br>1 轮询注册到Selector上所有的Channel的IO事件<br>2 处理产生网络IO事件的Channel<br>3 处理队列中的任务</p>
<p><img src="/img/remote/1460000013039332?w=654&h=294" alt="" title=""></p>
<h4>ServerBootstrap</h4>
<p>本章重点,Netty是如何通过NIO辅助启动类来初始化Channel的?先看下面的源码。</p>
<pre><code class="java">@Override
void init(Channel channel) throws Exception {
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
@SuppressWarnings("unchecked")
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
ChannelPipeline p = channel.pipeline();
final EventLoopGroup currentChildGroup = childGroup;
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}</code></pre>
<p>服务器启动和连接过程:<br>第一步:是给Channel设置options和attrs,<br>第二步:复制childGroup,childHandler,childOptions和childAttrs等待服务器和客户端连接,<br>第三步:实例化一个ChannelInitializer,添加到Pipeline的末尾。<br>第四步:当Channel注册到NioEventLoop时,ChannelInitializer触发initChannel方法,pipeline装入自定义的Handler,给Channel设置一下child配置。</p>
<p>小结:<br>1 <strong>group,options,attrs,handler,是在服务器端初始化时配置,是AbstractBootstrap的方法。</strong><br>2 <strong>childGroup,childOption,childAttr,childHandler,是在服务器与客户端建立Channel后配置,是ServerBootstrap的方法。</strong><br>3 <strong>Bootstrap 和 ServerBootstrap 都继承了AbstractBootstrap类。</strong><br>4 <strong>若不设置childGroup,则默认取group值。</strong><br>5 <strong>Bootstrap 和 ServerBootstrap 启动服务时,都会执行验证方法,判断必填参数是否都有配置。</strong></p>
<p><img src="/img/remote/1460000013039333" alt="" title=""></p>
<h3>Netty 拆包粘包</h3>
<p>这里通过介绍Netty拆包粘包问题来对Netty进行入门学习。<br>在基于流的传输中,即便客户端发送独立的数据包,操作系统也会将其转换成<strong>一串字节队列</strong>,而服务端一次<strong>读取到的字节数又不确定</strong>。再加上网络传输的快慢。服务端很难完整的接收到数据。<br>常见的拆包粘包方法有三种<br>1 <strong>服务端设置一次接收字节的长度</strong>。若服务端接收的字节长度不满足要求则一直处于等待。客户端为满足传输的字节长度合格,可以考虑使用空格填充。<br>2 <strong>服务端设置特殊分隔符</strong>。客户端通过特殊分隔符粘包,服务端通过特殊分隔符拆包。<br>3 <strong>自定义协议</strong>。数据传输一般分消息头和消息体,消息头中包含了数据的长度。服务端先接收到消息头,得知需要接收N个数据,然后服务端接收直到数据为N个为止。<br>本章采用第二种,用特殊分隔符的方式。</p>
<h4>创建服务端代码流程</h4>
<p>第一步:准备两个线程池。一个用于接收事件的boss线程池,另一个用于处理事件的worker线程池。<br>第二步:服务端实例化ServerBootstrap NIO服务辅助启动类。用于简化提高开发效率。<br>第三步:配置服务器启动参数。比如channel的类型,接收channel的EventLoop,初始化的日志打印事件,建立连接后的事件(拆包,对象转字符串,自定义事件),初始化的配置和建立连接后的配置。<br>第四步:绑定端口,启动服务。Netty会根据第三步配置的参数启动服务。<br>第五步:关闭资源。</p>
<pre><code class="java">package com.itdragon.delimiter;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
public class ITDragonServer {
private static final Integer PORT = 8888; // 被监听端口号
private static final String DELIMITER = "_$"; // 拆包分隔符
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于接收进来的连接
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用于处理进来的连接
try {
ServerBootstrap serverbootstrap = new ServerBootstrap(); // 启动NIO服务的辅助启动类
serverbootstrap.group(bossGroup, workerGroup) // 分别设置bossGroup, workerGroup 顺序不能反
.channel(NioServerSocketChannel.class) // Channel的创建工厂,启动服务时会通过反射的方式来创建一个NioServerSocketChannel对象
.handler(new LoggingHandler(LogLevel.INFO)) // handler在初始化时就会执行,可以设置打印日志级别
.childHandler(new ChannelInitializer<SocketChannel>() { // childHandler会在客户端成功connect后才执行,这里实例化ChannelInitializer
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception { // initChannel方法执行后删除实例ChannelInitializer,添加以下内容
ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER.getBytes()); // 获取特殊分隔符的ByteBuffer
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128, delimiter)); // 设置特殊分隔符用于拆包
// socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8)); 设置指定长度分割
socketChannel.pipeline().addLast(new StringDecoder()); // 设置字符串形式的解码
socketChannel.pipeline().addLast(new ITDragonServerHandler()); // 自定义的服务器处理类,负责处理事件
}
})
.option(ChannelOption.SO_BACKLOG, 128) // option在初始化时就会执行,设置tcp缓冲区
.childOption(ChannelOption.SO_KEEPALIVE, true); // childOption会在客户端成功connect后才执行,设置保持连接
ChannelFuture future = serverbootstrap.bind(PORT).sync(); // 绑定端口, 阻塞等待服务器启动完成,调用sync()方法会一直阻塞等待channel的停止
future.channel().closeFuture().sync(); // 等待关闭 ,等待服务器套接字关闭
} catch (Exception e) {
e.printStackTrace();
} finally {
workerGroup.shutdownGracefully(); // 关闭线程组,先打开的后关闭
bossGroup.shutdownGracefully();
}
}
} </code></pre>
<h4>核心参数说明</h4>
<p><strong>NioEventLoopGroup</strong> : 是用来处理I/O操作的多线程事件循环器。 Netty提供了许多不同的EventLoopGroup的实现用来处理不同传输协议。<br><strong>ServerBootstrap</strong> : 启动NIO服务的辅助启动类。先配置Netty服务端启动参数,执行bind(PORT)方法才算真正启动服务。<br><strong>group</strong> : 注册EventLoopGroup<br><strong>channel</strong> : channelFactory,用于配置通道的类型。<br><strong>handler</strong> : 服务器始化时就会执行的事件。<br><strong>childHandler</strong> : 服务器在和客户端成功连接后会执行的事件。<br><strong>initChannel</strong> : channelRegistered事件触发后执行,删除ChannelInitializer实例,添加该方法体中的handler。<br><strong>option</strong> : 服务器始化的配置。<br><strong>childOption</strong> : 服务器在和客户端成功连接后的配置。<br><strong>SocketChannel</strong> : 继承了Channel,通过Channel可以对Socket进行各种操作。<br><strong>ChannelHandler</strong> : 通过ChannelHandler来间接操纵Channel,简化了开发。<br><strong>ChannelPipeline</strong> : 可以看成是一个ChandlerHandler的链表。<br><strong>ChannelHandlerContext</strong> : ChannelPipeline通过ChannelHandlerContext来间接管理ChannelHandler。</p>
<h4>自定义服务器处理类</h4>
<p>第一步:继承 ChannelInboundHandlerAdapter,其父类已经实现了ChannelHandler接口,简化了开发。<br>第二步:覆盖 chanelRead()事件处理方法 ,每当服务器从客户端收到新的数据时,该方法会在收到消息时被调用。<br>第三步:释放 ByteBuffer,ByteBuf是一个引用计数对象,这个对象必须显示地调用release()方法来释放。<br>第四步:异常处理,即当Netty由于IO错误或者处理器在处理事件时抛出的异常时触发。在大部分情况下,捕获的异常应该被记录下来并且把关联的channel给关闭掉。</p>
<pre><code class="java">package com.itdragon.delimiter;
import com.itdragon.utils.ITDragonUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ITDragonServerHandler extends ChannelInboundHandlerAdapter{
private static final String DELIMITER = "_$"; // 拆包分隔符
@Override
public void channelRead(ChannelHandlerContext chc, Object msg) {
try {
// 普通读写数据
/* 设置字符串形式的解码 new StringDecoder() 后可以直接使用
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
*/
System.out.println("Netty Server : " + msg.toString());
// 分隔符拆包
String response = ITDragonUtil.cal(msg.toString())+ DELIMITER;
chc.channel().writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg); // 写入方法writeAndFlush ,Netty已经释放了
}
}
// 当出现Throwable对象才会被调用
@Override
public void exceptionCaught(ChannelHandlerContext chc, Throwable cause) {
// 这个方法的处理方式会在遇到不同异常的情况下有不同的实现,比如你可能想在关闭连接之前发送一个错误码的响应消息。
cause.printStackTrace();
chc.close();
}
} </code></pre>
<h4>客户端启动流程</h4>
<p>第一步:创建一个用于发送请求的线程池。<br>第二步:客户端实例化Bootstrap NIO服务启动辅助类,简化开发。<br>第三步:配置参数,粘包,发送请求。<br>第四步:关闭资源。<br>值得注意的是,和ServerBootstrap不同,它并没有childHandler和childOption方法。</p>
<pre><code class="java">package com.itdragon.delimiter;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class ITDragonClient {
private static final Integer PORT = 8888;
private static final String HOST = "127.0.0.1";
private static final String DELIMITER = "_$"; // 拆包分隔符
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer(DELIMITER.getBytes());
// 设置特殊分隔符
socketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(128, delimiter));
// 设置指定长度分割 不推荐,两者选其一
// socketChannel.pipeline().addLast(new FixedLengthFrameDecoder(8));
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new ITDragonClientHandler());
}
})
.option(ChannelOption.SO_KEEPALIVE, true);
ChannelFuture future = bootstrap.connect(HOST, PORT).sync(); // 建立连接
future.channel().writeAndFlush(Unpooled.copiedBuffer(("1+1"+DELIMITER).getBytes()));
future.channel().writeAndFlush(Unpooled.copiedBuffer(("6+1"+DELIMITER).getBytes()));
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
group.shutdownGracefully();
}
}
} </code></pre>
<h4>客户端请求接收类</h4>
<p>和服务器处理类一样,这里只负责打印数据。</p>
<pre><code class="java">package com.itdragon.delimiter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
public class ITDragonClientHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext chc, Object msg) {
try {
/* 设置字符串形式的解码 new StringDecoder() 后可以直接使用
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "utf-8");
*/
System.out.println("Netty Client :" + msg);
} catch (Exception e) {
e.printStackTrace();
} finally {
ReferenceCountUtil.release(msg);
}
}
public void exceptionCaught(ChannelHandlerContext chc, Throwable cause) {
cause.printStackTrace();
chc.close();
}
} </code></pre>
<p>打印结果</p>
<pre><code class="java">一月 29, 2018 11:31:10 上午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xcf3a3ac1] REGISTERED
一月 29, 2018 11:31:11 上午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0xcf3a3ac1] BIND: 0.0.0.0/0.0.0.0:8888
一月 29, 2018 11:31:11 上午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] ACTIVE
一月 29, 2018 11:31:18 上午 io.netty.handler.logging.LoggingHandler channelRead
信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] READ: [id: 0xf1b8096b, L:/127.0.0.1:8888 - R:/127.0.0.1:4777]
一月 29, 2018 11:31:18 上午 io.netty.handler.logging.LoggingHandler channelReadComplete
信息: [id: 0xcf3a3ac1, L:/0:0:0:0:0:0:0:0:8888] READ COMPLETE
Netty Server : 1+1
Netty Server : 6+1
Netty Client :2
Netty Client :7</code></pre>
<p>从日志中可以看出Channel的状态从REGISTERED ---> ACTIVE ---> READ ---> READ COMPLETE。服务端也是按照特殊分割符拆包。</p>
<h3>总结</h3>
<p>看完本章,你必须要掌握的三个知识点:NioEventLoopGroup,ServerBootstrap,ChannelHandlerAdapter<br>1 <strong>NioEventLoopGroup 本质就是一个线程池,管理多个NioEventLoop,一个NioEventLoop管理多个Channel。</strong><br>2 <strong>NioEventLoop 负责不停地轮询IO事件,处理IO事件和执行任务。</strong><br>3 <strong>ServerBootstrap 是NIO服务的辅助启动类,先配置服务参数,后执行bind方法启动服务。</strong><br>4 <strong>Bootstrap 是NIO客户端的辅助启动类,用法和ServerBootstrap类似。</strong><br>5 <strong>Netty 使用FixedLengthFrameDecoder 固定长度拆包,DelimiterBasedFrameDecoder 分隔符拆包。</strong></p>
<p>到这里,Netty的拆包粘包,以及Netty的重要组件,服务器启动流程到这里就结束了,如果觉得不错可以点一个<strong> "推荐" </strong> ,也可以<strong> "关注" </strong>我哦。</p>
<h3>优质文章</h3>
<p><a href="https://link.segmentfault.com/?enc=v3YWHtMVHXzLH90ptOI75w%3D%3D.U%2FSkYCj6bmMwDpfdusTO357DCJ5Pc%2F%2FnTurBKHa6b25XmmEXg7nuz02w4D2s69aquXPv8vuOl1xA7eZFDUVaMg%3D%3D" rel="nofollow">http://blog.csdn.net/spiderdo...</a><br><a href="https://link.segmentfault.com/?enc=XthdgKWnDLUT3xTjyliPzA%3D%3D.s4PZPeHIxTIvyA0VeCsaZTNkGzvTi6RL2tltE9zbO9G6CTst8xhtr8ZGTBC70dRo" rel="nofollow">https://www.jianshu.com/p/c50...</a></p>
Netty序章之BIO NIO AIO演变
https://segmentfault.com/a/1190000012976683
2018-01-24T18:31:41+08:00
2018-01-24T18:31:41+08:00
itdragon
https://segmentfault.com/u/itdragon
26
<h2>Netty序章之BIO NIO AIO演变</h2>
<p>Netty是一个提供异步事件驱动的网络应用框架,用以快速开发<strong>高性能</strong>、<strong>高可靠</strong>的网络服务器和客户端程序。Netty简化了网络程序的开发,是很多框架和公司都在使用的技术。更是面试的加分项。Netty并非横空出世,它是在BIO,NIO,AIO演变中的产物,是一种NIO框架。而BIO,NIO,AIO更是笔试中要考,面试中要问的技术。也是一个很好的加分项,加分就是加工资,你还在等什么?本章带你细细品味三者的不同!<br>流程图:<img src="/img/remote/1460000012976688?w=858&h=851" alt="BIO NIO AIO 流程图" title="BIO NIO AIO 流程图"></p>
<p>技术:BIO,NIO,AIO<br>说明:github上有更全的源码。<br>源码:<a href="https://link.segmentfault.com/?enc=JuxZG8lcl5T7O8yuSwl9cQ%3D%3D.AAI39pVacWevuASkF6sJt%2BGRqFxQKG67cRZx%2FxUoxXujEC0GrSvhkfuPAu3NBMX25tRhg8qRxYs3OnE%2B5ACnXWT5RQaeAVznd2kR%2F%2B7XG5A%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>BIO</h3>
<p>BIO 全称Block-IO 是一种<strong>阻塞同步</strong>的通信模式。我们常说的Stock IO 一般指的是BIO。是一个比较传统的通信方式,<strong>模式简单</strong>,<strong>使用方便</strong>。但<strong>并发处理能力低</strong>,<strong>通信耗时</strong>,<strong>依赖网速</strong>。<br>BIO 设计原理:<br>服务器通过一个Acceptor线程负责监听客户端请求和为每个客户端创建一个新的线程进行链路处理。典型的一请求一应答模式。若客户端数量增多,频繁地创建和销毁线程会给服务器打开很大的压力。后改良为用线程池的方式代替新增线程,被称为伪异步IO。</p>
<p>服务器提供IP地址和监听的端口,客户端通过TCP的三次握手与服务器连接,连接成功后,双放才能通过套接字(Stock)通信。<br>小结:<strong>BIO模型中通过Socket和ServerSocket完成套接字通道的实现。阻塞,同步,建立连接耗时</strong>。</p>
<p>BIO服务器代码,负责启动服务,阻塞服务,监听客户端请求,新建线程处理任务。</p>
<pre><code class="java">import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* IO 也称为 BIO,Block IO 阻塞同步的通讯方式
* 比较传统的技术,实际开发中基本上用Netty或者是AIO。熟悉BIO,NIO,体会其中变化的过程。作为一个web开发人员,stock通讯面试经常问题。
* BIO最大的问题是:阻塞,同步。
* BIO通讯方式很依赖于网络,若网速不好,阻塞时间会很长。每次请求都由程序执行并返回,这是同步的缺陷。
* BIO工作流程:
* 第一步:server端服务器启动
* 第二步:server端服务器阻塞监听client请求
* 第三步:server端服务器接收请求,创建线程实现任务
*/
public class ITDragonBIOServer {
private static final Integer PORT = 8888; // 服务器对外的端口号
public static void main(String[] args) {
ServerSocket server = null;
Socket socket = null;
ThreadPoolExecutor executor = null;
try {
server = new ServerSocket(PORT); // ServerSocket 启动监听端口
System.out.println("BIO Server 服务器启动.........");
/*--------------传统的新增线程处理----------------*/
/*while (true) {
// 服务器监听:阻塞,等待Client请求
socket = server.accept();
System.out.println("server 服务器确认请求 : " + socket);
// 服务器连接确认:确认Client请求后,创建线程执行任务 。很明显的问题,若每接收一次请求就要创建一个线程,显然是不合理的。
new Thread(new ITDragonBIOServerHandler(socket)).start();
} */
/*--------------通过线程池处理缓解高并发给程序带来的压力(伪异步IO编程)----------------*/
executor = new ThreadPoolExecutor(10, 100, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));
while (true) {
socket = server.accept(); // 服务器监听:阻塞,等待Client请求
ITDragonBIOServerHandler serverHandler = new ITDragonBIOServerHandler(socket);
executor.execute(serverHandler);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != socket) {
socket.close();
socket = null;
}
if (null != server) {
server.close();
server = null;
System.out.println("BIO Server 服务器关闭了!!!!");
}
executor.shutdown();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}</code></pre>
<p>BIO服务端处理任务代码,负责处理Stock套接字,返回套接字给客户端,解耦。</p>
<pre><code class="java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import com.itdragon.util.CalculatorUtil;
public class ITDragonBIOServerHandler implements Runnable{
private Socket socket;
public ITDragonBIOServerHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader reader = null;
PrintWriter writer = null;
try {
reader = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
writer = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while (true) {
body = reader.readLine(); // 若客户端用的是 writer.print() 传值,那readerLine() 是不能获取值,细节
if (null == body) {
break;
}
System.out.println("server服务端接收参数 : " + body);
writer.println(body + " = " + CalculatorUtil.cal(body).toString());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != writer) {
writer.close();
}
try {
if (null != reader) {
reader.close();
}
if (null != this.socket) {
this.socket.close();
this.socket = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}</code></pre>
<p>BIO客户端代码,负责启动客户端,向服务器发送请求,接收服务器返回的Stock套接字。</p>
<pre><code class="java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Random;
/**
* BIO 客户端
* Socket : 向服务端发送连接
* PrintWriter : 向服务端传递参数
* BufferedReader : 从服务端接收参数
*/
public class ITDragonBIOClient {
private static Integer PORT = 8888;
private static String IP_ADDRESS = "127.0.0.1";
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
clientReq(i);
}
}
private static void clientReq(int i) {
Socket socket = null;
BufferedReader reader = null;
PrintWriter writer = null;
try {
socket = new Socket(IP_ADDRESS, PORT); // Socket 发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信
reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); // 获取返回内容
writer = new PrintWriter(socket.getOutputStream(), true);
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
writer.println(expression); // 向服务器端发送数据
System.out.println(i + " 客户端打印返回数据 : " + reader.readLine());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != reader) {
reader.close();
}
if (null != socket) {
socket.close();
socket = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}</code></pre>
<h3>NIO</h3>
<p>NIO 全称New IO,也叫Non-Block IO 是一种<strong>非阻塞同步</strong>的通信模式。<br>NIO 设计原理:<br>NIO 相对于BIO来说一大进步。客户端和服务器之间通过Channel通信。NIO可以在Channel进行读写操作。这些Channel都会被注册在Selector多路复用器上。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。<br>NIO 通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。<br>1)<strong>缓冲区Buffer</strong>:它是NIO与BIO的一个重要区别。BIO是将数据直接写入或读取到Stream对象中。而NIO的数据操作都是在缓冲区中进行的。缓冲区实际上是一个数组。Buffer最常见的类型是ByteBuffer,另外还有CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。<br>2)<strong>通道Channel</strong>:和流不同,通道是双向的。NIO可以通过Channel进行数据的读,写和同时读写操作。通道分为两大类:一类是网络读写(SelectableChannel),一类是用于文件操作(FileChannel),我们使用的SocketChannel和ServerSocketChannel都是SelectableChannel的子类。<br>3)<strong>多路复用器Selector</strong>:NIO编程的基础。多路复用器提供选择已经就绪的任务的能力。就是Selector会不断地轮询注册在其上的通道(Channel),如果某个通道处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。服务器端只要提供一个线程负责Selector的轮询,就可以接入成千上万个客户端,这就是JDK NIO库的巨大进步。</p>
<p>说明:这里的代码只实现了客户端发送请求,服务端接收数据的功能。其目的是简化代码,方便理解。github源码中有完整代码。<br>小结:<strong>NIO模型中通过SocketChannel和ServerSocketChannel完成套接字通道的实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。</strong></p>
<p>NIO服务器代码,负责开启多路复用器,打开通道,注册通道,轮询通道,处理通道。</p>
<pre><code class="java">import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO 也称 New IO, Non-Block IO,非阻塞同步通信方式
* 从BIO的阻塞到NIO的非阻塞,这是一大进步。功归于Buffer,Channel,Selector三个设计实现。
* Buffer : 缓冲区。NIO的数据操作都是在缓冲区中进行。缓冲区实际上是一个数组。而BIO是将数据直接写入或读取到Stream对象。
* Channel : 通道。NIO可以通过Channel进行数据的读,写和同时读写操作。
* Selector : 多路复用器。NIO编程的基础。多路复用器提供选择已经就绪状态任务的能力。
* 客户端和服务器通过Channel连接,而这些Channel都要注册在Selector。Selector通过一个线程不停的轮询这些Channel。找出已经准备就绪的Channel执行IO操作。
* NIO通过一个线程轮询,实现千万个客户端的请求,这就是非阻塞NIO的特点。
*/
public class ITDragonNIOServer implements Runnable{
private final int BUFFER_SIZE = 1024; // 缓冲区大小
private final int PORT = 8888; // 监听的端口
private Selector selector; // 多路复用器,NIO编程的基础,负责管理通道Channel
private ByteBuffer readBuffer = ByteBuffer.allocate(BUFFER_SIZE); // 缓冲区Buffer
public ITDragonNIOServer() {
startServer();
}
private void startServer() {
try {
// 1.开启多路复用器
selector = Selector.open();
// 2.打开服务器通道(网络读写通道)
ServerSocketChannel channel = ServerSocketChannel.open();
// 3.设置服务器通道为非阻塞模式,true为阻塞,false为非阻塞
channel.configureBlocking(false);
// 4.绑定端口
channel.socket().bind(new InetSocketAddress(PORT));
// 5.把通道注册到多路复用器上,并监听阻塞事件
/**
* SelectionKey.OP_READ : 表示关注读数据就绪事件
* SelectionKey.OP_WRITE : 表示关注写数据就绪事件
* SelectionKey.OP_CONNECT: 表示关注socket channel的连接完成事件
* SelectionKey.OP_ACCEPT : 表示关注server-socket channel的accept事件
*/
channel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server start >>>>>>>>> port :" + PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 需要一个线程负责Selector的轮询
@Override
public void run() {
while (true) {
try {
/**
* a.select() 阻塞到至少有一个通道在你注册的事件上就绪
* b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut
* c.selectNow() 立即返回。如果没有就绪的通道则返回0
* select方法的返回值表示就绪通道的个数。
*/
// 1.多路复用器监听阻塞
selector.select();
// 2.多路复用器已经选择的结果集
Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
// 3.不停的轮询
while (selectionKeys.hasNext()) {
// 4.获取一个选中的key
SelectionKey key = selectionKeys.next();
// 5.获取后便将其从容器中移除
selectionKeys.remove();
// 6.只获取有效的key
if (!key.isValid()){
continue;
}
// 阻塞状态处理
if (key.isAcceptable()){
accept(key);
}
// 可读状态处理
if (key.isReadable()){
read(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 设置阻塞,等待Client请求。在传统IO编程中,用的是ServerSocket和Socket。在NIO中采用的ServerSocketChannel和SocketChannel
private void accept(SelectionKey selectionKey) {
try {
// 1.获取通道服务
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
// 2.执行阻塞方法
SocketChannel socketChannel = serverSocketChannel.accept();
// 3.设置服务器通道为非阻塞模式,true为阻塞,false为非阻塞
socketChannel.configureBlocking(false);
// 4.把通道注册到多路复用器上,并设置读取标识
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
private void read(SelectionKey selectionKey) {
try {
// 1.清空缓冲区数据
readBuffer.clear();
// 2.获取在多路复用器上注册的通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 3.读取数据,返回
int count = socketChannel.read(readBuffer);
// 4.返回内容为-1 表示没有数据
if (-1 == count) {
selectionKey.channel().close();
selectionKey.cancel();
return ;
}
// 5.有数据则在读取数据前进行复位操作
readBuffer.flip();
// 6.根据缓冲区大小创建一个相应大小的bytes数组,用来获取值
byte[] bytes = new byte[readBuffer.remaining()];
// 7.接收缓冲区数据
readBuffer.get(bytes);
// 8.打印获取到的数据
System.out.println("NIO Server : " + new String(bytes)); // 不能用bytes.toString()
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new ITDragonNIOServer()).start();
}
}</code></pre>
<p>NIO客户端代码,负责连接服务器,声明通道,连接通道</p>
<pre><code class="java">import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ITDragonNIOClient {
private final static int PORT = 8888;
private final static int BUFFER_SIZE = 1024;
private final static String IP_ADDRESS = "127.0.0.1";
public static void main(String[] args) {
clientReq();
}
private static void clientReq() {
// 1.创建连接地址
InetSocketAddress inetSocketAddress = new InetSocketAddress(IP_ADDRESS, PORT);
// 2.声明一个连接通道
SocketChannel socketChannel = null;
// 3.创建一个缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
try {
// 4.打开通道
socketChannel = SocketChannel.open();
// 5.连接服务器
socketChannel.connect(inetSocketAddress);
while(true){
// 6.定义一个字节数组,然后使用系统录入功能:
byte[] bytes = new byte[BUFFER_SIZE];
// 7.键盘输入数据
System.in.read(bytes);
// 8.把数据放到缓冲区中
byteBuffer.put(bytes);
// 9.对缓冲区进行复位
byteBuffer.flip();
// 10.写出数据
socketChannel.write(byteBuffer);
// 11.清空缓冲区数据
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != socketChannel) {
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}</code></pre>
<h3>AIO</h3>
<p>AIO 也叫NIO2.0 是一种<strong>非阻塞异步</strong>的通信模式。在NIO的基础上引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。<br>AIO 并没有采用NIO的多路复用器,而是使用异步通道的概念。其read,write方法的返回类型都是Future对象。而Future模型是异步的,其核心思想是:去主函数等待时间。</p>
<p>小结:<strong>AIO模型中通过AsynchronousSocketChannel和AsynchronousServerSocketChannel完成套接字通道的实现。非阻塞,异步</strong>。</p>
<p>AIO服务端代码,负责创建服务器通道,绑定端口,等待请求。</p>
<pre><code class="java">import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* AIO, 也叫 NIO2.0 是一种异步非阻塞的通信方式
* AIO 引入了异步通道的概念 AsynchronousServerSocketChannel和AsynchronousSocketChannel 其read和write方法返回值类型是Future对象。
*/
public class ITDragonAIOServer {
private ExecutorService executorService; // 线程池
private AsynchronousChannelGroup threadGroup; // 通道组
public AsynchronousServerSocketChannel asynServerSocketChannel; // 服务器通道
public void start(Integer port){
try {
// 1.创建一个缓存池
executorService = Executors.newCachedThreadPool();
// 2.创建通道组
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
// 3.创建服务器通道
asynServerSocketChannel = AsynchronousServerSocketChannel.open(threadGroup);
// 4.进行绑定
asynServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("server start , port : " + port);
// 5.等待客户端请求
asynServerSocketChannel.accept(this, new ITDragonAIOServerHandler());
// 一直阻塞 不让服务器停止,真实环境是在tomcat下运行,所以不需要这行代码
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ITDragonAIOServer server = new ITDragonAIOServer();
server.start(8888);
}
}</code></pre>
<p>AIO服务器任务处理代码,负责,读取数据,写入数据</p>
<pre><code class="java">import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
import com.itdragon.util.CalculatorUtil;
public class ITDragonAIOServerHandler implements CompletionHandler<AsynchronousSocketChannel, ITDragonAIOServer> {
private final Integer BUFFER_SIZE = 1024;
@Override
public void completed(AsynchronousSocketChannel asynSocketChannel, ITDragonAIOServer attachment) {
// 保证多个客户端都可以阻塞
attachment.asynServerSocketChannel.accept(attachment, this);
read(asynSocketChannel);
}
//读取数据
private void read(final AsynchronousSocketChannel asynSocketChannel) {
ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
asynSocketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//进行读取之后,重置标识位
attachment.flip();
//获取读取的数据
String resultData = new String(attachment.array()).trim();
System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData);
String response = resultData + " = " + CalculatorUtil.cal(resultData);
write(asynSocketChannel, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
// 写入数据
private void write(AsynchronousSocketChannel asynSocketChannel, String response) {
try {
// 把数据写入到缓冲区中
ByteBuffer buf = ByteBuffer.allocate(BUFFER_SIZE);
buf.put(response.getBytes());
buf.flip();
// 在从缓冲区写入到通道中
asynSocketChannel.write(buf).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, ITDragonAIOServer attachment) {
exc.printStackTrace();
}
}</code></pre>
<p>AIO客户端代码,负责连接服务器,声明通道,连接通道</p>
<pre><code class="java">import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.Random;
public class ITDragonAIOClient implements Runnable{
private static Integer PORT = 8888;
private static String IP_ADDRESS = "127.0.0.1";
private AsynchronousSocketChannel asynSocketChannel ;
public ITDragonAIOClient() throws Exception {
asynSocketChannel = AsynchronousSocketChannel.open(); // 打开通道
}
public void connect(){
asynSocketChannel.connect(new InetSocketAddress(IP_ADDRESS, PORT)); // 创建连接 和NIO一样
}
public void write(String request){
try {
asynSocketChannel.write(ByteBuffer.wrap(request.getBytes())).get();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
asynSocketChannel.read(byteBuffer).get();
byteBuffer.flip();
byte[] respByte = new byte[byteBuffer.remaining()];
byteBuffer.get(respByte); // 将缓冲区的数据放入到 byte数组中
System.out.println(new String(respByte,"utf-8").trim());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
ITDragonAIOClient myClient = new ITDragonAIOClient();
myClient.connect();
new Thread(myClient, "myClient").start();
String []operators = {"+","-","*","/"};
Random random = new Random(System.currentTimeMillis());
String expression = random.nextInt(10)+operators[random.nextInt(4)]+(random.nextInt(10)+1);
myClient.write(expression);
}
}
}</code></pre>
<h3>常见面试题</h3>
<p><strong>1 IO,NIO,AIO区别</strong><br>IO 阻塞同步通信模式,客户端和服务器连接需要三次握手,使用简单,但吞吐量小<br>NIO 非阻塞同步通信模式,客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。<br>AIO 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法均是异步方法。</p>
<p><strong>2 Stock通信的伪代码实现流程</strong><br>服务器绑定端口:server = new ServerSocket(PORT)<br>服务器阻塞监听:socket = server.accept()<br>服务器开启线程:new Thread(Handle handle)<br>服务器读写数据:BufferedReader PrintWriter <br>客户端绑定IP和PORT:new Socket(IP_ADDRESS, PORT)<br>客户端传输接收数据:BufferedReader PrintWriter </p>
<p><strong>3 TCP协议与UDP协议有什么区别</strong><br>TCP : 传输控制协议是基于连接的协议,在正式收发数据前,必须和对方建立可靠的连接。速度慢,合适传输大量数据。<br>UDP : 用户数据报协议是与TCP相对应的协议。面向非连接的协议,不与对方建立连接,而是直接就把数据包发送过去,速度快,适合传输少量数据。</p>
<p><strong>4 什么是同步阻塞BIO,同步非阻塞NIO,异步非阻塞AIO</strong><br>同步阻塞IO : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。<br>同步非阻塞IO: 用户进程发起一个IO操作以后,可做其它事情,但用户进程需要经常询问IO操作是否完成,这样造成不必要的CPU资源浪费。<br>异步非阻塞IO: 用户进程发起一个IO操作然后,立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类比Future模式。</p>
<h3>总结</h3>
<p>1 BIO模型中通过<strong>Socket</strong>和<strong>ServerSocket</strong>完成套接字通道实现。阻塞,同步,连接耗时。<br>2 NIO模型中通过<strong>SocketChannel</strong>和<strong>ServerSocketChannel</strong>完成套接字通道实现。非阻塞/阻塞,同步,避免TCP建立连接使用三次握手带来的开销。<br>3 AIO模型中通过<strong>AsynchronousSocketChannel</strong>和<strong>AsynchronousServerSocketChannel</strong>完成套接字通道实现。非阻塞,异步。<br><img src="/img/remote/1460000012976689?w=535&h=252" alt="BIO NIO AIO 对比" title="BIO NIO AIO 对比"></p>
<p>到这里BIO,NIO,AIO的知识点就梳理完了。下一章是Netty的入门 编解码 数据通信知识。如果觉得不错可以点个<strong>"推荐"</strong>。也可以<strong>"关注"</strong>我,一起学习,一起成长。正常情况一周一更。学习方向是JAVA架构师。</p>
从线程池到synchronized关键字详解
https://segmentfault.com/a/1190000012916473
2018-01-20T15:39:15+08:00
2018-01-20T15:39:15+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>线程池 BlockingQueue synchronized volatile</h2>
<p>前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击。很多熟悉而又陌生的知识让我怀疑自己是一个假的程序员。本章从线程池,阻塞队列,synchronized 和 volatile关键字,wait,notify方法实现线程之间的通讯,死锁,常考面试题。将这些零碎的知识整合在一起。如下图所示。</p>
<p>学习流程图:<br><img src="/img/remote/1460000012916479?w=1000&h=827" alt="学习流程图" title="学习流程图"><br>技术:Executors,BlockingQueue,synchronized,volatile,wait,notify<br>说明:文章学习思路:线程池---->队列---->关键字---->死锁---->线程池实战<br>源码:<a href="https://link.segmentfault.com/?enc=br4SLkXT3mnut1R8CCQy3Q%3D%3D.Orr7vaYq2%2FL%2FFkRDRKLotnj5drzwB1zZm3OhvN0yCRLP5u5UYp2Ntdyq1uUwoRipQkFABdqZ1niGtBG0T0MxBQ%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>线程池</h3>
<p>线程池,顾名思义存放线程的池子,可以类比数据库的连接池。因为频繁地创建和销毁线程会给服务器带来很大的压力。若能将创建的线程不再销毁而是存放在池中等待下一个任务使用,可以不仅减少了创建和销毁线程所用的时间,提高了性能,同时还减轻了服务器的压力。</p>
<h4>线程池的使用</h4>
<p>初始化线程池有五个核心参数,分别是 <strong>corePoolSize</strong>, <strong>maximumPoolSize</strong>, <strong>keepAliveTime</strong>, <strong>unit</strong>, <strong>workQueue</strong>。还有两个默认参数 threadFactory, handler<br><strong>corePoolSize</strong>:线程池初始核心线程数。初始化线程池的时候,池内是没有线程,只有在执行任务的时会创建线程。<br><strong>maximumPoolSize</strong>:线程池允许存在的最大线程数。若超过该数字,默认提示<code>RejectedExecutionException</code>异常<br><strong>keepAliveTime</strong>:当前线程数大于核心线程时,该参数生效,其目的是终止多余的空闲线程等待新任务的最长时间。即指定时间内将还未接收任务的线程销毁。<br><strong>unit</strong>:keepAliveTime 的时间单位<br><strong>workQueue</strong>:缓存任务的的队列,一般采用LinkedBlockingQueue。<br><strong>threadFactory</strong>:执行程序创建新线程时使用的工厂,一般采用默认值。<br><strong>handler</strong>:超出线程范围和队列容量而使执行被阻塞时所使用的处理程序,一般采用默认值。</p>
<h4>线程池工作流程</h4>
<p>开始,游泳馆来了一名学员,于是馆主安排一个教练负责培训这名学员;<br>然后,游泳馆来了六名学员,可馆主只招了五名教练,于是有一名学员被安排到休息室等待;<br>后来,游泳馆来了十六名学员,休息室已经满了,馆主核算了开支,预计最多可招十名教练;<br>最后,游泳馆只来了十名学员,馆主对教练说,如果半天内接不到学员的教练就可以走了;<br>结果,游泳馆没有学员,关闭了。<br>在接收任务前,线程池内是没有线程。只有当任务来了才开始新建线程。当任务数大于核心线程数时,任务进入队列中等待。若队列满了,则线程池新增线程直到最大线程数。再超过则会执行拒绝策略。</p>
<h4>线程池的三种关闭</h4>
<p><strong>shutdown</strong>: 线程池不再接收任务,等待线程池中所有任务完成后,关闭线程池。<strong>常用</strong><br><strong>shutdownNow</strong>: 线程池不再接收任务,忽略队列中的任务,尝试中断正在执行的任务,返回未执行任务列表,关闭线程池。<strong>慎用</strong><br><strong>awaitTermination</strong>: 线程池可以继续接收任务,当任务都完成后,或者超过设置的时间后,关闭线程池。方法是阻塞的,<strong>考虑使用</strong></p>
<h4>线程池的种类</h4>
<p><strong>1 newSingleThreadExecutor() 单线程线程池</strong><br>初始线程数和允许最大线程数都是一,keepAliveTime 也就失效了,队列是无界阻塞队列。该线程池的主要作用是负责缓存任务。</p>
<p><strong>2 newFixedThreadPool(n) 固定大小线程池</strong><br>初始线程数和允许最大线程数相同,且大小自定义,keepAliveTime 也就失效了,队列是无界阻塞队列。符合大部分业务要求,常用。</p>
<p><strong>3 newCachedThreadPool() 无缓存无界线程池</strong><br>初始线程数为零,最大线程数为无穷大,keepAliveTime 60秒类终止空闲线程,队列是无缓存无界队列。适合任务数不多的场景,慎用。</p>
<pre><code class="java">import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池
* 优势,类比数据库的连接池
* 1. 频繁的创建和销毁线程会给服务器带来很大的压力
* 2. 若创建的线程不销毁而是留在线程池中等待下次使用,则会很大地提高效率也减轻了服务器的压力
*
* 三种workQueue策略
* 直接提交 SynchronousQueue
* 无界队列 LinkedBlockingQueue
* 有界队列 ArrayBlockingQueue
*
* 四种拒绝策略
* AbortPolicy : JDK默认,超出 MAXIMUM_POOL_SIZE 放弃任务抛异常 RejectedExecutionException
* CallerRunsPolicy : 尝试直接调用被拒绝的任务,若线程池被关闭,则丢弃任务
* DiscardOldestPolicy : 放弃队列最前面的任务,然后重新尝试执被拒绝的任务。若线程池被关闭,则丢弃任务
* DiscardPolicy : 放弃不能执行的任务但不抛异常
*/
public class ThreadPoolExecutorStu {
// 线程池中初始线程个数
private final static Integer CORE_POOL_SIZE = 3;
// 线程池中允许的最大线程数
private final static Integer MAXIMUM_POOL_SIZE = 8;
// 当线程数大于初始线程时。终止多余的空闲线程等待新任务的最长时间
private final static Long KEEP_ALIVE_TIME = 10L;
// 任务缓存队列 ,即线程数大于初始线程数时先进入队列中等待,此数字可以稍微设置大点,避免线程数超过最大线程数时报错。或者直接用无界队列
private final static ArrayBlockingQueue<Runnable> WORK_QUEUE = new ArrayBlockingQueue<Runnable>(5);
public static void main(String[] args) {
Long start = System.currentTimeMillis();
/**
* ITDragonThreadPoolExecutor 耗时 1503
* ITDragonFixedThreadPool 耗时 505
* ITDragonSingleThreadExecutor 语法问题报错,
* ITDragonCachedThreadPool 耗时506
* 推荐使用自定义线程池,或newFixedThreadPool(n)
*/
ThreadPoolExecutor threadPoolExecutor = ITDragonThreadPoolExecutor();
for (int i = 0; i < 8; i++) { // 执行8个任务,若超过MAXIMUM_POOL_SIZE则会报错 RejectedExecutionException
MyRunnableTest myRunnable = new MyRunnableTest(i);
threadPoolExecutor.execute(myRunnable);
System.out.println("线程池中现在的线程数目是:"+threadPoolExecutor.getPoolSize()+", 队列中正在等待执行的任务数量为:"+
threadPoolExecutor.getQueue().size());
}
// 关掉线程池 ,并不会立即停止(停止接收外部的submit任务,等待内部任务完成后才停止),推荐使用。 与之对应的是shutdownNow,不推荐使用
threadPoolExecutor.shutdown();
try {
// 阻塞等待30秒关掉线程池,返回true表示已经关闭。和shutdown不同,它可以接收外部任务,并且还阻塞。这里为了方便统计时间,所以选择阻塞等待关闭。
threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("耗时 : " + (System.currentTimeMillis() - start));
}
// 自定义线程池,开发推荐使用
public static ThreadPoolExecutor ITDragonThreadPoolExecutor() {
// 构建一个,初始线程数量为3,最大线程数据为8,等待时间10分钟 ,队列长度为5 的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MINUTES, WORK_QUEUE);
return threadPoolExecutor;
}
/**
* 固定大小线程池
* corePoolSize初始线程数和maximumPoolSize最大线程数一样,keepAliveTime参数不起作用,workQueue用的是无界阻塞队列
*/
public static ThreadPoolExecutor ITDragonFixedThreadPool() {
ExecutorService executor = Executors.newFixedThreadPool(8);
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
return threadPoolExecutor;
}
/**
* 单线程线程池
* 等价与Executors.newFixedThreadPool(1);
*/
public static ThreadPoolExecutor ITDragonSingleThreadExecutor() {
ExecutorService executor = Executors.newSingleThreadExecutor();
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
return threadPoolExecutor;
}
/**
* 无界线程池
* corePoolSize 初始线程数为零
* maximumPoolSize 最大线程数无穷大
* keepAliveTime 60秒类将没有被用到的线程终止
* workQueue SynchronousQueue 队列,无容量,来任务就直接新增线程
* 不推荐使用
*/
public static ThreadPoolExecutor ITDragonCachedThreadPool() {
ExecutorService executor = Executors.newCachedThreadPool();
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
return threadPoolExecutor;
}
}
class MyRunnableTest implements Runnable {
private Integer num; // 正在执行的任务数
public MyRunnableTest(Integer num) {
this.num = num;
}
public void run() {
System.out.println("正在执行的MyRunnable " + num);
try {
Thread.sleep(500);// 模拟执行事务需要耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("MyRunnable " + num + "执行完毕");
}
}</code></pre>
<h3>队列</h3>
<p>队列,是一种数据结构。大部分的队列都是以FIFO(先进先出)的方式对各个元素进行排序的(PriorityBlockingQueue是根据优先级排序的)。队列的头移除元素,队列的末尾插入元素。插入的元素建议不能为null。Queue主要分两类,一类是高性能队列 ConcurrentLinkedQueue;一类是阻塞队列 BlockingQueue。本章重点介绍BlockingQueue</p>
<h4>ConcurrentLinkedQueue</h4>
<p>ConcurrentLinkedQueue性能好于BlockingQueue。是基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。不允许null元素。</p>
<h4>BlockingQueue</h4>
<p><strong>ArrayBlockingQueue</strong>: 基于数组的阻塞队列,在内部维护了一个定长数组,以便缓存队列中的数据对象。并没有实现读写分离,也就意味着生产和消费不能完全并行。是一个有界队列<br><strong>LinkedBlockingQueue</strong>:基于列表的阻塞队列,在内部维护了一个数据缓冲队列(由一个链表构成),实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。是一个无界队列,<br><strong>SynchronousQueue</strong>: 没有缓冲的队列,生存者生产的数据直接会被消费者获取并消费。若没有数据就直接调用出栈方法则会报错。</p>
<p>三种队列使用场景<br>newFixedThreadPool 线程池采用的队列是LinkedBlockingQueue。其优点是无界可缓存,内部实现读写分离,并发的处理能力高于ArrayBlockingQueue<br>newCachedThreadPool 线程池采用的队列是SynchronousQueue。其优点就是无缓存,接收到的任务均可直接处理,再次强调,慎用!<br>并发量不大,服务器性能较好,可以考虑使用SynchronousQueue。<br>并发量较大,服务器性能较好,可以考虑使用LinkedBlockingQueue。<br>并发量很大,服务器性能无法满足,可以考虑使用ArrayBlockingQueue。系统的稳定最重要。</p>
<pre><code class="java">import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
/**
* 阻塞队列
* ArrayBlockingQueue :有界
* LinkedBlockingQueue :无界
* SynchronousQueue :无缓冲直接用
* 非阻塞队列
* ConcurrentLinkedQueue :高性能
*/
public class ITDragonQueue {
/**
* ArrayBlockingQueue : 基于数组的阻塞队列实现,在内部维护了一个定长数组,以便缓存队列中的数据对象。
* 内部没有实现读写分离,生产和消费不能完全并行,
* 长度是需要定义的,
* 可以指定先进先出或者先进后出,
* 是一个有界队列。
*/
@Test
public void ITDragonArrayBlockingQueue() throws Exception {
ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5); // 可以尝试 队列长度由3改到5
array.offer("offer 插入数据方法---成功返回true 否则返回false");
array.offer("offer 3秒后插入数据方法", 3, TimeUnit.SECONDS);
array.put("put 插入数据方法---但超出队列长度则阻塞等待,没有返回值");
array.add("add 插入数据方法---但超出队列长度则提示 java.lang.IllegalStateException"); // java.lang.IllegalStateException: Queue full
System.out.println(array);
System.out.println(array.take() + " \t还剩元素 : " + array); // 从头部取出元素,并从队列里删除,若队列为null则一直等待
System.out.println(array.poll() + " \t还剩元素 : " + array); // 从头部取出元素,并从队列里删除,执行poll 后 元素减少一个
System.out.println(array.peek() + " \t还剩元素 : " + array); // 从头部取出元素,执行peek 不移除元素
}
/**
* LinkedBlockingQueue:基于列表的阻塞队列,在内部维护了一个数据缓冲队列(该队列由一个链表构成)。
* 其内部实现采用读写分离锁,能高效的处理并发数据,生产者和消费者操作的完全并行运行
* 可以不指定长度,
* 是一个无界队列。
*/
@Test
public void ITDragonLinkedBlockingQueue() throws Exception {
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>();
queue.offer("1.无界队列");
queue.add("2.语法和ArrayBlockingQueue差不多");
queue.put("3.实现采用读写分离");
List<String> list = new ArrayList<String>();
System.out.println("返回截取的长度 : " + queue.drainTo(list, 2));
System.out.println("list : " + list);
}
/**
* SynchronousQueue:没有缓冲的队列,生存者生产的数据直接会被消费者获取并消费。
*/
@Test
public void ITDragonSynchronousQueue() throws Exception {
final SynchronousQueue<String> queue = new SynchronousQueue<String>();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("take , 在没有取到值之前一直处理阻塞 : " + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
Thread.sleep(2000);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
queue.add("进值!!!");
}
});
thread2.start();
}
/**
* ConcurrentLinkedQueue:是一个适合高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,性能好于BlockingQueue。
* 它是一个基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最后加入的,不允许null元素。
* 无阻塞队列,没有 put 和 take 方法
*/
@Test
public void ITDragonConcurrentLinkedQueue() throws Exception {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
queue.offer("1.高性能无阻塞");
queue.add("2.无界队列");
System.out.println(queue);
System.out.println(queue.poll() + " \t : " + queue); // 从头部取出元素,并从队列里删除,执行poll 后 元素减少一个
System.out.println(queue.peek() + " \t : " + queue); // 从头部取出元素,执行peek 不移除元素
}
}</code></pre>
<h3>关键字</h3>
<p>关键字是为了线程安全服务的,哪什么是线程安全呢?<strong>当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的</strong>。<br>线程安全的两个特性:<strong>原子性</strong>和<strong>可见性</strong>。synchronized 同步,原子性。volatile 可见性。wait,notify 负责多个线程之间的通信。</p>
<h4>synchronized</h4>
<p>synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区",若一个线程想要执行synchronized修饰的代码块,首先要<br>step1 尝试获得锁<br>step2 如果拿到锁,执行synchronized代码体内容<br>step3 如果拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁。<br>注*(线程多了也就是会出现锁竞争的问题,多个线程执行的顺序是按照CPU分配的先后顺序而定的,而并非代码执行的先后顺序)</p>
<p>synchronized 可以修饰方法,修饰代码块,这些都是对象锁。若和static一起使用,则升级为类锁。<br>synchronized 锁是可以重入的,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。锁重入的机制,也支持在父子类继承的场景。<br>synchronized 同步异步,一个线程得到了一个对象的锁后,其他线程是可以执行非加锁的方法(异步)。但是不能执行其他加锁的方法(同步)。<br>synchronized 锁异常,当一个线程执行的代码出现异常时,其所持有的锁会自动释放。</p>
<pre><code class="java">/**
* synchronized 关键字,可以修饰方法,也可以修饰代码块。建议采用后者,通过减小锁的粒度,以提高系统性能。
* synchronized 关键字,如果以字符串作为锁,请注意String常量池的缓存功能和字符串改变后锁是否的情况。
* synchronized 锁重入,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。
* synchronized 同异步,一个线程获得锁后,另外一个线程可以执行非synchronized修饰的方法,这是异步。若另外一个线程执行任何synchronized修饰的方法则需要等待,这是同步
* synchronized 类锁,用static + synchronized 修饰则表示对整个类进行加锁
*/
public class ITDragonSynchronized {
private void thisLock () { // 对象锁
synchronized (this) {
System.out.println("this 对象锁!");
}
}
private void classLock () { // 类锁
synchronized (ITDragonSynchronized.class) {
System.out.println("class 类锁!");
}
}
private Object lock = new Object();
private void objectLock () { // 任何对象锁
synchronized (lock) {
System.out.println("object 任何对象锁!");
}
}
private void stringLock () { // 字符串锁,注意String常量池的缓存功能
synchronized ("string") { // 用 new String("string") t4 和 t5 同时进入。用string t4完成后,t5在开始
try {
for(int i = 0; i < 3; i++) {
System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private String strLock = "lock"; // 字符串锁改变
private void changeStrLock () {
synchronized (strLock) {
try {
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !");
strLock = "changeLock";
Thread.sleep(500);
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void method1() { // 锁重入
System.out.println("^^^^^^^^^^^^^^^^^^^^ method1");
method2();
}
private synchronized void method2() {
System.out.println("-------------------- method2");
method3();
}
private synchronized void method3() {
System.out.println("******************** method3");
}
private synchronized void syncMethod() {
try {
System.out.println(Thread.currentThread().getName() + " synchronized method!");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 若次方法也加上了synchronized,就必须等待t1线程执行完后,t2才能调用,两个synchronized块之间具有互斥性,synchronized块获得的是一个对象锁,锁定的是整个对象
private void asyncMethod() {
System.out.println(Thread.currentThread().getName() + " asynchronized method!");
}
// static + synchronized 修饰则表示类锁,打印的结果是thread1线程先执行完,然后在执行thread2线程。若没有被static修饰,则thread1和 thread2几乎同时执行,同时结束
private synchronized void classLock(String args) {
System.out.println(args + "start......");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(args + "end......");
}
public static void main(String[] args) throws Exception {
final ITDragonSynchronized itDragonSynchronized = new ITDragonSynchronized();
System.out.println("------------------------- synchronized 代码块加锁 -------------------------");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.thisLock();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.classLock();
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.objectLock();
}
});
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(2000);
System.out.println("------------------------- synchronized 字符串加锁 -------------------------");
// 如果字符串锁,用new String("string") t4,t5线程是可以获取锁的,如果直接使用"string" ,若锁不释放,t5线程一直处理等待中
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.stringLock();
}
}, "t4");
Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.stringLock();
}
}, "t5");
thread4.start();
thread5.start();
Thread.sleep(3000);
System.out.println("------------------------- synchronized 字符串变锁 -------------------------");
// 字符串变了,锁也会改变,导致t7线程在t6线程未结束后变开始执行,但一个对象的属性变了,不影响这个对象的锁。
Thread thread6 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.changeStrLock();
}
}, "t6");
Thread thread7 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.changeStrLock();
}
}, "t7");
thread6.start();
thread7.start();
Thread.sleep(2000);
System.out.println("------------------------- synchronized 锁重入 -------------------------");
Thread thread8 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.method1();
}
}, "t8");
thread8.start();
Thread thread9 = new Thread(new Runnable() {
@Override
public void run() {
SunClass sunClass = new SunClass();
sunClass.sunMethod();
}
}, "t9");
thread9.start();
Thread.sleep(2000);
System.out.println("------------------------- synchronized 同步异步 -------------------------");
Thread thread10 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.syncMethod();
}
}, "t10");
Thread thread11 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.asyncMethod();
}
}, "t11");
thread10.start();
thread11.start();
Thread.sleep(2000);
System.out.println("------------------------- synchronized 同步异步 -------------------------");
ITDragonSynchronized classLock1 = new ITDragonSynchronized();
ITDragonSynchronized classLock2 = new ITDragonSynchronized();
Thread thread12 = new Thread(new Runnable() {
@Override
public void run() {
classLock1.classLock("classLock1");
}
});
thread12.start();
Thread thread13 = new Thread(new Runnable() {
@Override
public void run() {
classLock2.classLock("classLock2");
}
});
thread13.start();
}
// 有父子继承关系的类,如果都使用了synchronized 关键字,也是线程安全的。
static class FatherClass {
public synchronized void fatherMethod(){
System.out.println("#################### fatherMethod");
}
}
static class SunClass extends FatherClass{
public synchronized void sunMethod() {
System.out.println("@@@@@@@@@@@@@@@@@@@@ sunMethod");
this.fatherMethod();
}
}
}</code></pre>
<h4>volatile</h4>
<p>volatile 关键字虽然不具备synchronized关键字的原子性(同步)但其主要作用就是使变量在多个线程中可见。也就是可见性。<br>用法很简单,直接用来修饰变量。因为其不具备原子性,可以用Atomic类代替。美中不足的是多个Atomic类也不具备原子性,所以还需要synchronized来修饰。<br>volatile 关键字工作原理<br>每个线程都有自己的工作内存,如果线程需要用到一个变量的时,会从主内存拷贝一份到自己的工作内存中。从而提高了效率。每次执行完线程后再将变量从工作内存同步回主内存中。<br>这样就存在一个问题,变量在不同线程中可能存在不同的值。如果用volatile 关键字修饰变量,则会让线程的执行引擎直接从主内存中获取值。<br><img src="/img/remote/1460000012916480?w=508&h=384" alt="volatile关键字" title="volatile关键字"></p>
<pre><code class="java">import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile 关键字主要作用就是使变量在多个线程中可见。
* volatile 关键字不具备原子性,但Atomic类是具备原子性和可见性。
* 美中不足的是多个Atomic类不具备原子性,还是需要synchronized 关键字帮忙。
*/
public class ITDragonVolatile{
private volatile boolean flag = true;
private static volatile int count;
private static AtomicInteger atomicCount = new AtomicInteger(0); // 加 static 是为了避免每次实例化对象时初始值为零
// 测试volatile 关键字的可见性
private void volatileMethod() {
System.out.println("thread start !");
while (flag) { // 如果flag为true则一直处于阻塞中,
}
System.out.println("thread end !");
}
// 验证volatile 关键字不具备原子性
private int volatileCountMethod() {
for (int i = 0; i < 10; i++) {
// 第一个线程还未将count加到10的时候,就可能被另一个线程开始修改。可能会导致最后一次打印的值不是1000
count++ ;
}
return count;
}
// 验证Atomic类具有原子性
private int atomicCountMethod() {
for (int i = 0; i < 10; i++) {
atomicCount.incrementAndGet();
}
// 若最后一次打印为1000则表示具备原子性,中间打印的信息可能是受println延迟影响。
return atomicCount.get();// 若最后一次打印为1000则表示具备原子性
}
// 验证多个 Atomic类操作不具备原子性,加synchronized关键字修饰即可
private synchronized int multiAtomicMethod(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicCount.addAndGet(1);
atomicCount.addAndGet(2);
atomicCount.addAndGet(3);
atomicCount.addAndGet(4);
return atomicCount.get(); //若具备原子性,则返回的结果一定都是10的倍数,需多次运行才能看到结果
}
/**
* volatile 关键字可见性原因
* 这里有两个线程 :一个是main的主线程,一个是thread的子线程
* jdk线程工作流程 :为了提高效率,每个线程都有一个工作内存,将主内存的变量拷贝一份到工作内存中。线程的执行引擎就直接从工作内存中获取变量。
* So 问题来了 :thread线程用的是自己的工作内存,主线程将变量修改后,thread线程不知道。这就是数据不可见的问题。
* 解决方法 :变量用volatile 关键字修饰后,线程的执行引擎就直接从主内存中获取变量。
*
*/
public static void main(String[] args) throws InterruptedException {
// 测试volatile 关键字的可见性
/*ITDragonVolatile itDragonVolatile = new ITDragonVolatile();
Thread thread = new Thread(itDragonVolatile);
thread.start();
Thread.sleep(1000); // 等线程启动了,再设置值
itDragonVolatile.setFlag(false);
System.out.println("flag : " + itDragonVolatile.isFlag());*/
// 验证volatile 关键字不具备原子性 和 Atomic类具有原子性
final ITDragonVolatile itDragonVolatile = new ITDragonVolatile();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 100; i++) {
threads.add(new Thread(new Runnable() {
@Override
public void run() {
// 中间打印的信息可能是受println延迟影响,请看最后一次打印的结果
System.out.println(itDragonVolatile.multiAtomicMethod());
}
}));
}
for(Thread thread : threads){
thread.start();
}
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}</code></pre>
<h4>wait,notify</h4>
<p>使用 wait/ notify 方法实现线程间的通信,模拟BlockingQueue队列。有两点需要注意:<br>1)wait 和 notify 必须要配合 synchronized 关键字使用<br>2)wait方法是释放锁的, notify方法不释放锁。<br>线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理,就不能成为一个整体,线程之间的通信就成为整体的必用方法之一。</p>
<pre><code class="java">import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
/**
* synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区",一般给代码块加锁,通过减小锁的粒度从而提高性能。
* Atomic* 是为了弥补volatile关键字不具备原子性的问题。虽然一个Atomic*对象是具备原子性的,但不能确保多个Atomic*对象也具备原子性。
* volatile 关键字不具备synchronized关键字的原子性其主要作用就是使变量在多个线程中可见。
* wait / notify
* wait() 使线程阻塞运行,notify() 随机唤醒等待队列中等待同一共享资源的一个线程继续运行,notifyAll() 唤醒所有等待队列中等待同一共享资源的线程继续运行。
* 1)wait 和 notify 必须要配合 synchronized 关键字使用
* 2)wait方法是释放锁的, notify方法不释放锁
*/
public class ITDragonMyQueue {
//1 需要一个承装元素的集合
private LinkedList<Object> list = new LinkedList<Object>();
//2 需要一个计数器 AtomicInteger (保证原子性和可见性)
private AtomicInteger count = new AtomicInteger(0);
//3 需要制定上限和下限
private final Integer minSize = 0;
private final Integer maxSize ;
//4 构造方法
public ITDragonMyQueue(Integer size){
this.maxSize = size;
}
//5 初始化一个对象 用于加锁
private final Object lock = new Object();
//put(anObject): 把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.
public void put(Object obj){
synchronized (lock) {
while(count.get() == this.maxSize){
try {
lock.wait(); // 当Queue没有空间时,线程被阻塞 ,这里为了区分,命名为wait1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(obj); //1 加入元素
count.incrementAndGet(); //2 计数器累加
lock.notify(); //3 新增元素后,通知另外一个线程wait2,队列多了一个元素,可以做移除操作了。
System.out.println("新加入的元素为: " + obj);
}
}
//take: 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入.
public Object take(){
Object ret = null;
synchronized (lock) {
while(count.get() == this.minSize){
try {
lock.wait(); // 当Queue没有值时,线程被阻塞 ,这里为了区分,命名为wait2
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ret = list.removeFirst(); //1 做移除元素操作
count.decrementAndGet(); //2 计数器递减
lock.notify(); //3 移除元素后,唤醒另外一个线程wait1,队列少元素了,可以再添加操作了
}
return ret;
}
public int getSize(){
return this.count.get();
}
public static void main(String[] args) throws Exception{
final ITDragonMyQueue queue = new ITDragonMyQueue(5);
queue.put("a");
queue.put("b");
queue.put("c");
queue.put("d");
queue.put("e");
System.out.println("当前容器的长度: " + queue.getSize());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
queue.put("f");
queue.put("g");
}
},"thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("移除的元素为:" + queue.take()); // 移除一个元素后再进一个,而并非同时移除两个,进入两个元素。
System.out.println("移除的元素为:" + queue.take());
}
},"thread2");
thread1.start();
Thread.sleep(2000);
thread2.start();
}
}</code></pre>
<h3>死锁</h3>
<p>死锁是一个很糟糕的情况,锁迟迟不能解开,其他线程只能一直处于等待阻塞状态。比如线程A拥有锁一,却还想要锁二。线程B拥有锁二,却还想要锁一。两个线程互不相让,两个线程将永远等待。<br>排查:<br>第一步:控制台输入jps用于获得当前JVM进程的pid<br>第二步:jstack pid 用于打印堆栈信息<br>第三步:解读,"Thread-1" 是线程的名字,prio 是线程的优先级,tid 是线程id, nid 是本地线程id, waiting to lock 等待去获取的锁,locked 自己拥有的锁。</p>
<pre><code>"Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37)
- waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object)
- locked <0x00000000ecfdf9e0> (a java.lang.Object)
at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54)
at java.lang.Thread.run(Thread.java:748)</code></pre>
<pre><code class="java">/**
* 死锁: 线程A拥有锁一,却还想要锁二。线程B拥有锁二,却还想要锁一。两个线程互不相让,两个线程将永远等待。
* 避免: 在设计阶段,了解锁的先后顺序,减少锁的交互数量。
* 排查:
* 第一步:控制台输入 jps 用于获得当前JVM进程的pid
* 第二步:jstack pid 用于打印堆栈信息
* "Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
* - waiting to lock <0x00000000ecfdf9d0> - locked <0x00000000ecfdf9e0>
* "Thread-0" #10 prio=5 os_prio=0 tid=0x0000000055ff0800 nid=0x1b14 waiting for monitor entry [0x0000000056c7f000]
* - waiting to lock <0x00000000ecfdf9e0> - locked <0x00000000ecfdf9d0>
* 可以看出,两个线程持有的锁都是对方想要得到的锁(得不到的永远在骚动),而且最后一行也打印了 Found 1 deadlock.
*/
public class ITDragonDeadLock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized (left) {
try {
Thread.sleep(2000); // 模拟持有锁的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (right) {
System.out.println("leftRight end!");
}
}
}
public void rightLeft(){
synchronized (right) {
try {
Thread.sleep(2000); // 模拟持有锁的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (left) {
System.out.println("rightLeft end!");
}
}
}
public static void main(String[] args) {
ITDragonDeadLock itDragonDeadLock = new ITDragonDeadLock();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
itDragonDeadLock.leftRight();
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
itDragonDeadLock.rightLeft();
}
});
thread2.start();
}
}</code></pre>
<h3>多线程案例</h3>
<p>若有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现</p>
<pre><code class="java">import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 若有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?
* 思考:汇总,说明要把四个线程的结果返回给第五个线程,若要线程有返回值,推荐使用callable。Thread和Runnable都没返回值
*/
public class ITDragonThreads {
public static void main(String[] args) throws Exception {
// 无缓冲无界线程池
ExecutorService executor = Executors.newFixedThreadPool(8);
// 相对ExecutorService,CompletionService可以更精确和简便地完成异步任务的执行
CompletionService<Long> completion = new ExecutorCompletionService<Long>(executor);
CountWorker countWorker = null;
for (int i = 0; i < 4; i++) { // 四个线程负责统计
countWorker = new CountWorker(i+1);
completion.submit(countWorker);
}
// 关闭线程池
executor.shutdown();
// 主线程相当于第五个线程,用于汇总数据
long total = 0;
for (int i = 0; i < 4; i++) {
total += completion.take().get();
}
System.out.println(total / 1024 / 1024 / 1024 +"G");
}
}
class CountWorker implements Callable<Long>{
private Integer type;
public CountWorker() {
}
public CountWorker(Integer type) {
this.type = type;
}
@Override
public Long call() throws Exception {
ArrayList<String> paths = new ArrayList<>(Arrays.asList("c:", "d:", "e:", "f:"));
return countDiskSpace(paths.get(type - 1));
}
// 统计磁盘大小
private Long countDiskSpace (String path) {
File file = new File(path);
long totalSpace = file.getTotalSpace();
System.out.println(path + " 总空间大小 : " + totalSpace / 1024 / 1024 / 1024 + "G");
return totalSpace;
}
} </code></pre>
<h3>查考面试题</h3>
<p><strong>1 常见创建线程的方式和其优缺点</strong><br>(1)继承Thread类 (2)实现Runnable接口<br>优缺点:实现一个接口比继承一个类要灵活,减少程序之间的耦合度。缺点就是代码多了一点。</p>
<p><strong>2 start()方法和run()方法的区别</strong><br>start方法可以启动线程,而run方法只是thread的一个普通方法调用。</p>
<p><strong>3 多线程的作用</strong><br>(1)发挥多核CPU的优势,提高CPU的利用率(2)防止阻塞,提高效率</p>
<p><strong>4 什么是线程安全</strong><br>当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。</p>
<p><strong>5 线程安全级别</strong><br>(1)不可变(2)绝对线程安全(3)相对线程安全(4)线程非安全</p>
<p><strong>6 如何在两个线程之间共享数据</strong><br>线程之间数据共享,其实可以理解为线程之间的通信,可以用wait/notify/notifyAll 进行等待和唤醒。</p>
<p><strong>7 用线程池的好处</strong><br>避免频繁地创建和销毁线程,达到线程对象的重用,提高性能,减轻服务器压力。使用线程池还可以根据项目灵活地控制并发的数目。</p>
<p><strong>8 sleep方法和wait方法有什么区别</strong> <br>sleep方法和wait方法都可以用来放弃CPU一定的时间,sleep是thread的方法,不会释放锁。wait是object的方法,会释放锁。</p>
<h3>总结</h3>
<p>1 线程池核心参数有 初始核心线程数,线程池运行最大线程数,空闲线程存活时间,时间单位,任务队列。<br>2 队列是一种数据结构,主要有两类 阻塞队列BlockingQueue,和非阻塞高性能队列ConcurrentLinkedQueue。<br>3 线程安全的两个特性,原子性和可见性。synchronized 关键字具备原子性。volatile 关键字具备可见性。<br>4 单个Atomic类具备原子性和可见性,多个Atomic类不具备原子性,需要synchronized 关键字修饰。<br>5 两个线程持有的锁都是对方想要得到的锁时容易出现死锁的情况,从设计上尽量减少锁的交互。</p>
<p>本章到这里就结束了,涉及的知识点比较多,请参考流程图来学习。如有什么问题可以指出。喜欢的朋友可以点个"推荐"</p>
MySQL 表锁和行锁机制
https://segmentfault.com/a/1190000012773157
2018-01-09T21:43:25+08:00
2018-01-09T21:43:25+08:00
itdragon
https://segmentfault.com/u/itdragon
37
<h2>MySQL 表锁和行锁机制</h2>
<p>行锁变表锁,是福还是坑?如果你不清楚MySQL加锁的原理,你会被它整的很惨!不知坑在何方?没事,我来给你们标记几个坑。遇到了可别乱踩。通过本章内容,带你学习MySQL的行锁,表锁,两种锁的优缺点,行锁变表锁的原因,以及开发中需要注意的事项。还在等啥?经验等你来拿!</p>
<p>MySQL的存储引擎是从MyISAM到InnoDB,锁从表锁到行锁。后者的出现从某种程度上是弥补前者的不足。比如:MyISAM不支持事务,InnoDB支持事务。表锁虽然开销小,锁表快,但高并发下性能低。行锁虽然开销大,锁表慢,但高并发下相比之下性能更高。事务和行锁都是在确保数据准确的基础上提高并发的处理能力。本章重点介绍InnoDB的行锁。</p>
<h3>案例分析</h3>
<p>目前,MySQL常用的存储引擎是InnoDB,相对于MyISAM而言。InnoDB更适合高并发场景,同时也支持事务处理。我们通过下面这个案例(坑),来了解行锁和表锁。<br>业务:因为订单重复导入,需要用脚本将订单状态为"待客服确认"且平台是"xxx"的数据批量修改为"已关闭"。<br>说明:避免直接修改订单表造成数据异常。这里用innodb_lock 表演示InnoDB的行锁。表中有三个字段:id,k(key值),v(value值)。<br>步骤:<br>第一步:连接数据库,这里为了方便区分命名为Transaction-A,设置autocommit为零,表示需手动提交事务。<br>第二步:Transaction-A,执行update修改id为1的命令。<br>第三步:新增一个连接,命名为Transaction-B,能正常修改id为2的数据。再执行修改id为1的数据命令时,却发现该命令一直处理阻塞等待中。<br>第四步:Transaction-A,执行commit命令。Transaction-B,修改id为1的命令自动执行,等待37.51秒。</p>
<p>总结:<strong>多个事务操作同一行数据时,后来的事务处于阻塞等待状态。这样可以避免了脏读等数据一致性的问题。后来的事务可以操作其他行数据,解决了表锁高并发性能低的问题</strong>。</p>
<pre><code class="sql"># Transaction-A
mysql> set autocommit = 0;
mysql> update innodb_lock set v='1001' where id=1;
mysql> commit;
# Transaction-B
mysql> update innodb_lock set v='2001' where id=2;
Query OK, 1 row affected (0.37 sec)
mysql> update innodb_lock set v='1002' where id=1;
Query OK, 1 row affected (37.51 sec)</code></pre>
<p>有了上面的模拟操作,结果和理论又惊奇的一致,似乎可以放心大胆的实战。。。。。。但现实真的很残酷。<br>现实:当执行批量修改数据脚本的时候,行锁升级为表锁。其他对订单的操作都处于等待中,,,<br>原因:InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁!而模拟操作正是通过id去作为检索条件,而id又是MySQL自动创建的唯一索引,所以才忽略了行锁变表锁的情况。<br>步骤:<br>第一步:还原问题,Transaction-A,通过k=1更新v。Transaction-B,通过k=2更新v,命令处于阻塞等待状态。<br>第二步:处理问题,给需要作为查询条件的字段添加索引。用完后可以删掉。</p>
<p>总结:<strong>InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁</strong>。索引失效的原因在上一章节中已经介绍:<a href="https://link.segmentfault.com/?enc=Jze5Eb%2FY0rO2DZE03SH45Q%3D%3D.mJPn7QjWOksyBMw%2FRtUGmXOkRtV36euLCxnEaDHry1g91sQ4J7QtIJXK1gxA84y9" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<pre><code class="sql">Transaction-A
mysql> update innodb_lock set v='1002' where k=1;
mysql> commit;
mysql> create index idx_k on innodb_lock(k);
Transaction-B
mysql> update innodb_lock set v='2002' where k=2;
Query OK, 1 row affected (19.82 sec)</code></pre>
<p>从上面的案例看出,行锁变表锁似乎是一个坑,可MySQL没有这么无聊给你挖坑。这是因为MySQL有自己的执行计划。<br>当你需要更新一张较大表的大部分甚至全表的数据时。而你又傻乎乎地用索引作为检索条件。一不小心开启了行锁(没毛病啊!保证数据的一致性!)。可MySQL却认为大量对一张表使用行锁,会导致事务执行效率低,从而可能造成其他事务长时间锁等待和更多的锁冲突问题,性能严重下降。所以MySQL会将行锁升级为表锁,即实际上并没有使用索引。<br>我们仔细想想也能理解,既然整张表的大部分数据都要更新数据,在一行一行地加锁效率则更低。其实我们可以通过explain命令查看MySQL的执行计划,你会发现key为null。表明MySQL实际上并没有使用索引,行锁升级为表锁也和上面的结论一致。<br>本章重点介绍InnoDB的行锁及其相关的事务知识。如果想了解MySQL的执行计划,请看<a href="https://link.segmentfault.com/?enc=CUXqxr%2BV8BtltWN3%2Ba2ryg%3D%3D.Jf49bwvnM0d1PIN91dlh8RRoYfwmiDPPDwNw8gm7pqzTphrNE3kC8aZJeM3VOuIR" rel="nofollow">上一章节</a>。</p>
<h3>行锁</h3>
<p>行锁的劣势:开销大;加锁慢;会出现死锁<br>行锁的优势:锁的粒度小,发生锁冲突的概率低;处理并发的能力强<br>加锁的方式:自动加锁。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁;当然我们也可以显示的加锁:<br>共享锁:select * from tableName where ... + lock in share more<br>排他锁:select * from tableName where ... + for update <br>InnoDB和MyISAM的最大不同点有两个:一,InnoDB支持事务(transaction);二,默认采用行级锁。加锁可以保证事务的一致性,可谓是有人(锁)的地方,就有江湖(事务);我们先简单了解一下事务知识。</p>
<h4>MySQL 事务属性</h4>
<p>事务是由一组SQL语句组成的逻辑处理单元,事务具有ACID属性。<br><strong>原子性</strong>(Atomicity):事务是一个原子操作单元。在当时原子是不可分割的最小元素,其对数据的修改,要么全部成功,要么全部都不成功。<br><strong>一致性</strong>(Consistent):事务开始到结束的时间段内,数据都必须保持一致状态。<br><strong>隔离性</strong>(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的"独立"环境执行。<br><strong>持久性</strong>(Durable):事务完成后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。</p>
<h4>事务常见问题</h4>
<p><strong>更新丢失</strong>(Lost Update)<br>原因:当多个事务选择同一行操作,并且都是基于最初选定的值,由于每个事务都不知道其他事务的存在,就会发生更新覆盖的问题。类比github提交冲突。</p>
<p><strong>脏读</strong>(Dirty Reads)<br>原因:事务A读取了事务B已经修改但尚未提交的数据。若事务B回滚数据,事务A的数据存在不一致性的问题。</p>
<p><strong>不可重复读</strong>(Non-Repeatable Reads)<br>原因:事务A第一次读取最初数据,第二次读取事务B已经提交的修改或删除数据。导致两次读取数据不一致。不符合事务的隔离性。</p>
<p><strong>幻读</strong>(Phantom Reads)<br>原因:事务A根据相同条件第二次查询到事务B提交的新增数据,两次数据结果集不一致。不符合事务的隔离性。</p>
<p>幻读和脏读有点类似<br>脏读是事务B里面修改了数据,<br>幻读是事务B里面新增了数据。</p>
<h4>事务的隔离级别</h4>
<p>数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大。这是因为事务隔离实质上是将事务在一定程度上"串行"进行,这显然与"并发"是矛盾的。根据自己的业务逻辑,权衡能接受的最大副作用。从而平衡了"隔离" 和 "并发"的问题。MySQL默认隔离级别是可重复读。<br>脏读,不可重复读,幻读,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。</p>
<pre><code class="sql">+------------------------------+---------------------+--------------+--------------+--------------+
| 隔离级别 | 读数据一致性 | 脏读 | 不可重复 读 | 幻读 |
+------------------------------+---------------------+--------------+--------------+--------------+
| 未提交读(Read uncommitted) | 最低级别 | 是 | 是 | 是 |
+------------------------------+---------------------+--------------+--------------+--------------+
| 已提交读(Read committed) | 语句级 | 否 | 是 | 是 |
+------------------------------+---------------------+--------------+--------------+--------------+
| 可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
+------------------------------+---------------------+--------------+--------------+--------------+
| 可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
+------------------------------+---------------------+--------------+--------------+--------------+</code></pre>
<p>查看当前数据库的事务隔离级别:show variables like 'tx_isolation';</p>
<pre><code class="sql">mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+</code></pre>
<h4>间隙锁</h4>
<p>当我们用范围条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做"间隙(GAP)"。InnoDB也会对这个"间隙"加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。</p>
<pre><code class="sql">Transaction-A
mysql> update innodb_lock set k=66 where id >=6;
Query OK, 1 row affected (0.63 sec)
mysql> commit;
Transaction-B
mysql> insert into innodb_lock (id,k,v) values(7,'7','7000');
Query OK, 1 row affected (18.99 sec)</code></pre>
<p>危害(坑):<strong>若执行的条件是范围过大,则InnoDB会将整个范围内所有的索引键值全部锁定,很容易对性能造成影响</strong>。</p>
<h4>排他锁</h4>
<p>排他锁,也称写锁,独占锁,当前写操作没有完成前,它会阻断其他写锁和读锁。<br><img src="/img/remote/1460000012773162?w=630&h=513" alt="排他锁" title="排他锁"></p>
<pre><code class="sql"># Transaction_A
mysql> set autocommit=0;
mysql> select * from innodb_lock where id=4 for update;
+----+------+------+
| id | k | v |
+----+------+------+
| 4 | 4 | 4000 |
+----+------+------+
1 row in set (0.00 sec)
mysql> update innodb_lock set v='4001' where id=4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.04 sec)</code></pre>
<pre><code class="sql"># Transaction_B
mysql> select * from innodb_lock where id=4 for update;
+----+------+------+
| id | k | v |
+----+------+------+
| 4 | 4 | 4001 |
+----+------+------+
1 row in set (9.53 sec)</code></pre>
<h4>共享锁</h4>
<p>共享锁,也称读锁,多用于判断数据是否存在,多个读操作可以同时进行而不会互相影响。当如果事务对读锁进行修改操作,很可能会造成死锁。如下图所示。<br><img src="/img/remote/1460000012773163?w=629&h=465" alt="共享锁" title="共享锁"></p>
<pre><code class="sql"># Transaction_A
mysql> set autocommit=0;
mysql> select * from innodb_lock where id=4 lock in share mode;
+----+------+------+
| id | k | v |
+----+------+------+
| 4 | 4 | 4001 |
+----+------+------+
1 row in set (0.00 sec)
mysql> update innodb_lock set v='4002' where id=4;
Query OK, 1 row affected (31.29 sec)
Rows matched: 1 Changed: 1 Warnings: 0</code></pre>
<pre><code class="sql"># Transaction_B
mysql> set autocommit=0;
mysql> select * from innodb_lock where id=4 lock in share mode;
+----+------+------+
| id | k | v |
+----+------+------+
| 4 | 4 | 4001 |
+----+------+------+
1 row in set (0.00 sec)
mysql> update innodb_lock set v='4002' where id=4;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction</code></pre>
<h4>分析行锁定</h4>
<p>通过检查InnoDB_row_lock 状态变量分析系统上的行锁的争夺情况 show status like 'innodb_row_lock%'</p>
<pre><code class="sql">mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 0 |
| Innodb_row_lock_time_avg | 0 |
| Innodb_row_lock_time_max | 0 |
| Innodb_row_lock_waits | 0 |
+-------------------------------+-------+</code></pre>
<p>innodb_row_lock_current_waits: 当前正在等待锁定的数量<br>innodb_row_lock_time: 从系统启动到现在锁定总时间长度;非常重要的参数,<br>innodb_row_lock_time_avg: 每次等待所花平均时间;非常重要的参数,<br>innodb_row_lock_time_max: 从系统启动到现在等待最常的一次所花的时间;<br>innodb_row_lock_waits: 系统启动后到现在总共等待的次数;非常重要的参数。直接决定优化的方向和策略。</p>
<h4>行锁优化</h4>
<p>1 尽可能让所有数据检索都通过索引来完成,避免无索引行或索引失效导致行锁升级为表锁。<br>2 尽可能避免间隙锁带来的性能下降,减少或使用合理的检索范围。<br>3 尽可能减少事务的粒度,比如控制事务大小,而从减少锁定资源量和时间长度,从而减少锁的竞争等,提供性能。<br>4 尽可能低级别事务隔离,隔离级别越高,并发的处理能力越低。</p>
<h3>表锁</h3>
<p>表锁的优势:开销小;加锁快;无死锁<br>表锁的劣势:锁粒度大,发生锁冲突的概率高,并发处理能力低<br>加锁的方式:自动加锁。查询操作(SELECT),会自动给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT),会自动给涉及的表加写锁。也可以显示加锁:<br>共享读锁:lock table tableName read;<br>独占写锁:lock table tableName write;<br>批量解锁:unlock tables;</p>
<h4>共享读锁</h4>
<p>对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读操作,但会阻塞对同一表的写操作。只有当读锁释放后,才能执行其他进程的写操作。在锁释放前不能取其他表。<br><img src="/img/remote/1460000012773164?w=645&h=461" alt="读锁" title="读锁"></p>
<pre><code class="sql">Transaction-A
mysql> lock table myisam_lock read;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from myisam_lock;
9 rows in set (0.00 sec)
mysql> select * from innodb_lock;
ERROR 1100 (HY000): Table 'innodb_lock' was not locked with LOCK TABLES
mysql> update myisam_lock set v='1001' where k='1';
ERROR 1099 (HY000): Table 'myisam_lock' was locked with a READ lock and can't be updated
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)</code></pre>
<pre><code class="sql">Transaction-B
mysql> select * from myisam_lock;
9 rows in set (0.00 sec)
mysql> select * from innodb_lock;
8 rows in set (0.01 sec)
mysql> update myisam_lock set v='1001' where k='1';
Query OK, 1 row affected (18.67 sec)</code></pre>
<h4>独占写锁</h4>
<p>对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其他进程的读写操作。在锁释放前不能写其他表。<br><img src="/img/remote/1460000012773165" alt="写锁" title="写锁"></p>
<pre><code class="sql">Transaction-A
mysql> set autocommit=0;
Query OK, 0 rows affected (0.05 sec)
mysql> lock table myisam_lock write;
Query OK, 0 rows affected (0.03 sec)
mysql> update myisam_lock set v='2001' where k='2';
Query OK, 1 row affected (0.00 sec)
mysql> select * from myisam_lock;
9 rows in set (0.00 sec)
mysql> update innodb_lock set v='1001' where k='1';
ERROR 1100 (HY000): Table 'innodb_lock' was not locked with LOCK TABLES
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)</code></pre>
<pre><code class="sql">Transaction-B
mysql> select * from myisam_lock;
9 rows in set (42.83 sec)</code></pre>
<p>总结:<strong>表锁,读锁会阻塞写,不会阻塞读。而写锁则会把读写都阻塞</strong>。</p>
<h4>查看加锁情况</h4>
<p>show open tables; 1表示加锁,0表示未加锁。</p>
<pre><code class="sql">mysql> show open tables where in_use > 0;
+----------+-------------+--------+-------------+
| Database | Table | In_use | Name_locked |
+----------+-------------+--------+-------------+
| lock | myisam_lock | 1 | 0 |
+----------+-------------+--------+-------------+</code></pre>
<h4>分析表锁定</h4>
<p>可以通过检查table_locks_waited 和 table_locks_immediate 状态变量分析系统上的表锁定:show status like 'table_locks%'</p>
<pre><code class="sql">mysql> show status like 'table_locks%';
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| Table_locks_immediate | 104 |
| Table_locks_waited | 0 |
+----------------------------+-------+</code></pre>
<p>table_locks_immediate: 表示立即释放表锁数。<br>table_locks_waited: 表示需要等待的表锁数。此值越高则说明存在着越严重的表级锁争用情况。</p>
<p>此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合做写为主表的存储引擎。因为写锁后,其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永久阻塞。</p>
<h3>什么场景下用表锁</h3>
<p>InnoDB默认采用行锁,在未使用索引字段查询时升级为表锁。MySQL这样设计并不是给你挖坑。它有自己的设计目的。<br>即便你在条件中使用了索引字段,MySQL会根据自身的执行计划,考虑是否使用索引(所以explain命令中会有possible_key 和 key)。如果MySQL认为全表扫描效率更高,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引。</p>
<p>第一种情况:<strong>全表更新</strong>。事务需要更新大部分或全部数据,且表又比较大。若使用行锁,会导致事务执行效率低,从而可能造成其他事务长时间锁等待和更多的锁冲突。</p>
<p>第二种情况:<strong>多表查询</strong>。事务涉及多个表,比较复杂的关联查询,很可能引起死锁,造成大量事务回滚。这种情况若能一次性锁定事务涉及的表,从而可以避免死锁、减少数据库因事务回滚带来的开销。</p>
<h3>页锁</h3>
<p>开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发处理能力一般。只需了解一下。</p>
<h3>总结</h3>
<p>1 InnoDB 支持表锁和行锁,使用索引作为检索条件修改数据时采用行锁,否则采用表锁。<br>2 InnoDB 自动给修改操作加锁,给查询操作不自动加锁<br>3 行锁可能因为未使用索引而升级为表锁,所以除了检查索引是否创建的同时,也需要通过explain执行计划查询索引是否被实际使用。<br>4 行锁相对于表锁来说,优势在于高并发场景下表现更突出,毕竟锁的粒度小。<br>5 当表的大部分数据需要被修改,或者是多表复杂关联查询时,建议使用表锁优于行锁。<br>6 为了保证数据的一致完整性,任何一个数据库都存在锁定机制。锁定机制的优劣直接影响到一个数据库的并发处理能力和性能。</p>
<p>到这里,Mysql的表锁和行锁机制就介绍完了,若你不清楚InnoDB的行锁会升级为表锁,那以后会吃大亏的。若有打什么不对的地方请指正。若觉得文章不错,麻烦点个赞!来都来了,留下你的痕迹吧!</p>
MySQL索引优化分析
https://segmentfault.com/a/1190000012691711
2018-01-03T22:22:56+08:00
2018-01-03T22:22:56+08:00
itdragon
https://segmentfault.com/u/itdragon
5
<h2>MySQL索引优化分析</h2>
<p>为什么你写的sql查询慢?为什么你建的索引常失效?通过本章内容,你将学会MySQL性能下降的原因,索引的简介,索引创建的原则,explain命令的使用,以及explain输出字段的意义。助你了解索引,分析索引,使用索引,从而写出更高性能的sql语句。还在等啥子?撸起袖子就是干!</p>
<h3>案例分析</h3>
<p>我们先简单了解一下<strong>非关系型数据库</strong>和<strong>关系型数据库</strong>的区别。<br>MongoDB是NoSQL中的一种。NoSQL的全称是Not only SQL,非关系型数据库。它的特点是<strong>性能高</strong>,<strong>扩张性强</strong>,<strong>模式灵活</strong>,在高并发场景表现得尤为突出。但目前它还只是关系型数据库的补充,它在数据的一致性,数据的安全性,查询的复杂性问题上和关系型数据库还存在一定差距。<br>MySQL是关系性数据库中的一种,<strong>查询功能强</strong>,<strong>数据一致性高</strong>,<strong>数据安全性高</strong>,<strong>支持二级索引</strong>。但性能方面稍逊与MongoDB,特别是百万级别以上的数据,很容易出现查询慢的现象。这时候需要分析查询慢的原因,一般情况下是程序员sql写的烂,或者是没有键索引,或者是索引失效等原因导致的。<br>公司ERP系统数据库主要是MongoDB(最接近关系型数据的NoSQL),其次是Redis,MySQL只占很少的部分。现在又重新使用MySQL,归功于阿里巴巴的奇门系统和聚石塔系统。考虑到订单数量已经是百万级以上,对MySQL的性能分析也就显得格外重要。</p>
<p>我们先通过两个简单的例子来入门。后面会详细介绍各个参数的作用和意义。<br>说明:需要用到的sql已经放在了github上了,喜欢的同学可以点一下star,哈哈。<a href="https://link.segmentfault.com/?enc=ryrxjk8aGgQk4yrR7sZ8WA%3D%3D.jDlbCVzCuITLfznkpVzIaz6i08WOhRbwsS5G8XSPH%2BellmqGS6h4zJauHzBJZJ5qBJoVk2BR7YBsi%2B3DFAXoXw%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h4>场景一:订单导入,通过交易号避免重复导单</h4>
<p>业务逻辑:订单导入时,为了避免重复导单,一般会通过交易号去数据库中查询,判断该订单是否已经存在。</p>
<h5>最基础的sql语句</h5>
<pre><code class="sql">mysql> select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
+-------+--------------------+-------+------+----------+--------------+----------+------------------+-------------+-------------+------------+---------------------+
| id | transaction_id | gross | net | stock_id | order_status | descript | finance_descript | create_type | order_level | input_user | input_date |
+-------+--------------------+-------+------+----------+--------------+----------+------------------+-------------+-------------+------------+---------------------+
| 10000 | 81X97310V32236260E | 6.6 | 6.13 | 1 | 10 | ok | ok | auto | 1 | itdragon | 2017-08-18 17:01:49 |
+-------+--------------------+-------+------+----------+--------------+----------+------------------+-------------+-------------+------------+---------------------+
mysql> explain select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | itdragon_order_list | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+-------------+</code></pre>
<p>查询的本身没有任何问题,在线下的测试环境也没有任何问题。可是,功能一旦上线,查询慢的问题就迎面而来。几百上千万的订单,用全表扫描?啊?哼! <br>怎么知道该sql是全表扫描呢?通过explain命令可以清楚MySQL是如何处理sql语句的。打印的内容分别表示:<br><strong>id</strong> : 查询序列号为1。<br><strong>select_type</strong> : 查询类型是简单查询,简单的select语句没有union和子查询。<br><strong>table</strong> : 表是 itdragon_order_list。<br><strong>partitions</strong> : 没有分区。<br><strong>type</strong> : 连接类型,all表示采用全表扫描的方式。<br><strong>possible_keys</strong> : 可能用到索引为null。<br><strong>key</strong> : 实际用到索引是null。<br><strong>key_len</strong> : 索引长度当然也是null。<br><strong>ref</strong> : 没有哪个列或者参数和key一起被使用。<br><strong>Extra</strong> : 使用了where查询。<br>因为数据库中只有三条数据,所以rows和filtered的信息作用不大。这里需要重点了解的是type为ALL,全表扫描的性能是最差的,假设数据库中有几百万条数据,在没有索引的帮助下会异常卡顿。</p>
<h5>初步优化:为transaction_id创建索引</h5>
<pre><code class="sql">mysql> create unique index idx_order_transaID on itdragon_order_list (transaction_id);
mysql> explain select * from itdragon_order_list where transaction_id = "81X97310V32236260E";
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | itdragon_order_list | NULL | const | idx_order_transaID | idx_order_transaID | 453 | const | 1 | 100 | NULL |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------+</code></pre>
<p>这里创建的索引是唯一索引,而非普通索引。<br>唯一索引打印的type值是const。表示通过索引一次就可以找到。即找到值就结束扫描返回查询结果。<br>普通索引打印的type值是ref。表示非唯一性索引扫描。找到值还要继续扫描,直到将索引文件扫描完为止。(这里没有贴出代码)<br>显而易见,const的性能要远高于ref。并且根据业务逻辑来判断,创建唯一索引是合情合理的。</p>
<h5>再次优化:覆盖索引</h5>
<pre><code class="sql">mysql> explain select transaction_id from itdragon_order_list where transaction_id = "81X97310V32236260E";
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | itdragon_order_list | NULL | const | idx_order_transaID | idx_order_transaID | 453 | const | 1 | 100 | Using index |
+----+-------------+---------------------+------------+-------+--------------------+--------------------+---------+-------+------+----------+-------------+</code></pre>
<p>这里将<code>select * from</code> 改为了 <code>select transaction_id from</code> 后<br>Extra 显示 Using index,表示该查询使用了覆盖索引,这是一个非常好的消息,说明该sql语句的性能很好。若提示的是Using filesort(使用内部排序)和Using temporary(使用临时表)则表明该sql需要立即优化了。<br>根据业务逻辑来的,查询结构返回transaction_id 是可以满足业务逻辑要求的。</p>
<h4>场景二,订单管理页面,通过订单级别和订单录入时间排序</h4>
<p>业务逻辑:优先处理订单级别高,录入时间长的订单。<br>既然是排序,首先想到的应该是order by, 还有一个可怕的 Using filesort 等着你。</p>
<h5>最基础的sql语句</h5>
<pre><code class="sql">mysql> explain select * from itdragon_order_list order by order_level,input_date;
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | itdragon_order_list | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100 | Using filesort |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+</code></pre>
<p>首先,采用全表扫描就不合理,还使用了文件排序Using filesort,更加拖慢了性能。<br>MySQL在4.1版本之前文件排序是采用双路排序的算法,由于两次扫描磁盘,I/O耗时太长。后优化成单路排序算法。其本质就是用空间换时间,但如果数据量太大,buffer的空间不足,会导致多次I/O的情况。其效果反而更差。与其找运维同事修改MySQL配置,还不如自己乖乖地建索引。</p>
<h5>初步优化:为order_level,input_date 创建复合索引</h5>
<pre><code class="sql">mysql> create index idx_order_levelDate on itdragon_order_list (order_level,input_date);
mysql> explain select * from itdragon_order_list order by order_level,input_date;
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | itdragon_order_list | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100 | Using filesort |
+----+-------------+---------------------+------------+------+---------------+------+---------+------+------+----------+----------------+</code></pre>
<p>创建复合索引后你会惊奇的发现,和没创建索引一样???都是全表扫描,都用到了文件排序。是索引失效?还是索引创建失败?我们试着看看下面打印情况</p>
<pre><code class="sql">mysql> explain select order_level,input_date from itdragon_order_list order by order_level,input_date;
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | itdragon_order_list | NULL | index | NULL | idx_order_levelDate | 68 | NULL | 3 | 100 | Using index |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------------+</code></pre>
<p>将<code>select * from</code> 换成了 <code>select order_level,input_date from</code> 后。type从all升级为index,表示(full index scan)全索引文件扫描,Extra也显示使用了覆盖索引。可是不对啊!!!!检索虽然快了,但返回的内容只有order_level和input_date 两个字段,让业务同事怎么用?难道把每个字段都建一个复合索引?<br>MySQL没有这么笨,可以使用force index 强制指定索引。在原来的sql语句上修改 <code> force index(idx_order_levelDate) </code> 即可。</p>
<pre><code class="sql">mysql> explain select * from itdragon_order_list force index(idx_order_levelDate) order by order_level,input_date;
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------+
| 1 | SIMPLE | itdragon_order_list | NULL | index | NULL | idx_order_levelDate | 68 | NULL | 3 | 100 | NULL |
+----+-------------+---------------------+------------+-------+---------------+---------------------+---------+------+------+----------+-------+</code></pre>
<h5>再次优化:订单级别真的要排序么?</h5>
<p>其实给订单级别排序意义并不大,给订单级别添加索引意义也不大。因为order_level的值可能只有,低,中,高,加急,这四种。对于这种重复且分布平均的字段,排序和加索引的作用不大。<br>我们能否先固定 order_level 的值,然后再给 input_date 排序?如果查询效果明显,是可以推荐业务同事使用该查询方式。</p>
<pre><code class="sql">mysql> explain select * from itdragon_order_list where order_level=3 order by input_date;
+----+-------------+---------------------+------------+------+---------------------+---------------------+---------+-------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------+------------+------+---------------------+---------------------+---------+-------+------+----------+-----------------------+
| 1 | SIMPLE | itdragon_order_list | NULL | ref | idx_order_levelDate | idx_order_levelDate | 5 | const | 1 | 100 | Using index condition |
+----+-------------+---------------------+------------+------+---------------------+---------------------+---------+-------+------+----------+-----------------------+</code></pre>
<p>和之前的sql比起来,type从index 升级为 ref(非唯一性索引扫描)。索引的长度从68变成了5,说明只用了一个索引。ref也是一个常量。Extra 为Using index condition 表示自动根据临界值,选择索引扫描还是全表扫描。总的来说性能远胜于之前的sql。</p>
<p>上面两个案例只是快速入门,我们需严记一点:优化是基于业务逻辑来的。绝对不能为了优化而擅自修改业务逻辑。如果能修改当然是最好的。</p>
<h3>索引简介</h3>
<p>官方定义:索引(Index) 是帮助MySQL高效获取数据的数据结构。<br>大家一定很好奇,索引为什么是一种数据结构,它又是怎么提高查询的速度?我们拿最常用的二叉树来分析索引的工作原理。看下面的图片:<br><img src="/img/remote/1460000012691716" alt="" title=""></p>
<p>创建索引的优势<br>1 提高数据的检索速度,降低数据库IO成本:使用索引的意义就是通过缩小表中需要查询的记录的数目从而加快搜索的速度。<br>2 降低数据排序的成本,降低CPU消耗:索引之所以查的快,是因为先将数据排好序,若该字段正好需要排序,则真好降低了排序的成本。</p>
<p>创建索引的劣势<br>1 占用存储空间:索引实际上也是一张表,记录了主键与索引字段,一般以索引文件的形式存储在磁盘上。<br>2 降低更新表的速度:表的数据发生了变化,对应的索引也需要一起变更,从而减低的更新速度。否则索引指向的物理数据可能不对,这也是索引失效的原因之一。<br>3 优质索引创建难:索引的创建并非一日之功,也并非一直不变。需要频繁根据用户的行为和具体的业务逻辑去创建最佳的索引。</p>
<h3>索引分类</h3>
<p>我们常说的索引一般指的是BTree(多路搜索树)结构组织的索引。其中还有聚合索引,次要索引,复合索引,前缀索引,唯一索引,统称索引,当然除了B+树外,还有哈希索引(hash index)等。</p>
<p><strong>单值索引</strong>:一个索引只包含单个列,一个表可以有多个单列索引<br><strong>唯一索引</strong>:索引列的值必须唯一,但允许有空值<br><strong>复合索引</strong>:一个索引包含多个列,实际开发中推荐使用<br>实际开发中推荐使用复合索引,并且单表创建的索引个数建议不要超过五个</p>
<p>基本语法:<br>创建:</p>
<pre><code class="sql">create [unique] index indexName on tableName (columnName...)
alter tableName add [unique] index [indexName] on (columnName...)</code></pre>
<p>删除:</p>
<pre><code class="sql">drop index [indexName] on tableName</code></pre>
<p>查看:</p>
<pre><code class="sql">show index from tableName</code></pre>
<p>哪些情况需要建索引:<br>1 主键,唯一索引<br>2 经常用作查询条件的字段需要创建索引<br>3 经常需要排序、分组和统计的字段需要建立索引<br>4 查询中与其他表关联的字段,外键关系建立索引</p>
<p>哪些情况不要建索引:<br>1 表的记录太少,百万级以下的数据不需要创建索引<br>2 经常增删改的表不需要创建索引<br>3 数据重复且分布平均的字段不需要创建索引,如 true,false 之类。<br>4 频发更新的字段不适合创建索引<br>5 where条件里用不到的字段不需要创建索引</p>
<h3>性能分析</h3>
<h4>MySQL 自身瓶颈</h4>
<p>MySQL自身参见的性能问题有磁盘空间不足,磁盘I/O太大,服务器硬件性能低。<br>1 CPU:CPU 在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候<br>2 IO:磁盘I/O 瓶颈发生在装入数据远大于内存容量的时候<br>3 服务器硬件的性能瓶颈:top,free,iostat 和 vmstat来查看系统的性能状态</p>
<h4>explain 分析sql语句</h4>
<p>使用explain关键字可以模拟优化器执行sql查询语句,从而得知MySQL 是如何处理sql语句。</p>
<table><thead><tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>partitions</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr></thead></table>
<h5>id</h5>
<p>select 查询的序列号,包含一组可以重复的数字,表示查询中执行sql语句的顺序。一般有三种情况:<br>第一种:id全部相同,sql的执行顺序是由上至下;<br>第二种:id全部不同,sql的执行顺序是根据id大的优先执行;<br>第三种:id既存在相同,又存在不同的。先根据id大的优先执行,再根据相同id从上至下的执行。</p>
<h5>select_type</h5>
<p>select 查询的类型,主要是用于区别普通查询,联合查询,嵌套的复杂查询<br><strong>simple</strong>:简单的select 查询,查询中不包含子查询或者union<br><strong>primary</strong>:查询中若包含任何复杂的子查询,最外层查询则被标记为primary<br><strong>subquery</strong>:在select或where 列表中包含了子查询<br><strong>derived</strong>:在from列表中包含的子查询被标记为derived(衍生)MySQL会递归执行这些子查询,把结果放在临时表里。<br><strong>union</strong>:若第二个select出现在union之后,则被标记为union,若union包含在from子句的子查询中,外层select将被标记为:derived<br><strong>union result</strong>:从union表获取结果的select</p>
<h5>partitions</h5>
<p>表所使用的分区,如果要统计十年公司订单的金额,可以把数据分为十个区,每一年代表一个区。这样可以大大的提高查询效率。</p>
<h5>type</h5>
<p>这是一个非常重要的参数,连接类型,常见的有:all , index , range , ref , eq_ref , const , system , null 八个级别。<br>性能从最优到最差的排序:system > const > eq_ref > ref > range > index > all<br>对java程序员来说,若保证查询至少达到range级别或者最好能达到ref则算是一个优秀而又负责的程序员。<br><strong>all</strong>:(full table scan)全表扫描无疑是最差,若是百万千万级数据量,全表扫描会非常慢。<br><strong>index</strong>:(full index scan)全索引文件扫描比all好很多,毕竟从索引树中找数据,比从全表中找数据要快。<br><strong>range</strong>:只检索给定范围的行,使用索引来匹配行。范围缩小了,当然比全表扫描和全索引文件扫描要快。sql语句中一般会有between,in,>,< 等查询。<br><strong>ref</strong>:非唯一性索引扫描,本质上也是一种索引访问,返回所有匹配某个单独值的行。比如查询公司所有属于研发团队的同事,匹配的结果是多个并非唯一值。<br><strong>eq_ref</strong>:唯一性索引扫描,对于每个索引键,表中有一条记录与之匹配。比如查询公司的CEO,匹配的结果只可能是一条记录,<br><strong>const</strong>:表示通过索引一次就可以找到,const用于比较primary key 或者unique索引。因为只匹配一行数据,所以很快,若将主键至于where列表中,MySQL就能将该查询转换为一个常量。<br><strong>system</strong>:表只有一条记录(等于系统表),这是const类型的特列,平时不会出现,了解即可</p>
<h5>possible_keys</h5>
<p>显示查询语句可能用到的索引(一个或多个或为null),不一定被查询实际使用。仅供参考使用。</p>
<h5>key</h5>
<p>显示查询语句实际使用的索引。若为null,则表示没有使用索引。</p>
<h5>key_len</h5>
<p>显示索引中使用的字节数,可通过key_len计算查询中使用的索引长度。在不损失精确性的情况下索引长度越短越好。key_len 显示的值为索引字段的最可能长度,并非实际使用长度,即key_len是根据表定义计算而得,并不是通过表内检索出的。</p>
<h5>ref</h5>
<p>显示索引的哪一列或常量被用于查找索引列上的值。</p>
<h5>rows</h5>
<p>根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数,值越大越不好。</p>
<h5>extra</h5>
<p><strong>Using filesort</strong>: 说明MySQL会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL中无法利用索引完成的排序操作称为“文件排序” 。出现这个就要立刻优化sql。<br><strong>Using temporary</strong>: 使用了临时表保存中间结果,MySQL在对查询结果排序时使用临时表。常见于排序 order by 和 分组查询 group by。 出现这个更要立刻优化sql。<br><strong>Using index</strong>: 表示相应的select 操作中使用了覆盖索引(Covering index),避免访问了表的数据行,效果不错!如果同时出现Using where,表明索引被用来执行索引键值的查找。如果没有同时出现Using where,表示索引用来读取数据而非执行查找动作。<br>覆盖索引(Covering Index) :也叫索引覆盖,就是select 的数据列只用从索引中就能够取得,不必读取数据行,MySQL可以利用索引返回select 列表中的字段,而不必根据索引再次读取数据文件。<br><strong>Using index condition</strong>: 在5.6版本后加入的新特性,优化器会在索引存在的情况下,通过符合RANGE范围的条数 和 总数的比例来选择是使用索引还是进行全表遍历。<br><strong>Using where</strong>: 表明使用了where 过滤<br><strong>Using join buffer</strong>: 表明使用了连接缓存<br><strong>impossible where</strong>: where 语句的值总是false,不可用,不能用来获取任何元素<br><strong>distinct</strong>: 优化distinct操作,在找到第一匹配的元组后即停止找同样值的动作。</p>
<h5>filtered</h5>
<p>一个百分比的值,和rows 列的值一起使用,可以估计出查询执行计划(QEP)中的前一个表的结果集,从而确定join操作的循环次数。小表驱动大表,减轻连接的次数。</p>
<p>通过explain的参数介绍,我们可以得知:<br>1 表的读取顺序(id)<br>2 数据读取操作的操作类型(type)<br>3 哪些索引被实际使用(key)<br>4 表之间的引用(ref)<br>5 每张表有多少行被优化器查询(rows)</p>
<h3>性能下降的原因</h3>
<p>从程序员的角度<br>1 查询语句写的不好<br>2 没建索引,索引建的不合理或索引失效<br>3 关联查询有太多的join<br>从服务器的角度<br>1 服务器磁盘空间不足<br>2 服务器调优配置参数设置不合理</p>
<h3>总结</h3>
<p>1 索引是排好序且快速查找的数据结构。其目的是为了提高查询的效率。<br>2 创建索引后,查询数据变快,但更新数据变慢。<br>3 性能下降的原因很可能是索引失效导致。<br>4 索引创建的原则,经常查询的字段适合创建索引,频繁需要更新的数据不适合创建索引。<br>5 索引字段频繁更新,或者表数据物理删除容易造成索引失效。<br>6 擅用 explain 分析sql语句<br>7 除了优化sql语句外,还可以优化表的设计。如尽量做成单表查询,减少表之间的关联。设计归档表等。</p>
<p>到这里,MySQL的索引优化分析就结束了,有什么不对的地方,大家可以提出来。如果觉得不错可以点一下推荐。</p>
<h3>参考文献</h3>
<p>MySQL order by排序优化: <a href="https://link.segmentfault.com/?enc=iimkLCp3y%2BS3efz2wBKK0A%3D%3D.Mf4A9l2F1nMvLixmfErs04RgoqdQ%2B5Q3N%2BkWUApH178AqjnZUkG%2FZykR3N5dQMBj" rel="nofollow">http://blog.51cto.com/ustb80/...</a></p>
单点登录系统实现
https://segmentfault.com/a/1190000012556776
2017-12-23T22:51:40+08:00
2017-12-23T22:51:40+08:00
itdragon
https://segmentfault.com/u/itdragon
29
<h2>单点登录系统实现基于SpringBoot</h2>
<p>今天的干货有点湿,里面夹杂着我的泪水。可能也只有代码才能让我暂时的平静。通过本章内容你将学到单点登录系统和传统登录系统的区别,单点登录系统设计思路,Spring4 Java配置方式整合HttpClient,整合SolrJ ,HttpClient简易教程。还在等什么?撸起袖子开始干吧!<br>效果图:8081端口是sso系统,其他两个8082和8083端口模拟两个系统。登录成功后检查Redis数据库中是否有值。<br><img src="/img/remote/1460000012556781?w=803&h=623" alt="效果图" title="效果图"></p>
<p>技术:SpringBoot,SpringMVC,Spring,SpringData,Redis,HttpClient<br>说明:本章的用户登录注册的代码部分已经在SpringBoot基础入门中介绍过了,这里不会重复贴代码。<br>源码: <a href="https://link.segmentfault.com/?enc=TSUy%2BWHoIUL7rDEo5fkGTg%3D%3D.fe5Im%2F3KtZzxJkPtHiW%2ByDFiqOFlwTfYdzpH8Mvf2IU%2FDnMdT6MyWx8y3lofIA1twldVk%2FvwjhxzWBK6ISQQzsLGteCE7XG0Kg4H1P8Nq1Q%3D" rel="nofollow">https://github.com/ITDragonBlog/daydayup/tree/master/SpringBoot/SSO</a><br>SpringBoot基础入门:<a href="https://link.segmentfault.com/?enc=D2vbt4qS7SbnwJ59CWk8ZA%3D%3D.CHyQG%2BDlNPZsQaboWmoqdAylJcIMtMgWuzeYmxwD3uYd1S9wEjz%2BTV%2BSisJNPloM" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<h3>单点登录系统简介</h3>
<p><img src="/img/remote/1460000012556782?w=1109&h=926" alt="单点登录系统" title="单点登录系统"><br>在传统的系统,或者是只有一个服务器的系统中。Session在一个服务器中,各个模块都可以直接获取,只需登录一次就进入各个模块。若在服务器集群或者是分布式系统架构中,每个服务器之间的Session并不是共享的,这会出现每个模块都要登录的情况。这时候需要通过单点登录系统(Single Sign On)将用户信息存在Redis数据库中实现Session共享的效果。从而实现一次登录就可以访问所有相互信任的应用系统。</p>
<h3>单点登录系统实现</h3>
<p>Maven项目核心配置文件 pom.xml 需要在原来的基础上添加 httpclient和jedis jar包</p>
<pre><code> <dependency> <!-- http client version is 4.5.3 -->
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency> <!-- redis java client version is 2.9.0 -->
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency></code></pre>
<h3>Spring4 Java配置方式</h3>
<p>这里,我们需要整合httpclient用于各服务之间的通讯(也可以用okhttp)。同时还需要整合redis用于存储用户信息(Session共享)。<br>在Spring3.x之前,一般在<strong>应用的基本配置</strong>用xml,比如数据源、资源文件等。<strong>业务开发</strong>用注解,比如Component,Service,Controller等。其实在Spring3.x的时候就已经提供了Java配置方式。现在的Spring4.x和SpringBoot都开始推荐使用Java配置方式配置bean。它可以使bean的结构更加的清晰。</p>
<h4>整合 HttpClient</h4>
<p>HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。<br>HttpClient4.5系列教程 : <a href="https://link.segmentfault.com/?enc=%2BoAI3R1N%2FGAiD0kBNRVd7Q%3D%3D.%2F4wkXD%2Fpkzlr5P8vT78p3UC2femOi9avnc0A4pAR9Na%2BWRUgN0z6Z9IHsp2dDibJ9KeuKmhl7n2tIX8onZVnuA%3D%3D" rel="nofollow">http://blog.csdn.net/column/d...</a></p>
<p>首先在src/main/resources 目录下创建 httpclient.properties 配置文件</p>
<pre><code>#设置整个连接池默认最大连接数
http.defaultMaxPerRoute=100
#设置整个连接池最大连接数
http.maxTotal=300
#设置请求超时
http.connectTimeout=1000
#设置从连接池中获取到连接的最长时间
http.connectionRequestTimeout=500
#设置数据传输的最长时间
http.socketTimeout=10000</code></pre>
<p>然后在 src/main/java/com/itdragon/config 目录下创建 HttpclientSpringConfig.java 文件<br>这里用到了四个很重要的注解<br>@Configuration : 作用于类上,指明该类就相当于一个xml配置文件<br>@Bean : 作用于方法上,指明该方法相当于xml配置中的<bean>,注意方法名的命名规范<br>@PropertySource : 指定读取的配置文件,引入多个value={"xxx:xxx","xxx:xxx"},ignoreResourceNotFound=true 文件不存在是忽略<br>@Value : 获取配置文件的值,该注解还有很多语法知识,这里暂时不扩展开</p>
<pre><code>package com.itdragon.config;
import java.util.concurrent.TimeUnit;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.IdleConnectionEvictor;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Scope;
/**
* @Configuration 作用于类上,相当于一个xml配置文件
* @Bean 作用于方法上,相当于xml配置中的<bean>
* @PropertySource 指定读取的配置文件
* @Value 获取配置文件的值
*/
@Configuration
@PropertySource(value = "classpath:httpclient.properties")
public class HttpclientSpringConfig {
@Value("${http.maxTotal}")
private Integer httpMaxTotal;
@Value("${http.defaultMaxPerRoute}")
private Integer httpDefaultMaxPerRoute;
@Value("${http.connectTimeout}")
private Integer httpConnectTimeout;
@Value("${http.connectionRequestTimeout}")
private Integer httpConnectionRequestTimeout;
@Value("${http.socketTimeout}")
private Integer httpSocketTimeout;
@Autowired
private PoolingHttpClientConnectionManager manager;
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
// 最大连接数
poolingHttpClientConnectionManager.setMaxTotal(httpMaxTotal);
// 每个主机的最大并发数
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(httpDefaultMaxPerRoute);
return poolingHttpClientConnectionManager;
}
@Bean // 定期清理无效连接
public IdleConnectionEvictor idleConnectionEvictor() {
return new IdleConnectionEvictor(manager, 1L, TimeUnit.HOURS);
}
@Bean // 定义HttpClient对象 注意该对象需要设置scope="prototype":多例对象
@Scope("prototype")
public CloseableHttpClient closeableHttpClient() {
return HttpClients.custom().setConnectionManager(this.manager).build();
}
@Bean // 请求配置
public RequestConfig requestConfig() {
return RequestConfig.custom().setConnectTimeout(httpConnectTimeout) // 创建连接的最长时间
.setConnectionRequestTimeout(httpConnectionRequestTimeout) // 从连接池中获取到连接的最长时间
.setSocketTimeout(httpSocketTimeout) // 数据传输的最长时间
.build();
}
}</code></pre>
<h4>整合 Redis</h4>
<p>SpringBoot官方其实提供了spring-boot-starter-redis pom 帮助我们快速开发,但我们也可以自定义配置,这样可以更方便地掌控。<br>Redis 系列教程 : <a href="https://link.segmentfault.com/?enc=Jp7JMcedcA4HP6YE5pRnhA%3D%3D.9s1%2F8c29cAv%2FDhLLdWpJulU6BRiwOAiHbqM7R%2BBInJLVp82G7XFoY7XvRdojTKJAUbPXSppbLjqeF7ichS7dlg%3D%3D" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<p>首先在src/main/resources 目录下创建 redis.properties 配置文件<br>设置Redis主机的ip地址和端口号,和存入Redis数据库中的key以及存活时间。这里为了方便测试,存活时间设置的比较小。这里的配置是单例Redis。</p>
<pre><code>redis.node.host=192.168.225.131
redis.node.port=6379
REDIS_USER_SESSION_KEY=REDIS_USER_SESSION
SSO_SESSION_EXPIRE=30</code></pre>
<p>在src/main/java/com/itdragon/config 目录下创建 RedisSpringConfig.java 文件</p>
<pre><code>package com.itdragon.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedisPool;
@Configuration
@PropertySource(value = "classpath:redis.properties")
public class RedisSpringConfig {
@Value("${redis.maxTotal}")
private Integer redisMaxTotal;
@Value("${redis.node.host}")
private String redisNodeHost;
@Value("${redis.node.port}")
private Integer redisNodePort;
private JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(redisMaxTotal);
return jedisPoolConfig;
}
@Bean
public JedisPool getJedisPool(){ // 省略第一个参数则是采用 Protocol.DEFAULT_DATABASE
JedisPool jedisPool = new JedisPool(jedisPoolConfig(), redisNodeHost, redisNodePort);
return jedisPool;
}
@Bean
public ShardedJedisPool shardedJedisPool() {
List<JedisShardInfo> jedisShardInfos = new ArrayList<JedisShardInfo>();
jedisShardInfos.add(new JedisShardInfo(redisNodeHost, redisNodePort));
return new ShardedJedisPool(jedisPoolConfig(), jedisShardInfos);
}
}</code></pre>
<h4>Service 层</h4>
<p>在src/main/java/com/itdragon/service 目录下创建 UserService.java 文件,它负责三件事情<br>第一件事件:验证用户信息是否正确,并将登录成功的用户信息保存到Redis数据库中。<br>第二件事件:负责判断用户令牌是否过期,若没有则刷新令牌存活时间。<br>第三件事件:负责从Redis数据库中删除用户信息。<br>这里用到了一些工具类,不影响学习,可以从源码中直接获取。</p>
<pre><code>package com.itdragon.service;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import com.itdragon.pojo.ItdragonResult;
import com.itdragon.pojo.User;
import com.itdragon.repository.JedisClient;
import com.itdragon.repository.UserRepository;
import com.itdragon.utils.CookieUtils;
import com.itdragon.utils.ItdragonUtils;
import com.itdragon.utils.JsonUtils;
@Service
@Transactional
@PropertySource(value = "classpath:redis.properties")
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private JedisClient jedisClient;
@Value("${REDIS_USER_SESSION_KEY}")
private String REDIS_USER_SESSION_KEY;
@Value("${SSO_SESSION_EXPIRE}")
private Integer SSO_SESSION_EXPIRE;
public ItdragonResult userLogin(String account, String password,
HttpServletRequest request, HttpServletResponse response) {
// 判断账号密码是否正确
User user = userRepository.findByAccount(account);
if (!ItdragonUtils.decryptPassword(user, password)) {
return ItdragonResult.build(400, "账号名或密码错误");
}
// 生成token
String token = UUID.randomUUID().toString();
// 清空密码和盐避免泄漏
String userPassword = user.getPassword();
String userSalt = user.getSalt();
user.setPassword(null);
user.setSalt(null);
// 把用户信息写入 redis
jedisClient.set(REDIS_USER_SESSION_KEY + ":" + token, JsonUtils.objectToJson(user));
// user 已经是持久化对象,被保存在session缓存当中,若user又重新修改属性值,那么在提交事务时,此时 hibernate对象就会拿当前这个user对象和保存在session缓存中的user对象进行比较,如果两个对象相同,则不会发送update语句,否则会发出update语句。
user.setPassword(userPassword);
user.setSalt(userSalt);
// 设置 session 的过期时间
jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
// 添加写 cookie 的逻辑,cookie 的有效期是关闭浏览器就失效。
CookieUtils.setCookie(request, response, "USER_TOKEN", token);
// 返回token
return ItdragonResult.ok(token);
}
public void logout(String token) {
jedisClient.del(REDIS_USER_SESSION_KEY + ":" + token);
}
public ItdragonResult queryUserByToken(String token) {
// 根据token从redis中查询用户信息
String json = jedisClient.get(REDIS_USER_SESSION_KEY + ":" + token);
// 判断是否为空
if (StringUtils.isEmpty(json)) {
return ItdragonResult.build(400, "此session已经过期,请重新登录");
}
// 更新过期时间
jedisClient.expire(REDIS_USER_SESSION_KEY + ":" + token, SSO_SESSION_EXPIRE);
// 返回用户信息
return ItdragonResult.ok(JsonUtils.jsonToPojo(json, User.class));
}
}</code></pre>
<h3>Controller 层</h3>
<p>负责跳转登录页面跳转</p>
<pre><code>package com.itdragon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PageController {
@RequestMapping("/login")
public String showLogin(String redirect, Model model) {
model.addAttribute("redirect", redirect);
return "login";
}
}</code></pre>
<p>负责用户的登录,退出,获取令牌的操作</p>
<pre><code>package com.itdragon.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.itdragon.pojo.ItdragonResult;
import com.itdragon.service.UserService;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value="/login", method=RequestMethod.POST)
@ResponseBody
public ItdragonResult userLogin(String username, String password,
HttpServletRequest request, HttpServletResponse response) {
try {
ItdragonResult result = userService.userLogin(username, password, request, response);
return result;
} catch (Exception e) {
e.printStackTrace();
return ItdragonResult.build(500, "");
}
}
@RequestMapping(value="/logout/{token}")
public String logout(@PathVariable String token) {
userService.logout(token); // 思路是从Redis中删除key,实际情况请和业务逻辑结合
return "index";
}
@RequestMapping("/token/{token}")
@ResponseBody
public Object getUserByToken(@PathVariable String token) {
ItdragonResult result = null;
try {
result = userService.queryUserByToken(token);
} catch (Exception e) {
e.printStackTrace();
result = ItdragonResult.build(500, "");
}
return result;
}
}</code></pre>
<h3>视图层</h3>
<p>一个简单的登录页面</p>
<pre><code><%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!doctype html>
<html lang="zh">
<head>
<meta name="viewport" content="initial-scale=1.0, width=device-width, user-scalable=no" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,Chrome=1" />
<meta http-equiv="X-UA-Compatible" content="IE=8" />
<title>欢迎登录</title>
<link type="image/x-icon" href="images/favicon.ico" rel="shortcut icon">
<link rel="stylesheet" href="static/css/main.css" />
</head>
<body>
<div class="wrapper">
<div class="container">
<h1>Welcome</h1>
<form method="post" onsubmit="return false;" class="form">
<input type="text" value="itdragon" name="username" placeholder="Account"/>
<input type="password" value="123456789" name="password" placeholder="Password"/>
<button type="button" id="login-button">Login</button>
</form>
</div>
<ul class="bg-bubbles">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<script type="text/javascript" src="static/js/jquery-1.10.1.min.js" ></script>
<script type="text/javascript">
var redirectUrl = "${redirect}"; // 浏览器中返回的URL
function doLogin() {
$.post("/user/login", $(".form").serialize(),function(data){
if (data.status == 200) {
if (redirectUrl == "") {
location.href = "http://localhost:8082";
} else {
location.href = redirectUrl;
}
} else {
alert("登录失败,原因是:" + data.msg);
}
});
}
$(function(){
$("#login-button").click(function(){
doLogin();
});
});
</script>
</body>
</html></code></pre>
<h3>HttpClient 基础语法</h3>
<p>这里封装了get,post请求的方法</p>
<pre><code>package com.itdragon.utils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
public class HttpClientUtil {
public static String doGet(String url) { // 无参数get请求
return doGet(url, null);
}
public static String doGet(String url, Map<String, String> param) { // 带参数get请求
CloseableHttpClient httpClient = HttpClients.createDefault(); // 创建一个默认可关闭的Httpclient 对象
String resultMsg = ""; // 设置返回值
CloseableHttpResponse response = null; // 定义HttpResponse 对象
try {
URIBuilder builder = new URIBuilder(url); // 创建URI,可以设置host,设置参数等
if (param != null) {
for (String key : param.keySet()) {
builder.addParameter(key, param.get(key));
}
}
URI uri = builder.build();
HttpGet httpGet = new HttpGet(uri); // 创建http GET请求
response = httpClient.execute(httpGet); // 执行请求
if (response.getStatusLine().getStatusCode() == 200) { // 判断返回状态为200则给返回值赋值
resultMsg = EntityUtils.toString(response.getEntity(), "UTF-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally { // 不要忘记关闭
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultMsg;
}
public static String doPost(String url) { // 无参数post请求
return doPost(url, null);
}
public static String doPost(String url, Map<String, String> param) {// 带参数post请求
CloseableHttpClient httpClient = HttpClients.createDefault(); // 创建一个默认可关闭的Httpclient 对象
CloseableHttpResponse response = null;
String resultMsg = "";
try {
HttpPost httpPost = new HttpPost(url); // 创建Http Post请求
if (param != null) { // 创建参数列表
List<NameValuePair> paramList = new ArrayList<NameValuePair>();
for (String key : param.keySet()) {
paramList.add(new BasicNameValuePair(key, param.get(key)));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);// 模拟表单
httpPost.setEntity(entity);
}
response = httpClient.execute(httpPost); // 执行http请求
if (response.getStatusLine().getStatusCode() == 200) {
resultMsg = EntityUtils.toString(response.getEntity(), "utf-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultMsg;
}
public static String doPostJson(String url, String json) {
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
HttpPost httpPost = new HttpPost(url);
StringEntity entity = new StringEntity(json, ContentType.APPLICATION_JSON);
httpPost.setEntity(entity);
response = httpClient.execute(httpPost);
if (response.getStatusLine().getStatusCode() == 200) {
resultString = EntityUtils.toString(response.getEntity(), "utf-8");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
}</code></pre>
<h3>Spring 自定义拦截器</h3>
<p>这里是另外一个项目 itdragon-service-test-sso 中的代码,<br>首先在src/main/resources/spring/springmvc.xml 中配置拦截器,设置那些请求需要拦截</p>
<pre><code> <!-- 拦截器配置 -->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/github/**"/>
<bean class="com.itdragon.interceptors.UserLoginHandlerInterceptor"/>
</mvc:interceptor>
</mvc:interceptors></code></pre>
<p>然后在 src/main/java/com/itdragon/interceptors 目录下创建 UserLoginHandlerInterceptor.java 文件</p>
<pre><code>package com.itdragon.interceptors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.itdragon.pojo.User;
import com.itdragon.service.UserService;
import com.itdragon.utils.CookieUtils;
public class UserLoginHandlerInterceptor implements HandlerInterceptor {
public static final String COOKIE_NAME = "USER_TOKEN";
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = CookieUtils.getCookieValue(request, COOKIE_NAME);
User user = this.userService.getUserByToken(token);
if (StringUtils.isEmpty(token) || null == user) {
// 跳转到登录页面,把用户请求的url作为参数传递给登录页面。
response.sendRedirect("http://localhost:8081/login?redirect=" + request.getRequestURL());
// 返回false
return false;
}
// 把用户信息放入Request
request.setAttribute("user", user);
// 返回值决定handler是否执行。true:执行,false:不执行。
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
}
}</code></pre>
<h3>可能存在的问题</h3>
<h5>SpringData 自动更新问题</h5>
<p>SpringData 是基于Hibernate的。当User 已经是持久化对象,被保存在session缓存当中。若User又重新修改属性值,在提交事务时,此时hibernate对象就会拿当前这个User对象和保存在session缓存中的User对象进行比较,如果两个对象相同,则不会发送update语句,否则,会发出update语句。<br>笔者采用比较傻的方法,就是在提交事务之前把数据还原。各位如果有更好的办法请告知,谢谢!<br>参考博客:<a href="https://link.segmentfault.com/?enc=aa1CxWJBA88x5Cbc1O5d%2Bw%3D%3D.2Htu8%2BugREukLA7%2B2YQEEg7x03bejte9xq3lU3KIrcJhchq81drblBxyTt8%2BUedCrzdn0gFqjIyh9wqLWyRP1A%3D%3D" rel="nofollow">http://www.cnblogs.com/xiaolu...</a></p>
<h5>检查用户信息是否保存</h5>
<p>登录成功后,进入Redis客户端查看用户信息是否保存成功。同时为了方便测试,也可以删除这个key。</p>
<pre><code>[root@localhost bin]# ./redis-cli -h 192.168.225.131 -p 6379
192.168.225.131:6379>
192.168.225.131:6379> keys *
1) "REDIS_USER_SESSION:1d869ac0-3d22-4e22-bca0-37c8dfade9ad"
192.168.225.131:6379> get REDIS_USER_SESSION:1d869ac0-3d22-4e22-bca0-37c8dfade9ad
"{\"id\":3,\"account\":\"itdragon\",\"userName\":\"ITDragonGit\",\"plainPassword\":null,\"password\":null,\"salt\":null,\"iphone\":\"12349857999\",\"email\":\"itdragon@git.com\",\"platform\":\"github\",\"createdDate\":\"2017-12-22 21:11:19\",\"updatedDate\":\"2017-12-22 21:11:19\"}"</code></pre>
<h3>总结</h3>
<p>1 单点登录系统通过将用户信息放在Redis数据库中实现共享Session效果。<br>2 Java 配置方式使用四个注解 @Configuration @Bean @PropertySource @Value 。<br>3 Spring 拦截器的设置。<br>4 HttpClient 的使用<br>5 祝大家圣诞节快乐</p>
<p>源码:<a href="https://link.segmentfault.com/?enc=F5WsLytGecc%2FMdrZiLeM%2Fw%3D%3D.4lpEUdZDctDggNJJEoJXi6HRlAAb1TVTtLsqrq21Gwt3h2I3vWjrwwuAXLTGHGHv4rI4iOS5XbZuhTnwpMo79XfCT2dEE7h7dYkYQY7nJAQ%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>到这里,基于SpringBoot的单点登录系统就结束了,有什么不对的地方请指出。</p>
Nginx 反向代理 负载均衡 虚拟主机配置
https://segmentfault.com/a/1190000012479902
2017-12-18T21:53:10+08:00
2017-12-18T21:53:10+08:00
itdragon
https://segmentfault.com/u/itdragon
5
<h2>Nginx 反向代理 负载均衡 虚拟主机配置</h2>
<p>通过本章你将学会利用Nginx配置多台虚拟主机,清楚代理服务器的作用,区分正向代理和反向代理的区别,搭建使用Nginx反向搭理和负载均衡,了解Nginx常用配置的说明。即学即用,你还在等什么?一睹为快先了解Nginx的三大功能<br>Nginx 可以作为一台http服务器。可以做网站静态服务器,比如图片服务器,高效,减轻服务器压力。同时它也支持https服务。<br>Nginx 可以配置多台虚拟主机。可以实现在一台服务器虚拟出多个网站效果,省钱。<br>Nginx 最重要的是反向代理,负载均衡。在服务器集群中,通过Nginx通过反向代理让性能高的服务器分担更多的负载,从而实现负载均衡的效果,利用率高。</p>
<p>效果图:包含基于ip的虚拟主机测试,基于域名的虚拟主机测试,反向代理和负载均衡的测试<br><img src="/img/remote/1460000012479907?w=586&h=321" alt="" title=""><br>环境:CentOS 7 , nginx-1.13.6 ,<br>说明:Nginx 反向代理和负载均衡的操作前提都是基于域名的虚拟主机。不同的tomcat模拟不同的服务器,和生产环境最大的区别就是ip和port<br>Nginx 安装:<a href="https://link.segmentfault.com/?enc=u9ibhUu2cG5nYG%2FbvBGUuA%3D%3D.Wpa%2BNFYA39ozqowNT4Qy3v6m4idNvG%2B1d7AZRHqNMFaUtUnvt06AnALyURstBLrJ" rel="nofollow">http://www.cnblogs.com/itdrag...</a><br>Nginx http服务器:<a href="https://link.segmentfault.com/?enc=HVqWug6VNLN6qU1hs0tKrg%3D%3D.vBueeO%2BE2M097eX8ZftxtIOI59B%2BVYPzB%2Fvf%2BSo8NXwC8htW%2BGB0MsGTj3gVI5uw" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<h3>配置多台虚拟主机</h3>
<p>虚拟主机是一种特殊的软硬件技术,它可以将网络上的每一台计算机分成多个虚拟主机,每个虚拟主机都可以独立对外提供www服务。从而实现一台主机能对外提供多个web服务,而且每个虚拟主机之间是互不影响的。<br>Nginx提供了三种虚拟主机配置方式,1、基于ip的虚拟主机,2、基于端口的虚拟主机,3、基于域名的虚拟主机。最常用的是第三种,相对于 ip地址和端口号,域名更方便记忆和使用。</p>
<h4>基于ip的虚拟主机</h4>
<pre><code>[root@itdragon ~]# cd /etc/sysconfig/network-scripts/
[root@itdragon network-scripts]# ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
inet 192.168.225.131/24 brd 192.168.225.255 scope global dynamic ens33
[root@itdragon network-scripts]# vim ifcfg-ens33
# 添加
IPADDR1="192.168.225.132"
IPADDR2="192.168.225.133"
[root@itdragon network-scripts]# systemctl restart network
[root@itdragon network-scripts]# ip addr
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
inet 192.168.225.131/24 brd 192.168.225.255 scope global dynamic ens33
inet 192.168.225.132/24 brd 192.168.225.255 scope global secondary ens33
inet 192.168.225.133/24 brd 192.168.225.255 scope global secondary ens33
[root@itdragon ~]# cd /usr/local/nginx
[root@itdragon nginx]# cp -r html/ html-131/
[root@itdragon nginx]# cp -r html/ html-132/
[root@itdragon nginx]# cp -r html/ html-133/
[root@itdragon nginx]# vim html-131/index.html
[root@itdragon nginx]# vim html-132/index.html
[root@itdragon nginx]# vim html-133/index.html
[root@itdragon nginx]# vim conf/nginx.conf
# 添加
server {
listen 80;
server_name 192.168.225.132;
location / {
root html-132;
index index.html index.htm;
}
}
server {
listen 80;
server_name 192.168.225.133;
location / {
root html-133;
index index.html index.htm;
}
}
[root@itdragon nginx]# sbin/nginx -s reload</code></pre>
<p>第一步:执行命令ip addr 打印协议地址,得知网卡名是ens33,ip地址是192.168.225.131<br>第二步:进入到/etc/sysconfig/network-scripts/ 修改ifcfg-ens33 文件添加两个ip地址<br>第三步:重启网络,并检查配置是否生效,发现ens33对应三个ip地址<br>第四步:进入到/usr/local/nginx/ 目录下,拷贝三份html目录,并分别修改index.html 文件便于区分测试<br>第五步:修改Nginx配置文件,监听的端口不变,修改server_name为对应ip地址,修改root为对应的html目录<br>第六步:重启Nginx服务,在浏览器上分别访问三个ip地址,观察页面变化<br>若你发现不同的ip地址打印不同页面,和效果图相似,则代表配置成功。</p>
<p>基于端口的虚拟主机和基于ip的虚拟主机配置几乎一样,只是在修改Nginx配置文件时,只修改监听的端口和root对应的目录,其他的没有变。这里就不贴命令了。</p>
<h4>基于域名的虚拟主机</h4>
<p>这是Nginx比较常用的配置,也是有利于人类使用的配置方式。这里通过修改window系统下的host文件来模拟DNS服务器。</p>
<pre><code># Windows
C:\Windows\System32\drivers\etc\hosts文件
# nginx 域名配置虚拟主机
192.168.225.131 www.itdragon.com
192.168.225.131 picture.itdragon.com
192.168.225.131 search.itdragon.com
# CentOS
[root@itdragon nginx]# cp -r html/ html-search
[root@itdragon nginx]# cp -r html/ html-picture
[root@itdragon nginx]# vim html-search/index.html
[root@itdragon nginx]# vim html-picture/index.html
[root@itdragon nginx]# vim conf/nginx.conf
server {
listen 80;
server_name search.itdragon.com;
location / {
root html-search;
index index.html index.htm;
}
}
server {
listen 80;
server_name picture.itdragon.com;
location / {
root html-picture;
index index.html index.htm;
}
}
[root@itdragon nginx]# sbin/nginx -s reload</code></pre>
<p>第一步:在window环境中,修改host文件,添加ip 域名映射关系,用来模拟DNS服务器<br>第二步:进入到/usr/local/nginx/ 目录下,拷贝两份html目录,分别修改index.html 文件便于区分测试<br>第三步:修改Nginx配置文件,监听的端口不变,修改server_name为对应域名地址,修改root为对应的html目录<br>第四步:重启Nginx服务,在浏览器上分别访问两个域名地址,观察页面变化<br>若你发现不同的域名地址打印不同页面,和效果图相似,则代表配置成功。</p>
<h3>Nginx 反向代理</h3>
<p>在了解Nginx 反向代理之前,我们先熟悉一下什么是代理服务器<br><strong>代理服务器</strong>:是一个夹在客户机和目标主机中间的服务器。能提高客户机访问响应速度,还能设置防火墙过滤不安全信息。<br><strong>响应速度快</strong>:客户机发送请求,代理服务器接收请求后,再转发给目标主机。目标主机接收请求并将数据返回给代理服务器,代理服务器将数据返回给客户机同时也会保存数据到本地。若客户机下次有相同的请求,则直接从本地数据返回。从而提高了响应的速度。<br><strong>设置防火墙</strong>:因为代理服务器夹在客户机和目标主机中间。客户机所有的请求都会经过代理服务器,所以如果在代理服务器上设置防火墙,则可以过滤一些不安全的信息,同时也方便管理。</p>
<p>清楚了代理服务器后,我们再来了解正向代理和反向代理的区别<br><strong>正向代理</strong>:顾客:"服务员,我就要厨师A做的七彩红烧肉"; 服务员:"好嘞,我这就安排厨师A给您做!"<br><strong>反向代理</strong>:顾客:"服务员,我要一份七彩红烧肉"; 服务员:"好嘞,我们的厨师B炒菜贼好吃!"<br>不知道大家看懂没有。顾客就是客户机,服务员就是代理服务器,厨师们就是目标主机。正向代理就相当于客户机明确指定目标主机提供服务(目标主机被动接收请求)。反向代理就相当于客户机提供需求,代理服务器从一群目标主机中找一台去实现该需求(目标主机主动接收请求)。</p>
<p>现在开始配置Nginx的反向代理</p>
<pre><code>[root@itdragon ~]# vim /usr/local/solr/tomcat1/webapps/ROOT/index.jsp
[root@itdragon ~]# vim /usr/local/solr/tomcat2/webapps/ROOT/index.jsp
[root@itdragon ~]# cd /usr/local/nginx
[root@itdragon nginx]# vim conf/nginx.conf
upstream searchserver {
server 192.168.225.133:8081;
}
upstream pictureserver {
server 192.168.225.133:8082;
}
server {
listen 80;
server_name search.itdragon.com;
location / {
proxy_pass http://searchserver;
index index.html index.htm;
}
}
server {
listen 80;
server_name picture.itdragon.com;
location / {
proxy_pass http://pictureserver;
index index.html index.htm;
}
}
[root@itdragon nginx]# sbin/nginx -s reload</code></pre>
<p>第一步:准备两个tomcat服务器,端口分别是8081和8082,并分别修改index.jsp 文件便于区分测试<br>第二步:进入到/usr/local/nginx/ 目录下,修改Nginx配置文件。upstream 定义每个设备的状态,server 配置服务,server_name 指定域名,proxy_pass 代理转发到那台设备上<br>第三步:重启服务,在浏览器上输入不同的域名,会跳到对应的页面<br>Nginx的反向代理其实是在做请求的转发,后台有多个http服务器提供服务,Nginx的功能就是把请求转发给后面的服务器,并决定把请求转发给哪台服务器。</p>
<p><strong>反向代理流程</strong><br>浏览器访问search.itdragon.com,通过本地host文件域名解析,找到192.168.225.131 Nginx虚拟主机,Nginx接收客户机请求,找到server_name为search.itdragon.com的节点,再根据proxy_pass对应的http路径,将请求转发到upstream searchserver上,即端口号为8081的tomcat服务器。<br>客户机访问 ---> search.itdragon.com ---> host ---> Nginx ---> server_name ---> proxy_pass ---> upstream---> tomcat<br><img src="/img/remote/1460000012479908?w=357&h=232" alt="反向代理流程" title="反向代理流程"></p>
<h3>Nginx 负载均衡</h3>
<p>负载均衡 在高性能的主机上分配更多的负载,在性能低的主机分配少一些的负载,充分利用主机的性能,将其服务器的总压力。Nginx的 upstream默认是以轮询的方式实现负载均衡,也可以分配权值。</p>
<pre><code>[root@itdragon ~]# vim /usr/local/solr/tomcat3/webapps/ROOT/index.jsp
[root@itdragon ~]# vim /usr/local/solr/tomcat4/webapps/ROOT/index.jsp
[root@itdragon ~]# cd /usr/local/nginx
[root@itdragon nginx]# vim conf/nginx.conf
upstream pictureserver {
server 192.168.225.133:8082 weight=2;
server 192.168.225.133:8083 weight=1;
server 192.168.225.133:8084 weight=1;
}
[root@itdragon nginx]# sbin/nginx -s reload</code></pre>
<p>第一步:新增两个tomcat服务器,端口分别为8083和8084,并分别修改index.jsp 文件便于区分测试<br>第二步:进入到/usr/local/nginx/ 目录下,修改Nginx配置文件,在pictureserver 内新增两个server<br>第三步:重启服务<br>负载均衡的配置是在反向代理的基础上修改的,所以请先完成反向代理的配置。</p>
<h3>常用配置说明</h3>
<pre><code>events { # 工作模式
worker_connections 1024; # 最大连接数
}
http { # 配置http服务器
include mime.types; # 定义mime的文件类型
default_type application/octet-stream; # 默认文件类型
sendfile on; # 开启 sendfile 函数(zero copy 方式)输出文件
keepalive_timeout 65; # 连接超时时间,单位秒
upstream pictureserver { # 定义负载均衡设备的ip和状态
server 192.168.225.133:8081 ; # 默认权重值为一
server 192.168.225.133:8082 weight=2; # 值越高,负载的权重越高
server 192.168.225.133:8083 down; # 当前server 暂时不参与负载
server 192.168.225.133:8084 backup; # 当其他非backup状态的server 不能正常工作时,才请求该server,简称热备
}
server { # 设定虚拟主机配置
listen 80; # 监听的端口
server_name picture.itdragon.com; # 监听的地址,多个域名用空格隔开
location / { # 默认请求 ,后面 "/" 表示开启反向代理,也可以是正则表达式
root html; # 监听地址的默认网站根目录位置
proxy_pass http://pictureserver; # 代理转发
index index.html index.htm; # 欢迎页面
deny 127.0.0.1; # 拒绝的ip
allow 192.168.225.133; # 允许的ip
}
error_page 500 502 503 504 /50x.html;# 定义错误提示页面
location = /50x.html { # 配置错误提示页面
root html;
}
}
</code></pre>
<p>具体配置详情可以参考:<a href="https://link.segmentfault.com/?enc=7NPGq6JFT40BYVxUrg6z8A%3D%3D.QgZUfLlA9M%2B7ymcmGWJO6REjdjoi%2Bvja25esrgbXPcYVOg6abFuzjIFD57VZwJU%2BLsm6gzije6ui3gUAkGMnkw%3D%3D" rel="nofollow">http://blog.csdn.net/happydre...</a></p>
<h3>总结</h3>
<p>1 Nginx 通过修改nginx.conf server_name配置,达到配置多台基于ip,基于域名的虚拟主机<br>2 Nginx 通过修改nginx.conf upstream 和 proxy_pass配置,达到反向代理效果<br>3 Nginx 通过修改nginx.conf upstream server 状态,达到负载均衡效果<br>4 代理服务器有提高客户端获取数据的速度,和方便管理设置防火墙的功能</p>
<p>到这里Nginx的多虚拟主机,反向代理和负载均衡就结束了,感谢阅读!欢迎点赞!</p>
SpringData 基于SpringBoot快速入门
https://segmentfault.com/a/1190000012456030
2017-12-16T20:58:23+08:00
2017-12-16T20:58:23+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>SpringData 基于SpringBoot快速入门</h2>
<p>本章通过学习SpringData 和SpringBoot 相关知识将面向服务架构(SOA)的单点登录系统(SSO)需要的代码实现。这样可以从实战中学习两个框架的知识,又可以为单点登录系统打下基础。通过本章你将掌握 SpringBoot项目的搭建,Starter pom的使用,配置全局文件,核心注解SpringBootApplication 介绍以及单元测试 SpringBootTest注解的使用。SpringData 的入门使用,Repository接口的使用,查询方法关键字的规则定义,@Query,@Modifying 注解的使用,最后是开发中的建议和遇到的问题。</p>
<h3>SpringBoot 知识</h3>
<p>SpringBoot 是一个用于简化Spring应用搭建开发的框架。开发过程中,我们经常通过配置xml文件来整合第三方技术。而这些重复整合的工作交给了SpringBoot完成。SpringBoot使用"习惯优于配置"的理念帮我们快速搭建并运行项目。对主流的开发框架能无配置集成。笔者用的开发工具是sts(Spring Tool Suite),其操作和eclipse几乎一致。若没有这个工具,创建Maven项目是一样的。文章底部提供源码地址!</p>
<p><img src="/img/remote/1460000012456035?w=554&h=711" alt="项目搭建" title="项目搭建"></p>
<h4>Starter pom</h4>
<p>先看看Maven项目核心配置文件 pom.xml</p>
<pre><code><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itdragon</groupId>
<artifactId>springbootStudy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>springbootStudy</name>
<description>Demo project for Spring Boot</description>
<!--
添加 spring boot的父级依赖,它是SpringBoot项目的标志
spring-boot-starter-parent 它是一个特殊的starter,提供了很多相关的Maven依赖,不用再为version而头疼了,大大简化了开发
-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency><!-- 添加web依赖 ,包含spring和springmvc等-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><!-- 添加对jpa的支持,包含spring-data和Hibernate -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><!-- mysql连接的jar包 -->
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency><!-- 因为SpringBoot内嵌的tomcat不支持jsp页面,同时SpringBoot也不推荐用jsp -->
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency><!-- jsp标签库 -->
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin><!-- SpringBoot 编译插件 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project></code></pre>
<p>细心的同学会发现该文件中出现大量的 spring-boot-starter-* 的语句。SpringBoot之所以能简化开发的秘密就在这里------ Starter pom<br><strong>spring-boot-starter-parent</strong> :父级依赖,SpringBoot项目的标志。里面封装了很多jar的版本<br><strong>spring-boot-starter-web</strong> :对web项目的支持,其中包含了SpringMVC和tomcat<br><strong>spring-boot-starter-data-jpa</strong> :对JPA的支持,其中包含了常用的SpringData和Hibernate,没有Mybatis哦<br><strong>spring-boot-starter-tomcat</strong> :使用tomcat作为Servlet容器<br><strong>spring-boot-starter-test</strong> :对常用测试框架的支持,如JUnit<br>还有很多......</p>
<h4>配置全局文件</h4>
<p>再看看SpringBoot项目全局配置文件 application.properties</p>
<pre><code># 配置tomcat端口号
server.port=8081
# 配置SpringMVC视图解析器
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
# 配置连接池,默认使用的是tomcat的连接池,但实际很少用tomcat的连接池
spring.datasource.url=jdbc:mysql://localhost:3306/jpa?useUnicode=true&characterEncoding=UTF8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 配置方言 否则提示:Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
# 自动更新数据库表结构,也可以是 validate | update | create | create-drop
spring.jpa.properties.hibernate.hbm2ddl.auto=update
# 显示sql语句
spring.jpa.show-sql=true</code></pre>
<p>全局配置文件可以是application.properties 也可以是 application.yml,建议放在resources目录下。更多配置: <a href="https://link.segmentfault.com/?enc=OML89MdCLS4aiClrwKNPNw%3D%3D.0NwtUmNQYo0RwcryrUJ9VbMYpZ9rxhuXUL9mhzQrVwfksbbLfIhqNi7OECWBGG8G8tKQhafmXcBia2QCh6cMxEy1RcW6rjthoI0FljpIAg9FhhrL7wsAOo1nryGTG9s7csyNZSVv4UzOnTnZekgTnvDGU%2B2ogOWTC4KAhPjM2ons3gG5dgyj66rcRbEpRzcU" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h4>核心注解</h4>
<p>最后是SpringBoot HelloWorld项目的入口类,只需要下面一个java文件,执行main方法,即可实现页面的跳转和数据返回的功能。</p>
<pre><code>import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@SpringBootApplication
public class SpringbootStudyApplication {
@RequestMapping("/")
public String index() {
return "index";
}
@RequestMapping("hello")
@ResponseBody
public String helloWorld() {
return "Hello SpringBoot !";
}
public static void main(String[] args) {
SpringApplication.run(SpringbootStudyApplication.class, args);
}
}</code></pre>
<p><strong>@SpringBootApplication</strong> 是 SpringBoot 的核心注解,一般用在入口类上。它是一个组合注解,其中主要内容有一下三个<br><strong>@SpringBootConfiguration</strong>:是一个类级注释,指示对象是一个bean定义的源,可以理解为xml中的beans,一般和 @Bean 注解一起使用。<br><strong>@EnableAutoConfiguration</strong>:启用 Spring 应用程序上下文的自动配置,试图猜测和配置您可能需要的bean。自动配置类通常采用基于你的 classpath 和已经定义的 beans 对象进行应用。<br><strong>@ComponentScan</strong>:该注解会自动扫描指定包下的全部标有 @Component、@Service、@Repository、@Controller注解 的类,并注册成bean</p>
<p>SpringData入口类</p>
<pre><code>package com.itdragon;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StartApplication {
public static void main(String[] args) {
SpringApplication.run(StartApplication.class, args);
}
}</code></pre>
<h3>SpringDataJPA 知识</h3>
<p>SpringData 是一个用于简化数据库访问,并支持云服务的开源框架。支持非关系型数据库(NoSQL) 和 关系型数据库。其主要目的是使数据库的访问变得方便快捷。<br>SpringData JPA 是由Spring提供的简化JPA开发的框架,致力于减少数据访问层的开发量。</p>
<h4>POJO层</h4>
<p>创建实体类User 表,对应数据库表名是 itdragon_user,id作为自增长的主键,plainPassword是不保存到数据库的明文密码。</p>
<pre><code>import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
/**
* 用户实体类
* @author itdragon
*
*/
@Table(name="itdragon_user")
@Entity
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id; // 自增长主键
private String account; // 登录的账号
private String userName; // 注册的昵称
@Transient
private String plainPassword; // 登录时的密码,不持久化到数据库
private String password; // 加密后的密码
private String salt; // 用于加密的盐
private String iphone; // 手机号
private String email; // 邮箱
private String platform; // 用户来自的平台
private String createdDate; // 用户注册时间
private String updatedDate; // 用户最后一次登录时间
// 省略get/set/toString 方法
}
</code></pre>
<h4>Repository接口层</h4>
<p>创建UserRepository,这是SpringData 的核心知识点,我们先看代码</p>
<pre><code>import java.util.List;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import com.itdragon.pojo.User;
/**
* 核心知识:SpringData Repository 接口
*
* CrudRepository 接口提供了最基本的对实体类的添删改查操作
* - T save(T entity); //保存单个实体
* - T findOne(ID id); // 根据id查找实体
* - void delete(ID/T/Iterable); // 根据Id删除实体,删除实体,批量删除
* PagingAndSortingRepository 提供了分页与排序功能
* - <T, ID extends Serializable> // 第一个参数传实体类,第二个参数传注解数据类型
* - Iterable<T> findAll(Sort sort); // 排序
* - Page<T> findAll(Pageable pageable); // 分页查询(含排序功能)
* JpaSpecificationExecutor 提供了Specification(封装 JPA Criteria查询条件)的查询功能
* - List<T> findAll(Specification<T> spec);
* - Page<T> findAll(Specification<T> spec, Pageable pageable);
* - List<T> findAll(Specification<T> spec, Sort sort);
*
* 开发建议
* 1. 这里值列出的是常用方法
* 2. CrudRepository 中的findAll() 方法要慎用。当数据库中数据量大,多线程脚本调用findAll方法,系统可能会宕机。
* 3. CrudRepository 中的deletAll()方法要慎用。这是物理删除,现在企业一般采用逻辑删除。
* 4. PagingAndSortingRepository 和 JpaSpecificationExecutor 能满足大部分业务需求。
*/
public interface UserRepository extends PagingAndSortingRepository<User, Long>,
JpaSpecificationExecutor<User>{
/**
* 重点知识:SpringData 查询方法定义规范
*
* 1. 查询方法名一般以 find | read | get 开头,建议用find
* findByAccount : 通过account查询User
* account是User的属性,拼接时首字母需大写
* 2. 支持的关键词有很多比如 Or,Between,isNull,Like,In等
* findByEmailEndingWithAndCreatedDateLessThan : 查询在指定时间前注册,并以xx邮箱结尾的用户
* And : 并且
* EndingWith : 以某某结尾
* LessThan : 小于
*
* 注意
* 若有User(用户表) Platform(用户平台表) 存在一对一的关系,且User表中有platformId字段
* SpringData 为了区分:
* findByPlatFormId 表示通过platformId字段查询
* findByPlatForm_Id 表示通过platform实体类中id字段查询
*
* 开发建议
* 表的设计,尽量做单表查询,以确保高并发场景减轻数据库的压力。
*/
// 1 通过账号查用户信息
User findByAccount(String account);
// 2 获取指定时间内以xx邮箱结尾的用户信息
List<User> findByEmailEndingWithAndCreatedDateLessThan(String email, String createdDate);
/**
* 重点知识:使用 @Query 注解
*
* 上面的方法虽然简单(不用写sql语句),但它有最为致命的问题-----不支持复杂查询,其次是命名太长
* 1. 使用@Query 注解实现复杂查询,设置 nativeQuery=true 使查询支持原生sql
* 2. 配合@Modifying 注解实现创建,修改,删除操作
* 3. SpringData 默认查询事件为只读事务,若要修改数据则需手动添加事务注解
*
* 注意
* 若@Query 中有多个参数,SpringData 提供两种方法:
* 第一种 ?1 ... ?2 要求参数顺序一致
* 第二种 :xxx ... :yyy xxx 和 yyy 必须是实体类对应的属性值,不要求参数顺序但参数前要加上@Param("xxx")
* 模糊查询可使用 %xxx%
*
* 开发建议
* 1. 参数填写的顺序要保持一致,不要给自己添加麻烦
* 2. 建议使用@Query,可读性较高
*/
// 3 获取某平台活跃用户数量
@Query(value="SELECT count(u.id) FROM User u WHERE u.platform = :platform AND u.updatedDate <= :updatedDate")
long getActiveUserCount(@Param("platform")String platform, @Param("updatedDate")String updatedDate);
// 4 通过邮箱或者手机号模糊查询用户信息
@Query(value="SELECT u FROM User u WHERE u.email LIKE %?1% OR u.iphone LIKE %?2%")
List<User> findByEmailAndIhpneLike(String email, String iphone);
// 5 修改用户邮箱
@Modifying
@Query("UPDATE User u SET u.email = :email WHERE u.id = :id")
void updateUserEmail(@Param("id") Long id, @Param("email") String email);
}</code></pre>
<p>代码中共有五个方法,每个方法都包含了很多的知识点。<br>方法一和方法二主要介绍的是SpringData关键字的用法。<br>1 <strong>关键字的解析</strong><br>这里用findByPlatFormId() 方法来介绍SpringData 解析查询方法的流程。<br>第一步:去除关键字findBy<br>第二步:剩下的PlatFormId 首字母小写并在User对象中找是否有该属性,若有则查询并结束。若没有则第三步<br>第三步:platFormId,从右到左截取到第一个大写字母,再判断剩下的platForm是否为User对象,如此循环直到结束为止。<br>2 <strong>级联属性区分</strong><br>若查询的属性是实体类,为了避免误会和冲突,用"_"表示属性中的属性<br>3 <strong>查询分页排序</strong><br>若findByPlatFormId() 方法想要排序或者分页,是可以在后面加Pageable,Sort参数。<br>findByPlatFormId(String platFormId, Pageable pageable)<br>findByPlatFormId(String platFormId, Sort sort)<br>4 <strong>其他的关键字</strong><br><img src="/img/remote/1460000012456036?w=746&h=482" alt="关键字" title="关键字"></p>
<p>方法三到方法五主要介绍的是 @Query 注解的使用。<br>1 <strong>传参方式</strong><br>索引参数:?n ,n从1开始,表示第一个参数。方法传入的参数的照顺序和个数要和 n 保持一致。<br>命名参数::key ,传参必须有 @Param("key") 注解修饰<br>2 <strong>原生的sql</strong><br>@Query 注解支持本地查询,即用原生的sql语句。如:@Query(value="xxxx", nativeQuery=true)<br>3 <strong>@Modifying</strong><br>若直接执行修改操作,SpringDataJPA 会提示错误信息 Executing an update/delete query 。是因为Spring Data 默认所有的查询均声明为只读事务。<br>所以我们要在Service层添加 @Transactional 注解。</p>
<p>SpringDataJPA 核心知识Repository接口<br><strong>Repository</strong>: 空接口,标识作用,表明任何继承它的均为Repository接口类<br><strong>CrudRepository</strong>: 继承 Repository,实现了一组 CRUD 相关的方法 <br><strong>PagingAndSortingRepository</strong>: 继承 CrudRepository,实现了一组分页排序相关的方法<br><strong>JpaRepository</strong>: 继承 PagingAndSortingRepository,实现一组 JPA 规范相关的方法<br><strong>JpaSpecificationExecutor</strong>: 不属于Repository体系,实现一组 JPA Criteria 查询相关的方法 <br>PagingAndSortingRepository 和 JpaSpecificationExecutor 基本满足企业中大部分的需求。也可以自定义Repository,只需继承 JpaRepository 即可具备了通用的数据访问控制层的能力。<br>进入各自接口类中,使用快捷键 Ctrl + o 即可查看当前类的所有方法,所以这里就不贴出来了。</p>
<p><img src="/img/remote/1460000012456037?w=374&h=154" alt="JpaSpecificationExecutor" title="JpaSpecificationExecutor"><br><img src="/img/remote/1460000012456038?w=409&h=96" alt="PagingAndSortingRepository" title="PagingAndSortingRepository"><br><img src="/img/remote/1460000012456039" alt="CrudRepository" title="CrudRepository"></p>
<p> </p>
<h4>Service层</h4>
<p>创建UserService 并加上注解 @Transactional</p>
<pre><code>import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.itdragon.common.ItdragonResult;
import com.itdragon.pojo.User;
import com.itdragon.repository.UserRepository;
@Service
@Transactional
public class UserService {
@Autowired
private UserRepository userRepository;
public ItdragonResult registerUser(User user) {
// 检查用户名是否注册,一般在前端验证的时候处理,因为注册不存在高并发的情况,这里再加一层查询是不影响性能的
if (null != userRepository.findByAccount(user.getAccount())) {
return ItdragonResult.build(400, "");
}
userRepository.save(user);
// 注册成功后选择发送邮件激活。现在一般都是短信验证码
return ItdragonResult.build(200, "");
}
public ItdragonResult editUserEmail(String email) {
// 通过Session 获取用户信息, 这里假装从Session中获取了用户的id,后面讲解SOA面向服务架构中的单点登录系统时,修改此处代码 FIXME
long id = 3L;
// 添加一些验证,比如短信验证
userRepository.updateUserEmail(id, email);
return ItdragonResult.ok();
}
}</code></pre>
<h3>单元测试</h3>
<p>SpringBoot 的单元测试,需要用到 @RunWith 和 @SpringBootTest注解,代码注释中有详细介绍。</p>
<pre><code>import java.util.List;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.junit4.SpringRunner;
import com.itdragon.StartApplication;
import com.itdragon.common.ItdragonUtils;
import com.itdragon.pojo.User;
import com.itdragon.repository.UserRepository;
import com.itdragon.service.UserService;
/**
* @RunWith 它是一个运行器
* @RunWith(SpringRunner.class) 表示让测试运行于Spring测试环境,不用启动spring容器即可使用Spring环境
* @SpringBootTest(classes=StartApplication.class) 表示将StartApplication.class纳入到测试环境中,若不加这个则提示bean找不到。
*
* @author itdragon
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes=StartApplication.class)
public class SpringbootStudyApplicationTests {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
public void contextLoads() {
}
@Test // 测试注册,新增数据
public void registerUser() {
User user = new User();
user.setAccount("gitLiu");
user.setUserName("ITDragonGit");
user.setEmail("itdragon@git.com");
user.setIphone("12349857999");
user.setPlainPassword("adminroot");
user.setPlatform("github");
user.setCreatedDate(ItdragonUtils.getCurrentDateTime());
user.setUpdatedDate(ItdragonUtils.getCurrentDateTime());
ItdragonUtils.entryptPassword(user);
userService.registerUser(user);
}
@Test // 测试SpringData 关键字
public void findByEmailEndingWithAndCreatedDateLessThan() {
List<User> users = userRepository.findByEmailEndingWithAndCreatedDateLessThan("qq.com", ItdragonUtils.getCurrentDateTime());
System.out.println(users.toString());
}
@Test // 测试SpringData @Query 注解和传多个参数
public void getActiveUserCount() {
long activeUserCount = userRepository.getActiveUserCount("weixin", ItdragonUtils.getCurrentDateTime());
System.out.println(activeUserCount);
}
@Test // 测试SpringData @Query 注解,传多个参数 和 like 查询
public void findByEmailAndIhpneLike() {
List<User> users = userRepository.findByEmailAndIhpneLike("163.com", "6666");
System.out.println(users.toString());
}
@Test // 测试SpringData @Query 注解 和 @Modifying 注解
public void updateUserEmail() {
/**
* org.springframework.dao.InvalidDataAccessApiUsageException:Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query
* userRepository.updateUserEmail(3L, "update@email.com");
*/
userService.editUserEmail("update@email.com");
}
@Test // 测试SpringData PagingAndSortingRepository 接口
public void testPagingAndSortingRepository() {
int page = 1; // 从0开始,第二页
int size = 3; // 每页三天数据
PageRequest pageable = new PageRequest(page, size, new Sort(new Order(Direction.ASC, "id")));
Page<User> users = userRepository.findAll(pageable);
System.out.println(users.getContent().toString()); // 当前数据库中有5条数据,正常情况可以打印两条数据,id分别为4,5 (先排序,后分页)
}
@Test // 测试SpringData JpaSpecificationExecutor 接口
public void testJpaSpecificationExecutor(){
int pageNo = 1;
int pageSize = 3;
PageRequest pageable = new PageRequest(pageNo, pageSize);
Specification<User> specification = new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root,
CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate predicate = cb.gt(root.get("id"), 1); // 查询id 大于 1的数据
return predicate;
}
};
Page<User> users = userRepository.findAll(specification, pageable);
System.out.println(users.getContent().toString()); // 当前数据库中有5条数据,正常情况可以打印一条数据,id为5
}
}</code></pre>
<h3>可能存在的问题</h3>
<h5>项目启动时提示 Unknown character set: 'utf8mb4'</h5>
<p>导致的原因可能是mysql服务器版本安装不正确,解决方法有两种。<br>第一种:换mysql-connector-java jar包版本为 5.1.6 (不推荐); 当前jar版本为 5.1.44。<br>第二种:重装mysql版本,当前最新版本是5.7。教程都准备好了。<br><a href="https://link.segmentfault.com/?enc=%2BawudwKMmJZaKJGZsYrbmQ%3D%3D.K2RgdyYifDmnNfot0Iot81xmDcflT6D%2B89S4aFKNr0KBCCipAh0308JO7rqSgHtC" rel="nofollow">https://www.cnblogs.com/sshou...</a> (mysql安装)<br><a href="https://link.segmentfault.com/?enc=Al5Qqxvg3B1TvAs7XD90ig%3D%3D.Jkq3Dk15LMcLgHf0d6LlHFMMxZdBa%2FMV41gNUt4wE2V7Be0I92dNTTMPGoYBcml2DwN836u2qu2lmt3PhtqzcQ%3D%3D" rel="nofollow">http://blog.csdn.net/y6947219...</a> (mysql卸载)</p>
<h5>SpringBoot 连接池配置疑惑</h5>
<p>我们只是在全局配置文件中设置了相关值,就完成了连接池的配置,想必大家都有所疑惑。其实当我们在pom.xml文件中加入spring-boot-starter-data-jpa 依赖时,SpringBoot就会自动使用tomcat-jdbc连接池。<br>当然我们也可以使用其他的连接池。<br><a href="https://link.segmentfault.com/?enc=ySUwxQZF5jt3qJZZArBCyQ%3D%3D.b%2FMFvfyK0DU3BqRb1YJ88thvKE0RJCder9WW9CXDa7WTAwk08ng5M4iZwEhIMW9x" rel="nofollow">https://www.cnblogs.com/gslbl...</a> (springBoot数据库连接池常用配置)<br><a href="https://link.segmentfault.com/?enc=tabw%2BK2WDiI2I%2BEqx2lD1A%3D%3D.oNGt3rIpSMlQ02svZPfal2ggr8Lq019vg3ppZt6FWxGqcaiTrXFYTEwpH5r%2Fa2MrHRYv27BQVuFBGPNpkGfVfw%3D%3D" rel="nofollow">https://www.cnblogs.com/xiaos...</a> (SpringBoot使用c3p0)</p>
<h5>STS工具 ctrl + shift + o 重新导包快捷键失效</h5>
<p>解决方法:preference -> general -> keys ,找到 Organize Imports ,然后 在 When 里面选择 Editing Java Source</p>
<h3>总结</h3>
<p>1 SpringDataJPA 是简化JPA开发的框架,SpringBoot是简化项目开发的框架。<br>2 spring-boot-starter-parent 是SpringBoot项目的标志<br>3 @SpringBootApplication 注解是SpringBoot项目的入口<br>4 SpringData 通过查询关键字和 @Query注解实现对数据库的访问<br>5 SpringData 通过PagingAndSortingRepository 实现分页,排序和常用的crud操作</p>
<p>源码地址:<a href="https://link.segmentfault.com/?enc=ztXM1N1hn%2Briv2JWS9Izgw%3D%3D.A4Yfl0%2BuljtP%2BefebmzbrFWSF4GCeJbJvX%2FUAdNbkT68soQE03K3IlIZ3QJQq8zfDFsffFVIbH5MrALOSWNX0%2FQy4oB3q9Eby5jdKWEjYdA%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>到这里 SpringData 基于SpringBoot快速入门就结束了,如果有什么问题请指教,如果觉得不错可以点一下推荐。</p>
SolrJ 复杂查询 高亮显示
https://segmentfault.com/a/1190000012349643
2017-12-08T21:07:35+08:00
2017-12-08T21:07:35+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>SolrJ 复杂查询 高亮显示</h2>
<p>上一章搭建了Solr服务器和导入了商品数据,本章通过SolrJ去学习Solr在企业中的运用。笔者最先是通过公司的云客服系统接触的Solr,几百万的留言秒秒钟就查询并高亮显示,不同的广告员还可以只检索自己所属的国家的留言。瞬间就跪拜在Solr的石榴裙下。现在看来其实就是 q + fq + lg 的用法。通过本章内容你将会学习到Solr分词配置;SolrJ的复杂查询和关键字高亮语法;Solr各版本之间SolrJ的语法差异;Solr7与Spring的整合等使用技能。</p>
<p>需求:搜索栏输入关键字全文检索商品,选择类目或价格区间筛选商品,选择价格排序商品。<br>技术:SolrJ,Spring,SpringMVC<br>说明:通过本章内容你将会学习到Solr分词配置;SolrJ的复杂查询和关键字高亮语法;Solr各版本之间SolrJ的语法差异;Solr7与Spring的整合;搜索商品开发思路;本教程页面来源网络,笔者做了简单的修改,仅供学习使用。关注文章底部微信公众号领红包哦!<br>源码:见文章底部<br>项目结构:<br><img src="/img/remote/1460000012349648?w=356&h=421" alt="" title=""></p>
<p>效果图:<br><img src="/img/remote/1460000012349649?w=971&h=569" alt="效果图" title="效果图"></p>
<h3>Solr分词配置</h3>
<p>solr7 安装部署:<a href="https://link.segmentfault.com/?enc=jl3w%2F77Hb1L7anN1dlVdTg%3D%3D.S5o4rOShduCnMMqRhzFTnJJC%2FTtOdGYs8Z%2BnoEWOCgdSxgrIG%2B5gnpVnWJ3SGqKv" rel="nofollow">http://www.cnblogs.com/itdrag...</a></p>
<pre><code>[tomtop926@localhost lib]$ll IK*
-rw-rw-r--. 1 tomtop926 tomtop926 1165908 Dec 6 15:06 IKAnalyzer2012FF_u1.jar
[tomtop926@localhost lib]$cd ../classes/
[tomtop926@localhost classes]$ll
total 16
-rw-rw-r--. 1 tomtop926 tomtop926 168 Dec 6 15:08 ext_stopword.dic
-rw-rw-r--. 1 tomtop926 tomtop926 419 Dec 6 15:07 IKAnalyzer.cfg.xml
-rw-r--r--. 1 tomtop926 tomtop926 1421 Dec 5 12:09 log4j.properties
-rw-rw-r--. 1 tomtop926 tomtop926 12 Dec 6 15:08 mydict.dic
[tomtop926@localhost classes]$cd
[tomtop926@localhost ~]$vim solr/apache-tomcat-8.5/solrhome/new_core/conf/managed-schema
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.apache.lucene.analysis.ik.IKTokenizerFactory" useSmart="true"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.apache.lucene.analysis.ik.IKTokenizerFactory" useSmart="true"/>
</analyzer>
</fieldType>
......
<field name="product_catalog" type="string" indexed="true" stored="true"/>
<field name="product_catalog_name" type="string" indexed="true" stored="true"/>
<field name="product_description" type="text_ik" indexed="true" stored="false"/>
<field name="product_name" type="text_ik" indexed="true" stored="true"/>
<field name="product_picture" type="string" indexed="false" stored="true"/>
<field name="product_price" type="pfloat" indexed="true" stored="true"/>
<field name="product_keywords" type="text_ik" multiValued="true" indexed="true" stored="false"/>
<copyField source="product_description" dest="product_keywords"/>
<copyField source="product_name" dest="product_keywords"/></code></pre>
<p>第一步:将IKAnalyzer2012.jar导入到tomcat/webapps/solr/WEB-INF/lib目录下<br>第二步:将ext_stopword.dic(停用词词典),IKAnalyzer.cfg.xml(配置文件),mydict.dic(自定义词典) 导入到tomcat/webapps/solr/WEB-INF/classes<br>第三步:编辑managed-schema 文件,配置中文分析器<br>第四步:并配置业务域(solr字段),给product_description,product_name设置中文分词并放在product_keywords目标域中。下一章会重点介绍schema文件。<br><img src="/img/remote/1460000012349650?w=554&h=340" alt="" title=""></p>
<h3>SolrJ 单元测试</h3>
<p>在采用Solr实现全文检索功能时,我们需对Solr的java客户端SolrJ语法有一定的了解。这里创建 SolrJTest.java 类测试SolrJ的连接,查询,过滤,排序,分页,高亮等功能,以及索引的维护。</p>
<pre><code class="java">package com.itdragon.test;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.MapSolrParams;
import org.junit.Test;
public class SolrJTest {
/**
* 不同Solr 版本之间创建连接方式不同
* Solr4 : SolrServer solrServer = new HttpSolrServer("http://localhost:8080/solr");
* Solr5 :HttpSolrClient solrClient = new HttpSolrClient("http://localhost:8080/solr/new_core");
* Solr7 :HttpSolrClient solrClient = new HttpSolrClient.Builder("http://localhost:8080/solr/new_core").build();
*
* SolrQuery 对应的api接口
* q : setQuery
* fq : addFilterQuery
* sort : setSort
* start : setStart(0);
* rows : setRows(10);
* fl : setFields
*
*/
private final static String BASE_SOLR_URL = "http://localhost:8080/solr/new_core";
// solrJ 基础用法
@Test
public void queryDocumentBasic() throws Exception {
// 创建连接
HttpSolrClient solrClient = new HttpSolrClient.Builder(BASE_SOLR_URL).build();
/* 不推荐
Map<String, String> queryParamMap = new HashMap<String, String>();
// 封装查询参数
queryParamMap.put("q", "*:*");
// 添加到SolrParams对象
MapSolrParams query = new MapSolrParams(queryParamMap);
*/
// 设置查询条件
SolrQuery query = new SolrQuery();
query.setQuery("*:*");
// 执行查询
QueryResponse response = solrClient.query(query);
// 获取doc文档
SolrDocumentList documents = response.getResults();
System.out.println("共查询到记录:" + documents.getNumFound());
for (SolrDocument solrDocument : documents) {
System.out.println("ID : " + solrDocument.get("id") + " \t Name : " + solrDocument.get("product_name"));
}
}
// solrJ 的复杂查询
@Test
public void queryDocument() throws Exception {
// 创建连接
HttpSolrClient solrClient = new HttpSolrClient.Builder(BASE_SOLR_URL).build();
SolrQuery query = new SolrQuery();
// 设置查询条件
query.setQuery("product_name:街头原木电话亭");
// 设置分页信息
query.setStart(0);
query.setRows(10);
// 设置排序
query.setSort("id", ORDER.desc);
// 设置显示的Field的域集合,即设置那些值有返回值
query.setFields("id,product_name");
// 设置默认域
query.set("df", "product_keywords");
// 设置高亮信息
query.setHighlight(true);
query.addHighlightField("product_name");
query.setHighlightSimplePre("<em>");
query.setHighlightSimplePost("</em>");
// 执行查询
QueryResponse response = solrClient.query(query);
// 获取doc文档
SolrDocumentList documents = response.getResults();
System.out.println("共查询到记录:" + documents.getNumFound());
// 获取高亮显示信息
Map<String, Map<String, List<String>>> highlighting = response.getHighlighting();
for (SolrDocument doc : documents) {
System.out.println(doc.get("id"));
List<String> hightDocs = highlighting.get(doc.get("id")).get("product_name");
if (hightDocs != null)
System.out.println("高亮显示的商品名称:" + hightDocs.get(0));
else {
System.out.println(doc.get("product_name"));
}
}
}
// 添加索引
@Test
public void addDocuments() throws SolrServerException, IOException {
HttpSolrClient solrClient = new HttpSolrClient.Builder(BASE_SOLR_URL).withConnectionTimeout(10000)
.withSocketTimeout(60000).build();
// 创建一个文档对象
SolrInputDocument document = new SolrInputDocument();
document.addField("id", "add-001");
document.addField("product_name", "ITDragon-Solr7系列博客");
// 将文档对象写入到索引库中
solrClient.add(document);
// 提交
solrClient.commit();
}
// 更新的逻辑:先通过id将直接的数据删掉,然后再创建,所以update 和 create 是同一代码
// 删除/批量删除索引
@Test
public void deleteDocument() throws SolrServerException, IOException {
HttpSolrClient solrClient = new HttpSolrClient.Builder(BASE_SOLR_URL).build();
//solrClient.deleteById("add-001"); 通过id删除
/* 批量删除
ArrayList<String> ids = new ArrayList<String>();
ids.add("1");
ids.add("2");
solrClient.deleteById(ids);
*/
solrClient.deleteByQuery("id:add-001"); // 通过查询条件删除
solrClient.commit();
}
}</code></pre>
<h3>SolrJ Web项目</h3>
<h4>项目构建思路</h4>
<p>单元测试通过后,说明核心技术已经掌握,现在开始搭建web项目实现需求。通过Maven 搭建MVC web项目。<br>第一步:导入SolrJ的jar包,新增用于方便管理 Spring整合Solr的 xml文件。<br>第二步:创建商品Product实体类。为了方便前端分页显示,将查询的商品实体类集合,查询的商品总数,总页数,当前页封装到实体类SearchProductResult中。<br>第三步:为了方便管理SolrJ核心方法。创建SolrSearchDao接口,并实现查询接口,传入的参数是SolrQuery,不添加任何业务逻辑。<br>第四步:创建用于处理业务的ProductService接口,并实现高可用查询接口。负责拼接查询条件实现业务逻辑。<br>第五步:为了提高ProductService 查询接口的可用性,新增枚举类FilterQueryKey,管理复杂查询的key。<br>第六步:Controller层新增ProductController类,负责接收,返回参数并跳转页面。<br>第七步:页面展示,通过c:forEach 遍历查询结果,fmt:formatNumber格式化价格,以及负责查询的form表单。</p>
<h4>配置文件</h4>
<p>Maven 项目核心文件 pom.xml ,导入solrj jar,Solr7对应的SolrJ版本是 7.1.0</p>
<pre><code class="xml"><dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>7.1.0</version>
</dependency></code></pre>
<p>Spring 整合Solr 配置文件 applicationContext-solr.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 配置SolrServer对象 -->
<!-- 单机版 -->
<bean id="httpSolrClient" class="org.apache.solr.client.solrj.impl.HttpSolrClient">
<constructor-arg name="builder" value="${SOLR.SERVER.URL}"/>
</bean>
<bean id="solrSearchDaoImpl" class="com.itdragon.solr.service.impl.SolrSearchDaoImpl" />
</beans></code></pre>
<p>资源文件 resource.properties</p>
<pre><code>SOLR.SERVER.URL=http://localhost:8080/solr/new_core</code></pre>
<h4>实体类</h4>
<p>这里有三个文件,一个是商品类 Product.java ,一个是返回参数封装类 SearchProductResult.java ,一个是管理查询key的枚举类FilterQueryKey.java</p>
<pre><code class="java">package com.itdragon.mapper.pojo;
import org.apache.solr.client.solrj.beans.Field;
public class Product {
@Field
private String id; // 商品编号
@Field
private String name; // 商品名称
@Field
private String catalog_name; // 商品分类名称
@Field
private Float price; // 价格
@Field
private Long number; // 数量
@Field
private String picture; //图片名称
@Field
private String description; // 商品描述
// get,set 方法省略
}</code></pre>
<pre><code class="java">import java.util.List;
public class SearchProductResult {
private List<Product> productList; // 商品列表
private Long recordCount; // 商品总数
private Integer pageCount; // 总页数
private Integer curPage; // 当前页
// get,set 方法省略
}</code></pre>
<pre><code class="java">package com.itdragon.mapper.pojo;
public enum FilterQueryKey {
PRICE("price"), // 价格
CATALOG_NAME("catalog_name"), // 类目名
SORT("sort"); // 价格排序
private String value;
private FilterQueryKey(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public static FilterQueryKey fromValue(String v) {
for (FilterQueryKey c: FilterQueryKey.values()) {
if (c.value.equals(v)) {
return c;
}
}
throw new IllegalArgumentException(v);
}
}</code></pre>
<h4>Dao 层</h4>
<p>提供了SolrJ的查询接口及其实现类 SolrSearchDaoImpl.java ,这里不要有任何业务相关的代码。这里需要注意关键词高亮显示。在Solr管理页面分析高亮返回的机构,以方便获取对应的值。<br><img src="/img/remote/1460000012349651?w=784&h=472" alt="高亮的结构" title="高亮的结构"></p>
<pre><code class="java">package com.itdragon.solr.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.CollectionUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.springframework.beans.factory.annotation.Autowired;
import com.itdragon.mapper.pojo.Product;
import com.itdragon.mapper.pojo.SearchProductResult;
import com.itdragon.solr.service.SolrSearchDao;
public class SolrSearchDaoImpl implements SolrSearchDao {
@Autowired
private HttpSolrClient httpSolrClient;
@Override
public SearchProductResult search(SolrQuery query) throws Exception {
// 返回值对象
SearchProductResult result = new SearchProductResult();
// 根据查询条件查询索引库
System.out.println("solr Query : " + query);
QueryResponse queryResponse = httpSolrClient.query(query);
// 获取查询结果
SolrDocumentList solrDocumentList = queryResponse.getResults();
// 获取查询结果总数量
result.setRecordCount(solrDocumentList.getNumFound());
// 初始化返回结果商品列表
List<Product> products = new ArrayList<>();
// 获取高亮显示数据
Map<String, Map<String, List<String>>> highlighting = queryResponse.getHighlighting();
// 遍历查询结果
for (SolrDocument solrDocument : solrDocumentList) {
Product product = new Product();
product.setId((String) solrDocument.get("id"));
// 获取高亮显示的集合
List<String> hightNames = highlighting.get(solrDocument.get("id")).get("product_name");
String pName = CollectionUtils.isNotEmpty(hightNames)? hightNames.get(0) : (String) solrDocument.get("product_name");
product.setName(pName);
product.setPicture((String) solrDocument.get("product_picture"));
product.setPrice((float) solrDocument.get("product_price"));
product.setCatalog_name((String) solrDocument.get("product_category_name"));
products.add(product);
}
result.setProductList(products);
return result;
}
}</code></pre>
<h4>Service 层</h4>
<p>提供了根据业务需求逻辑查询商品的接口及其实现类 ProductServiceImpl.java</p>
<pre><code class="java">package com.itdragon.service.impl;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.itdragon.mapper.pojo.FilterQueryKey;
import com.itdragon.mapper.pojo.SearchProductResult;
import com.itdragon.service.ProductService;
import com.itdragon.solr.service.SolrSearchDao;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private SolrSearchDao solrSearchDao;
@Override
public SearchProductResult search(String queryString, Map<String, String> filterQueryMap, Integer page, Integer rows) throws Exception {
// 创建查询对象
SolrQuery query = new SolrQuery();
// 设置查询条件
queryString = StringUtils.isEmpty(queryString)? "*:*" : queryString;
query.setQuery(queryString);
// 设置复杂查询条件
if (null != filterQueryMap.get(FilterQueryKey.CATALOG_NAME.getValue())) {
query.addFilterQuery("product_catalog_name:" + filterQueryMap.get(FilterQueryKey.CATALOG_NAME.getValue()));
}
if (null != filterQueryMap.get(FilterQueryKey.PRICE.getValue())) {
String price = filterQueryMap.get(FilterQueryKey.PRICE.getValue());
query.addFilterQuery("product_price:[" + price.split("-")[0] + " TO " + price.split("-")[1] + "]");
}
if (null != filterQueryMap.get(FilterQueryKey.SORT.getValue())) {
String priceSort = filterQueryMap.get(FilterQueryKey.SORT.getValue());
query.setSort("product_price", "1".equals(priceSort) ? ORDER.desc : ORDER.asc);
}
// 设置分页
query.setStart((page - 1) * rows); // 当前页面开始下标
query.setRows(rows);
// 设置默认搜素域
query.set("df", "product_keywords");
// 设置高亮显示
query.setHighlight(true);
query.addHighlightField("product_name");
query.setHighlightSimplePre("<em style=\"color:red\">");
query.setHighlightSimplePost("</em>");
// 执行查询
SearchProductResult searchResult = solrSearchDao.search(query);
// 计算查询结果总页数
long recordCount = searchResult.getRecordCount();
int pageCount = (int) (recordCount % rows > 0? (recordCount / rows) + 1 : (recordCount / rows));
searchResult.setPageCount(pageCount);
searchResult.setCurPage(page);
return searchResult;
}
}</code></pre>
<h4>Controller 层</h4>
<p>实现主页初始化和检索功能</p>
<pre><code class="java">package com.itdragon.controller;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
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 org.springframework.web.bind.annotation.RequestParam;
import com.itdragon.mapper.pojo.FilterQueryKey;
import com.itdragon.mapper.pojo.SearchProductResult;
import com.itdragon.service.ProductService;
@Controller
public class ProductController {
@Autowired
private ProductService searchService;
@RequestMapping("/")
public String showIndex(Model model) {
SearchProductResult searchResult;
try {
searchResult = searchService.search("*:*", new HashMap<>(), 1, 12);
model.addAttribute("result", searchResult);
} catch (Exception e) {
e.printStackTrace();
}
return "index";
}
@RequestMapping(value="/query")
public String search(@RequestParam("queryString")String query,
@RequestParam("catalog_name") String catalog_name,
@RequestParam("price") String price,
@RequestParam("sort") String sort,
@RequestParam(defaultValue="1")Integer page,
@RequestParam(defaultValue="12")Integer rows, Model model) {
SearchProductResult searchResult = null;
try {
// 拼接复杂的查询语句
Map<String, String> filterQueryMap = new HashMap<>();
if (StringUtils.isNotBlank(catalog_name)) {
filterQueryMap.put(FilterQueryKey.CATALOG_NAME.getValue(), catalog_name);
}
if (StringUtils.isNotBlank(price)) {
filterQueryMap.put(FilterQueryKey.PRICE.getValue(), price);
}
if (StringUtils.isNotBlank(sort)) {
filterQueryMap.put(FilterQueryKey.SORT.getValue(), sort);
}
searchResult = searchService.search(query, filterQueryMap, page, rows);
model.addAttribute("result", searchResult);
model.addAttribute("queryString", query);
model.addAttribute("catalog_name", catalog_name);
model.addAttribute("price", price);
model.addAttribute("sort", sort);
model.addAttribute("page", page);
} catch (Exception e) {
e.printStackTrace();
}
return "index";
}
}</code></pre>
<h4>View 层</h4>
<p>实现点击搜索框进行查询,点击类目筛选查询,点击价格区间查询,点击价格排序,查询结果回显,分页的功能。</p>
<pre><code class="java"><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
......省略
<script type="text/javascript">
// 关键字查询
function query() {
//执行关键词查询时清空过滤条件
document.getElementById("catalog_name").value="";
document.getElementById("price").value="";
document.getElementById("page").value="";
queryList();
}
// 表单提交
function queryList() {
document.getElementById("actionForm").submit();
}
// 特殊值查询
function filter(key, value) {
document.getElementById(key).value=value;
queryList();
}
// 排序
function sort() {
var s = document.getElementById("sort").value;
if (s != "1") {
s = "1";
} else {
s = "0";
}
document.getElementById("sort").value = s;
queryList();
}
// 翻页
function changePage(p) {
var curpage = Number(document.getElementById("page").value);
curpage = curpage + p;
document.getElementById("page").value = curpage;
queryList();
}
</script>
</head>
<body class="root61">
......省略
<div id="o-header-2013">
<div class="w" id="header-2013">
<div id="logo-2013" class="ld"><a href="#" hidefocus="true"><b></b><img src="" width="270" height="60" alt="Logo"></a></div>
<!--logo end-->
<div id="search-2013">
<div class="i-search ld">
<ul id="shelper" class="hide"></ul>
<form id="actionForm" action="/query" method="POST">
<div class="form">
<input type="text" class="text" accesskey="s" name="queryString" id="key" value="${queryString}"
autocomplete="off" onkeydown="javascript:if(event.keyCode==13) {query()}">
<input type="button" value="搜索" class="button" onclick="query()">
</div>
<input type="hidden" name="catalog_name" id="catalog_name" value="${catalog_name}"/>
<input type="hidden" name="price" id="price" value="${price}"/>
<input type="hidden" name="page" id="page" value="${result.curPage}"/>
<input type="hidden" name="sort" id="sort" value="${sort}"/>
</form>
</div>
<div id="hotwords"></div>
</div>
<!--search end-->
</div>
<!--header end-->
......省略
</div>
<div class="w">
<div class="breadcrumb">
<strong><a href="#">服饰内衣</a></strong><span>&nbsp;&gt;&nbsp;<a href="#">女装</a>&nbsp;&gt;&nbsp;<a href="#">T恤</a></span>
</div>
</div>
<div class="w main">
<div class="right-extra">
<div id="select" clstag="thirdtype|keycount|thirdtype|select" class="m">
<div class="mt">
<h1>T恤 -<strong>&nbsp;商品筛选</strong></h1>
</div>
<!-- 类目查询和价格区间查询 -->
<div class="mc attrs">
<div data-id="100001" class="brand-attr">
<div class="attr">
<div class="a-key">商品类别:</div>
<div class="a-values">
<div class="v-tabs">
<div class="tabcon">
<div><a href="javascript:filter('catalog_name', '幽默杂货')">幽默杂货</a></div>
<div><a href="javascript:filter('catalog_name', '时尚卫浴')">时尚卫浴</a></div>
<div><a href="javascript:filter('catalog_name', '另类文体')">另类文体</a></div>
<div><a href="javascript:filter('catalog_name', '创意相架')">创意相架</a></div>
<div><a href="javascript:filter('catalog_name', '巧妙收纳')">巧妙收纳</a></div>
</div>
</div>
</div>
</div>
</div>
<div data-id="100002" class="prop-attrs">
<div class="attr">
<div class="a-key">价格:</div>
<div class="a-values">
<div class="v-fold">
<ul class="f-list">
<li><a href="javascript:filter('price','0-9')">0-9</a></li>
<li><a href="javascript:filter('price','10-29')">10-29</a></li>
<li><a href="javascript:filter('price','30-49')">30-49</a></li>
<li><a href="javascript:filter('price','50-*')">50以上</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 价格排序,分页 -->
<div id="filter">
<div class="cls"></div>
<div class="fore1">
<dl class="order">
<dt>排序:</dt>
<dd><a href="javascript:sort()">价格</a><b></b></dd>
</dl>
<dl class="activity">
<dd></dd>
</dl>
<div class="pagin pagin-m">
<span class="text"><i>${result.curPage }</i>/${result.pageCount }</span>
<a href="javascript:changePage(-1)" class="prev">上一页<b></b></a>
<a href="javascript:changePage(1)" class="next">下一页<b></b></a>
</div>
<div class="total">
<span>共<strong>${result.recordCount }</strong>个商品
</span>
</div>
<span class="clr"></span>
</div>
</div>
<!--核心代码,商品列表开始-->
<div id="plist" class="m plist-n7 plist-n8 prebuy">
<ul class="list-h">
<c:forEach var="item" items="${result.productList }">
<li pid="${item.id }">
<div class="lh-wrap">
<div class="p-img">
<a target="_blank" href="#">
<img width="220" height="282" class="err-product" src="/images/${item.picture}">
</a>
</div>
<div class="p-name">
<a target="_blank" href="#">${item.name }</a>
</div>
<div class="p-price">
<strong>¥<fmt:formatNumber value="${item.price}" maxFractionDigits="2"/>
</div>
</div>
</li>
</c:forEach>
</ul>
</div>
......省略
</body>
</html></code></pre>
<h3>总结</h3>
<ul>
<li>Solr的中文分词器,虽然已经停更了,但是免费。</li>
<li>SolrJ连接solr全文检索服务器,实现查询,拦截,排序,分页,高亮等功能。</li>
<li>Spring整合SolrJ实现电商商品页面检索,筛选,分页功能。</li>
</ul>
<p>源码:<a href="https://link.segmentfault.com/?enc=vDVKHw6jXcrU%2F%2FmUJp%2FZsg%3D%3D.esIQmcZ8LsW7Reoet0JVrDbZ09qrZb0kjGCyD7oWxKbEaFb5QXKKrpz5D5l65GUm%2FKEX12aFzqcvP2iusCl%2B0cHFgeY%2Ft8BLrGpzTZDDx60%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>到这里SolrJ 复杂查询 高亮显示就结束了。有什么不对的地方请指出,最后感谢阅读!如果觉得文章不错,麻烦点一下"推荐"</p>
Solr7 安装部署 管理界面介绍
https://segmentfault.com/a/1190000012315173
2017-12-06T21:42:19+08:00
2017-12-06T21:42:19+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Solr7 安装部署 管理界面介绍</h2>
<p>本章重点介绍CentOS 安装部署Solr7 ,Solr的管理界面介绍,添加核心Core配置,Dataimport导入数据,Documents 在线维护索引,Query复杂查询和一些常见问题处理办法。</p>
<h3>什么是Solr</h3>
<blockquote><p>Solr 是Apache下的一个顶级开源项目,采用Java开发,基于Lucene的全文搜索服务器。Solr可以独立运行在Jetty、Tomcat等这些Servlet容器中。</p></blockquote>
<p>这里谈到了Lucene,它是一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,目的是为开发人员提供工具包,以方便的在系统中实现全文检索的功能。<br>而Solr 的目标是打造一款企业级的搜索引擎系统,可以独立运行。并且Solr提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展,并对索引、搜索性能进行了优化。</p>
<h3>Solr7 安装部署</h3>
<p>首先安装环境的jdk是 jdk1.8 或者更高,建议tomcat是tomcat8.0或者更高</p>
<blockquote><p>You will need the Java Runtime Environment (JRE) version 1.8 or higher</p></blockquote>
<pre><code>[itdragon@localhost solr-server]$wget http://mirror.bit.edu.cn/apache/lucene/solr/7.1.0/solr-7.1.0.zip
[itdragon@localhost solr-server]$unzip solr-7.1.0.zip
[itdragon@localhost solr-server]$ls
apache-tomcat-8.5-solr solr-7.1.0
[itdragon@localhost solr-server]$mkdir -p apache-tomcat-8.5-solr/webapps/solr
[itdragon@localhost solr-server]$cd apache-tomcat-8.5-solr/webapps/solr/
[itdragon@localhost solr]$cp -r /home/itdragon/solr-server/solr-7.1.0/server/solr-webapp/webapp/* ./
[itdragon@localhost solr]$cp -r /home/itdragon/solr-server/solr-7.1.0/server/lib/ext/* ./WEB-INF/lib/
[itdragon@localhost solr]$cp -r /home/itdragon/solr-server/solr-7.1.0/server/lib/metrics*.* ./WEB-INF/lib/
[itdragon@localhost solr]$cp -r /home/itdragon/solr-server/solr-7.1.0/dist/solr-dataimporthandler-* ./WEB-INF/lib/
[itdragon@localhost solr]$cd ../../
[itdragon@localhost apache-tomcat-8.5-solr]$mkdir solrhome
[itdragon@localhost apache-tomcat-8.5-solr]$cp -r /home/itdragon/solr-server/solr-7.1.0/server/solr/* ./solrhome/
[itdragon@localhost apache-tomcat-8.5-solr]$vim webapps/solr/WEB-INF/web.xml
<env-entry>
<env-entry-name>solr/home</env-entry-name>
<env-entry-value>solrhome地址,pwd查看</env-entry-value>
<env-entry-type>java.lang.String</env-entry-type>
</env-entry>
<!--
<security-constraint>
......省略
</security-constraint>
-->
[itdragon@localhost apache-tomcat-8.5-solr]$cd bin/
[itdragon@localhost apache-tomcat-8.5-solr]$./startup.sh</code></pre>
<p>第一步:系统环境准备:jdk版本在1.8,tomcat8.5<br>第二步:下载solr7,并解压在当前目录<br>第三步:在tomcat,webapps目录下创建solr目录,并将solr-7.1.0/server/solr-webapp/webapp/* 目录下的所有内容拷贝过去<br>第四步:将需要的jar导入到 WEB-INF/lib/ 下<br>第五步:在tomcat目录下创建solrhome(目录名自定义),并将solr-7.1.0/server/solr/* 目录下的所有内容拷贝过去<br>第六步:修改WEB-INF/web.xml 文件,指定solrhome的位置,并注释security-constraint 权限内容<br>第七步:启动tomcat,并访问<a href="https://link.segmentfault.com/?enc=iN2DQea%2FIcjX9YyHi0qHNA%3D%3D.4ETIXlvbgXhK7c50%2Bcq5xQ%3D%3D" rel="nofollow">http://ip</a>:port/solr/index.html#/ <br>注意:访问<a href="https://link.segmentfault.com/?enc=WMCWXAyvGx44INFCUqOG%2Fw%3D%3D.pZsUWU2%2FqxYAcspZRUf5YA%3D%3D" rel="nofollow">http://ip</a>:port/solr/ 显示404,目前还没有找到原因,网上说jar没到导入,可是笔者都导入了。</p>
<p>看到管理页面说明安装成功<br><img src="/img/remote/1460000012315178?w=1034&h=513" alt="Solr管理页面" title="Solr管理页面"></p>
<p>存在的问题<br>1 80端口占用<br>修改 tomcat/conf/server.xml 文件,更换端口号。<br>2 Logging页面,日志不能正常显示</p>
<pre><code>[itdragon@localhost solr]$mkdir -p WEB-INF/classes
[itdragon@localhost solr]$cp /home/itdragon/solr-server/solr-7.1.0/server/resources/log4j.properties ./WEB-INF/classes/</code></pre>
<p>第一步:在tomcat WEB-INF目录下创建classes目录<br>第二步:将solr-7.1.0/server/resources/目录下的log4j.properties文件拷贝到classes目录中,重启Solr</p>
<h3>管理界面介绍</h3>
<h4>添加核心Core</h4>
<p>在管理页面,点击Core Admin,选择AddCore,添加核心<br><img src="/img/remote/1460000012315179" alt="添加核心" title="添加核心"></p>
<p><strong>name</strong>:自定义的名字,建议和instanceDir保持一致<br><strong>instanceDir</strong>: solrhome目录下的实例类目<br><strong>dataDir</strong>:默认填data即可<br><strong>config</strong>:指定配置文件,new_core/conf/solrconfig.xml<br><strong>schema</strong>:指定schema.xml文件,new_core/conf/schema文件(实际上是managed-schema文件) <br>注意!在scheme下面有一个感叹号!<br>instanceDir and dataDir need to exist before you can create the core</p>
<p>如果你不管他,直接点击Add Core 会提示 solrconfig.xml 文件找不到</p>
<blockquote><p>Error CREATEing SolrCore 'new_core': Unable to create core [new_core] Caused by: Can't find resource 'solrconfig.xml' in classpath or '/home/itdragon/solr/apache-tomcat-8.5/solrhome/new_core'</p></blockquote>
<p>解决方法如下</p>
<pre><code>[itdragon@localhost new_core]$mkdir conf
[itdragon@localhost new_core]$cp -r /home/itdragon/solr-server/solr-7.1.0/server/solr/configsets/_default/conf/* ./conf/
[itdragon@localhost solrhome]$cp -r /home/itdragon/solr-server/solr-7.1.0/contrib/ ./
[itdragon@localhost solrhome]$cp -r /home/itdragon/solr-server/solr-7.1.0/dist/ ./
检查solrconfig.xml和contrib目录,dist目录的相对位置
<!--
<lib dir="${solr.install.dir:../../../..}/contrib/extraction/lib" regex=".*\.jar" />
......省略
-->
<lib dir="${solr.install.dir:../../}/contrib/extraction/lib" regex=".*\.jar" />
......省略</code></pre>
<p>第一步:将solr-7.1.0/server/solr/configsets/_default/目录下的conf 拷贝到 new_core 目录下。正确的目录结构:new_core/conf/solrconfig.xml <br>第二步:将contrib目录,dist目录拷贝到solrhome目录中<br>第三步:检查solrconfig.xml文件配置的路径是否正确,重启服务</p>
<p>重启服务后即可正常创建Core,然后instanceDir文件夹(new_core)里会自动生成一个core.properties文件</p>
<pre><code>name=new_core
config=solrconfig.xml
schema=schema.xml
dataDir=data</code></pre>
<p>第二次创建core,就不用这么麻烦了,直接把第一次创建的new_core目录复制一份,修改core.properties文件中的name 即可。<br>在Core Selector 中选择刚创建的 new_core,会出现很多菜单。这是本章的另一个重点。重点学习的内容:Query(查询页面),Documents (索引文档),Dataimport(导入数据),Analysis(分析,下章节和中文分词一起介绍)。其他了解即可。</p>
<h4>Dataimport(导入数据)</h4>
<p>点击Dataimport 显示 Sorry, no dataimport-handler defined! 解决方法如下</p>
<pre><code>[itdragon@localhost ~]$cd solr-server/apache-tomcat-8.5-solr/solrhome/contrib/dataimporthandler/lib
# 导入solr-dataimporthandler 和 mysql-connector-java jar包
[itdragon@localhost solrhome]$vim new_core/conf/solrconfig.xml
<lib dir="${solr.install.dir:../../}/contrib/dataimporthandler/lib" regex=".*\.jar" />
......省略
<requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
<lst name="defaults">
<str name="config">data-config.xml</str>
</lst>
</requestHandler>
[itdragon@localhost solrhome]$vim new_core/conf/data-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<dataConfig>
<dataSource type="JdbcDataSource"
driver="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/jpa"
user="root"
password="root"/>
<document>
<entity name="product" query="SELECT pid,name,catalog,catalog_name,price,description,picture FROM products ">
<field column="pid" name="id"/>
<field column="name" name="product_name"/>
<field column="catalog" name="product_catalog"/>
<field column="catalog_name" name="product_catalog_name"/>
<field column="price" name="product_price"/>
<field column="description" name="product_description"/>
<field column="picture" name="product_picture"/>
</entity>
</document>
</dataConfig></code></pre>
<p>第一步:进入solrhome/contrib/dataimporthandler/lib 目录下,若没有lib则创建一个,导入solr-dataimporthandler-7.1.0.jar 和 mysql-connector-java-5.1.17.jar 包<br>第二步:修改new_core/conf/solrconfig.xml 文件,使其加载dataimporthandler/lib下的jar包<br>第三步:在new_core/conf/solrconfig.xml 文件底部添加DataImportHandler 内容<br>第四步:在new_core/conf/ 目录下创建data-config.xml(数据库配置和对应的字段),重启服务</p>
<p>jar包和sql文件: <br><a href="https://link.segmentfault.com/?enc=nccrIQDv%2F0v5RrtuPCceOw%3D%3D.kRwVHhbKjurHGAd95q2qLIaBms74Lv0YWLxQM1dWRLClkaD7TpZ%2F%2B49zS6lBwRt%2FWr2yW4CL%2Fp%2BHqqBVYXN4McHyjKb0T2Qe3qux826Icc8%3D" rel="nofollow">https://github.com/ITDragonBl...</a><br>如果出现下图内容则说明配置成功。<br><img src="/img/remote/1460000012315180?w=710&h=527" alt="导入数据" title="导入数据"></p>
<p><strong>Command</strong>:full_import:全量导入;delta_import:增量导入。<br>选择 全量导入,Execute执行,Refresh Status刷新查看状态,其他都选默认即可。<br>Clean:在索引开始构建之前是否删除之前的索引,默认为true <br>Commit:在索引完成之后是否提交。默认为true <br><strong>Execute</strong>:执行导入 <br><strong>Refresh Status</strong>:刷新后才能看到数据发生了变化(点一次刷新一次)</p>
<h4>Documents (索引文档)</h4>
<p>索引的增加,修改,删除相关操作。其中修改的逻辑是先删除后增加。<br><img src="/img/remote/1460000012315181?w=885&h=507" alt="增加索引" title="增加索引"></p>
<p>比较重要的是前三个参数<br><strong>Request-Handler(qt)</strong>:update(新增,更新和删除都用update)<br><strong>Document Type</strong>:提交的索引文档类型,有JSON、XML等格式 <br><strong>Document(s)</strong>:提交的索引文档内容<br>Commit Within:每1000毫秒执行<br>Overwrite:true,若文档存在则默认覆盖</p>
<p><strong>删除索引</strong>:删除用json格式会出错,用xml格式后面需添加< commit/> <br><img src="/img/remote/1460000012315182" alt="删除索引" title="删除索引"></p>
<p>工作中,我们不可能为了个别数据去写代码修改数据,那么熟练使用Documents,对我们的工作有很大的帮助。</p>
<h4>Query(查询页面)</h4>
<p>查询所有价格在10到20之间的数据,并以价格降序输出商品类目名,商品标题,商品价格信息。<br><img src="/img/remote/1460000012315183?w=1006&h=665" alt="查询" title="查询"></p>
<p><strong>Request-Handler(qt)</strong>:select查询操作<br><strong>q(query)</strong>:查询条件,key:value 形式,只能满足简单的查询<br><strong>fq(filter query)</strong>:过滤条件。对q的补充,实现复杂的查询。如:product_price:[10.0 TO 20.0] 表示价格在10~20之间。" <em> " 表示无限,[ </em> TO 20.0] 表示小于20.0<br><strong>sort</strong>:对查询结果排序。如:product_price desc 表示价格降序<br><strong>start,rows</strong>,开始页数,和每页多少条,简称页码<br><strong>fl(field list)</strong>:指定那些字段有返回值。多个值用","分隔。如:product_catalog_name,product_name,product_price <br><strong>df(default field)</strong>:默认域,当q查询没有key的时候,发挥作用<br><strong>wt(write type)</strong>:输出格式,一般都是json<br><strong>hl(high light)</strong>:高亮,搜索的结果若不高亮,那就没啥意义了。下一章会介绍</p>
<h4>其他</h4>
<p>Dashboard:<br>显示了该Solr实例开始启动运行的时间、版本、系统资源(物理内存,交换空间)、jvm等信息<br>Logging:Solr运行日志信息<br>Java Properties:<br>Solr在JVM 运行环境中的属性信息,包括类路径、文件编码、jvm内存设置等信息。<br>Tread Dump:<br>显示Solr Server中当前活跃线程信息,同时也可以跟踪线程运行栈信息。<br>Overview:<br>包含基本统计如当前文档数;和实例信息如当前核心的配置目录<br>Files:<br>在线预览solrhome/new_core/conf/* 文件或者目录<br>Ping:<br>请求来检查核心是否启动并响应请求,点击后显示响应的毫秒数<br>Plugins / Stats:<br>插件及其状态</p>
<h3>总结</h3>
<ul>
<li>Solr7是基于Lucene的全文检索服务器,可以独立运行在servlet容器中</li>
<li>Solr7的安装部署需要注意 tomcat/webapps/solr 和 solrhome 两个目录</li>
<li>创建Solr Core需要注意 solrconfig.xml文件在new_core/conf目录中</li>
<li>Query查询,q + fq 实现复杂的查询,sort排序,fl指定回显数据,hl高亮</li>
<li>Documents,支持新增,更新,删除索引文档</li>
<li>Dataimport,导入数据,需要注意配置 solrconfig.xml 文件和创建 data-config.xml 文件</li>
</ul>
<p>到这里Solr7 的安装部署,管理界面介绍就结束了。感谢阅读!欢迎点评!!</p>
Redis 高可用集群
https://segmentfault.com/a/1190000012255984
2017-12-02T19:03:05+08:00
2017-12-02T19:03:05+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Redis 高可用集群</h2>
<p>Redis 的集群主从模型是一种高可用的集群架构。本章主要内容有:高可用集群的搭建,Jedis连接集群,新增集群节点,删除集群节点,其他配置补充说明。</p>
<h3>高可用集群搭建</h3>
<blockquote><p>集群(cluster)技术是一种较新的技术,通过集群技术,可以在付出较低成本的情况下获得在性能、可靠性、灵活性方面的相对较高的收益,其任务调度则是集群系统中的核心技术。</p></blockquote>
<p>Redis 3.0 之后便支持集群。Redis 集群中内置了 16384 个哈希槽。Redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。<br>所有节点之间彼此互联(PING-PONG机制),当超过半数的主机认为某台主机挂了,则该主机就是真的挂掉了,整个集群就不可用了。<br>若给集群中的每台主机分配多台从机。主机挂了,从机上位,依然能正常工作。但是若集群中超过半数的主机挂了,无论是否有从机,该集群也是不可用的。</p>
<h4>搭建前的准备工作</h4>
<h5>搭建ruby环境</h5>
<p>redis集群管理工具 redis-trib.rb 是依赖 ruby 环境。</p>
<pre><code>[root@itdragon ~]# yum install ruby
[root@itdragon ~]# yum install rubygems
[root@itdragon ~]# gem install redis
[root@itdragon ~]# cd redis-4.0.2/src/
[root@itdragon src]# cp redis-trib.rb /usr/local/redis-4/bin/</code></pre>
<p>第一步:安装 ruby 环境<br>第二步:安装 gem 软件包(gem是用来扩展或修改Ruby应用程序的)。参考地址:<a href="https://link.segmentfault.com/?enc=bRCI8oyPSc6pKctK0fP3nw%3D%3D.EhKzKL8gJObZF9Ag58AVacTpLhoq1iBZ5SdQWJbCKIimfx2zFPcS4PF6vICa8m39" rel="nofollow">https://rubygems.org/gems/red...</a><br>第三步:在redis解压目录中找到 redis-trib.rb 文件,将其拷贝到启动redis服务的目录下,方便管理。</p>
<p>可能存在的问题<br>1 redis requires Ruby version >= 2.2.2,解决方法如下<br>2 没有/usr/local/rvm/scripts/rvm 这个目录.可能是上一步执行失败</p>
<pre><code>[root@itdragon ~]# ruby --version
ruby 1.8.7 (2013-06-27 patchlevel 374) [x86_64-linux]
[root@itdragon ~]# yum install curl
[root@itdragon ~]# curl -L get.rvm.io | bash -s stable
[root@itdragon ~]# source /usr/local/rvm/scripts/rvm
[root@itdragon ~]# rvm list known
[root@itdragon ~]# rvm install 2.3.3
[root@itdragon ~]# rvm use 2.3.3
[root@itdragon ~]# gem install redis</code></pre>
<h5>准备六台redis服务器</h5>
<p>和上一章节主从复制逻辑一样,将redis.conf文件拷贝6次,端口从6000~6005</p>
<pre><code>[root@itdragon bin]# cp redis.conf redis6000.conf
[root@itdragon bin]# vim redis6000.conf
port xxxx #修改端口
cluster-enabled yes #打开注释,开启集群模式
cluster-config-file nodes-xxxx.conf #集群的配置文件
pidfile /var/run/redis_xxxx.pid #pidfile文件
logfile "xxxx.log" #日志文件
dbfilename dump_xxxx.rdb #rdb持久化文件
cluster-node-timeout 5000 #请求超时,单位毫秒
appendonly yes #开启aof持久化方式
[root@itdragon bin]# vim start-all.sh
./redis-server redis6000.conf
./redis-server redis6001.conf
./redis-server redis6002.conf
./redis-server redis6003.conf
./redis-server redis6004.conf
./redis-server redis6005.conf
[root@itdragon bin]# chmod u+x start-all.sh
[root@itdragon bin]# ./start-all.sh
[root@itdragon bin]# ps aux | grep redis
root 28001 0.0 0.9 145964 9696 ? Ssl 17:45 0:00 ./redis-server 112.74.83.71:6000 [cluster]
root 28003 0.0 0.9 145964 9696 ? Ssl 17:45 0:00 ./redis-server 112.74.83.71:6001 [cluster]
root 28008 0.0 0.9 145964 9656 ? Ssl 17:45 0:00 ./redis-server 112.74.83.71:6002 [cluster]
root 28013 0.0 0.9 145964 9656 ? Ssl 17:45 0:00 ./redis-server 112.74.83.71:6003 [cluster]
root 28018 0.1 0.9 145964 9652 ? Ssl 17:45 0:00 ./redis-server 112.74.83.71:6004 [cluster]
root 28023 0.0 0.9 145964 9656 ? Ssl 17:45 0:00 ./redis-server 112.74.83.71:6005 [cluster]</code></pre>
<p>第一步:复制六个redis.conf 并修改相关配置,如果觉得麻烦可以用我配置的文件:<a href="https://link.segmentfault.com/?enc=Amcd%2BEtV27jwYzgy0K5vUw%3D%3D.Z8Bf2inV84R4Fj5DS4YyXhgWa%2FRvTuhZ0QVuDnMvqj4HkbGTqU4COzNrmYeSMvOKI6K2rFEBIKiA33HpP7L49fg4hT1gu0zME6tvXSc3%2BhA%3D" rel="nofollow">https://github.com/ITDragonBl...</a><br>第二步:新增批量开启redis服务程序,并增加执行权限<br>第三步:查看六台redis服务是否启动成功</p>
<h4>主从集群搭建</h4>
<p>集群创建命令: ./redis-trib.rb create 创建集群,--replicas 1 给每个主机分配一个从机,后面其他参数都是redis服务的ip:port。最后输入yes来接受建议的配置</p>
<pre><code>[root@itdragon bin]# ./redis-trib.rb create --replicas 1 112.74.83.71:6000 112.74.83.71:6001 112.74.83.71:6002 112.74.83.71:6003 112.74.83.71:6004 112.74.83.71:6005
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
112.74.83.71:6000
112.74.83.71:6001
112.74.83.71:6002
Adding replica 112.74.83.71:6003 to 112.74.83.71:6000
Adding replica 112.74.83.71:6004 to 112.74.83.71:6001
Adding replica 112.74.83.71:6005 to 112.74.83.71:6002
...... #省略
Can I set the above configuration? (type 'yes' to accept): yes
...... #省略
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered. #有16384个可用的插槽提供服务说明搭建成功
[root@itdragon bin]# ./redis-cli -h 112.74.83.71 -p 6002 -c
112.74.83.71:6002> set testKey value
-> Redirected to slot [5203] located at 112.74.83.71:6000
OK
112.74.83.71:6000> cluster info
cluster_state:ok
......
112.74.83.71:6000> cluster nodes
0968ef8f5ca96681da4abaaf4ca556c2e6dd0242 112.74.83.71:6002@16002 master - 0 1512035804722 3 connected 10923-16383
13ddd4c1b8c00926f61aa6daaa7fd8d87ee97830 112.74.83.71:6005@16005 slave 0968ef8f5ca96681da4abaaf4ca556c2e6dd0242 0 1512035803720 6 connected
a3bb22e04deec2fca653c606edf5b02b819f924f 112.74.83.71:6003@16003 slave 1d4779469053930f30162e89b6711d27a112b601 0 1512035802000 4 connected
1d4779469053930f30162e89b6711d27a112b601 112.74.83.71:6000@16000 myself,master - 0 1512035802000 1 connected 0-5460
a3b99cb5d22f5cbd293179e262f5eda931733c88 112.74.83.71:6001@16001 master - 0 1512035802719 2 connected 5461-10922
915a47afc4f9b94389676b4e14f78cba66be9e5d 112.74.83.71:6004@16004 slave a3b99cb5d22f5cbd293179e262f5eda931733c88 0 1512035801717 5 connected</code></pre>
<p>第一步:搭建集群 ./redis-trib.rb create ,选择yes接受建议的配置<br>第二步:进入集群客户端 ./redis-cli -h 任意主机host -p 任意主机port -c,-c表示以集群方式连接redis<br>第三步:保存数据<br>第四步:cluster info 查询集群状态信息<br>第五步:cluster nodes 查询集群结点信息,这里有一个坑,后面会介绍</p>
<p>可能存在的问题</p>
<blockquote><p>Sorry, the cluster configuration file nodes.conf is already used by a different Redis Cluster node. Please make sure that different nodes use different cluster configuration files.</p></blockquote>
<p>说的很明确,修改cluster-config-file nodes.conf 文件避免重名,或者删除该文件重新创建集群。</p>
<p>cluster nodes 查询集群节点信息<br>这是很重要的命令,我们需要关心的信息有:<br>第一个参数:节点ID <br>第二个参数:IP:PORT@TCP 这里一个坑,jedis-2.9.0之前的版本解析@出错<br>第三个参数:标志(Master,Slave,Myself,Fail...) <br>第四个参数:如果是从机则是主机的节点ID<br>最后两个参数:连接的状态和槽的位置。</p>
<h3>Jedis 连接集群</h3>
<p>首先要配置防火墙</p>
<pre><code>[root@itdragon ~]# vim /etc/sysconfig/iptables
-A INPUT -p tcp -m tcp --dport 6000 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 6001 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 6002 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 6003 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 6004 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 6005 -j ACCEPT
[root@itdragon ~]# service iptables restart</code></pre>
<p>最后是整合Spring</p>
<pre><code><!-- jedis集群版配置 -->
<bean id="redisClient" class="redis.clients.jedis.JedisCluster">
<constructor-arg name="nodes">
<set>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="6000" />
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="6001" />
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="6002" />
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="6003" />
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="6004" />
</bean>
<bean class="redis.clients.jedis.HostAndPort">
<constructor-arg name="host" value="${redis.host}"></constructor-arg>
<constructor-arg name="port" value="6005" />
</bean>
</set>
</constructor-arg>
<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
</bean>
<bean id="jedisClientCluster" class="com.itdragon.service.impl.JedisClientCluster"></bean></code></pre>
<p>单元测试</p>
<pre><code class="java">/**
* 集群版测试
* 若提示以下类似的错误:
* java.lang.NumberFormatException: For input string: "6002@16002"
* 若安装的redis 版本大于4,则可能是jedis 的版本低了。选择 2.9.0
* 因为 cluster nodes 打印的信息中,4版本之前的是没有 @16002 tcp端口信息
* 0968ef8f5ca96681da4abaaf4ca556c2e6dd0242 112.74.83.71:6002@16002 master - 0 1512035804722 3 connected 10923-16383
*/
@Test
public void testJedisCluster() throws IOException {
HashSet<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort(HOST, 6000));
nodes.add(new HostAndPort(HOST, 6001));
nodes.add(new HostAndPort(HOST, 6002));
nodes.add(new HostAndPort(HOST, 6003));
nodes.add(new HostAndPort(HOST, 6004));
nodes.add(new HostAndPort(HOST, 6005));
JedisCluster cluster = new JedisCluster(nodes);
cluster.set("cluster-key", "cluster-value");
System.out.println("集群测试 : " + cluster.get("cluster-key"));
cluster.close();
}</code></pre>
<p>可能存在的问题</p>
<blockquote><p>java.lang.NumberFormatException: For input string: "6002@16002"</p></blockquote>
<p>若redis 的版本在4.0.0之上,建议使用jedis-2.9.0以上。</p>
<p>源码:<br><a href="https://link.segmentfault.com/?enc=VsLhavIb3L5mRERL1BOIng%3D%3D.SCC2xQ50%2BN2PahOzmL%2BB7dTKWH61OP7aX7f5lmkIFHUswmaD1tQ3rECDQ6kmG9yBbcuf9%2BTQ7NEE170azCVLlon6PIWPX3BnyuUT0aIONC4%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>集群节点操作</h3>
<h4>添加主节点</h4>
<pre><code>[root@itdragon bin]# cp redis6005.conf redis6006.conf
[root@itdragon bin]# ./redis-server redis6006.conf
[root@itdragon bin]# ./redis-trib.rb add-node 112.74.83.71:6006 112.74.83.71:6000
[root@itdragon bin]# ./redis-cli -h 112.74.83.71 -p 6000 cluster nodes
916d26e9638dc51e168f32969da11e19c875f48f 112.74.83.71:6006@16006 master - 0 1512115612162 0 connected # 没有分配槽
[root@itdragon bin]# ./redis-trib.rb reshard 112.74.83.71:6000
How many slots do you want to move (from 1 to 16384)? 500
What is the receiving node ID? 916d26e9638dc51e168f32969da11e19c875f48f
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1:all
Do you want to proceed with the proposed reshard plan (yes/no)? yes
[root@itdragon bin]# ./redis-cli -h 112.74.83.71 -p 6000 cluster nodes
916d26e9638dc51e168f32969da11e19c875f48f 112.74.83.71:6006@16006 master - 0 1512116047897 7 connected 0-165 5461-5627 10923-11088
</code></pre>
<p>第一步:创建 redis6006.conf 的新主机,并启动Redis服务<br>第二步:新增主机节点,若打印"[OK] New node added correctly." 表示添加成功<br>第三步:查询集群节点信息,发现6006端口的主机虽然已经添加,但连接状态后面没有内容,即没分配槽<br>第四步:给6006端口主机分配槽,</p>
<ul>
<li>第一个参数:需要移动槽的个数,</li>
<li>第二个参数:接受槽的节点ID,</li>
<li>第三个参数:输入"all"表示从所有原节点中获取槽,</li>
<li>第四个参数:输入"yes"开始移动槽到目标结点id</li>
</ul>
<p>第五步:查询集群节点信息,发现6006端口的主机已经分配了槽</p>
<p>核心命令:<br>./redis-trib.rb add-node 新增主机ip:port 集群任意节点ip:port<br>./redis-trib.rb reshard 集群任意节点ip:port</p>
<p>可能存在的问题<br>[ERR] Sorry, can't connect to node 112.74.83.71:6006<br>说明:新增的主机必须要是启动状态。</p>
<h4>添加从节点</h4>
<pre><code>[root@itdragon bin]# cp redis6006.conf redis6007.conf
[root@itdragon bin]# vim redis6007.conf
[root@itdragon bin]# ./redis-server redis6007.conf
[root@itdragon bin]# ./redis-trib.rb add-node --slave --master-id 916d26e9638dc51e168f32969da11e19c875f48f 112.74.83.71:6007 112.74.83.71:6006
[root@itdragon bin]# ./redis-cli -h 112.74.83.71 -p 6000 cluster nodes
80315a4dee2d0fa46b8ac722962567fc903e797a 112.74.83.71:6007@16007 slave 916d26e9638dc51e168f32969da11e19c875f48f 0 1512117377000 7 connected</code></pre>
<p>第一步:创建 redis6007.conf 的新主机,并启动Redis服务<br>第二步:新增从机节点,在原来的命令上多了 --slave --master-id 主节点ID<br>第三步:查询集群节点信息</p>
<h4>删除结点</h4>
<p>删除节点前,要确保该节点没有值,否则提示:is not empty! Reshard data away and try again. 若该节点有值,则需要把槽分配出去</p>
<pre><code>./redis-trib.rb del-node 112.74.83.71:6006 916d26e9638dc51e168f32969da11e19c875f48f </code></pre>
<h3>配置文件补充</h3>
<p>前几章Redis教程中介绍了以下配置<br>1 开启Redis 的守护进程 :daemonize yes<br>2 指定pid文件写入文件名 :pidfile /var/run/redis.pid<br>3 指定Redis 端口:port 6379<br>4 绑定的主机地址 :bind 127.0.0.1<br>5 Redis持久化默认开启压缩数据:rdbcompression yes<br>6 指定rdb文件名:dbfilename dump.rdb<br>7 指定rdb文件位置:dir ./<br>8 从机启动时,它会自动从master进行数据同步:slaveof < masterip> < masterport><br>9 开启aof持久化方式:appendonly yes<br>10 指定aof文件名:appendfilename appendonly.aof<br>11 触发aof快照机制:appendfsync everysec (no/always) </p>
<p>本章节是Redis教程中的最后一章,把剩下的配置也一起说了吧<br>1 设置客户端连接超时时间,0表示关闭 :timeout 300<br>2 设置Redis日志级别,debug、verbose(默认)、notice、warning:loglevel verbose<br>3 设置数据库的数量:databases 16<br>4 设置Redis连接密码:requirepass foobared<br>5 设置同一时间最大客户端连接数,默认无限制:maxclients 128<br>6 指定Redis最大内存限制:maxmemory < bytes><br>7 指定是否启用虚拟内存机制:vm-enabled no<br>8 指定虚拟内存文件路径:vm-swap-file /tmp/redis.swap<br>9 指定包含其它的配置文件:include /path/to/local.conf</p>
<p>到这里Redis 的教程就结束了,还有其他知识可以访问官网学习。有什么不对的地方请指正。谢谢!</p>
<p>更多内容可学习官网:<a href="https://link.segmentfault.com/?enc=1JeEp3u0W2qL%2BErccw9ryA%3D%3D.XikqJcYyuuVqzAz7mXMZof8U%2FYtdN6t%2BdgZ5L5aNVwT8v5%2BIxd6hRprxXDdQyWmZ" rel="nofollow">https://redis.io/topics/clust...</a></p>
Redis 主从复制
https://segmentfault.com/a/1190000012234255
2017-11-30T22:15:25+08:00
2017-11-30T22:15:25+08:00
itdragon
https://segmentfault.com/u/itdragon
4
<h2>Redis 主从复制</h2>
<p>本章介绍Redis的一个强大功能--主从复制。一台master主机可以拥有多台slave从机。而一台slave从机又可以拥有多个slave从机。如此下去,形成强大的多级服务器集群架构(高扩展)。可以避免Redis单点故障,实现容灾恢复效果(高可用)。读写分离的架构,满足读多写少的并发应用场景。</p>
<h3>主从复制的作用</h3>
<p>主从复制,读写分离,容灾恢复。一台主机负责写入数据,多台从机负责备份数据。在高并发的场景下,即便是主机挂了,可以用从机代替主机继续工作,避免单点故障导致系统性能问题。读写分离,让读多写少的应用性能更佳。</p>
<h3>主从复制架构</h3>
<blockquote><p>At the base of Redis replication there is a very simple to use and configure master-slave replication that allows slave Redis servers to be exact copies of master servers.</p></blockquote>
<p>官方说了,搭建主从架构 <strong>is a very simple</strong> 。官方连接:<a href="https://link.segmentfault.com/?enc=4UlfcI5ApV9EdIwk35L5fA%3D%3D.4PRO8U9DqL5%2BjXBN0qId1tiZlPLuSObEMBPs%2FZ0qSJL4yQtks44gVUfhsnpoBp6B" rel="nofollow">https://redis.io/topics/repli...</a><br>确实是简单的,一个命令: slaveof 主机ip 主机port ,就可以确定主从关系;一个命令:./redis-sentinel sentinel.conf ,就可以开启哨兵监控。<br>搭建是简单的,维护是痛苦的。在高并发场景下,会有很多想不到的问题出现。我们只有清楚复制的原理,熟悉主机,从机宕机后的变化。才能很好的跨过这些坑。下面的每一个步骤都是一个小的知识点,小的场景。每做完一个步骤,你都会收获到知识。加油!!!daydayup!!!!!!</p>
<p>架构图:一主二仆一兵(也可以多主多仆多兵)<br><img src="/img/remote/1460000012234260?w=498&h=275" alt="主从复制架构" title="主从复制架构"></p>
<h4>搭建前的准备工作</h4>
<p>因为穷,笔者选择用一台服务器模拟三台主机。和生产环境的区别仅仅是ip地址和port端口不同。<br>第一步:将redis.conf 拷贝三份,名字分别是,redis6379.conf,redis6380.conf,redis6381.conf<br>第二步:修改三个文件的port端口,pid文件名,日志文件名,rdb文件名<br>第三步:分别打开三个窗口模拟三台服务器,开启redis服务。</p>
<pre><code>[root@itdragon bin]# cp redis.conf redis6379.conf
[root@itdragon bin]# cp redis.conf redis6380.conf
[root@itdragon bin]# cp redis.conf redis6381.conf
[root@itdragon bin]# vim redis6379.conf
logfile "6379.log"
dbfilename dump_6379.rdb
[root@itdragon bin]# vim redis6380.conf
pidfile /var/run/redis_6380.pid
port 6380
logfile "6380.log"
dbfilename dump_6380.rdb
[root@itdragon bin]# vim redis6381.conf
port 6381
pidfile /var/run/redis_6381.pid
logfile "6381.log"
dbfilename dump_6381.rdb
[root@itdragon bin]# ./redis-server redis6379.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> keys *
(empty list or set)
[root@itdragon bin]# ./redis-server redis6380.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> keys *
(empty list or set)
[root@itdragon bin]# ./redis-server redis6381.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6381
127.0.0.1:6381> keys *
(empty list or set)</code></pre>
<h4>主从复制搭建步骤</h4>
<h5>基础搭建 (๑ŐдŐ)b</h5>
<p><strong>第一步:查询主从复制信息</strong>,分别选择三个端口,执行命令:info replication。</p>
<pre><code># 6379 端口
[root@itdragon bin]# ./redis-server redis6379.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
......
# 6380 端口
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
......
# 6381 端口
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
......</code></pre>
<p>三个端口都打印相同的信息:role:master 角色是master,connected_slaves:0 连接从机数量为零。了解更多参数含义可访问连接: <a href="https://link.segmentfault.com/?enc=Iw39WkUISnuxsHe5xuKOcg%3D%3D.BK5GrJy8ur7lIehTKSYj12sxzdYTm04fCQEF%2BgkQRenrnvDfyWIzA%2FzljegIOoaw" rel="nofollow">http://redisdoc.com/server/in...</a></p>
<p>第二步:选择6379端口,执行命令:set k1 v1</p>
<pre><code>127.0.0.1:6379> set k1 v1
OK</code></pre>
<p><strong>第三步:设置主从关系</strong>,分别选择6380端口和6381端口,执行命令:SLAVEOF 127.0.0.1 6379</p>
<pre><code># 6380 端口
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......
# 6381 端口
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......
# 6379 端口
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=98,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=98,lag=1
......</code></pre>
<p>主从关系发生了变化:<br>6380端口和6381端口打印的信息: role:slave 从机;master_host:127.0.0.1 主机的ip地址;master_port:6379 主机的port 端口。<br>6379端口打印的信息: role:master 主机;connected_slaves:2 连了两个从机; slaveX : ID、IP 地址、端口号、连接状态、从库信息</p>
<p><strong>第四步:全量复制</strong>,分别选择6380端口和6381端口,执行命令:get k1</p>
<pre><code># 6380 端口
127.0.0.1:6380> get k1
"v1"
# 6381 端口
127.0.0.1:6381> get k1
"v1"</code></pre>
<p>两个端口都可以打印k1的值,说明在建立主从关系时,从机便拥有了主机的数据。</p>
<p><strong>第五步:增量复制</strong>,选择6379端口,执行命令:set k2 v2。然后分别选择6380端口和6381端口,执行命令:get k2</p>
<pre><code># 6379 端口
127.0.0.1:6379> set k2 v2
OK
# 6380 端口
127.0.0.1:6380> get k2
"v2"
# 6381 端口
127.0.0.1:6381> get k2
"v2"</code></pre>
<p>两个端口都可以打印k2的值,说明建立主从关系后,主机新增的数据都会复制给从机。</p>
<p><strong>第六步:主从的读写分离</strong>,选择6380端口,执行命令:set k3 v3</p>
<pre><code># 6380 端口
127.0.0.1:6380> set k3 v3
(error) READONLY You can't write against a read only slave.
# 6379 端口
127.0.0.1:6379> set k3 v3
OK</code></pre>
<p>从机6380写入失败,是因为读写分离的机制。</p>
<p><strong>第七步:主机宕机的情况</strong>,选择6379端口,执行命令:shutdown</p>
<pre><code># 6379 端口
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
# 6380 端口
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......
# 6381 端口
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......</code></pre>
<p>从打印的结果得知:从机原地待命</p>
<p><strong>第八步:主机宕机后恢复</strong>,选择6379端口,重启Redis服务,执行命令:set k4 v4。分别选择6380端口和6381端口,执行命令:get k4</p>
<pre><code># 6379 端口
[root@itdragon bin]# ./redis-server redis6379.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> set k4 v4
OK
# 6380 端口
127.0.0.1:6380> get k4
"v4"
# 6381 端口
127.0.0.1:6381> get k4
"v4"</code></pre>
<p>主机重启后,一切正常。</p>
<p><strong>第九步:从机宕机后恢复</strong>,选择6380端口,执行命令:shutdown。选择6379端口,执行命令:set k5 v5。选择6380端口,重启Redis服务后执行命令:get k5</p>
<pre><code># 6380 端口
127.0.0.1:6380> SHUTDOWN
not connected> QUIT
[root@itdragon bin]# ./redis-server redis6380.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6380
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......
127.0.0.1:6380> get k5
"v5"
# 6379 端口
127.0.0.1:6379> set k5 v5
OK</code></pre>
<p>从机宕机后,一切正常。笔者用的是redis.4.0.2版本的。看过其他教程,从机宕机恢复后,只能同步主机新增数据,也就是k5是没有值的,可是笔者反复试过,均有值。留着备忘!</p>
<p><strong>第十步:去中性化思想</strong>,选择6380端口,执行命令:SLAVEOF 127.0.0.1 6381。选择6381端口,执行命令:info replication</p>
<pre><code># 6380 端口
127.0.0.1:6380> SLAVEOF 127.0.0.1 6381
OK
# 6381 端口
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=1677,lag=1
......</code></pre>
<p>虽然6381 是6380的主机,是6379的从机。在Redis眼中,6381依旧是从机。一台主机配多台从机,一台从机在配多台从机,从而实现了庞大的集群架构。同时也减轻了一台主机的压力,缺点是增加了服务器间的延迟。</p>
<h5>从机上位 (๑ŐдŐ)b</h5>
<p>模拟主机宕机,人为手动怂恿从机上位的场景。先将三个端口恢复成6379是主机,6380和6381是从机的架构。<br>从机上位步骤:<br>第一步:模拟主机宕机,选择6379端口,执行命令:shutdown<br>第二步:断开主从关系,选择6380端口,执行命令:SLAVEOF no one<br>第三步:重新搭建主从,选择6381端口,执行命令:info replication,SLAVEOF 127.0.0.1 6380<br>第四步:之前主机恢复,选择6379端口,重启Redis服务,执行命令:info replication<br>在6379主机宕机后,6380从机断开主从关系,6381开始还在原地待命,后来投靠6380主机后,6379主机回来了当它已是孤寡老人,空头司令。</p>
<pre><code># 6379端口
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
# 6380端口
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> set k6 v6
OK
# 6381端口
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
......
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
OK
127.0.0.1:6381> get k6
"v6"</code></pre>
<h5>哨兵监控 (๑ŐдŐ)b</h5>
<p>从机上位是需要人为控制,在生产环境中是不可取的,不可能有人实时盯着它,也不可能大半夜起床重新搭建主从关系。在这样的需求促使下,哨兵模式来了!!!<br>哨兵有三大任务:<br><strong>1 监控</strong>:哨兵会不断地检查你的Master和Slave是否运作正常<br><strong>2 提醒</strong>:当被监控的某个Redis出现问题时, 哨兵可以通过API向管理员或者其他应用程序发送通知<br><strong>3 故障迁移</strong>:若一台主机出现问题时,哨兵会自动将该主机下的某一个从机设置为新的主机,并让其他从机和新主机建立主从关系。</p>
<p>哨兵搭建步骤:<br>第一步:新开一个窗口,取名sentinel,方便观察哨兵日志信息<br>第二步:创建sentinel.conf文件,也可以从redis的解压文件夹中拷贝一份。<br>第三步:设置监控的主机和上位的规则,编辑sentinel.conf,输入 sentinel monitor itdragon-redis 127.0.0.1 6379 1 保存退出。解说:指定监控主机的ip地址,port端口,得票数。<br>第四步:前端启动哨兵,执行命令:./redis-sentinel sentinel.conf。<br>第五步:模拟主机宕机,选择6379窗口,执行命令:shutdown。<br>第六步:等待从机投票,在sentinel窗口中查看打印信息。<br>第七步:启动6379服务器,<br>语法结构:sentinel monitor 自定义数据库名 主机ip 主机port 得票数<br>若从机得票数大于设置值,则成为新的主机。若之前的主机恢复后,<br>如果哨兵也宕机了???那就多配几个哨兵并且相互监控。</p>
<pre><code># sentinel窗口
[root@itdragon bin]# vim sentinel.conf
sentinel monitor itdragon-redis 127.0.0.1 6379 1
[root@itdragon bin]# ./redis-sentinel sentinel.conf
......
21401:X 29 Nov 15:39:15.052 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ itdragon-redis 127.0.0.1 6380
21401:X 29 Nov 15:39:15.052 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ itdragon-redis 127.0.0.1 6380
21401:X 29 Nov 15:39:45.081 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ itdragon-redis 127.0.0.1 6380
21401:X 29 Nov 16:40:52.055 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ itdragon-redis 127.0.0.1 6380
21401:X 29 Nov 16:41:02.028 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ itdragon-redis 127.0.0.1 6380
......
# 6379端口
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
# 6380端口
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=72590,lag=0
......
# 6381端口
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
......</code></pre>
<p>+slave :一个新的从服务器已经被 Sentinel 识别并关联。<br>+sdown :给定的实例现在处于主观下线状态。<br>-sdown :给定的实例已经不再处于主观下线状态。<br>更多内容可以访问官网:<a href="https://link.segmentfault.com/?enc=sOFc%2FmClafKPm0z6vi1yuA%3D%3D.mf%2BrskyHwefWZjoJxWL%2FuKmGKfM%2FhD5VU5yv8JjtQK%2B5vcWPDeXLBMX0%2BWlaDP%2BV" rel="nofollow">https://redis.io/topics/sentinel</a><br><img src="/img/remote/1460000012234261?w=697&h=223" alt="" title=""><br><img src="/img/remote/1460000012234262?w=698&h=227" alt="" title=""></p>
<h3>主从复制的原理</h3>
<p><strong>全量复制</strong><br>实现原理:建立主从关系时,从机会给主机发送sync命令,主机接收命令,后台启动的存盘进程,同时收集所有用于<strong>修改命令</strong>,传送给从机。<br><strong>增量复制</strong><br>实现原理:主机会继续将新收集到的<strong>修改命令</strong>依次传给从机,实现数据的同步效果。</p>
<h3>主从复制的缺点</h3>
<p>Redis的主从复制最大的缺点就是延迟,主机负责写,从机负责备份,这个过程有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,从机器数量的增加也会使这个问题更加严重。</p>
<h3>总结</h3>
<p>1 查看主从复制关系命令:info replication<br>2 设置主从关系命令:slaveof 主机ip 主机port<br>3 开启哨兵模式命令:./redis-sentinel sentinel.conf<br>4 主从复制原则:开始是全量赋值,之后是增量赋值<br>5 哨兵模式三大任务:监控,提醒,自动故障迁移</p>
<p>Redis 的主从复制到这里就结束了,有什么不对的地方欢迎指正。下一章Redis 的集群搭建和整合Spring</p>
Redis 持久化之RDB和AOF
https://segmentfault.com/a/1190000012185846
2017-11-27T22:23:03+08:00
2017-11-27T22:23:03+08:00
itdragon
https://segmentfault.com/u/itdragon
3
<h2>Redis 持久化之RDB和AOF</h2>
<p>Redis 有两种持久化方案,RDB (Redis DataBase)和 AOF (Append Only File)。如果你想快速了解和使用RDB和AOF,可以直接跳到文章底部看总结。本章节通过配置文件,触发快照的方式,恢复数据的操作,命令操作演示,优缺点来学习 Redis 的重点知识<strong>持久化</strong>。</p>
<h3>RDB 详解</h3>
<p>RDB 是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中。即在指定目录下生成一个dump.rdb文件。Redis 重启会通过加载dump.rdb文件恢复数据。</p>
<h4>从配置文件了解RDB</h4>
<p>打开 redis.conf 文件,找到 SNAPSHOTTING 对应内容<br>1 RDB核心规则配置(重点)</p>
<pre><code>save <seconds> <changes>
# save ""
save 900 1
save 300 10
save 60 10000</code></pre>
<p>解说:save <指定时间间隔> <执行指定次数更新操作>,满足条件就将内存中的数据同步到硬盘中。官方出厂配置默认是 900秒内有1个更改,300秒内有10个更改以及60秒内有10000个更改,则将内存中的数据快照写入磁盘。<br>若不想用RDB方案,可以把 save "" 的注释打开,下面三个注释。</p>
<p>2 指定本地数据库文件名,一般采用默认的 dump.rdb</p>
<pre><code>dbfilename dump.rdb</code></pre>
<p>3 指定本地数据库存放目录,一般也用默认配置</p>
<pre><code>dir ./</code></pre>
<p>4 默认开启数据压缩</p>
<pre><code>rdbcompression yes</code></pre>
<p>解说:配置存储至本地数据库时是否压缩数据,默认为yes。Redis采用LZF压缩方式,但占用了一点CPU的时间。若关闭该选项,但会导致数据库文件变的巨大。建议开启。</p>
<h4>触发RDB快照</h4>
<p>1 在指定的时间间隔内,执行指定次数的写操作<br>2 执行save(阻塞, 只管保存快照,其他的等待) 或者是bgsave (异步)命令<br>3 执行flushall 命令,清空数据库所有数据,意义不大。<br>4 执行shutdown 命令,保证服务器正常关闭且不丢失任何数据,意义...也不大。</p>
<h4>通过RDB文件恢复数据</h4>
<p>将dump.rdb 文件拷贝到redis的安装目录的bin目录下,重启redis服务即可。在实际开发中,一般会考虑到物理机硬盘损坏情况,选择备份dump.rdb 。可以从下面的操作演示中可以体会到。</p>
<h4>RDB 的优缺点</h4>
<p>优点:<br> 1 适合大规模的数据恢复。<br> 2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。</p>
<p>缺点:<br> 1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。<br> 2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。<br>所以Redis 的持久化和数据的恢复要选择在夜深人静的时候执行是比较合理的。</p>
<h4>操作演示</h4>
<pre><code class="xml">[root@itdragon bin]# vim redis.conf
save 900 1
save 120 5
save 60 10000
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key2 value2
OK
127.0.0.1:6379> set key3 value3
OK
127.0.0.1:6379> set key4 value4
OK
127.0.0.1:6379> set key5 value5
OK
127.0.0.1:6379> set key6 value6
OK
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
[root@itdragon bin]# cp dump.rdb dump_bk.rdb
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
[root@itdragon bin]# cp dump_bk.rdb dump.rdb
cp: overwrite `dump.rdb'? y
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> keys *
1) "key5"
2) "key1"
3) "key3"
4) "key4"
5) "key6"
6) "key2"</code></pre>
<p>第一步:vim 修改持久化配置时间,120秒内修改5次则持久化一次。<br>第二步:重启服务使配置生效。<br>第三步:分别set 5个key,过两分钟后,在bin的当前目录下会自动生产一个dump.rdb文件。(set key6 是为了验证shutdown有触发RDB快照的作用)<br>第四步:将当前的dump.rdb 备份一份(模拟线上工作)。<br>第五步:执行FLUSHALL命令清空数据库数据(模拟数据丢失)。<br>第六步:重启Redis服务,恢复数据.....咦????( ′◔ ‸◔`)。数据是空的????这是因为FLUSHALL也有触发RDB快照的功能。<br>第七步:将备份的 dump_bk.rdb 替换 dump.rdb 然后重新Redis。</p>
<p>注意点:SHUTDOWN 和 FLUSHALL 命令都会触发RDB快照,这是一个坑,请大家注意。</p>
<p>其他命令:</p>
<ul>
<li>keys * 匹配数据库中所有 key</li>
<li>save 阻塞触发RDB快照,使其备份数据</li>
<li>FLUSHALL 清空整个 Redis 服务器的数据(几乎不用)</li>
<li>SHUTDOWN 关机走人(很少用)</li>
</ul>
<hr>
<h3>AOF 详解</h3>
<p>AOF :Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个<strong>写操作</strong>,并<strong>追加</strong>到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。</p>
<h4>从配置文件了解AOF</h4>
<p>打开 redis.conf 文件,找到 APPEND ONLY MODE 对应内容<br>1 redis 默认关闭,开启需要手动把no改为yes</p>
<pre><code>appendonly yes</code></pre>
<p>2 指定本地数据库文件名,默认值为 appendonly.aof</p>
<pre><code>appendfilename "appendonly.aof"</code></pre>
<p>3 指定更新日志条件</p>
<pre><code># appendfsync always
appendfsync everysec
# appendfsync no</code></pre>
<p>解说: <br>always:同步持久化,每次发生数据变化会立刻写入到磁盘中。性能较差当数据完整性比较好(慢,安全) <br>everysec:出厂默认推荐,每秒异步记录一次(默认值)<br>no:不同步</p>
<p>4 配置重写触发机制</p>
<pre><code>auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb</code></pre>
<p>解说:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。一般都设置为3G,64M太小了。</p>
<h4>触发AOF快照</h4>
<p>根据配置文件触发,可以是每次执行触发,可以是每秒触发,可以不同步。</p>
<h4>根据AOF文件恢复数据</h4>
<p>正常情况下,将appendonly.aof 文件拷贝到redis的安装目录的bin目录下,重启redis服务即可。但在实际开发中,可能因为某些原因导致appendonly.aof 文件格式异常,从而导致数据还原失败,可以通过命令redis-check-aof --fix appendonly.aof 进行修复 。从下面的操作演示中体会。</p>
<h4>AOF的重写机制</h4>
<p>前面也说到了,AOF的工作原理是将写操作追加到文件中,文件的冗余内容会越来越多。所以聪明的 Redis 新增了重写机制。当AOF文件的大小超过所设定的阈值时,Redis就会对AOF文件的内容压缩。</p>
<p>重写的原理:Redis 会fork出一条新进程,读取内存中的数据,并重新写到一个临时文件中。并没有读取旧文件(你都那么大了,我还去读你??? o(゚Д゚)っ傻啊!)。最后替换旧的aof文件。</p>
<p>触发机制:当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。这里的“一倍”和“64M” 可以通过配置文件修改。</p>
<h4>AOF 的优缺点</h4>
<p>优点:数据的完整性和一致性更高<br>缺点:因为AOF记录的内容多,文件会越来越大,数据恢复也会越来越慢。</p>
<h4>操作演示</h4>
<pre><code>[root@itdragon bin]# vim appendonly.aof
appendonly yes
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set keyAOf valueAof
OK
127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> keys *
1) "keyAOf"
127.0.0.1:6379> SHUTDOWN
not connected> QUIT
[root@itdragon bin]# vim appendonly.aof
fjewofjwojfoewifjowejfwf
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
Could not connect to Redis at 127.0.0.1:6379: Connection refused
not connected> QUIT
[root@itdragon bin]# redis-check-aof --fix appendonly.aof
'x 3e: Expected prefix '*', got: '
AOF analyzed: size=92, ok_up_to=62, diff=30
This will shrink the AOF from 92 bytes, with 30 bytes, to 62 bytes
Continue? [y/N]: y
Successfully truncated AOF
[root@itdragon bin]# ./redis-server redis.conf
[root@itdragon bin]# ./redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> keys *
1) "keyAOf"</code></pre>
<p>第一步:修改配置文件,开启AOF持久化配置。<br>第二步:重启Redis服务,并进入Redis 自带的客户端中。<br>第三步:保存值,然后模拟数据丢失,关闭Redis服务。<br>第四步:重启服务,发现数据恢复了。(额外提一点:有教程显示FLUSHALL 命令会被写入AOF文件中,导致数据恢复失败。我安装的是redis-4.0.2没有遇到这个问题)。<br>第五步:修改appendonly.aof,模拟文件异常情况。<br>第六步:重启 Redis 服务失败。这同时也说明了,RDB和AOF可以同时存在,且优先加载AOF文件。<br>第七步:校验appendonly.aof 文件。重启Redis 服务后正常。</p>
<p>补充点:aof 的校验是通过 redis-check-aof 文件,那么rdb 的校验是不是可以通过 redis-check-rdb 文件呢???</p>
<h3>总结</h3>
<ol>
<li>Redis 默认开启RDB持久化方式,在指定的时间间隔内,执行指定次数的写操作,则将内存中的数据写入到磁盘中。</li>
<li>RDB 持久化适合大规模的数据恢复但它的数据一致性和完整性较差。</li>
<li>Redis 需要手动开启AOF持久化方式,默认是每秒将写操作日志追加到AOF文件中。</li>
<li>AOF 的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。</li>
<li>Redis 针对 AOF文件大的问题,提供重写的瘦身机制。</li>
<li>若只打算用Redis 做缓存,可以关闭持久化。</li>
<li>若打算使用Redis 的持久化。建议RDB和AOF都开启。其实RDB更适合做数据的备份,留一后手。AOF出问题了,还有RDB。</li>
</ol>
<p>到这里Redis 的持久化就介绍完了,有什么不对的地方可以指出。<br>Redis 快速入门:<a href="https://segmentfault.com/a/1190000012161524">https://segmentfault.com/a/11...</a></p>
Redis 快速入门
https://segmentfault.com/a/1190000012161524
2017-11-25T23:44:20+08:00
2017-11-25T23:44:20+08:00
itdragon
https://segmentfault.com/u/itdragon
2
<h2>Redis 快速入门</h2>
<p>谈到Redis,大家应该都不陌生。它是用c语言开发的一个高性能键值数据库,主要用于缓存领域。本章通过Redis的安装,Redis的五大数据类型,Redis的Java客户端,Redis与Spring 的整合 。来让读者对它有一个初步的了解。下一章再通过介绍配置文件来搭建Redis的主从模式和集群模式(配置大于编程,先从简单的编程入手)。</p>
<p><strong>效果图</strong>:<br><img src="/img/remote/1460000012161529?w=1366&h=736" alt="Redis 缓存项目效果图" title="Redis 缓存项目效果图"></p>
<p><strong>需求</strong>:对商品类目进行Redis缓存处理<br><strong>技术</strong>:Redis,Spring,SpringMVC,Mybatis,EasyUI<br><strong>说明</strong>:EasyUI的树菜单上一章节有介绍,这里是为了方便展示效果。项目结构图中箭头所指的文件是需要重点学习的。若对EasyUI 树菜单感兴趣的可以访问:(该章节源码中提供商品类名的sql文件) <br><a href="https://link.segmentfault.com/?enc=%2BjmosEhMva68RG9HhsoKgw%3D%3D.BVzNs4d4Psvv408g5LgyQjLeW12wrbun%2FIt25LZW8jTb499X7i6Oob77qSK6eDc0pmj0h0EjWffREyhCkSA2pQ%3D%3D" rel="nofollow">http://blog.csdn.net/qq_19558...</a><br><strong>源码</strong>:见文章底部<br><strong>项目结构</strong>:<br><img src="/img/remote/1460000012161530?w=364&h=446" alt="项目结构图" title="项目结构图"></p>
<h3>Redis 安装</h3>
<p>安装文档: <br><a href="https://link.segmentfault.com/?enc=AB6eiyRxGV7dP8BuS9M6YQ%3D%3D.Kkc2AFcJVFLr%2BYb4M80%2FMrJe2uO5LgthZMOKNSONC98pbJU0k4v4%2FBWZxPrG0N%2F1g%2BTDzMfonuB8yb1dtF9140JqSSEyTqOTOodNW6nO%2BXRsbjYYbrWyJN0DbDbJfCcq" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<h3>Redis 五大数据类型</h3>
<p>Redis 五大数据类型有String 类型,Hash 类型,List 类型,Set 类型,Zset(Sortedset)类型。其中常用的是前三个。<br>官方提供的操作手册:<a href="https://link.segmentfault.com/?enc=UhfIRCQ8Mz6ZtWmsr31rug%3D%3D.O%2BAkWHjvXtWActm3Fdvjh7%2F9weIf4SxQrSMqpIGiFHE%3D" rel="nofollow">http://redisdoc.com/</a><br>在redis 自带的客户端中输入命令时,可以使用tab自动补齐,新手建议不要偷懒。</p>
<h4>String 类型</h4>
<p>String 是 redis 最基本的类型,一个key对应一个value。<br>赋值:set key value<br>取值:get key<br>批量赋值:mset key value ... keyN valueN<br>批量取值:mget key ... keyN<br>取值并赋值:getset key value<br>删除key:del key ... keyN<br>数值加一:incr key<br>数值加N:incrby key n<br>数值减一:decr key<br>数值减N:decrby key n<br>字符串追加:append key value<br>字符串长度:strlen key<br>*注 形如"key ... keyN" 表示可以批量操作</p>
<pre><code>127.0.0.1:6379> set key value
OK
127.0.0.1:6379> get key
"value"
127.0.0.1:6379> mset key1 1 key2 2 key3 3
OK
127.0.0.1:6379> mget key1 key3
1) "1"
2) "3"
127.0.0.1:6379> del key
(integer) 1
127.0.0.1:6379> incr count
(integer) 1
127.0.0.1:6379> incrby count 10
(integer) 11
127.0.0.1:6379> decr count
(integer) 10
127.0.0.1:6379> decrby count 5
(integer) 5
127.0.0.1:6379> set str itdragon
OK
127.0.0.1:6379> append str " blog!"
(integer) 14
127.0.0.1:6379> get str
"itdragon blog!"
127.0.0.1:6379> strlen str
(integer) 14</code></pre>
<h4>Hash 散列类型</h4>
<p>Redis hash 是一个键值对集合,和Java 的HashMap 类似。<br>Redis hash 是一个String 类型的 field 和 value 的映射表,hash特别适合用于存储对象(key 可以是对象+id,field 是对象属性,value则是属性值)。<br>给一个字段赋值:hset key field value<br>给多个字段赋值:hmset key field value ... fieldN valueN<br>取一个字段的值:hget key field<br>取多个字段的值:gmset key field ... fieldN<br>取所有的字段名和值:hgetall key<br>删除字段名和值:hdel key field ... fieldN<br>判断字段是否存在:hexists key field <br>获取key的所有field:hkeys key<br>获取key的所有value:hvals key<br>获取field个数:hlen key<br>*注:这里的field 就是 字段名,value 就是字段值</p>
<pre><code>127.0.0.1:6379> hset user name itdragon
(integer) 1
127.0.0.1:6379> hget user name
"itdragon"
127.0.0.1:6379> hmset user position java study redis
OK
127.0.0.1:6379> hmget user position study
1) "java"
2) "redis"
127.0.0.1:6379> hgetall user
1) "name"
2) "itdragon"
3) "position"
4) "java"
5) "study"
6) "redis"
127.0.0.1:6379> hdel user name
(integer) 1
127.0.0.1:6379> hdel user position study
(integer) 2
127.0.0.1:6379> hexists user name
(integer) 1
127.0.0.1:6379> hexists user age
(integer) 0
127.0.0.1:6379> hkeys user
1) "name"
2) "position"
3) "study"
127.0.0.1:6379> hvals user
1) "itdragon"
2) "java"
3) "redis"
127.0.0.1:6379> hlen user
(integer) 3</code></pre>
<h4>List 类型</h4>
<p>Redis 列表是采用来链表来存储的简单字符串列表,按照插入顺序排序。添加元素一般从链表两端开始。<br>向列表左侧加元素:lpush key value ... valueN<br>向列表右侧加元素:rpush key value ... valueN<br>遍历列表:lrange key startIndex endIndex<br>获取List长度:llen key<br>通过下标获取值:lindex key index<br>通过下标设置值:lset key index value<br>列表左侧移除第一个元素:lpop key<br>列表右侧移除第一个元素:rpop key<br>截取保留剩下的列表:ltrim key startIndex endIndex<br>在制定元素插入值:linsert key after/before index value<br>把集合第一个元素移到其他集合中:rpoplpush key otherListKey</p>
<ul><li>注:若endIndex=-1 表示最后一位;otherListKey 表示其他集合</li></ul>
<pre><code>127.0.0.1:6379> lpush list 1 2
(integer) 2
127.0.0.1:6379> rpush list 3 4
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "2"
2) "1"
3) "3"
4) "4"
127.0.0.1:6379> lpop list
"2"
127.0.0.1:6379> rpop list
"4"
127.0.0.1:6379> llen list
(integer) 2
127.0.0.1:6379> lindex list 1
"3"
127.0.0.1:6379> linsert list after 1 2
(integer) 3
127.0.0.1:6379> linsert list before 3 4
(integer) 4
127.0.0.1:6379> ltrim list 0 1
OK
127.0.0.1:6379> rpoplpush list newlist
"1"</code></pre>
<h4>Set 类型</h4>
<p>Redis 的 Set 是String类型的无序集合。它是通过HashTable实现实现的,用法和 List 类型很相似。<br>新增集合元素:sadd key value ... valueN<br>删除集合元素:srem key value ... valueN<br>获取集合所有元素:smembers key <br>判断集合元素是否存在:sismember key value<br>集合差集:sdiff key1 key2<br>集合交集:sinter key1 key2<br>集合并集:sunion key1 key2<br>获取集合长度:scard key1</p>
<pre><code>127.0.0.1:6379> sadd set a b c d
(integer) 4
127.0.0.1:6379> srem set a b c
(integer) 3
127.0.0.1:6379> smembers set
1) "d"
127.0.0.1:6379> sismember set a
(integer) 0
127.0.0.1:6379> sismember set d
(integer) 1
127.0.0.1:6379> sadd setA 1 2 3
(integer) 3
127.0.0.1:6379> sadd setB 2 3 4
(integer) 3
127.0.0.1:6379> sdiff setA setB
1) "1"
127.0.0.1:6379> sdiff setB setA
1) "4"
127.0.0.1:6379> sinter setA setB
1) "2"
2) "3"
127.0.0.1:6379> sunion setA setB
1) "1"
2) "2"
3) "3"
4) "4"
127.0.0.1:6379> scard setA
(integer) 3</code></pre>
<h4>Zset 类型</h4>
<p>Redis 的 zset(sorted set)和 set 一样也是string类型元素的集合,且不允许有重复的成员。不同的是 zset 的每个元素都会关联一个double类型的分数。zset正是通过分数来为集合中的成员进行排序。zset的成员是唯一的,但分数(score)却可以重复。<br>新增集合元素:zadd key score value ... scoreN valueN<br>获取元素分数:zscore key value<br>按照分数从小到大排序:zrange key startIndex endIndex<br>按照分数从大到小排序:zrevrange key startIndex endIndex<br>遍历时显示分数:withscores<br>统计分数比value少的个数:zrank key value<br>统计分数比value高的个数:zrevrank key value<br>输出分数在制定值内的元素:zrangebyscore key score1 score2<br>给元素加分:zincrby key score value <br>获取元素个数:zcard()<br>统计分数内的个数:zcount key score1 score2<br>删除制定排名内的元素:zremrangebyrank key no1 no2<br>删除指定分数内的元素:zremrangebyscore key score1 score2<br>删除指定元素:zrem key value</p>
<ul><li>注: zcount 统计分数内的个数,score1 <= keyScore =< score2;zremrangebyrank 的 no1 和 no2 表示排名的第几位。</li></ul>
<pre><code>127.0.0.1:6379> zadd zset 65 A 67 C 66 B
(integer) 3
127.0.0.1:6379> zscore zset C
"67"
127.0.0.1:6379> zrange zset 0 -1
1) "A"
2) "B"
3) "C"
127.0.0.1:6379> zrevrange zset 0 -1
1) "C"
2) "B"
3) "A"
127.0.0.1:6379> zrevrange zset 0 -1 withscores
1) "C"
2) "67"
3) "B"
4) "66"
5) "A"
6) "65"
127.0.0.1:6379> zrank zset C
(integer) 2
127.0.0.1:6379> zrevrank zset C
(integer) 0
127.0.0.1:6379> zrangebyscore zset 65 66
1) "A"
2) "B"
127.0.0.1:6379> zrangebyscore zset 65 66 limit 1 2
1) "B"
127.0.0.1:6379> zincrby zset 10 A
"75"
127.0.0.1:6379> zcard zset
(integer) 3
127.0.0.1:6379> zcount zset 65 66
(integer) 1
127.0.0.1:6379> zremrangebyrank zset 0 1
(integer) 2
127.0.0.1:6379> zremrangebyscore zset 100 200
(integer) 0
127.0.0.1:6379> zrem zset A
(integer) 1</code></pre>
<h3>Jedis客户端</h3>
<p>Jedis 是比较主流的 Redis Java 客户端。<br>第一步:导入Jedis需要的jar</p>
<pre><code class="xml"><!-- Redis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<jedis.version>2.7.2</jedis.version>
</dependency></code></pre>
<p>第二步:单元测试类<br>Jedis 的语法和 Redis 几乎一样,如果学好了Redis,Jedis也就没问题了,可谓是买一送一。建议使用连接池的方式。</p>
<pre><code class="java">package com.itdragon.redis;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class TestJedisOperate {
private final static String HOST = "112.74.83.71";
private final static int PORT = 6379;
/**
* jedis 的语法和 redis 的语法几乎一致,比较常用的有Hash,String,List
*/
@Test
public void jedisSignle() {
Jedis jedis = new Jedis(HOST, PORT);
jedis.set("account", "itdragon");
System.out.println("set , get 操作 : " + jedis.get("account"));
jedis.mset("account:01", "itdragon01", "account:02", "itdragon02");
System.out.println("mset , mget 操作 : " + jedis.mget("account:01", "account:02"));
jedis.hset("user", "name", "ITDragon");
System.out.println("hset , hget 操作 : " + jedis.hget("user", "name"));
Map<String, String> userMap = new HashMap<>();
userMap.put("password", "123456");
userMap.put("position", "Java");
jedis.hmset("user", userMap);
System.out.println("hmset , hmget 操作 : " + jedis.hmget("user", "name", "password", "position"));
if (0 == jedis.llen("userList")) {
jedis.lpush("userList", "1", "2", "3");
}
System.out.println("List 类型 lpush , lrange 操作 : " + jedis.lrange("userList", 0, -1));
jedis.sadd("userSet", "1", "2", "2");
System.out.println("Set 类型 sadd , smembers 操作 : " + jedis.smembers("userSet"));
Map<String, Double> scoreMembers = new HashMap<>();
scoreMembers.put("A", 65.0);
scoreMembers.put("C", 67.0);
scoreMembers.put("B", 66.0);
jedis.zadd("userScore", scoreMembers);
System.out.println("Set 类型 zadd , zrange 操作 : " + jedis.zrange("userScore", 0, -1));
jedis.close();
}
@Test
public void testJedisPool() {
JedisPool pool = new JedisPool(HOST, PORT);
Jedis jedis = pool.getResource();
System.out.println("通过连接池获取 key 为 account 的值 : " + jedis.get("account"));
jedis.close();
pool.close();
}
}</code></pre>
<h3>Spring 整合 Redis</h3>
<p>创建用于整合redis的文件 applicationContext-jedis.xml<br>建议使用redis 默认配置(默认,让生活更美好)</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:resource/*.properties" />
<!-- 连接池配置 (可以用 redis 默认配置,效果可能会更好)-->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<!-- 最大连接数 -->
<property name="maxTotal" value="30" />
<!-- 最大空闲连接数 -->
<property name="maxIdle" value="10" />
<!-- 每次释放连接的最大数目 -->
<property name="numTestsPerEvictionRun" value="1024" />
<!-- 释放连接的扫描间隔(毫秒) -->
<property name="timeBetweenEvictionRunsMillis" value="30000" />
<!-- 连接最小空闲时间 -->
<property name="minEvictableIdleTimeMillis" value="1800000" />
<!-- 连接空闲多久后释放, 当空闲时间>该值 且 空闲连接>最大空闲连接数 时直接释放 -->
<property name="softMinEvictableIdleTimeMillis" value="10000" />
<!-- 获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 -->
<property name="maxWaitMillis" value="1500" />
<!-- 在获取连接的时候检查有效性, 默认false -->
<property name="testOnBorrow" value="true" />
<!-- 在空闲时检查有效性, 默认false -->
<property name="testWhileIdle" value="true" />
<!-- 连接耗尽时是否阻塞, false报异常,ture阻塞直到超时, 默认true -->
<property name="blockWhenExhausted" value="false" />
</bean>
<!-- jedis客户端单机版 -->
<bean id="redisClient" class="redis.clients.jedis.JedisPool">
<constructor-arg name="host" value="${redis.host}" />
<constructor-arg name="port" value="${redis.ip}" />
<!-- <constructor-arg name="poolConfig" ref="jedisPoolConfig" /> -->
</bean>
<bean id="jedisClient" class="com.itdragon.common.utils.JedisClientSingle"/>
</beans></code></pre>
<p>简单封装了Jedis 常用方法 JedisClientSingle.java</p>
<pre><code class="java">package com.itdragon.common.utils;
import org.springframework.beans.factory.annotation.Autowired;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
// 单例的Redis 工具类
public class JedisClientSingle {
/**
* connect timed out 问题:
* 1. 检查redis服务是否开启
* 2. 检查是否是因为防火墙的问题
* 3. 检查网络问题(如果在同一个局域网内几乎不会出现这个问题)
* Jedis jedis =new Jedis(HOST,PORT,100000);
* JedisPool pool = new JedisPool(poolConfig, HOST, PORT, 100000);
*/
@Autowired
private JedisPool jedisPool;
public String get(String key) {
Jedis jedis = jedisPool.getResource();
String string = jedis.get(key);
jedis.close();
return string;
}
public String set(String key, String value) {
Jedis jedis = jedisPool.getResource();
String string = jedis.set(key, value);
jedis.close();
return string;
}
public String hget(String hkey, String key) {
Jedis jedis = jedisPool.getResource();
String string = jedis.hget(hkey, key);
jedis.close();
return string;
}
public long hset(String hkey, String key, String value) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hset(hkey, key, value);
jedis.close();
return result;
}
public long del(String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.del(key);
jedis.close();
return result;
}
public long hdel(String hkey, String key) {
Jedis jedis = jedisPool.getResource();
Long result = jedis.hdel(hkey, key);
jedis.close();
return result;
}
}</code></pre>
<p>获取商品类名接口实现类 ProductCategoryServiceImpl.java</p>
<pre><code class="java">package com.itdragon.service.impl;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.CollectionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.itdragon.common.pojo.EUTreeNode;
import com.itdragon.common.pojo.ResponseResult;
import com.itdragon.common.utils.JedisClientSingle;
import com.itdragon.common.utils.JsonUtils;
import com.itdragon.mapper.ProductCategoryMapper;
import com.itdragon.pojo.ProductCategory;
import com.itdragon.pojo.ProductCategoryExample;
import com.itdragon.pojo.ProductCategoryExample.Criteria;
import com.itdragon.service.ProductCategoryService;
@Service
public class ProductCategoryServiceImpl implements ProductCategoryService {
@Autowired
private ProductCategoryMapper categoryMapper;
@Autowired
private JedisClientSingle jedisClientSingle;
@Value("${CATEGROY_ID_CACHE_REDIS_KEY}")
private String CATEGROY_ID_CACHE_REDIS_KEY;
@Override
public List<EUTreeNode> getCategoryList(Long parentId) {
long startTime = System.currentTimeMillis();
List<EUTreeNode> resultList = new ArrayList<>();
// 从redis缓存中取内容
try {
String cacheDatas = jedisClientSingle.hget(CATEGROY_ID_CACHE_REDIS_KEY, parentId.toString());
if (StringUtils.isNotBlank(cacheDatas)) {
List<ProductCategory> categories = JsonUtils.jsonToList(cacheDatas, ProductCategory.class);
for (ProductCategory category : categories) {
EUTreeNode node = new EUTreeNode();
node.setId(category.getId());
node.setText(category.getName());
node.setState(category.getIsParent()?"closed":"open");
resultList.add(node);
}
System.out.println("redis cache Time : " + (System.currentTimeMillis() - startTime));
return resultList;
}
} catch (Exception e) {
e.printStackTrace();
}
ProductCategoryExample example = new ProductCategoryExample();
Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo(1);
criteria.andParentIdEqualTo(parentId); // 查询父节点下的所有子节点
List<ProductCategory> productCategories = categoryMapper.selectByExample(example);
for (ProductCategory category : productCategories) {
EUTreeNode node = new EUTreeNode();
node.setId(category.getId());
node.setText(category.getName());
node.setState(category.getIsParent()?"closed":"open");
resultList.add(node);
}
System.out.println("No redis cache Time : " + (System.currentTimeMillis() - startTime));
// 向redis缓存中添加内容
try {
jedisClientSingle.hset(CATEGROY_ID_CACHE_REDIS_KEY, parentId.toString(), JsonUtils.objectToJson(productCategories));
} catch (Exception e) {
e.printStackTrace();
}
return resultList;
}
// 后面的内容看源码...
}</code></pre>
<p>源码:<a href="https://link.segmentfault.com/?enc=KkOqaYpF%2BcZ72rJFEASgXQ%3D%3D.H8DAFGGlPZokk%2F%2FpxwJOFHEIZLd7ynUo3NriOMYh76h9%2BBZrIDENoXQAqLDflOTbnwkL0J0lZOphxXX0nx%2BBPw%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>到这里,Redis 的快速入门就结束了。下一章节介绍Redis 的主从和集群。</p>
EasyUI 树菜单
https://segmentfault.com/a/1190000012086183
2017-11-20T21:14:03+08:00
2017-11-20T21:14:03+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>EasyUI 树菜单</h2>
<p>通过ssm框架项目实现EasyUI 的树菜单的单选,复选,异步加载树,同步加载树和树权限控制等功能。</p>
<h3>本章知识点</h3>
<p><strong>效果图</strong>:<br><img src="/img/remote/1460000012086188?w=730&h=555" alt="项目演示" title="项目演示"></p>
<p><strong>需求</strong>:通过SSM框架,实现EasyUI 树菜单的单选,多选,异步加载,同步加载的功能<br><strong>技术</strong>:Spring,SpringMVC,Mybatis,EasyUI<br><strong>明说</strong>:使用EasyUI-Tree,必须严格遵守它的规则,如异步加载树节点的 id,异步加载树返回值的格式等。如果按照其规则来做,你会发现 EasyUI 很简单。反之到处都是吭!<br><strong>源码</strong>:见文章底部<br><strong>场景</strong>:树菜单,在电商中很场景。笔者是在电商公司上班,类目树菜单随处可见。比如给广告员设置类目级别,刊登商品选择类目加载对应的产品规格参数等等<br><strong>项目结构</strong>:<br><img src="/img/remote/1460000012086189" alt="项目结构" title="项目结构"></p>
<h3>初始化静态树</h3>
<p>大部分的功能,并非一步完成。都是从最基础的功能开始。这里是EasyUI-Tree 基础结构</p>
<pre><code><ul class="easyui-tree">
<li>
<span>根目录</span>
<ul>
<li data-options="state:'closed'">
<span>关闭状态的子目录</span>
<ul>
<li>ITDragon</li>
<li>博客</li>
</ul>
</li>
<li>
<span>默认展开的子目录</span>
<ul>
<li>欢迎</li>
<li>You!</li>
</ul>
</li>
<li>你是最棒的!</li>
</ul>
</li>
</ul></code></pre>
<h3>Maven Web项目实战</h3>
<p>项目框架结构是:Spring,SpringMVC,Mybatis。 没有其他的额外配置,都是基础的整合配置。这里就不贴代码。读者也可以直接从github上clone下来(sql文件也在项目中)。</p>
<h4>POJO层</h4>
<p>本章的主角,类目实体类 Category.java</p>
<pre><code>package com.itdragon.pojo;
import java.util.Date;
public class Category {
private Integer id;
private String name;
private Integer isLeaf;
private Integer parentId;
private Date createddate;
private Date updateddate;
private Integer status;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public Integer getIsLeaf() {
return isLeaf;
}
public void setIsLeaf(Integer isLeaf) {
this.isLeaf = isLeaf;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public Date getCreateddate() {
return createddate;
}
public void setCreateddate(Date createddate) {
this.createddate = createddate;
}
public Date getUpdateddate() {
return updateddate;
}
public void setUpdateddate(Date updateddate) {
this.updateddate = updateddate;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}</code></pre>
<p>按照EasyUI规范封装的Tree节点实体类 EUTreeNode.java</p>
<pre><code>package com.itdragon.common.pojo;
/**
* 树的数据格式(Tree Data Format)
* 每个节点可以包括下列属性:
* id:节点的 id,它对于加载远程数据很重要。
* text:要显示的节点文本。
* state:节点状态,'open' 或 'closed',默认是 'open'。当设置为 'closed' 时,该节点有子节点,并且将从远程站点加载它们。
* checked:指示节点是否被选中。
* attributes:给一个节点添加的自定义属性。
* children:定义了一些子节点的节点数组
*
* 这里先封装常用的 id,text,state
*/
public class EUTreeNode {
private long id;
private long parentId;
private String text;
private String state;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public long getParentId() {
return parentId;
}
public void setParentId(long parentId) {
this.parentId = parentId;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}</code></pre>
<p>说明:<br>① Category.java 属性 createdDate 和 updatedDate 的类型都是java.util.Date。实际上也可以是 String 类型,这样可以在显示(日期格式化),排序,筛选时减少很多工作量。<br>② 这里的 Category.java,CategoryExample.java,CategoryMapper.java,CategoryMapper.xml 是通过 Mybatis 提供的逆向工程自动生成的。文章底部会提供链接。</p>
<h4>Service 层</h4>
<p>提供查询类目的接口 CategoryService.java 感觉怪怪的 -.-||</p>
<pre><code>package com.itdragon.service;
import java.util.List;
import com.itdragon.common.pojo.EUTreeNode;
public interface CategoryService {
/**
* 通过父节点,异步加载树菜单
* @param parentId
*/
List<EUTreeNode> getCategoryList(int parentId);
/**
* 一次全部加载所有树节点
*/
List<EUTreeNode> getCategoryList();
}
</code></pre>
<p>类目接口的实现类 CategoryServiceImpl.java</p>
<pre><code>package com.itdragon.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.itdragon.common.pojo.EUTreeNode;
import com.itdragon.mapper.CategoryMapper;
import com.itdragon.pojo.Category;
import com.itdragon.pojo.CategoryExample;
import com.itdragon.pojo.CategoryExample.Criteria;
import com.itdragon.service.CategoryService;
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public List<EUTreeNode> getCategoryList(int parentId) {
// 1. 创建查询条件
CategoryExample example = new CategoryExample();
Criteria criteria = example.createCriteria();
criteria.andParentIdEqualTo(parentId); // 查询父节点下的所有子节点
criteria.andStatusEqualTo(0); // 查询未删除状态的菜单
// TODO 权限拦截
// 2. 根据条件查询
List<Category> list = categoryMapper.selectByExample(example);
List<EUTreeNode> resultList = new ArrayList<>();
// 3. 把列表转换成 EasyUI Tree 需要的json格式
for (Category category : list) {
EUTreeNode node = new EUTreeNode();
node.setId(category.getId());
node.setText(category.getName());
node.setState(category.getIsLeaf() == 1?"open":"closed");
resultList.add(node);
}
// 4. 返回结果
return resultList;
}
@Override
public List<EUTreeNode> getCategoryList() {
// 1. 创建查询条件
CategoryExample example = new CategoryExample();
Criteria criteria = example.createCriteria();
criteria.andStatusEqualTo(0); // 查询未删除状态的菜单
// TODO 权限拦截
// 2. 根据条件查询
List<Category> list = categoryMapper.selectByExample(example);
List<EUTreeNode> resultList = new ArrayList<>();
// 3. 把列表转换成 EasyUI Tree 需要的json格式
for (Category category : list) {
EUTreeNode node = new EUTreeNode();
node.setId(category.getId());
node.setText(category.getName());
node.setState(category.getIsLeaf() == 1?"open":"closed");
node.setParentId(category.getParentId());
resultList.add(node);
}
// 4. 返回结果
return resultList;
}
}</code></pre>
<p>说明:树菜单的权限拦截,并没有提供代码,是考虑到涉及其他实体类。其实有了思路,其他的都好说..................好吧!我承认自己懒=。=</p>
<h4>Controller 层</h4>
<p>用于页面跳转的 PageController.java</p>
<pre><code>package com.itdragon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PageController {
@RequestMapping("/")
public String showIndex() {
return "tree";
}
@RequestMapping("/{page}")
public String showpage(@PathVariable String page) {
return page;
}
}</code></pre>
<p>负责加载类目树菜单的 CategoryController.java</p>
<pre><code>package com.itdragon.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.itdragon.common.pojo.EUTreeNode;
import com.itdragon.service.CategoryService;
@Controller
@RequestMapping("/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* http://www.jeasyui.net/plugins/185.html
* 当展开一个关闭的节点时,如果该节点没有子节点加载,它将通过上面定义的 URL 向服务器发送节点的 id 值作为名为 'id' 的 http 请求参数,以便检索子节点。
* 所以这里的参数value必须是id,若是其他值则接收不到。缺省值是0,表示初始化一级菜单。
*
* @param parentId
* @return
*/
@RequestMapping("/async")
@ResponseBody
private List<EUTreeNode> getAsyncCatList(@RequestParam(value="id",defaultValue="0") int parentId) {
List<EUTreeNode> results = categoryService.getCategoryList(parentId);
return results;
}
@RequestMapping("/sync")
@ResponseBody
private List<EUTreeNode> getSyncCatList() {
List<EUTreeNode> results = categoryService.getCategoryList();
return results;
}
}</code></pre>
<p>说明:这里的@RequestParam(value="id",defaultValue="0"),value值必须是id,不能是其他值。</p>
<h4>Views视图层</h4>
<p>演示EasyUI-Tree 类目树菜单的 tree.jsp</p>
<pre><code><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>EasyUI-Tree</title>
<link rel="stylesheet" type="text/css" href="js/jquery-easyui-1.4.1/themes/default/easyui.css" />
<link rel="stylesheet" type="text/css" href="js/jquery-easyui-1.4.1/themes/icon.css" />
<script type="text/javascript" src="js/jquery-easyui-1.4.1/jquery.min.js"></script>
<script type="text/javascript" src="js/jquery-easyui-1.4.1/jquery.easyui.min.js"></script>
<script type="text/javascript" src="js/jquery-easyui-1.4.1/locale/easyui-lang-zh_CN.js"></script>
</head>
<body class="easyui-layout">
<div data-options="region:'west',title:'EasyUI 树菜单',split:true" style="width:205px;">
<ul id="menu" class="easyui-tree" style="margin-top: 10px;margin-left: 5px;">
<li>
<span>EasyUI</span>
<ul>
<li>静态树</li>
<li>结构为ul li 标签</li>
<li>ul定义class为easyui-tree</li>
</ul>
</li>
<li>
<span>本章知识点</span>
<ul>
<li>创建静态树菜单</li>
<li>创建异步树菜单</li>
<li>创建异步树多选菜单</li>
<li>树菜单权限管理</li>
</ul>
</li>
</ul>
</div>
<div id="content" region="center" title="ITDragon博客" style="padding:5px;">
<span>
<h3>创建静态树菜单</h3>
<ul id="" class="easyui-tree">
<li>
<span>父节点</span>
<ul>
<li>子节点一</li>
<li>子节点二</li>
</ul>
</li>
</ul>
<h4>使用方法</h4>
<p>ul 标签 定义 class="easyui-tree"</p>
<a href="http://www.jeasyui.net/plugins/185.html">EasyUI 树菜单教程 </a> <br/>
<a href="http://www.jeasyui.net/plugins/180.html">EasyUI 窗口教程 </a>
</span>
<hr/>
<span>
<h3>创建异步树菜单</h3>
<a href="javascript:void(0)" class="easyui-linkbutton selectCategory">创建异步树菜单</a>
<input type="hidden" name="categoryId" style="width: 280px;"></input>
<br/>
<h4>创建思路</h4>
<p>一:初始加载一级类目菜单,通过点击一级类目菜单再查询其子节点菜单</p>
<p>二:类目表设计实例,一级类目的parentId为0,子节点类目的parentId是父节点类目的id</p>
<p>三:返回数据结构类型只要满足EasyUI的规范即可</p>
</span>
<hr/>
<span>
<h3>创建异步树多选菜单</h3>
<a href="javascript:void(0)" class="easyui-linkbutton selectMoreCategory">创建异步树多选菜单</a>
<input type="hidden" name="categoryIds" style="width: 280px;"></input>
<br/>
<h4>注意</h4>
<p>若采用异步树加载菜单,会出现勾选父节点。保存后只打印了父节点信息,未打印子节点(因为子节点都没有加载)</p>
<h4>解决思路</h4>
<p>让业务每个都点开(不合实际);本章节采用同步加载的方式;你们有没有更好的办法?</p>
<a href="http://www.jeasyui.net/tutorial/57.html"> EasyUI 采用同步加载教程 </a>
</span>
<hr/>
<span>
<h3>树菜单权限管理:</h3>
<p>业务逻辑:需要一张用户组管理表,设置当前登录用户所属组。</p>
<p>后台逻辑:树菜单表新增字段permission用来匹配用户所属组,说简单点就是多了一层查询条件。</p>
</span>
</div>
<script type="text/javascript">
$(function(){
initAsyncCategory ();
initMoreSyncCategory ();
});
// 异步加载树菜单
function initAsyncCategory (){
$(".selectCategory").each(function(i,e){
var _ele = $(e);
_ele.after("<span style='margin-left:10px;'></span>"); // 避免被按钮遮住
_ele.unbind('click').click(function(){
$("<div>").html("<ul>").window({ // 使用 javascript 创建窗口(window)
width:'500', height:"450", modal:true, closed:true, iconCls:'icon-save', title:'异步树菜单',
onOpen : function(){ // 窗口打开后执行
var _win = this;
$("ul",_win).tree({
url:'/category/async', // 采用异步加载树节点,返回数据的格式要满足EasyUI Tree 的要求
animate:true,
onClick:function(node){ // 树菜单点击后执行
if($(this).tree("isLeaf",node.target)){ // 如果该节点是叶节点就填写到categoryId中,并关闭窗口
_ele.parent().find("[name=categoryId]").val(node.id);
_ele.next().text(node.text).attr("categoryId",node.id);
$(_win).window('close');
}
}
});
},
onClose : function(){ // 窗口关闭后执行
$(this).window("destroy");
}
}).window('open'); // 使用 javascript 打开窗口(window)
});
});
}
// 同步加载复选树菜单
function initMoreSyncCategory (){
$(".selectMoreCategory").each(function(i,e){
var _ele = $(e);
_ele.after("<span style='margin-left:10px;'></span>");
_ele.unbind('click').click(function(){
$("<div>").html("<ul id='moreItemCat'>").window({ // 使用 javascript 创建窗口(window)
width:'500', height:"450", modal:true, closed:true, iconCls:'icon-save', title:'多选树菜单,关闭窗口后保存数据',
onOpen : function(){ // 窗口打开后执行
var _win = this;
$("ul",_win).tree({
url:'/category/sync', // 采用同步的方式加载所有树节点
animate:true,
checkbox:true, // js 声明树菜单可以复选
loadFilter: function(rows){
return convert(rows);
}
});
},
onClose : function(){ // 窗口关闭后执行
var nodes = $("#moreItemCat").tree('getChecked');
var categoryIds = '';
var categoryTexts = '';
for(var i = 0; i < nodes.length; i++){
if ('' != categoryIds) {
categoryIds += ',';
categoryTexts += ' , ';
}
categoryIds += nodes[i].id;
categoryTexts += nodes[i].text;
}
_ele.parent().find("[name=categoryIds]").val(categoryIds);
_ele.next().text(categoryTexts).attr("categoryId",categoryTexts);
$(this).window("destroy");
}
}).window('open'); // 使用 javascript 打开窗口(window)
});
});
}
// 官方提供的 js 解析 json 代码
function convert(rows){
function exists(rows, parentId){
for(var i=0; i<rows.length; i++){
if (rows[i].id == parentId) return true;
}
return false;
}
var nodes = [];
for(var i=0; i<rows.length; i++){ // get the top level nodes
var row = rows[i];
if (!exists(rows, row.parentId)){
nodes.push({
id:row.id,
text:row.text,
state:row.state
});
}
}
var toDo = [];
for(var i=0; i<nodes.length; i++){
toDo.push(nodes[i]);
}
while(toDo.length){
var node = toDo.shift(); // the parent node
for(var i=0; i<rows.length; i++){ // get the children nodes
var row = rows[i];
if (row.parentId == node.id){
var child = {id:row.id,text:row.text,state:row.state};
if (node.children){
node.children.push(child);
} else {
node.children = [child];
}
toDo.push(child);
}
}
}
return nodes;
}
</script>
</body>
</html></code></pre>
<p>说明:<br>① tree.jsp 除了EasyUI-Tree 的知识点外,还涉及了一点点窗口的知识</p>
<ul><li><p>使用 javascript 创建窗口(window)</p></li></ul>
<pre><code><div id="win"></div>
$('#win').window({
width:600,
height:400,
modal:true
});</code></pre>
<ul><li><p>打开和关闭窗口(window)</p></li></ul>
<pre><code>$('#win').window('open'); // open a window
$('#win').window('close'); // close a window</code></pre>
<p>② tree.jsp 主要包含了单选异步加载树菜单和多选同步加载树菜单两大知识点,所以内容较长,请耐心阅读。<br>③ 若异步加载树菜单,支持多选,会出现子节点没有打印的问题</p>
<h3>总结</h3>
<ul>
<li><p>如何初始化静态的树菜单。</p></li>
<li><p>如何实现异步加载树菜单,单选后显示在页面上。</p></li>
<li><p>如何实现同步加载树菜单,多选后显示在页面上。</p></li>
<li><p>树菜单表的设计思路。</p></li>
</ul>
<p>源码: <br><a href="https://link.segmentfault.com/?enc=LeXdkrpTOOF%2FyzMDlYafog%3D%3D.eCPKKmCeqzwDWqkz8cWqTaMUZTR0p2ZhMaXgU9Gfsn1q3RvPIIFgYKyeuXQy8JDL3l0avNmZfxj5o94Zr1kodYV77U2YMtrevFkFDVGSCDM%3D" rel="nofollow">https://github.com/ITDragonBl...</a><br>逆向工程: <br><a href="https://link.segmentfault.com/?enc=BVHHFIY7OxPS0sxaATAXcg%3D%3D.tpJ4GyFRODnvl2asuHFJbUuji4uKkUtdv78KIATKn3fGJconSCf8WDXR0dTR%2BGBgTUN8aNO48%2F%2FJTtHDdfl9O3S2lbCTrcMCCeBWLD5UPpSFlImBvw%2F%2FbuEKVf7NGCoZ" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>最后,EasyUI 树菜单到这里就结束了,感谢大家的阅读。觉得不错的可以点个赞!</p>
Nginx 搭建图片服务器
https://segmentfault.com/a/1190000012064754
2017-11-18T21:25:27+08:00
2017-11-18T21:25:27+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Nginx 搭建图片服务器</h2>
<p>本章内容通过Nginx 和 FTP 搭建图片服务器。在学习本章内容前,请确保您的Linux 系统已经安装了Nginx和Vsftpd。</p>
<p>Nginx 安装:<a href="https://segmentfault.com/a/1190000012048460">https://segmentfault.com/a/11...</a><br>Vsftpd 安装:<a href="https://segmentfault.com/a/1190000012063842">https://segmentfault.com/a/11...</a></p>
<h3>本章知识点</h3>
<p>效果图:<br><img src="/img/remote/1460000012064759?w=689&h=549" alt="效果图演示" title="效果图演示"></p>
<p>需求:实现图片的上传和批量上传<br>技术:Nginx,Vsftpd,Spring,SpringMVC,KindEditor,CentOS<br>说明:本章节内容主要是实现图片的上传功能。使用 KindEditer 是为了更好的演示图片的上传,回显,批量效果。后台代码与KindEditer没有直接关系,放心阅读。另外源码中有Mybatis的jar,不用理会,本章内容用不到,是为后续内容做准备!<br>源码:见文章底部<br>场景:用户将图片上传到 tomcat 服务器上,再由 tomcat 服务器通过FTP上传到 Nginx 服务器上。<br><img src="/img/remote/1460000012048465?w=589&h=582" alt="用户将图片上传到 tomcat 服务器上,再由 tomcat 服务器通过FTP上传到 Nginx 服务器" title="用户将图片上传到 tomcat 服务器上,再由 tomcat 服务器通过FTP上传到 Nginx 服务器"><br>项目结构:<br><img src="/img/bVYMKE?w=437&h=478" alt="![项目结构](http://images2017.cnblogs.com/blog/806956/201711/806956-20171118191938390-910467653.png)" title="![项目结构](http://images2017.cnblogs.com/blog/806956/201711/806956-20171118191938390-910467653.png)"></p>
<h3>单元测试</h3>
<p>首先要攻破核心技术。通过单元测试实现图片上传的功能。</p>
<pre><code>package com.itdragon.test;
import java.io.File;
import java.io.FileInputStream;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.junit.Test;
public class PictureFTPTest {
// 测试 ftp 上传图片功能
@Test
public void testFtpClient() throws Exception {
// 1. 创建一个FtpClient对象
FTPClient ftpClient = new FTPClient();
// 2. 创建 ftp 连接
ftpClient.connect("192.168.0.11", 21);
// 3. 登录 ftp 服务器
ftpClient.login("ftpuser", "root");
// 4. 读取本地文件
FileInputStream inputStream = new FileInputStream(new File("F:\\hello.png"));
// 5. 设置上传的路径
ftpClient.changeWorkingDirectory("/usr/local/nginx/html/images");
// 6. 修改上传文件的格式为二进制
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 7. 服务器存储文件,第一个参数是存储在服务器的文件名,第二个参数是文件流
ftpClient.storeFile("hello.jpg", inputStream);
// 8. 关闭连接
ftpClient.logout();
}
}</code></pre>
<p>说明:这里的ip地址,端口,ftp用户名,密码,本地文件路径,以及Nginx服务器图片路径等,这些字符串参数都要根据自己实际设置的来填写的。如果你的Nginx和Vsftpd安装是按照我提供的链接来做的。那你只需要改ip地址即可。</p>
<h3>Maven 的Web 项目</h3>
<p>搭建Maven的Web 项目,之前有写过。这里就不过多描述。</p>
<h4>项目核心配置文件</h4>
<p>首先是 Maven 的核心文件 pom.xml</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itdragon.upload</groupId>
<artifactId>pictrue-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!-- 集中定义依赖版本号 -->
<properties>
<junit.version>4.12</junit.version>
<spring.version>4.1.3.RELEASE</spring.version>
<mybatis.version>3.2.8</mybatis.version>
<mybatis.spring.version>1.2.2</mybatis.spring.version>
<mybatis.paginator.version>1.2.15</mybatis.paginator.version>
<mysql.version>5.1.6</mysql.version>
<slf4j.version>1.6.4</slf4j.version>
<jackson.version>2.4.2</jackson.version>
<druid.version>1.0.9</druid.version>
<httpclient.version>4.3.5</httpclient.version>
<jstl.version>1.2</jstl.version>
<servlet-api.version>2.5</servlet-api.version>
<jsp-api.version>2.0</jsp-api.version>
<joda-time.version>2.5</joda-time.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-io.version>1.3.2</commons-io.version>
<commons-net.version>3.3</commons-net.version>
<pagehelper.version>3.4.2</pagehelper.version>
<jsqlparser.version>0.9.1</jsqlparser.version>
<commons-fileupload.version>1.3.1</commons-fileupload.version>
<jedis.version>2.7.2</jedis.version>
<solrj.version>4.10.3</solrj.version>
</properties>
<dependencies>
<!-- 时间操作组件 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda-time.version}</version>
</dependency>
<!-- Apache工具组件 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<!-- Jackson Json处理工具包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<dependency>
<groupId>com.github.miemiedev</groupId>
<artifactId>mybatis-paginator</artifactId>
<version>${mybatis.paginator.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- JSP相关 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>${jsp-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- 文件上传组件 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<!-- Redis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>${jedis.version}</version>
</dependency>
<!-- solr客户端 -->
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>${solrj.version}</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 资源文件拷贝插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project></code></pre>
<p>说明:和文件上传有直接关系的是:</p>
<pre><code><dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency></code></pre>
<p>然后是 Web 项目的核心文件 web.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="taotao" version="2.5">
<display-name>pictrue-service</display-name>
<!-- 加载spring容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 解决post乱码 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- springmvc的前端控制器 -->
<servlet>
<servlet-name>pictrue-service</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>pictrue-service</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app></code></pre>
<p>再是 SpringMVC 配置文件 springmvc.xml,需要添加文件上传解析器</p>
<pre><code><!-- 定义文件上传解析器 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设定默认编码 -->
<property name="defaultEncoding" value="UTF-8"></property>
<!-- 设定文件上传的最大值5MB,5*1024*1024 -->
<property name="maxUploadSize" value="5242880"></property>
</bean></code></pre>
<p>最后是 Ftp 配置文件 resource.properties</p>
<pre><code>FTP_ADDRESS=192.168.0.11
FTP_PORT=21
FTP_USERNAME=ftpuser
FTP_PASSWORD=root
FTP_BASE_PATH=/usr/local/nginx/html/images
IMAGE_BASE_URL=http://192.168.0.11/images</code></pre>
<h4>Service 层</h4>
<p>上传图片的接口 PictureService.java</p>
<pre><code>package com.itdragon.service;
import java.util.Map;
import org.springframework.web.multipart.MultipartFile;
public interface PictureService {
/**
* 上传,批量上传接口
* @param uploadFile
* @return
*/
Map uploadPicture(MultipartFile uploadFile);
}</code></pre>
<p>上传图片接口实现类 PictureServiceImpl.java</p>
<pre><code>package com.itdragon.service.impl;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.itdragon.service.PictureService;
@Service
@SuppressWarnings({"rawtypes", "unchecked"})
public class PictureServiceImpl implements PictureService {
// 通过 Spring4 的 Value注解,获取配置文件中的属性值
@Value("${FTP_ADDRESS}")
private String FTP_ADDRESS; // ftp 服务器ip地址
@Value("${FTP_PORT}")
private Integer FTP_PORT; // ftp 服务器port,默认是21
@Value("${FTP_USERNAME}")
private String FTP_USERNAME; // ftp 服务器用户名
@Value("${FTP_PASSWORD}")
private String FTP_PASSWORD; // ftp 服务器密码
@Value("${FTP_BASE_PATH}")
private String FTP_BASE_PATH; // ftp 服务器存储图片的绝对路径
@Value("${IMAGE_BASE_URL}")
private String IMAGE_BASE_URL; // ftp 服务器外网访问图片路径
@Override
public Map uploadPicture(MultipartFile uploadFile) {
Map resultMap = new HashMap<>();
try {
// 1. 取原始文件名
String oldName = uploadFile.getOriginalFilename();
// 2. ftp 服务器的文件名
String newName = oldName;
//图片上传
boolean result = uploadFile(FTP_ADDRESS, FTP_PORT, FTP_USERNAME, FTP_PASSWORD,
uploadFile.getInputStream(), FTP_BASE_PATH, newName);
//返回结果
if(!result) {
resultMap.put("error", 1);
resultMap.put("message", "upload Fail");
return resultMap;
}
resultMap.put("error", 0);
resultMap.put("url", IMAGE_BASE_URL + "/" + newName);
return resultMap;
} catch (Exception e) {
e.printStackTrace();
resultMap.put("error", 1);
resultMap.put("message", "upload Fail");
return resultMap;
}
}
/**
* ftp 上传图片方法
* @param ip ftp 服务器ip地址
* @param port ftp 服务器port,默认是21
* @param account ftp 服务器用户名
* @param passwd ftp 服务器密码
* @param inputStream 文件流
* @param workingDir ftp 服务器存储图片的绝对路径
* @param fileName 上传到ftp 服务器文件名
* @throws Exception
*
*/
public boolean uploadFile(String ip, Integer port, String account, String passwd,
InputStream inputStream, String workingDir, String fileName) throws Exception{
boolean result = false;
// 1. 创建一个FtpClient对象
FTPClient ftpClient = new FTPClient();
try {
// 2. 创建 ftp 连接
ftpClient.connect(ip, port);
// 3. 登录 ftp 服务器
ftpClient.login(account, passwd);
int reply = ftpClient.getReplyCode(); // 获取连接ftp 状态返回值
System.out.println("code : " + reply);
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect(); // 如果返回状态不再 200 ~ 300 则认为连接失败
return result;
}
// 4. 读取本地文件
// FileInputStream inputStream = new FileInputStream(new File("F:\\hello.png"));
// 5. 设置上传的路径
ftpClient.changeWorkingDirectory(workingDir);
// 6. 修改上传文件的格式为二进制
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 7. 服务器存储文件,第一个参数是存储在服务器的文件名,第二个参数是文件流
if (!ftpClient.storeFile(fileName, inputStream)) {
return result;
}
// 8. 关闭连接
inputStream.close();
ftpClient.logout();
result = true;
} catch (Exception e) {
e.printStackTrace();
}finally {
// FIXME 听说,项目里面最好少用try catch 捕获异常,这样会导致Spring的事务回滚出问题???难道之前写的代码都是假代码!!!
if (ftpClient.isConnected()) {
try {
ftpClient.disconnect();
} catch (IOException ioe) {
}
}
}
return result;
}
}</code></pre>
<p>说明:<br>① @Value 注解是Spring4 中提供的,@Value("${XXX}") <br>② 返回值是一个Map,并且key有error,url,message。这是根据KindEditer的语法要求来的。详情见链接。<a href="https://link.segmentfault.com/?enc=cwf6EtCH1CF5NbkRmF1oNQ%3D%3D.NXmJEzZe%2BmEUee5xKdcpJx%2FIcmMGtyyBHS8ACP%2BQvzOtPKapOJDVsrl5wI5r0YhL" rel="nofollow">http://kindeditor.net/docs/up...</a></p>
<h4>Controller 层</h4>
<p>负责页面跳转的 PageController.java</p>
<pre><code>package com.itdragon.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class PageController {
/**
* 打开首页
*/
@RequestMapping("/")
public String showIndex() {
return "index";
}
@RequestMapping("/{page}")
public String showpage(@PathVariable String page) {
System.out.println("page : " + page);
return page;
}
}
</code></pre>
<p>负责图片上传的 PictureController.java</p>
<pre><code>package com.itdragon.controller;
import java.util.Map;
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 org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itdragon.service.PictureService;
@RestController
public class PictureController {
@Autowired
private PictureService pictureService;
@RequestMapping("pic/upload")
public String pictureUpload(@RequestParam(value = "fileUpload") MultipartFile uploadFile) {
String json = "";
try {
Map result = pictureService.uploadPicture(uploadFile);
// 浏览器擅长处理json格式的字符串,为了减少因为浏览器内核不同导致的bug,建议用json
json = new ObjectMapper().writeValueAsString(result);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return json;
}
}</code></pre>
<p>说明:<br>① @RestController 也是Spring4 提供的,是 @Controller + @ResponseBody 的组合注解。<br>② Controller层的返回值是一个json格式的字符串。是考虑到浏览器对json解析兼容性比较好。</p>
<h4>Views视图层</h4>
<p>负责上传图片的jsp页面 pic-upload.jsp</p>
<pre><code><%@ page language="java" contentType="text/html; UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>ITDragon 图片上传</title>
</head>
<link href="/js/kindeditor-4.1.10/themes/default/default.css" type="text/css" rel="stylesheet">
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" charset="utf-8" src="/js/kindeditor-4.1.10/kindeditor-all-min.js"></script>
<script type="text/javascript" charset="utf-8" src="/js/kindeditor-4.1.10/lang/zh_CN.js"></script>
</head>
<body>
<h3>测试上传图片功能接口的form表单</h3>
<form action="pic/upload" method="post" enctype="multipart/form-data">
<input type="file" name="fileUpload" />
<input type="submit" value="上传文件" />
</form>
<hr />
<h3>借用KindEditor富文本编辑器实现批量上传图片</h3>
<textarea id="kindEditorDesc" style="width:800px;height:300px;visibility:hidden;"></textarea>
<script type="text/javascript">
$(function(){
//初始化富文本编辑器
KindEditor.create("#kindEditorDesc", {
// name值,必须和Controller 的参数对应,不然会提示 400 的错误
filePostName : "fileUpload",
// action值,
uploadJson : '/pic/upload',
// 设置上传类型,分别为image、flash、media、file
dir : "image"
});
});
</script>
</body>
</html></code></pre>
<p>说明:pic-upload.jsp 分为两个部分,第一个部分是为了测试上传图片功能的form表单。第二个部分是为了更好的体验上传,批量上传,回显功能的KindEditer 富文本编辑器。</p>
<h3>总结</h3>
<ul>
<li><p>Nginx 搭建服务器的思维</p></li>
<li><p>Java实现 Ftp上传图片的功能</p></li>
<li><p>KindEditer 上传图片的功能</p></li>
</ul>
<p>源码:<a href="https://link.segmentfault.com/?enc=s54nKgK1aPZAUZRgxujlCQ%3D%3D.nGv2%2FJAPiV9Der9n9OILpKjkOMx%2Be1XbVgCZlbhDQqCSCECj7xkNdLqhBZrzTvl5SvvwSMtS7HimveENBQGp7A%3D%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>Nginx 搭建图片服务器到这里就结束了,有什么不足的地方,请赐教。如果觉得不错,可以点个赞哦!</p>
Nginx 安装部署
https://segmentfault.com/a/1190000012048460
2017-11-17T12:28:17+08:00
2017-11-17T12:28:17+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Nginx 安装部署</h2>
<p>Nginx,一个被贴满,高性能,低消耗,低成本标签的web服务器。想必大家都早有耳闻。我是在接触了公司的图片服务器的时候,才开始真正接触它。本文从Nginx 和传统项目的区别 和 Nginx的安装部署两个方面来了解它。</p>
<h3>1 Nginx 和 传统项目的区别</h3>
<h4>1.1 传统项目管理图片的思路</h4>
<p>在传统项目中,我们一般通过在web项目的根目录下创建一个用于存储图片的images文件夹来方便管理图片。但随着业务和规模的逐渐扩大,一台服务器已经无法满足我们的需求,我们可以通过搭建服务器集群来处理高并发的场景。 </p>
<p>好景不长,集群刚搭好,就有用户反馈,图片为什么时而有,时而没有? 这是因为:图片存储在 服务器/web根目录/images文件夹 中,当用户在上传图片的时候,只将图片传给了一台服务器,在获取图片时,可能调用了其他服务器。这样会出现该问题。</p>
<p>解决这个问题很简单,就是把图片单独放在一个服务器。如果选择Apache的tomcat服务器,在处理业务逻辑简单的图片服务器中似乎显得有些笨重。一款高性能,低成本轻量级web服务器 nginx 脱颖而出。不仅如此它还是一款反向代理服务器和电子邮件代理服务器。</p>
<p><img src="/img/remote/1460000012048465?w=589&h=582" alt="传统项目管理图片的思路" title="传统项目管理图片的思路"></p>
<h3>2 安装部署</h3>
<h4>2.1 理想流程</h4>
<pre><code>[root@itdragon ~]# wget http://nginx.org/download/nginx-1.13.6.tar.gz
[root@itdragon ~]# tar -zxvf nginx-1.13.6.tar.gz
[root@itdragon ~]# ll
total 824
drwxr-xr-x 9 1001 1001 4096 Nov 14 14:26 nginx-1.13.6
-rw-r--r-- 1 root root 832104 Nov 14 14:18 nginx-1.13.6.tar.gz
[root@itdragon ~]# cd nginx-1.13.6
[root@itdragon nginx-1.13.6]# ./configure
[root@itdragon nginx-1.13.6]# make
[root@itdragon nginx-1.13.6]# make install
[root@itdragon nginx-1.13.6]# cd /usr/local/nginx/sbin/
[root@itdragon sbin]# ./nginx
[root@itdragon sbin]# ifconfig</code></pre>
<p>第一步:下载Nginx压缩包<br>第二步:解压<br>第三步:配置,编译,安装,启动<br>第四步:查看ip地址<br>第五步:浏览器访问:ip:port<br>若出现如下图片则说明安装成功。<br><img src="/img/remote/1460000012048466" alt="Nginx欢迎页面" title="Nginx欢迎页面"><br>但是,Nginx是调皮的,它不会让我们如此顺利</p>
<h4>2.2 常见问题</h4>
<p>踩坑?不存在的,我踩过的坑,不允许让你们再踩。它是我滴!</p>
<ul>
<li><p>./configure: error: C compiler cc is not found</p></li>
<li><p>./configure: error: the HTTP rewrite module requires the PCRE library.</p></li>
<li><p>./configure: error: the HTTP gzip module requires the zlib library</p></li>
<li><p>OpenSSL library is not used</p></li>
<li><p>nginx: [emerg] bind() to 0.0.0.0:88 failed (98: Address already in use)</p></li>
</ul>
<p>第一个问题,是因为 nginx 解压编译依赖 gcc 环境造成的。</p>
<pre><code>[root@itdragon ~]# yum install gcc-c++</code></pre>
<p>第二个问题,是因为 nginx 的 http 模块使用 pcre 来解析正则表达式</p>
<pre><code>[root@itdragon ~]# yum install -y pcre pcre-devel</code></pre>
<p>第三个问题,是因为 nginx 使用 zlib 对 http 包的内容进行 gzip 操作</p>
<pre><code>[root@itdragon ~]# yum install -y zlib zlib-devel</code></pre>
<p>第四个问题,建议安装,nginx 它是支持https 协议的</p>
<pre><code>[root@itdragon ~]# yum install -y openssl openssl-devel</code></pre>
<p>第五个问题,是很常见的端口占用,修改 nginx.config 文件中的端口即可。 /port,快速找到端口配置的地方。[Insert] 开启编辑模式。[Esc] :wq 退出保存</p>
<pre><code>[root@itdragon sbin]# ./nginx
nginx: [emerg] bind() to 0.0.0.0:88 failed (98: Address already in use)
[root@itdragon sbin]# vim ../conf/nginx.conf
server {
listen 88;
server_name localhost;
[root@itdragon sbin]# ./nginx</code></pre>
<p>若出现 Loaded plugins: fastestmirror 不是问题的问题。可以通过修改fastestmirror.conf 文件,这是一种不负责任的做法,如果自己玩 Nginx 可以这样做。如果是实际开发,就老老实实的按照提示来做。</p>
<pre><code>[root@plugins ~]# vim /etc/yum/pluginconf.d/fastestmirror.conf
enabled=0
[root@plugins ~]# vim /etc/yum.conf
plugins=0
[root@plugins ~]# yum clean dbcache</code></pre>
<p>到这里,Nginx的安装部署就完成了。下一章就利用Nginx搭建图片服务。</p>
Mybatis3 快速入门
https://segmentfault.com/a/1190000011971467
2017-11-12T17:33:00+08:00
2017-11-12T17:33:00+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Mybatis3 快速入门</h2>
<p>目前常见的持久层java框架有Hibernate,Mybatis,SpringData。笔者比较喜欢用SpringData。Hibernate 和 Mybatis 也经常用。今天通过 Mybatis 的简介,数据的增删改查,表的级联查询,动态SQL语句 来快速入门 Mybatis 。</p>
<h3>1 Mybatis 简介</h3>
<p>摘录百度百科的内容:MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录</p>
<p>如果 Hibernate 是自动化持久层框架,那么 Mybatis 就是半自动化持久层框架。 半自动 ??? 听起来好像 lower 了。其实不然,Mybatis 将 sql 和 java 分离开。让专业的db工程师负责 sql 的优化,提高其性能,在高并发的场景,系统依然 稳如dog 。程序员可以把更多的精力放在业务逻辑上。</p>
<p>Mybatis:<a href="https://link.segmentfault.com/?enc=8xZRVQAdBw3wYjDZpDU0MQ%3D%3D.Gcv5DhzGyEwTVHZbI0BgSraSfP2te%2BP7DcLmmNE%2B2K2aCkYzKTo4Vv3bvWX2tf05" rel="nofollow">https://github.com/mybatis/my...</a></p>
<h3>2 Mybatis 快速入门</h3>
<p>需求:使用 mybatis 框架完成数据的增删改查操作,和级联查询,模糊查询,调用存储过程,使用mybatis的一二级缓存</p>
<p>技术:mybatis,maven</p>
<p>源码:见文章底部</p>
<p>说明:本文内容属于快速入门,通过手写 xml 映射文件了解 mybatis 的工作原理。实际开发中,一般采用官方提供的逆向工程自动生成需要的 java 文件和 xml 文件</p>
<p>结构:<br><img src="/img/bVYotI?w=277&h=324" alt="806956-20171112163901606-1696704597.png" title="806956-20171112163901606-1696704597.png"></p>
<p>准备:</p>
<p>Mysql数据库表结构<br><img src="/img/bVYotK?w=193&h=112" alt="806956-20171112170403638-636083194.png" title="806956-20171112170403638-636083194.png"></p>
<p>创建四张表,其中 person 独立存在。classroom 和 student,teacher 存在主外键关系。</p>
<p>① classroom 的 student_id 和 student 的 class_id 存在主外键关系,并且是一对多的关系</p>
<p>② classroom 的 teacher_id 和 teacher 的 id 存在主外键关系,并且是一对一的关系</p>
<pre><code class="sql">SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for classroom
-- ----------------------------
DROP TABLE IF EXISTS `classroom`;
CREATE TABLE `classroom` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`room` varchar(255) DEFAULT NULL,
`teacher_id` int(11) DEFAULT NULL,
`student_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c_t_id` (`teacher_id`),
KEY `c_s_id` (`student_id`),
CONSTRAINT `c_t_id` FOREIGN KEY (`teacher_id`) REFERENCES `teacher` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of classroom
-- ----------------------------
INSERT INTO `classroom` VALUES ('1', 'JavaEE', '1', '1');
INSERT INTO `classroom` VALUES ('2', 'Linux', '2', '2');
-- ----------------------------
-- Table structure for person
-- ----------------------------
DROP TABLE IF EXISTS `person`;
CREATE TABLE `person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) DEFAULT NULL,
`last_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of person
-- ----------------------------
INSERT INTO `person` VALUES ('1', 'lxl@qq.com', 'lxl');
INSERT INTO `person` VALUES ('2', 'cyy@qq.com', 'cyy');
INSERT INTO `person` VALUES ('3', 'itdrgon@qq.com', 'itdragon');
INSERT INTO `person` VALUES ('4', 'java@qq.com', 'java');
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`class_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `s_c_id` (`class_id`),
CONSTRAINT `s_c_id` FOREIGN KEY (`class_id`) REFERENCES `classroom` (`student_id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES ('1', 'ITDragon', '1');
INSERT INTO `student` VALUES ('2', 'Marry', '1');
INSERT INTO `student` VALUES ('3', 'XiaoMing', '2');
-- ----------------------------
-- Table structure for teacher
-- ----------------------------
DROP TABLE IF EXISTS `teacher`;
CREATE TABLE `teacher` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`subject` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of teacher
-- ----------------------------
INSERT INTO `teacher` VALUES ('1', 'Java');
INSERT INTO `teacher` VALUES ('2', 'Docker');</code></pre>
<p>Maven 项目的核心文件 pom.xml (有些不是必要的,后续做整合会用到)</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itdragon.mybatis</groupId>
<artifactId>mybatis-basic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-io.version>1.3.2</commons-io.version>
<commons-net.version>3.3</commons-net.version>
<junit.version>4.12</junit.version>
<slf4j.version>1.6.4</slf4j.version>
<mybatis.version>3.2.8</mybatis.version>
<mybatis.spring.version>1.2.2</mybatis.spring.version>
<mybatis.paginator.version>1.2.15</mybatis.paginator.version>
<mysql.version>5.1.6</mysql.version>
<druid.version>1.0.9</druid.version>
</properties>
<dependencies>
<!-- Apache工具组件 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<dependency>
<groupId>com.github.miemiedev</groupId>
<artifactId>mybatis-paginator</artifactId>
<version>${mybatis.paginator.version}</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
</dependencies>
</project></code></pre>
<p>Mybatis 的配置文件 SqlMapConfig.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 标签必须按顺序写,否则会提示错误:The content of element type "configuration" must match "(properties?,settings?,...)". -->
<!-- 引入配置文件 -->
<properties resource="db.properties" />
<!-- 配置实体类的别名 -->
<typeAliases>
<!-- 给指定包取别名,别名为实体类对应的简单类名,如 com.itdragon.pojo.Person 的别名就是 Person -->
<package name="com.itdragon.pojo" />
</typeAliases>
<!-- 配置数据库链接 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
</environments>
<!-- 注册映射文件 -->
<mappers>
<mapper resource="com/itdragon/mapper/PersonMapper.xml" />
<mapper resource="com/itdragon/mapper/ClassroomMapper.xml" />
</mappers>
</configuration></code></pre>
<p>数据库的配置文件 db.properties</p>
<pre><code class="xml">jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jpa?characterEncoding=utf-8
jdbc.username=root
jdbc.password=root</code></pre>
<p>到这里准备工作就做完了。</p>
<h3>3 数据的增删改查</h3>
<p>Person.java 实体类</p>
<pre><code class="java">package com.itdragon.pojo;
// 学习 mybatis crud 实体类
public class Person {
private Integer id;
private String email;
private String lastName; // 这里lastName 在数据库中对应的是 last_name, 这会出现:字段名与实体类属性名不相同的冲突问题
public Person() {
}
public Person(Integer id, String email, String lastName) {
this.id = id;
this.email = email;
this.lastName = lastName;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email == null ? null : email.trim();
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName == null ? null : lastName.trim();
}
@Override
public String toString() {
return "Person [id=" + id + ", email=" + email + ", lastName=" + lastName + "]";
}
}</code></pre>
<p>PersonMapper.xml 查询数据的映射文件</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itdragon.mapper.PersonMapper">
<!-- CRUD 操作 -->
<!--
基础知识:
select 查询数据
insert 插入数据
delete 删除数据
update 更新数据
namespace 命名空间
id 方法名, 命名空间 + 方法名 = 唯一方法
parameterType 传入参数类型
resultType 返回值类型
resultMap 以键值对的类型返回结果
参数传值:#{xxx}
parameterType 如果不是实体类,对应的参数名可以自定义。如 #{id} 也可以是 #{xxxx}
如果是实体类,对应的参数名必须是实体类属性名。为了避免错误,尽量全部都用属性名。
扩展知识:
resultType 的值是 com.itdragon.pojo.Person 全类名,但为了方便,可以考虑使用别名
resultMap 为了避免类似 lastName 和 last_name 冲突,导致查询的 last_name 会是 null 问题,可以设置键值关系
-->
<select id="getPersonById" parameterType="int" resultType="com.itdragon.pojo.Person">
select * from person where id=#{id}
</select>
<!-- 解决字段名与实体类属性名不相同的冲突问题第一种办法(不推荐) -->
<select id="getPersonByIdOne" parameterType="int" resultType="com.itdragon.pojo.Person">
select id, email, last_name lastName from person where id=#{id}
</select>
<select id="getPersonByIdTwo" parameterType="int" resultMap="getPersonMap">
select * from person where id=#{id}
</select>
<!-- 使用 resultMap 设置冲突字段名和实体类属性名对应关系,(推荐) -->
<resultMap type="Person" id="getPersonMap">
<result property="lastName" column="last_name" />
</resultMap>
<!-- parameterType 中直接使用了 Person 是因为在 SqlMapConfig.xml 文件中设置了别名 -->
<insert id="createPerson" parameterType="Person">
insert into person(email, last_name) values(#{email}, #{lastName})
</insert>
<delete id="deletePersonById" parameterType="int">
delete from person where id=#{id}
</delete>
<update id="updatePersonById" parameterType="Person">
update person set email=#{email}, last_name=#{lastName} where id=#{id}
</update>
<select id="getAllperson" resultType="Person">
select * from person
</select>
</mapper></code></pre>
<p>测试方法:</p>
<pre><code class="java">package com.itdragon.test;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import com.itdragon.pojo.Classroom;
import com.itdragon.pojo.Person;
public class MyBatisTest {
public SqlSession getSqlSession() {
String resource = "SqlMapConfig.xml";
InputStream is = MyBatisTest.class.getClassLoader().getResourceAsStream(resource);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession(true); // false 默认手动提交, true 自动提交
return session;
}
// crud 操作
@Test
public void getPersonById() {
String statement = "com.itdragon.mapper.PersonMapper.getPersonById";
Person person = getSqlSession().selectOne(statement, 2);
System.out.println(person);
statement = "com.itdragon.mapper.PersonMapper.getPersonByIdOne";
person = getSqlSession().selectOne(statement, 2);
System.out.println(person);
statement = "com.itdragon.mapper.PersonMapper.getPersonByIdTwo";
person = getSqlSession().selectOne(statement, 2);
System.out.println(person);
}
@Test
public void getAllperson() {
String statement = "com.itdragon.mapper.PersonMapper.getAllperson";
List<Person> persons = getSqlSession().selectList(statement);
System.out.println(persons);
}
@Test
public void createPerson() {
String statement = "com.itdragon.mapper.PersonMapper.createPerson";
int result = getSqlSession().insert(statement, new Person(3, "itdragon@qq.com", "ITDragon"));
System.out.println(result);
}
@Test
public void updatePersonById() {
String statement = "com.itdragon.mapper.PersonMapper.updatePersonById";
int result = getSqlSession().update(statement, new Person(4, "itdragon@qq.com", "ITDragon博客"));
System.out.println(result);
}
@Test
public void deletePersonById() {
String statement = "com.itdragon.mapper.PersonMapper.deletePersonById";
int result = getSqlSession().delete(statement, 4);
System.out.println(result);
}
}</code></pre>
<h3>4 级联查询</h3>
<p>为了满足一对一和一对多的级联操作,新增三个实体类,分别是 Classroom(教室),Teacher(老师),Student(学生)</p>
<p>Classroom 和 Teacher 是一对一的关系,Classroom 和 Student 是一对多的关系</p>
<pre><code class="java">package com.itdragon.pojo;
import java.io.Serializable;
import java.util.List;
// 学习 表的关联关系所用字段,一个教室关联一个老师(一对一),一个教室关联一群学生(一对多)
public class Classroom implements Serializable {
private Integer id;
private String room;
private Teacher teacher;
private List<Student> students;
public Classroom() {
}
public Classroom(Integer id, String room, Teacher teacher, List<Student> students) {
this.id = id;
this.room = room;
this.teacher = teacher;
this.students = students;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRoom() {
return room;
}
public void setRoom(String room) {
this.room = room;
}
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
@Override
public String toString() {
return "Classroom [id=" + id + ", room=" + room + ", teacher=" + teacher + ", students=" + students + "]";
}
}</code></pre>
<pre><code class="java">package com.itdragon.pojo;
import java.io.Serializable;
public class Teacher implements Serializable{
private Integer id;
private String subject;
public Teacher() {
}
public Teacher(Integer id, String subject) {
this.id = id;
this.subject = subject;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
@Override
public String toString() {
return "Teacher [id=" + id + ", subject=" + subject + "]";
}
}</code></pre>
<pre><code class="java">package com.itdragon.pojo;
import java.io.Serializable;
public class Student implements Serializable {
private Integer id;
private String name;
public Student() {
}
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student [id=" + id + ", name=" + name + "]";
}
}</code></pre>
<p>ClassroomMapper.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itdragon.mapper.ClassroomMapper">
<!-- 关联表查询 -->
<!--
基础知识:
association:用于一对一的关联查询
property:对象的属性名
javaType:对象的类型
column:对应数据表中外键
select:使用另外一个查询的封装结果
collection:用于一对多的关联查询
ofType:指定集合对象的类型
-->
<!--
需求:通过 id 查询 classroom, 并打印 teacher 信息
嵌套查询:通过执行另外一个SQL映射语句来返回预期的复杂类型
第一步:先查询 classroom SELECT * FROM classroom WHERE id=?;
第二步:再查询 teacher SELECT * FROM teacher WHERE id=classroom.id //classroom 是第一步的查询结果
说明:嵌套查询的方法,虽然好理解,当时不建议
-->
<select id="getClassroomById" parameterType="int" resultMap="getClassroomMap">
SELECT * FROM classroom WHERE id=#{id}
</select>
<select id="getTeacherById" parameterType="int" resultType="Teacher">
SELECT * FROM teacher WHERE id=#{id}
</select>
<resultMap type="Classroom" id="getClassroomMap">
<association property="teacher" column="teacher_id" select="getTeacherById">
<!-- 如果 teacher 存在属性字段和字段冲突,需要在这里设置 -->
</association>
</resultMap>
<!--
需求:通过 id 查询 classroom, 并打印 teacher 信息
嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集,封装联表查询的数据(去除重复的数据)
select * from classroom, teacher where classroom.teacher_id=teacher.id and classroom.id=?
-->
<select id="getClassroom2ById" parameterType="int" resultMap="getClassroom2Map">
SELECT * FROM classroom c, teacher WHERE c.teacher_id = teacher.id AND c.id = #{id}
</select>
<resultMap type="Classroom" id="getClassroom2Map">
<id property="id" column="id"/>
<result property="room" column="room"/>
<association property="teacher" javaType="Teacher">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
</association>
</resultMap>
<!--
需求:通过 id 查询 classroom, 并打印 teacher 和 student 信息
嵌套查询:通过执行另外一个SQL映射语句来返回预期的复杂类型
第一步:先查询 classroom SELECT * FROM classroom WHERE id=?;
第二步:再查询 teacher SELECT * FROM teacher WHERE id=classroom.id //classroom 是第一步的查询结果
第三步:再查询 student SELECT * FROM student WHERE id=classroom.id //classroom 是第一步的查询结果
-->
<select id="getClassroom3ById" resultMap="getClassroom3Map">
SELECT * FROM classroom WHERE id=#{id}
</select>
<!-- getTeacherById 上面有了,就不重复写了 -->
<select id="getStudentById" parameterType="int" resultType="Student">
SELECT * FROM student WHERE class_id=#{id}
</select>
<resultMap type="Classroom" id="getClassroom3Map">
<association property="teacher" column="teacher_id" select="getTeacherById"></association>
<collection property="students" column="student_id" select="getStudentById"></collection>
</resultMap>
<!--
需求:通过 id 查询 classroom, 并打印 teacher 和 student 信息
嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集,封装联表查询的数据(去除重复的数据)
SELECT * FROM classroom c, teacher t,student s WHERE c.teacher_id=t.id AND c.id=s.class_id AND c.id=?
-->
<select id="getClassroom4ById" parameterType="int" resultMap="getClassroom4Map">
SELECT * FROM classroom c, teacher t, student s WHERE c.teacher_id=t.id AND c.student_id=s.class_id AND c.id=#{id}
</select>
<resultMap type="Classroom" id="getClassroom4Map">
<id property="id" column="id"/>
<result property="room" column="room"/>
<association property="teacher" javaType="Teacher">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
</association>
<collection property="students" ofType="Student">
<!--
存在问题
如果两表联查,主表和明细表的主键都是id的话,明细表的多条只能查询出来第一条。
<id property="id" column="s_id"/>
解决方法:https://www.cnblogs.com/junge/p/5145881.html
-->
<result property="name" column="name"/>
</collection>
</resultMap>
</mapper></code></pre>
<p>测试方法:</p>
<pre><code class="java">// 关联表的查询
@Test
public void getClassroomById() {
String statement = "com.itdragon.mapper.ClassroomMapper.getClassroomById";
Classroom classroom = getSqlSession().selectOne(statement, 1);
System.out.println(classroom);
statement = "com.itdragon.mapper.ClassroomMapper.getClassroom2ById";
classroom = getSqlSession().selectOne(statement, 1);
System.out.println(classroom);
statement = "com.itdragon.mapper.ClassroomMapper.getClassroom3ById";
classroom = getSqlSession().selectOne(statement, 1);
System.out.println(classroom);
statement = "com.itdragon.mapper.ClassroomMapper.getClassroom4ById";
classroom = getSqlSession().selectOne(statement, 1);
System.out.println(classroom);
}</code></pre>
<h3>5 动态SQL语句</h3>
<p>这里通过模糊查询 Email 来了解动态SQL语句,在 PersonMapper.xml 中添加如下代码</p>
<pre><code class="xml"><!-- 动态SQL与模糊查询 -->
<!--
需求:通过模糊查询邮箱和指定id范围查询数据
动态SQL:
if:判断语句 <if test=''></if>
where:去掉多余的 and 和 or
<where><if test=''>AND xxx</if></where>
set:去掉多余的 ","
<set><if test=''>xxx , </if></set>
trim: 代替 where , set
if + where == <trim prefix="WHERE" prefixOverrides="AND |OR "></trim>
if + set == <trim prefix="SET" suffixOverrides=","></trim>
choose: (when, otherwise) 类似java的switch case default
<choose><when test="">xxx</when><otherwise>xxx</otherwise></choose>
foreach:类似java的加强for循环
<foreach collection="array" item="xxx" open="(" separator="," close=")"></foreach>
说明:mybatis 提供了自动生成的逆向工程的工具,这里只需要了解即可,虽然是很重要的知识点
学习博客:http://limingnihao.iteye.com/blog/782190
-->
<select id="getPersonLikeKey" parameterType="Person" resultMap="getPersonMap">
select * from person where
<if test='email != "%null%"'>
email like #{email} and
</if>
id > #{id}
</select></code></pre>
<p>测试方法:</p>
<pre><code class="java">// 调用存储过程
@Test
public void getPersonCountGtId(){
String statement = "com.itdragon.mapper.PersonMapper.getPersonCountGtId";
Map<String, Integer> parameterMap = new HashMap<String, Integer>();
parameterMap.put("personId", 1);
parameterMap.put("personCount", -1);
getSqlSession().selectOne(statement, parameterMap);
Integer result = parameterMap.get("personCount");
System.out.println(result);
}</code></pre>
<h3>6 存储过程</h3>
<p>引用百度百科:存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。存储过程是数据库中的一个重要对象。</p>
<p>这里通过获取大于Person id 数量的逻辑来了解Mybatis 是如何调用存储过程的。首先在Mysql 命令行中执行一下代码</p>
<pre><code class="sql">#创建存储过程 传入Id的值,返回id大于该值的数量
DELIMITER $
#在 jpa 数据库中,创建一个名为get_person_count的方法,传入参数是person_id,返回参数是person_count
CREATE PROCEDURE jpa.get_person_count(IN person_id INT, OUT person_count INT)
BEGIN
SELECT COUNT(*) FROM jpa.person WHERE person.id > person_id INTO person_count;
END
$
#调用存储过程
DELIMITER ;
SET @person_count = 0;
CALL jpa.get_person_count(1, @person_count);
SELECT @person_count;</code></pre>
<p>打印结果如下,则说明创建成功了<br><img src="/img/bVYot9?w=716&h=443" alt="806956-20171112162939372-1875581711.png" title="806956-20171112162939372-1875581711.png"></p>
<p>还是在 PersonMapper.xml 文件中添加如下代码</p>
<pre><code class="xml"><!-- 调用存储过程 -->
<!--
通过id,获取大于该id的数量
CALL jpa.get_person_count(1, @person_count);
注意:需关闭二级缓存
Caching stored procedures with OUT params is not supported. Please configure useCache=false in ...
-->
<select id="getPersonCountGtId" parameterMap="getPersonCountMap" statementType="CALLABLE">
CALL jpa.get_person_count(?,?)
</select>
<parameterMap type="java.util.Map" id="getPersonCountMap">
<parameter property="personId" mode="IN" jdbcType="INTEGER"/>
<parameter property="personCount" mode="OUT" jdbcType="INTEGER"/>
</parameterMap></code></pre>
<p>测试方法:</p>
<pre><code class="java">// 调用存储过程
@Test
public void getPersonCountGtId(){
String statement = "com.itdragon.mapper.PersonMapper.getPersonCountGtId";
Map<String, Integer> parameterMap = new HashMap<String, Integer>();
parameterMap.put("personId", 1);
parameterMap.put("personCount", -1);
getSqlSession().selectOne(statement, parameterMap);
Integer result = parameterMap.get("personCount");
System.out.println(result);
}</code></pre>
<h3>7 一二级缓存</h3>
<p>一级缓存:基于PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该Session中的所有 Cache 就将清空。</p>
<p> ① 若Session 被关闭了,缓存清空</p>
<p> ② 若数据执行了 创建,更新,删除操作,缓存清空</p>
<p> ③ 如果不是同一个Session,缓存失效</p>
<p>二级缓存:与一级缓存其机制相同,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。</p>
<p> ① 默认是关闭的</p>
<p> ② 是一个映射文件级的缓存,</p>
<p> ③ 开启二级缓存 <cache/></p>
<p>还是在 PersonMapper.xml 文件中添加如下代码</p>
<pre><code class="xml"><!-- 开启二级缓存 -->
<!--
eviction="FIFO" 回收策略为先进先出
flushInterval="60000" 自动刷新时间60s
size="512" 最多缓存512个引用对象
readOnly="true" 只读
-->
<cache
eviction="FIFO"
flushInterval="60000"
size="1024"
readOnly="true"/></code></pre>
<p>打印结果如下</p>
<pre><code class="java">log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
[Person [id=2, email=cyy@qq.com, lastName=cyy]]
1
0
Classroom [id=1, room=JavaEE, teacher=Teacher [id=1, subject=Java], students=null]
Classroom [id=1, room=JavaEE, teacher=Teacher [id=1, subject=Java], students=null]
Classroom [id=1, room=JavaEE, teacher=Teacher [id=1, subject=Java], students=[Student [id=1, name=ITDragon], Student [id=2, name=Marry]]]
Classroom [id=1, room=JavaEE, teacher=Teacher [id=1, subject=Java], students=[Student [id=null, name=ITDragon], Student [id=null, name=Marry]]]
[Person [id=1, email=lxl@qq.com, lastName=null], Person [id=2, email=cyy@qq.com, lastName=null], Person [id=3, email=itdrgon@qq.com, lastName=null]]
Person [id=2, email=cyy@qq.com, lastName=null]
Person [id=2, email=cyy@qq.com, lastName=cyy]
Person [id=2, email=cyy@qq.com, lastName=cyy]
2
1</code></pre>
<p>源码地址:<a href="https://link.segmentfault.com/?enc=KnDY2B%2FOzY12AqfUAlFZGg%3D%3D.QKQQOwn5K9jxnrARIFHFbt8VVupxubN4iziQjf2NhGsKiPcFxrhH46K3NCVxZsg7qpV9ehzfBtvRvJ0fie6szwxr3JQB08MoobSF0CJSSRU%3D" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>到这里,Mybatis 的入门知识就讲完了。如果大家觉得不错,可以关注我!后续还有很多不错的内容提供。</p>
select2 取值 遍历 设置默认值
https://segmentfault.com/a/1190000011931289
2017-11-09T17:02:14+08:00
2017-11-09T17:02:14+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>select2 取值 遍历 设置默认值</h2>
<p>本章内容主要介绍Select2 的初始化,获取选中值,设置默认值,三个方法。Select2 美化了单选框,复选框和下拉框,特别是下拉框多选的问题。但同时,Select2也有很多吭。<br>效果图:<br><img src="/img/bVYd1T?w=896&h=184" alt="图片描述" title="图片描述"></p>
<hr>
<p>需求:使用Select2实现下拉框多选,并获取选中值,初始设置默认值<br>技术:select2.js ,prototype.js,jquery.js<br>源码:<a href="https://link.segmentfault.com/?enc=aA%2B%2BHmrAN%2BuyjvS2ZzXPDg%3D%3D.IZHegCK7%2BQ8enbnxLBKEbD2wEW%2BKyPVRsdxvw6KTKh8y5Qci%2FI3%2FaMYPsR%2FXdouLljCKSE%2FrMZW3fVZk9AOsJpo4GXGD3xcfdlfLkjREaY5J%2F81H28F4dLBDY0NknouH" rel="nofollow">https://github.com/ITDragonBl...</a><br>说明:select2是jquery插件,取值和设置默认值都可以用jquery单独完成。为什么用prototype.js ?因为在公司用prototype.js 写的,笔者因为各种原因,没有用jquery重写(原谅我比较懒)。还有一点值得注意:获取的文本值可能有空格哦!!!笔者就是被吭了一脸!<br>一切尽在代码中:</p>
<pre><code class="html"><!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gbk" />
<title>select2实例</title>
<<link rel="stylesheet" href="bootstrap/3.3.0/css/bootstrap.min.css" type="text/css" />
<link rel="stylesheet" href="select2-4.0.0/dist/css/select2.min.css" type="text/css" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.4/css/select2.min.css" rel="stylesheet" />
<script type="text/javascript" src="js/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="js/prototype.js"></script>
<script type="text/javascript" src="bootstrap/3.3.0/js/bootstrap.min.js"></script>
<script type="text/javascript" src="select2-4.0.0/dist/js/select2.min.js"></script>
</head>
<body>
<label class="control-label col-sm-1">个性标签(checkbox): </label>
<div class="col-sm-3">
<select class="select_gallery-multiple" multiple="multiple" style="width:100%;" onchange="getSelectData()" id="mul-itdragon">
<optgroup label="这样真的好么?">
<option value="0">打野</option>
<option value="01">上单</option>
<option value="02">中单</option>
<option value="03">送人头</option>
</optgroup>
<optgroup label="职位">
<option value="1">土豪</option>
<option value="2">屌丝</option>
<option value="3">单身dog</option>
<option value="4">苹果粉</option>
<option value="5">苦逼程序员</option>
</optgroup>
</select>
</div>
<label class="control-label col-sm-1">个性标签(radio): </label>
<div class="col-sm-3">
<select class="select_gallery" style="width:100%;" id="itdragon">
<optgroup label="这样真的好么?">
<option value="0">打野</option>
<option value="01">上单</option>
<option value="02">中单</option>
<option value="03">送人头</option>
</optgroup>
<optgroup label="职位">
<option value="1">土豪</option>
<option value="2">屌丝</option>
<option value="3">单身dog</option>
<option value="4">苹果粉</option>
<option value="5">苦逼程序员</option>
</optgroup>
</select>
</div>
<script type="text/javascript">
var $jq = jQuery.noConflict();
// 初始化多选select2
$jq(".select_gallery-multiple").select2();
// 初始化单选select2
$jq(".select_gallery").select2();
// 默认选择
select2ByText ("mul-itdragon", "苦逼程序员");
select2ByValue ("itdragon", "03");
// 通过id获取select2的value值
function getSelect2Value(obj) {
var select2Obj = $jq('#'+obj).select2();
return select2Obj.select2("val");
}
// 通过id获取select2的text值,这里的text值可能有空格,需注意
function getSelect2Text(obj) {
var select2Obj = $jq('#'+obj).select2();
return select2Obj.find("option:selected").text();
}
// 通过text 设置select2的默认值
function select2ByText (obj, text) {
var select2Obj = $jq('#'+obj).select2();
$(obj).select("option").each(function(data){
if (text == data.text.strip()) {
select2Obj.val(data.value).trigger("change");
}
});
}
// 通过value 设置select2的默认值
function select2ByValue (obj, value) {
var select2Obj = $jq('#'+obj).select2();
select2Obj.val(value).trigger("change");
}
function getSelectData(){
console.log(getSelect2Value("mul-itdragon"));
console.log(getSelect2Text("itdragon"));
var mulItdragonVal = $jq("#mul-itdragon").select2("val");
if (null == mulItdragonVal) {
console.log("Over !");
return ;
}
console.log(mulItdragonVal);
var mulItdragonData = $jq("#mul-itdragon").select2('data');
mulItdragonData.each(function(data){
console.log("value : ", data.id);
console.log("text : ", data.text);
});
}
</script>
</body>
</html></code></pre>
<p>这样就做好了,是不是很简单,如果不能满足你的需求,可以去官网学习:<a href="https://link.segmentfault.com/?enc=7L6pbcTKmBLhBNfyxSRrjw%3D%3D.MxnsaGPV46UQiB%2BeaUB%2BU4qoqIZwAiX09c3zKiIIMCp%2F8lzZ7rX3RbDxBFY9Zokw" rel="nofollow">http://select2.github.io/exam...</a></p>
Maven3 快速入门
https://segmentfault.com/a/1190000011864334
2017-11-05T17:03:23+08:00
2017-11-05T17:03:23+08:00
itdragon
https://segmentfault.com/u/itdragon
1
<h2>Maven3 快速入门</h2>
<p>Maven 是目前大型项目构建的必备知识。本章会通过介绍 Maven 的作用,Maven 的基本语法,以及搭建企业级项目架构来快速入门 Maven 。前两部分是理论知识只需要了解,第三部分是实战操作,请把重心和精力放在最后。</p>
<hr>
<h2>1 为什么用 Maven</h2>
<p>一个基本web项目是从 视图层(H5,CSS,Js等前端技术) 到 控制层(SpringMVC,Struts2) 到 事务处理层(Spring IOC,AOP) 再到 持久层(SpringData,Hibernate,Mybatis) 最后到 数据库(Mysql,Oracle,Mongodb等) 。咦!!!好像没有Maven什么事?</p>
<p>但我们试想:</p>
<p>① 如果给一个项目添加 jar 包,我们是不是手动COPY到WEB-INF/lib 目录下的?</p>
<p>② 如果 jar 包之间发生依赖问题和版本冲突,我们是不是抓耳挠腮,45度仰望天空,怀疑人生?</p>
<p>③ 如果项目多了,相同 jar 包占用的存储空间会越来越大,我们是不是要犯强迫症了?</p>
<p>...... 怎么解决?</p>
<p>① 其实我们可以借助Maven,使其以一种规范的方式下载设置的jar包(减少我们手动 COPY 的过程),</p>
<p>② 值得疯狂的是:Maven 在下载jar包的同时,还会自动将被依赖的jar导入(减少我们解决 jar 包依赖的精力),</p>
<p>③ 我们可以设置一个Maven pom 父项目来管理jar包,让其他项目继承它(减少 jar 包冗余占用存储空间的问题)</p>
<p>当项目越来越复杂,规模越来越大的时候,Maven的作用就会越来越明显!!!</p>
<p>Maven 是一款服务于 Java 平台用的自动化构建工具,同时它也是用 Java 编写的。</p>
<p>构建的流程主要分为七个步骤:</p>
<p>① 清理:删除之前编译的文件(如 .class 文件)</p>
<p>② 编译:将 .java 文件转化成 JVM 可读的 .class 文件</p>
<p>③ 测试:对一些核心关键方法进行测试</p>
<p>④ 报告:对测试结果进行报告,如:运行了几个测试方法,失败了几个,跳过了几个</p>
<p>⑤ 打包:若测试失败,则打包也会失败。普通的 Java 项目是打包成 jar 包,而 web 项目则是 war 包</p>
<p>⑥ 安装:将打好的包,安装到本地仓库中</p>
<p>⑦ 部署:将打好的包,部署到服务器上运行</p>
<h2>2 Maven 基础语法</h2>
<p>Maven 之所以能实现自动化构建。和以下几个基本要素密不可分:① pom.xml 文件 ; ② 项目结构; ③ 坐标;④ 依赖管理;⑤ 仓库管理;⑥ 生命周期;⑦ 插件和目标; ⑧ 继承和聚合</p>
<h3>2.1 pom.xml 文件</h3>
<p>pom 的全称是 Project Object Model (项目对象模型),pom.xml 是 Maven 项目的核心文件,jar包的依赖管理,坐标的定义,插件的设置都在该文件中。</p>
<h3>2.2 项目结构</h3>
<p>如果你的项目结构不是按照 Maven 的要求来。就不会实现自动化构建的。其项目结构如下:</p>
<pre><code class="xml">根目录 // 项目名
|---src // 源码
|---|---main // 主程序
|---|---|---java // 主程序Java 源码
|---|---|---resources // 主程序资源文件
|---|---test // 测试程序
|---|---|---java
|---|---|---resources
|---pom.xml // Maven 项目核心配置文件</code></pre>
<h3>2.3 坐标</h3>
<p>Maven 通过三个参数(groupId, artifactId, version)来确定唯一的 Maven 项目</p>
<p>groupId:组名,一般是 域名 + 公司/组织名 + 项目名</p>
<pre><code><groupId>com.itdragon</groupId></code></pre>
<p>artifactId:模块名</p>
<pre><code><artifactId>itdragon-parent</artifactId></code></pre>
<p>version:版本号</p>
<pre><code><version>0.0.1-SNAPSHOT</version></code></pre>
<h3>2.4 依赖管理</h3>
<p>compile 范围依赖: 对主程序 和 测试程序都有效,参加打包和部署</p>
<p>test 范围依赖: 只对测试程序有效,不参与打包和部署</p>
<p>provided 范围依赖: 对主程序 和 测试程序都有效,不参与打包和部署</p>
<h3>2.5 仓库管理</h3>
<p>Maven 仓库分两类:一是本地仓库;另外一个是远程仓库。</p>
<p>本地仓库:默认路径是 用户/.m2/repository</p>
<p>远程仓库:需要联网,可以直接去中央仓库下载,如果嫌慢还可以考虑中央仓库的镜像,也可以在局域网内配置私服 Nexus 。</p>
<p>私服 Nexus 的工作原理:因为在局域网内访问的速度远比访问外网要快。如果在一台机器上下载好 jar 包,其他同事则可以通过局域网高速下载该机器上的jar 包。若私服 Nexus 里面也没有,则会先去外网下载到私服 Nexus 上。有点类似把资源下载到自己电脑上,其他人用U盘开拷贝。</p>
<p>本地仓库中不仅有第三方的 jar 包,我们还可以将项目(jar / war)打包到仓库中</p>
<h3>2.6 生命周期</h3>
<p>Maven 生命周期定义了各个构建环节执行的先后顺序。只有这样,Maven才能正常完成自动化构建的功能。(大致了解即可)</p>
<h3>2.7 插件和目标</h3>
<p>① Maven 的核心仅仅是定义了抽象的生命周期,具体任务都是交给插件来完成</p>
<p>② 每个插件都可以实现多个功能,每个功能就是一个插件目标</p>
<p>③ Maven的生命周期与插件目标相互绑定,以完成某个具体的构建任务</p>
<h3>2.8 继承和聚合</h3>
<p>继承:为了很好的管理 jar 包,和解决 jar 包冗余问题。会创建一个 pom 的父项目,其他系统继承该父项目。</p>
<p>聚合:如果每个项目打包很麻烦,我们可以创建一个聚合项目,来批量执行 Maven 的命令。具体操作看第四部分 搭建企业级项目</p>
<pre><code><modules>
<module>itdragon-manager-pojo</module>
<module>itdragon-manager-mapper</module>
<module>itdragon-manager-service</module>
<module>itdragon-manager-web</module>
</modules></code></pre>
<h2>3 搭建企业级项目</h2>
<p>需求:通过整合 SSM 框架查询Mysql数据</p>
<p>技术:Spring , SpringMVC , Mybatis三大框架的整合,Druid 连接池连接 Mysql 数据库</p>
<p>环境:jdk1.8 , ide 是 sts(sts 使用和eclipse 几乎一样,笔者因为学习spring cloud 时换成了sts,有点吃内存)</p>
<p>步骤:① 搭建管理 jar 包的 itdragon-parent 项目;② 搭建公共方法的 itdragon-common 项目;③ 搭建模块开发的 itdragon-manager 项目;④ 创建四个模块项目 (pojo,mapper,service,web)</p>
<p>源码:<a href="https://link.segmentfault.com/?enc=%2FVcmBqT9r4Yii%2BYOFlL2%2Bw%3D%3D.3ax5sSlffZ3T3%2F7OG9iLLb0HghaG%2BkTS%2F7uk7sSYlp8p72IHsRyrCNRFUVjGVoh8" rel="nofollow">https://github.com/ITDragonBl...</a> 先maven install itdragon-parent 项目,然后是 itdragon-common 项目,最后是 maven build...(clean tomcat7:run) itdragon-manager 项目</p>
<p>结构:<br><img src="/img/bVXWBe?w=365&h=161" alt="806956-20171105150421826-789067331.png" title="806956-20171105150421826-789067331.png"></p>
<p>① 搭建管理 jar 包的 itdragon-parent 项目</p>
<p>第一步:创建一个 pom 项目 itdragon-parent ,该项目管理 jar 包的版本<br><img src="/img/bVXWBf?w=650&h=583" alt="806956-20171104213844779-1973742205.png" title="806956-20171104213844779-1973742205.png"></p>
<p>第二步:修改 pom.xml 文件,配置 jar 的版本和插件</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itdragon</groupId>
<artifactId>itdragon-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- 集中定义依赖版本号 -->
<properties>
<junit.version>4.12</junit.version>
<spring.version>4.1.3.RELEASE</spring.version>
<mybatis.version>3.2.8</mybatis.version>
<mybatis.spring.version>1.2.2</mybatis.spring.version>
<mybatis.paginator.version>1.2.15</mybatis.paginator.version>
<mysql.version>5.1.6</mysql.version>
<slf4j.version>1.6.4</slf4j.version>
<jackson.version>2.4.2</jackson.version>
<druid.version>1.0.9</druid.version>
<jstl.version>1.2</jstl.version>
<servlet-api.version>2.5</servlet-api.version>
<jsp-api.version>2.0</jsp-api.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-io.version>1.3.2</commons-io.version>
<commons-net.version>3.3</commons-net.version>
</properties>
<!-- 只定义依赖的版本,并不实际依赖 -->
<dependencyManagement>
<dependencies>
<!-- Apache工具组件 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<!-- Jackson Json处理工具包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- 日志处理 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<dependency>
<groupId>com.github.miemiedev</groupId>
<artifactId>mybatis-paginator</artifactId>
<version>${mybatis.paginator.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- JSP相关 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<version>${jsp-api.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project></code></pre>
<p>② 搭建公共方法的 itdragon-common 项目</p>
<p>第一步:创建一个 common 项目 itdragon-common ,该项目是一个 Java 项目,存放一下项目常用的工具类<br><img src="/img/bVXWBt?w=650&h=583" alt="806956-20171104213700295-130317446.png" title="806956-20171104213700295-130317446.png"></p>
<p>③ 搭建模块开发的 itdragon-manager 项目</p>
<p>第一步:创建一个 pom 聚合工程, itdragon-manager<br><img src="/img/bVXWBC?w=650&h=583" alt="806956-20171104214026029-1942575381.png" title="806956-20171104214026029-1942575381.png"></p>
<p>④ 创建四个模块项目 (pojo,mapper,service,web)</p>
<p>创建一个 itdragon-manager-pojo 项目,管理项目实体类模块。 新增 Maven Model项目<br><img src="/img/bVXWBD?w=650&h=581" alt="806956-20171104214348982-684397000.png" title="806956-20171104214348982-684397000.png"></p>
<p>第一步:通过 Mybatis 的逆向工程生成 Person.java 和 PersonExample.java(因为是自动生产的而且太长了,这里就不贴出来了)。 pom.xml 不用修改。</p>
<pre><code class="java">package com.itdragon.pojo;
public class Person {
private Integer id;
private String email;
private String lastName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email == null ? null : email.trim();
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName == null ? null : lastName.trim();
}
}</code></pre>
<p>PersonExample.java : <a href="https://link.segmentfault.com/?enc=vxsZdwyP%2Bac4U8uHpM6tig%3D%3D.j0q7JpfUl3154b7TwHzhTuKrOdaa7Cwq7ufnkJ7Kff35HE23LTlhVc1XFGHxr8e5EUaEeupeIzPCSxIecO0STQv2bNxUa%2FBAYBuounp%2BSdiIb6rUrAJlzoPBmCm8vKY6NNkOtqdUk7KHNHKpwbxCXgpDQddKdZtn1isoLK5Cq2yILnDBj%2Brplouyu4OJOTEJ" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=2GEEB0aKuSv2GsMhjmZKbg%3D%3D.2ZQ7T6k4ORMZ%2B6PvnsKmEhUQagcCNF%2BBaNyUtIoH8hIVec%2B0doHU3vV94TdV7qnqPkPQWJ8Zc5p0XJJ84PUZqViZomBWLawXaco09NJs8a3WkzR%2F4csGO7hG2jIJ3jShDjLNCzDtX8Bzw8CYiQtwJjknawGjeRBZv8s7RrRtrAn2otF2SpjVYtMVp%2FbszMqO" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>创建一个 itdragon-manager-mapper 项目,管理项目 Dao 模块 。创建方法同上。</p>
<p>第一步:通过 Mybatis 逆向工程生成 PersonMapper.java 和 PersonMapper.xml(因为是自动生产的而且太长了,这里就不贴出来了)</p>
<pre><code class="java">package com.itdragon.mapper;
import com.itdragon.pojo.Person;
import com.itdragon.pojo.PersonExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface PersonMapper {
int countByExample(PersonExample example);
int deleteByExample(PersonExample example);
int deleteByPrimaryKey(Integer id);
int insert(Person record);
int insertSelective(Person record);
List<Person> selectByExample(PersonExample example);
Person selectByPrimaryKey(Integer id);
int updateByExampleSelective(@Param("record") Person record, @Param("example") PersonExample example);
int updateByExample(@Param("record") Person record, @Param("example") PersonExample example);
int updateByPrimaryKeySelective(Person record);
int updateByPrimaryKey(Person record);
}</code></pre>
<p>PersonMapper.xml:<a href="https://link.segmentfault.com/?enc=lSPv9gAz6MQW9XKrIo%2Fwgw%3D%3D.z7jhPIBPaqIFnBgbUtvstewg9XbMn4MNVoXm1OauE4jFBWJOjPPDTKaPHD7mGvtho%2B9YLktOCO0A%2BsZGq7cIde9ImIWIFh%2BCihMdEQbKGIbmeeu%2F1mDUTFsMvzjJ1GMO11VnkHk3yzQuO0JlG4JKGlNdttTOcUhnhHzCy7xU5f1NXoHB%2Bo1WCo3iDe3hb6Re" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=LvSDJAjlIJtqJpyP1P9zsw%3D%3D.dgVmNaqFGimM4iPIJIq1SveOk4uZRvcZ66f8W50XShTcZBtN1EfO%2BR5tluaUScgVKSlBuwvQGMaWsPBKxd5D%2F2U1KBnCJz93eWWzxUWuTa9%2B9Z6roQRBNffkMW2opAUMqTI2FTrT6adBQDXEgfRXRonsYmy4AJAGp%2FclMaf0exazOCY6cZtBh7o8Jg6SSg0m" rel="nofollow">https://github.com/ITDragonBl...</a></p>
<p>第二步:修改 pom.xml 文件。</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.itdragon</groupId>
<artifactId>itdragon-manager</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>itdragon-manager-mapper</artifactId>
<!-- 依赖管理 -->
<dependencies>
<dependency>
<groupId>com.itdragon</groupId>
<artifactId>itdragon-manager-pojo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
<dependency>
<groupId>com.github.miemiedev</groupId>
<artifactId>mybatis-paginator</artifactId>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
</dependencies>
</project></code></pre>
<p>创建一个 itdragon-manager-service 项目。进行事务的管理,创建方法和上面一样。</p>
<p>第一步:创建 PersonService.java 和 PersonServiceImpl.java</p>
<pre><code class="java">package com.itdragon.service;
import com.itdragon.pojo.Person;
public interface PersonService {
Person findOneById(Integer id);
}</code></pre>
<pre><code class="java">package com.itdragon.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.itdragon.mapper.PersonMapper;
import com.itdragon.pojo.Person;
import com.itdragon.service.PersonService;
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
private PersonMapper personMapper;
@Override
public Person findOneById(Integer id) {
return personMapper.selectByPrimaryKey(id);
}
}</code></pre>
<p>第二步:修改 pom.xml 文件</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.itdragon</groupId>
<artifactId>itdragon-manager</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>itdragon-manager-service</artifactId>
<!-- 依赖管理 -->
<dependencies>
<dependency>
<groupId>com.itdragon</groupId>
<artifactId>itdragon-manager-mapper</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
</dependencies>
</project></code></pre>
<p>创建一个 itdragon-manager-web 项目。这里整合 Spring , SpringMVC, Mybatis </p>
<p>第一步:创建 Maven 的 web 项目 (很重要)</p>
<p>① 创建一个 Maven Project 项目,选择 war 包</p>
<p>② 右击项目,选择 properties ,选择 Project Facets</p>
<p>③ 取消 Dynamic Web Model ,然后点击 Apply ,然后选择 Dynamic Web Model,最后点击 Further configuration available...</p>
<p>④ 设置 web.xml 的目录<br><img src="/img/bVXWBS?w=650&h=581" alt="806956-20171104214453513-195794068.png" title="806956-20171104214453513-195794068.png"></p>
<p><img src="/img/bVXWBT?w=1097&h=703" alt="806956-20171104215036670-388546917.png" title="806956-20171104215036670-388546917.png"></p>
<p><img src="/img/bVXWB0?w=525&h=444" alt="806956-20171104215108716-1762076559.png" title="806956-20171104215108716-1762076559.png"></p>
<p>第二步:整合 Spring ,SpringMVC,Mybatis</p>
<p><img src="/img/bVXWB1?w=294&h=424" alt="806956-20171105145715357-1416116304.png" title="806956-20171105145715357-1416116304.png"></p>
<p>SqlMapConfig.xml ,mybatis 配置文件</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
</configuration></code></pre>
<p>db.properties,数据库配置文件</p>
<pre><code class="xml">jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jpa?characterEncoding=utf-8
jdbc.username=root
jdbc.password=root</code></pre>
<p>applicationContext-dao.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 数据库连接池 -->
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:resource/*.properties" />
<!-- 数据库连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="driverClassName" value="${jdbc.driver}" />
<property name="maxActive" value="10" />
<property name="minIdle" value="5" />
</bean>
<!-- 配置sqlsessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml"></property>
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置扫描包,加载mapper代理对象 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.itdragon.mapper"></property>
</bean>
</beans></code></pre>
<p>applicationContext-service.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 扫描包加载Service实现类 -->
<context:component-scan base-package="com.itdragon.service"></context:component-scan>
</beans></code></pre>
<p>applicationContext-trans.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="create*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<tx:method name="select*" propagation="SUPPORTS" read-only="true" />
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 切面 -->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.itdragon.service.*.*(..))" />
</aop:config>
</beans></code></pre>
<p>springmvc.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.itdragon.controller" />
<mvc:annotation-driven />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
</beans></code></pre>
<p>web.xml</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="itdragon" version="2.5">
<display-name>itdragon-manager</display-name>
<!-- 加载spring容器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/applicationContext-*.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 解决post乱码 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- springmvc的前端控制器 -->
<servlet>
<servlet-name>itdragon-manager</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>itdragon-manager</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app></code></pre>
<p>运行:右击 itdragon-manager 项目,选择run as ,选择 maven build... ,输入 clean tomcat7:run 即可 <br><img src="/img/bVXWB9?w=800&h=640" alt="806956-20171105162606826-1270096671.png" title="806956-20171105162606826-1270096671.png"></p>
<p>效果图:项目构建的日志信息这里就不贴出来了,内容已经很长了。<br><img src="/img/bVXWCd?w=329&h=179" alt="806956-20171105161538888-1968520080.gif" title="806956-20171105161538888-1968520080.gif"></p>
<h2>4 总结</h2>
<p>① 介绍了 Maven 是如何辅助 Java 项目开发,能帮助我们高效高质地完成开发。</p>
<p>② 介绍了 Maven 的基础语法如,如Maven 的坐标,继承,依赖,聚合,等都是常用语法。</p>
<p>③ 介绍了 整合 SSM 框架中 Maven 起到的作用。</p>
<p>到这里Maven的入门就结束了,笔者还会有很多Java 相关的技术博客,希望大家能关注我。如果有什么不对的地方,请指正!</p>
FreeMarker 快速入门
https://segmentfault.com/a/1190000011768799
2017-10-29T21:49:42+08:00
2017-10-29T21:49:42+08:00
itdragon
https://segmentfault.com/u/itdragon
42
<h2>FreeMarker 快速入门</h2>
<p>FreeMarker是一个很值得去学习的模版引擎。它是基于模板文件生成其他文本的通用工具。本章内容通过如何使用FreeMarker生成Html web 页面 和 代码自动生成工具来快速了解FreeMarker。</p>
<h3>1 简介</h3>
<p>FreeMarker是一款用java语言编写的模版引擎,它虽然不是web应用框架,但它很合适作为web应用框架的一个组件。</p>
<p>特点:</p>
<ol>
<li>轻量级模版引擎,不需要Servlet环境就可以很轻松的嵌入到应用程序中</li>
<li>能生成各种文本,如html,xml,java,等</li>
<li>入门简单,它是用java编写的,很多语法和java相似</li>
</ol>
<p>工作原理:(借用网上的图片)<br><img src="/img/bVXxK1?w=416&h=191" alt="806956-20171029172224289-1714466396.png" title="806956-20171029172224289-1714466396.png"></p>
<h3>2 FreeMarker 程序</h3>
<p>这里通过模拟简单的代码自动生产工具来感受第一个FreeMarker程序。</p>
<p>项目目录结构<br><img src="/img/bVXxK5?w=256&h=177" alt="806956-20171029173200820-322396228.png" title="806956-20171029173200820-322396228.png"></p>
<p>项目创建流程</p>
<p>第一步:创建一个maven项目导入 FreeMarker jar 包</p>
<p>第二步:创建目录templates,并创建一个 FreeMarker模版文件 hello.ftl</p>
<p>第三步:创建一个运行FreeMarker模版引擎的 FreeMarkerDemo.java 文件</p>
<p>第四步:运行main方法后刷新项目</p>
<p>pom.xml 文件 ,maven 项目核心文件,管理 jar 包。</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.freemark</groupId>
<artifactId>freemarkerStudy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.20</version>
</dependency>
</dependencies>
</project></code></pre>
<p>hello.ftl FreeMarker基本语法: ${xxx} xxx 相当于占位符,java后台给xxx赋值后,再通过${}输出</p>
<pre><code class="xml">package ${classPath};
public class ${className} {
public static void main(String[] args) {
System.out.println("${helloWorld}");
}
}</code></pre>
<p>FreeMarkerDemo.java 核心方法,使用 FreeMarker 模版引擎。</p>
<pre><code class="java">package com.freemark.hello;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import freemarker.template.Configuration;
import freemarker.template.Template;
/**
* 最常见的问题:
* java.io.FileNotFoundException: xxx does not exist. 解决方法:要有耐心
* FreeMarker jar 最新的版本(2.3.23)提示 Configuration 方法被弃用
* 代码自动生产基本原理:
* 数据填充 freeMarker 占位符
*/
public class FreemarkerDemo {
private static final String TEMPLATE_PATH = "src/main/java/com/freemark/hello/templates";
private static final String CLASS_PATH = "src/main/java/com/freemark/hello";
public static void main(String[] args) {
// step1 创建freeMarker配置实例
Configuration configuration = new Configuration();
Writer out = null;
try {
// step2 获取模版路径
configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));
// step3 创建数据模型
Map<String, Object> dataMap = new HashMap<String, Object>();
dataMap.put("classPath", "com.freemark.hello");
dataMap.put("className", "AutoCodeDemo");
dataMap.put("helloWorld", "通过简单的 <代码自动生产程序> 演示 FreeMarker的HelloWorld!");
// step4 加载模版文件
Template template = configuration.getTemplate("hello.ftl");
// step5 生成数据
File docFile = new File(CLASS_PATH + "\\" + "AutoCodeDemo.java");
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(docFile)));
// step6 输出文件
template.process(dataMap, out);
System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^AutoCodeDemo.java 文件创建成功 !");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != out) {
out.flush();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}</code></pre>
<p>运行程序后刷新项目,会发现多了一个AutoCodeDemo.java类。不仅仅是java类,xml也是可以。笔者就是通过FreeMarker做了一个简易的工具类,公司的一个标准管理页面及其增删改查等功能,以及相关的配置文件(十三个文件),一个回车就全部自动生成(偷懒ing)。</p>
<h3>3 FreeMarker 语法</h3>
<p>语法和java很类似,其中宏的概念可能比较陌生,先上代码<br><img src="/img/bVXxK9?w=229&h=160" alt="806956-20171029180814305-2003141104.png" title="806956-20171029180814305-2003141104.png"></p>
<p>stringFreeMarker.ftl FreeMarker主要核心知识点</p>
<pre><code>字符串输出:
${"Hello ${name} !"} / ${"Hello " + name + " !"}
<#assign cname=r"特殊字符完成输出(http:\www.baidu.com)">
${cname}
字符串截取 :
通过下标直接获取下标对应的字母: ${name[2]}
起点下标..结尾下标截取字符串:${name[0..5]}
算数运算:
<#-- 支持"+"、"-"、"*"、"/"、"%"运算符 -->
<#assign number1 = 10>
<#assign number2 = 5>
"+" : ${number1 + number2}
"-" : ${number1 - number2}
"*" : ${number1 * number2}
"/" : ${number1 / number2}
"%" : ${number1 % number2}
比较运算符:
<#if number1 + number2 gte 12 || number1 - number2 lt 6>
"*" : ${number1 * number2}
<#else>
"/" : ${number1 / number2}
</#if>
内建函数:
<#assign data = "abcd1234">
第一个字母大写:${data?cap_first}
所有字母小写:${data?lower_case}
所有字母大写:${data?upper_case}
<#assign floatData = 12.34>
数值取整数:${floatData?int}
获取集合的长度:${users?size}
时间格式化:${dateTime?string("yyyy-MM-dd")}
空判断和对象集合:
<#if users??>
<#list users as user >
${user.id} - ${user.name}
</#list>
<#else>
${user!"变量为空则给一个默认值"}
</#if>
Map集合:
<#assign mapData={"name":"程序员", "salary":15000}>
直接通过Key获取 Value值:${mapData["name"]}
通过Key遍历Map:
<#list mapData?keys as key>
Key: ${key} - Value: ${mapData[key]}
</#list>
通过Value遍历Map:
<#list mapData?values as value>
Value: ${value}
</#list>
List集合:
<#assign listData=["ITDragon", "blog", "is", "cool"]>
<#list listData as value>${value} </#list>
include指令:
引入其他文件:<#include "otherFreeMarker.ftl" />
macro宏指令:
<#macro mo>
定义无参数的宏macro--${name}
</#macro>
使用宏macro: <@mo />
<#macro moArgs a b c>
定义带参数的宏macro-- ${a+b+c}
</#macro>
使用带参数的宏macro: <@moArgs a=1 b=2 c=3 />
命名空间:
<#import "otherFreeMarker.ftl" as otherFtl>
${otherFtl.otherName}
<@otherFtl.addMethod a=10 b=20 />
<#assign otherName="修改otherFreeMarker.ftl中的otherName变量值"/>
${otherFtl.otherName}
<#assign otherName="修改otherFreeMarker.ftl中的otherName变量值" in otherFtl />
${otherFtl.otherName}</code></pre>
<p>otherFreeMarker.ftl 为了测试命名空间 和 include 指令的FreeMarker文件</p>
<pre><code>其他FreeMarker文件
<#macro addMethod a b >
result : ${a + b}
</#macro>
<#assign otherName="另外一个FreeMarker的变量"></code></pre>
<p>FreeMarkerDemo.java 核心方法</p>
<pre><code class="java">package com.freemark.demo;
import java.util.List;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Date;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import freemarker.template.Configuration;
import freemarker.template.Template;
public class FreeMarkerDemo {
private static final String TEMPLATE_PATH = "src/main/java/com/freemark/demo/templates";
public static void main(String[] args) {
// step1 创建freeMarker配置实例
Configuration configuration = new Configuration();
Writer out = null;
try {
// step2 获取模版路径
configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));
// step3 创建数据模型
Map<String, Object> dataMap = new HashMap<String, Object>();
dataMap.put("name", "itdragon博客");
dataMap.put("dateTime", new Date());
List<User> users = new ArrayList<User>();
users.add(new User(1, "ITDragon 博客"));
users.add(new User(2, "欢迎"));
users.add(new User(3, "You!"));
dataMap.put("users", users);
// step4 加载模版文件
Template template = configuration.getTemplate("stringFreeMarker.ftl");
// step5 生成数据
out = new OutputStreamWriter(System.out);
// step6 输出文件
template.process(dataMap, out);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != out) {
out.flush();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}</code></pre>
<p>User.java 为了测试 FreeMarker的集合对象</p>
<pre><code class="java">package com.freemark.demo;
public class User {
private Integer id;
private String name;
public User() {
}
public User(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + "]";
}
}</code></pre>
<p>最后的打印结果</p>
<pre><code>字符串输出:
Hello itdragon博客 ! / Hello itdragon博客 !
特殊字符完成输出(http:\www.baidu.com)
字符串截取 :
通过下标直接获取下标对应的字母: d
起点下标..结尾下标截取字符串:itdrag
算数运算:
"+" : 15
"-" : 5
"*" : 50
"/" : 2
"%" : 0
比较运算符:
"*" : 50
内建函数:
第一个字母大写:Abcd1234
所有字母小写:abcd1234
所有字母大写:ABCD1234
数值取整数:12
获取集合的长度:3
时间格式化:2017-10-29
空判断和对象集合:
1 - ITDragon 博客
2 - 欢迎
3 - You!
Map集合:
直接通过Key获取 Value值:程序员
通过Key遍历Map:
Key: name - Value: 程序员
Key: salary - Value: 15,000
通过Value遍历Map:
Value: 程序员
Value: 15,000
List集合:
ITDragon blog is cool
include指令:
其他FreeMarker文件
macro宏指令:
使用宏macro: 定义无参数的宏macro--itdragon博客
使用带参数的宏macro: 定义带参数的宏macro-- 6
命名空间:
另外一个FreeMarker的变量
result : 30
另外一个FreeMarker的变量
修改otherFreeMarker.ftl中的otherName变量值</code></pre>
<h4>语法详解</h4>
<p><strong>数据类型</strong><br>和java不同,FreeMarker不需要定义变量的类型,直接赋值即可。<br>字符串: value = "xxxx" 。如果有特殊字符 string = r"xxxx" 。单引号和双引号是一样的。<br>数值:value = 1.2。数值可以直接等于,但是不能用科学计数法。<br>布尔值:true or false。<br>List集合:list = [1,2,3] ; list=[1..100] 表示 1 到 100 的集合,反之亦然。<br>Map集合:map = {"key" : "value" , "key2" : "value2"},key 必须是字符串哦!<br>实体类:和EL表达式差不多,直接点出来。</p>
<p><strong>字符串操作</strong><br>字符串连接:可以直接嵌套${"hello , ${name}"} ; 也可以用加号${"hello , " + name}</p>
<p>字符串截取:string[index]。index 可以是一个值,也可以是形如 0..2 表示下标从0开始,到下标为2结束。一共是三个数。</p>
<p><strong>比较运算符</strong><br>== (等于),!= (不等于),gt(大于),gte(大于或者等于),lt(小于),lte(小于或者等于)。不建议用 >,< 可能会报错!<br>一般和 if 配合使用</p>
<p><strong>内建函数</strong><br>FreeMarker 提供了一些内建函数来转换输出,其结构:变量?内建函数,这样就可以通过内建函数来转换输出变量。<br>html: 对字符串进行HTML编码;<br>cap_first: 使字符串第一个字母大写;<br>lower_case: 将字符串转成小写;<br>upper_case: 将字符串转成大写;<br>size: 获得集合中元素的个数;<br>int: 取得数字的整数部分。</p>
<p><strong>变量空判断</strong><br> ! 指定缺失变量的默认值;一般配置变量输出使用<br>?? 判断变量是否存在。一般配合if使用 <#if value??></#if></p>
<p><strong>宏指令</strong><br>可以理解为java的封装方法,供其他地方使用。宏指令也称为自定义指令,macro指令<br>语法很简单:<#macro val > 声明macro </#macro>; 使用macro <@val /> </p>
<p><strong>命名空间</strong><br>可以理解为java的import语句,为避免变量重复。一个重要的规则就是:路径不应该包含大写字母,使用下划线_分隔词语,myName --> my_name<br>语法很简单:<#import "xxx.ftl" as val> </p>
<p>其他没有说明的语法是因为和java一样,没什么特别之处。所以没有列出来。</p>
<h3>4 Freemarker Web</h3>
<p>这里是和SpringMVC整合的,SpringMVC的配置就不多说了,笔者也写过相关的文章,同时也会提供源码</p>
<p>导入相关的jar pom.xml</p>
<pre><code class="xml"><!-- freeMarker start -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.20</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
</dependencies>
<!-- freeMarker end --></code></pre>
<p>springmvc的配置文件:</p>
<pre><code class="xml"><!-- 整合Freemarker -->
<!-- 放在InternalResourceViewResolver的前面,优先找freemarker -->
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/views/templates"/>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="prefix" value=""/>
<property name="suffix" value=".ftl"/>
<property name="contentType" value="text/html; charset=UTF-8"/>
</bean></code></pre>
<p>Controller 层</p>
<pre><code class="java">import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloFreeMarkerController {
@RequestMapping("/helloFreeMarker")
public String helloFreeMarker(Model model) {
model.addAttribute("name","ITDragon博客");
return "helloFreeMarker";
}
}</code></pre>
<p>最后是Freemarker文件</p>
<pre><code><html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>FreeMarker Web</title>
</head>
<body>
<h1>Hello ${name} !</h1>
</body>
</html></code></pre>
<p>源码地址:<a href="https://link.segmentfault.com/?enc=mLkrbYUYTd%2F2X5EV%2F76cog%3D%3D.knoevBieQhpcebwEiR8f1t2Y0J7VHHcarGO375TfMux0YaduaJKEIXr1zFdTUOT3" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=ss1KvjL9iqvn15t6P51D3g%3D%3D.sWRrI2ESF5y7EHUHqX82EloYv0aQyafoOswX9mcf%2FoXXS3380mhPnwD1e69Bvtgk" rel="nofollow">https://gitee.com/itdragon/sp...</a></p>
<h3>5 小结</h3>
<p>1 知道了FreeMarker是一块模版引擎,可以生产xml,html,java等文件</p>
<p>2 知道了FreeMarker文件提供占位符,java文件提供数据,通过FreeMarker模版引擎生产有数据的页面,文中是将数据放在Map中。web应用可以用setter/getter 方法</p>
<p>3 知道了FreeMarker语法中字符串的显示特殊字符,截取的操作。以及一些内置方法的使用</p>
<p>4 重点了解FreeMarker的空判断知识点。判断变量是否为空用 "??" ,如果变量为空设置默认值。如果不注意空问题,可能会出现黄色页面的提示哦!</p>
<p>5 FreeMarker的宏概念,命名空间,引入文件,给变量赋值,集合的遍历等。</p>
<p>6 Freemarker 整合SpringMVC。</p>
<p>到这里FreeMarker的入门就结束了,是不是很简单。如果有什么不对的地方,请指正!</p>
SpringMVC 上传下载 异常处理
https://segmentfault.com/a/1190000011709152
2017-10-25T10:22:13+08:00
2017-10-25T10:22:13+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>SpringMVC 上传下载 异常处理</h2>
<p>上一章节对SpringMVC的表单验证进行了详细的介绍,本章节介绍SpringMVC文件的上传和下载(重点),国际化以及异常处理问题。这也是SpringMVC系列教程中的最后一节,文章底部会提供该系列的源码地址。<br>首先看效果图(文件上传,下载和异常处理)<br><img src="/img/bVXied?w=841&h=547" alt="图片描述" title="图片描述"></p>
<p>pom.xml,文件上传和下载是需要两个jar包: commons-fileupload.jar 和 commons-io.jar</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.springmvc</groupId>
<artifactId>springmvc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!-- 若不配置,打包时会提示错误信息
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project springmvc: Compilation failure:
提示 未结束的字符串文字 ,若字符串后面加上空格后可以打包成功,但会乱码。
原因是:maven使用的是默认的compile插件来进行编译的。complier是maven的核心插件之一,然而complier插件默认只支持编译Java 1.4
-->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<spring.version>4.1.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring end -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!-- 缺少则提示 javax.servlet.jsp.JspException cannot be resolved to a type -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- JSR 303 start -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- JSR 303 end -->
<!-- 文件上传 start -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<!-- 文件上传 end -->
</dependencies>
</project> </code></pre>
<p>SpringMVC配置文件</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 配置自定扫描的包 -->
<context:component-scan base-package="com.itdragon.springmvc" />
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 配置注解驱动 -->
<mvc:annotation-driven />
<!-- 配置视图 BeanNameViewResolver 解析器
使用视图的名字来解析视图
通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高
-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="100"></property>
</bean>
<!-- 配置直接跳转的页面,无需经过Controller层
http://localhost:8080/springmvc/index
然后会跳转到 WEB-INF/views/index.jsp 页面
-->
<mvc:view-controller path="/index" view-name="index"/>
<mvc:default-servlet-handler/>
<!-- 配置国际化资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
<!-- 配置 SessionLocaleResolver 根据 Session 中特定属性确定本地化类型
必须将区域解析器的Bean名称设置为localeResolver,这样DispatcherServlet才能自动侦测到它。
请注意,每DispatcherServlet只能注册一个区域解析器。
* 第一步,把Locale对象设置为Session属性
* 第二步,从Session中获取Locale对象给应用程序
-->
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver"></bean>
<!-- 配置 LocaleChangeInterceptor 从请求参数中获取本次请求对应本地化类型
* 第一步,获取name=locale的请求参数
* 第二步,把locale的请求参数解析为Locale对象
* 第三步,获取LocaleResolver对象
-->
<mvc:interceptors>
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"></bean>
</mvc:interceptors>
<!-- 配置 CommonsMultipartResolver -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"></property>
<property name="maxUploadSize" value="2048000"></property>
</bean>
<!-- 配置使用 SimpleMappingExceptionResolver 来映射异常 -->
<bean id="simpleMappingExceptionResolver"
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!-- 这里是模型 exception -->
<property name="exceptionAttribute" value="exception"></property>
<property name="exceptionMappings">
<props>
<!-- 如果是该异常,则跳转到视图 exception 页面-->
<prop key="java.lang.ArrayIndexOutOfBoundsException">exception</prop>
</props>
</property>
</bean>
</beans> </code></pre>
<p>FileUploadController.java 文件上传,下载和国际化知识点<br>国际化步骤<br>第一步,在SpringMVC配置文件中配置 SessionLocaleResolver(bean的id必须是localeResolver) 和 LocaleChangeInterceptor(bean放在mvc:interceptors 拦截器中)两个bean。<br>第二步,视图页面引入 fmt 标签,并用 <fmt:message key="xxx" /> 设置值。<br>第三步,语言切换的链接,其格式:<a class="btn">English</a>。<br>第四步,创建链接的目标方法,其参数为Locale 类型参数。<br>第五步,准备语言文件,i18n_en_US.properties 和 i18n_zh_CN.properties,配置xxx的对应语言。</p>
<p>文件上传和下载<br>第一步,在SpringMVC配置文件中配置 CommonsMultipartResolver,并设置默认编码格式和最大尺寸<br>第二步,视图页面创建一个form表单,并设置 enctype="multipart/form-data"<br>第三步,目标方法接收参数的类型为 MultipartFile ,然后是文件流的操作。<br>第四步,看代码吧!</p>
<pre><code class="java">import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class FileUploadController {
@Autowired
private ResourceBundleMessageSource messageSource;
/**
* 国际化
* 第一步,在SpringMVC配置文件中,配置 SessionLocaleResolver 和 LocaleChangeInterceptor
* 第二步,准备语言文件,i18n_en_US.properties 和 i18n_zh_CN.properties
* 第三步,目标方法中,参数加入Locale对象。
*/
@RequestMapping("/fileUpload")
public String fileUpload(Locale locale) {
// String val = messageSource.getMessage("file", null, locale);
// System.out.println(val);
return "fileUpload";
}
// MultipartFile 上传文件必用的变量类型
@RequestMapping("/testFileUpload")
public String testFileUpload(@RequestParam("desc") String desc, @RequestParam("file") MultipartFile file,
Map<String, Object> map, HttpServletRequest request) {
InputStream in = null;
OutputStream out = null;
String fileName = file.getOriginalFilename(); // 获取文件名
try {
String realPath = request.getServletContext().getRealPath("uploads/");
in = file.getInputStream();
byte[] buffer = new byte[1024];
String filePath = realPath + "/" + fileName; // 文件上传路径
out = new FileOutputStream(filePath);
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != out) {
out.close();
}
if (null != in) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
map.put("fileName", fileName);
return "fileUpload";
}
// 不适合大文件的下载,适用于简单的下载场景。
@RequestMapping("/downLoadFile")
public ResponseEntity<byte[]> downLoadFile(@RequestParam("fileName") String fileName, HttpSession session) {
byte [] body = null;
ServletContext servletContext = session.getServletContext();
InputStream in = null;
ResponseEntity<byte[]> response = null;
try {
in = servletContext.getResourceAsStream("/uploads/"+fileName);
body = new byte[in.available()];
in.read(body);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition", "attachment;filename="+fileName);
HttpStatus statusCode = HttpStatus.OK;
response = new ResponseEntity<byte[]>(body, headers, statusCode);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != in) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return response;
}
} </code></pre>
<p>fileUpload.jsp 文件上传下载前端页面</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 快速入门</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="col-sm-6">
<div class="panel panel-info" style="margin-top:10px;">
<div class="panel-heading">
<h3 class="panel-title"><fmt:message key="file.upload" /></h3>
</div>
<div class="panel-body">
<!-- 缺少 enctype="multipart/form-data" 会提示
org.springframework.web.multipart.MultipartException: The current request is not a multipart request
-->
<form action="${pageContext.request.contextPath }/testFileUpload" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label class="col-sm-2 control-label"><fmt:message key="file" /></label>
<div class="col-sm-10">
<input type="file" name="file" class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label"><fmt:message key="desc" /></label>
<div class="col-sm-10">
<input type="text" name="desc" class="form-control" />
</div>
</div>
<input type="submit" value="Submit" class="btn btn-success" />
</form>
<a href="fileUpload?locale=zh_CN" class="btn" >中文</a>
<a href="fileUpload?locale=en_US" class="btn" >English</a>
</div>
</div>
</div>
<hr />
<a href="downLoadFile?fileName=${fileName}" >${fileName}</a>
<hr />
</div>
</div>
</body>
</html> </code></pre>
<p>常用的三种异常处理<br>@ExceptionHandler 注解<br>第一步,创建一个用注解@ControllerAdvice 修饰的切面类(也可以是普通类)。<br>第二步,创建一个目标方法,并用注解@ExceptionHandler 修饰,value值是一个数组,值是异常类。<br>第三步,目标方法的的参数必须有Exception 类型的参数,用于获取运行时发生的异常。<br>第四步,目标方法若想把异常返回给页面,可以用ModelAndView 类型作为返回值,而不能用Map作为参数返回。</p>
<p>@ResponseStatus 注解<br>第一步,创建一个被注解@ResponseStatus 修饰的自定义异常类,value值是状态码,reason值是字符串。<br>第二步,在目标方法执行时抛出自定义异常。</p>
<p>SimpleMappingExceptionResolver Bean<br>第一步,在SpringMVC的配置文件中 配置Bean SimpleMappingExceptionResolver 。<br>第二步,设置exceptionAttribute 模型Model,必须和页面上的值一致。<br>第三步,设置exceptionMappings 视图View,只有当异常触发时跳转到视图页面。<br>注意细节,看代码<br>StudyExceptionHandlerAdvice.java 切面类</p>
<pre><code class="java">import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
/**
* 1 @ExceptionHandler 注解修饰的方法可以放在普通类中,也可以放在切面类中(@ControllerAdvice 注解修饰的类)。前者表示只处理当前类的异常,后者表示处理全局的异常。
* 2 @ExceptionHandler 注解修饰的方法参数中,不能有Map,否则会提示:。若希望把异常信息返回给前端,可以使用ModelAndView
* 3 @ExceptionHandler 注解修饰的多个方法中,优先级原则是就近原则(和异常精度越近的异常,优先执行)。
*/
@ControllerAdvice
public class StudyExceptionHandlerAdvice {
@ExceptionHandler({ArithmeticException.class})
public ModelAndView handleArithmeticException(Exception exception){
System.out.println("ArithmeticException 出异常了: " + exception);
ModelAndView mv = new ModelAndView("exception");
mv.addObject("exception", "ArithmeticException 出异常了: " + exception);
return mv;
}
/*@ExceptionHandler({RuntimeException.class})
public ModelAndView handleRuntimeException(Exception exception){
System.out.println("RuntimeException 出异常了: " + exception);
ModelAndView mv = new ModelAndView("exception");
mv.addObject("exception", "RuntimeException 出异常了: " + exception);
return mv;
}*/
} </code></pre>
<p>ResponseStatusException.java 自定义异常类</p>
<pre><code class="java">import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "ResponseStatusException : 自定义异常原因")
public class ResponseStatusException extends RuntimeException{
} </code></pre>
<p>StudyExceptionController.java 异常处理测试类</p>
<pre><code class="java">import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class StudyExceptionController {
@RequestMapping("/exception")
public String exception(){
return "exception";
}
@RequestMapping("/simpleMappingExceptionResolver")
public String simpleMappingExceptionResolver(@RequestParam("num") int num){
String [] args = new String[10];
System.out.println("通过配置bean,来处理某一种异常导致的所有问题。" + args[num]);
return "exception";
}
@RequestMapping(value="/testResponseStatus")
public String testResponseStatus(@RequestParam("num") Integer num){
System.out.println("@ResponseStatus 自定义异常");
if (0 == num) {
throw new ResponseStatusException();
}
return "exception";
}
@RequestMapping("/testExceptionHandler")
public String testExceptionHandler(@RequestParam("num") Integer num){
System.out.println("@ExceptionHandler - result: " + (10 / num));
return "exception";
}
} </code></pre>
<p>exception.jsp 异常处理前端页面</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 快速入门</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<h2>SpringMVC 异常处理</h2>
<hr/>
@ExceptionHandler : <a href="testExceptionHandler?num=0" class="btn" >testExceptionHandler?num=0</a>
<hr/>
SimpleMappingExceptionResolver : <a href="simpleMappingExceptionResolver?num=20" class="btn" >simpleMappingExceptionResolver?num=20</a>
<hr/>
@ResponseStatus : <a href="testResponseStatus?num=0" class="btn" >testResponseStatus?num=0</a>
<hr/>
${exception}
</div>
</div>
</body>
</html> </code></pre>
<p>到这里,SpringMVC的教程就结束了,有什么好的建议和问题,可以提出来。大家一起成长!</p>
<p>SpringMVC源码地址:<a href="https://link.segmentfault.com/?enc=XwEFGMd1qYimyypWhOLsNg%3D%3D.xTjAK9asSy9s7OU2p9rNs5TVznFJim1e6zd0wXvURVqPIZcXSnvOntgrcAYB5R0Y" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=uKkhNyxbDt1JUCTH1%2BkhtA%3D%3D.4MhMYlpBUxUibVUsYsAVr87mdvaear5JL87e6z4u%2FvV2zmZFkJTOgkjEig3vhe%2Fa" rel="nofollow">https://gitee.com/itdragon/sp...</a></p>
SpringMVC 表单验证
https://segmentfault.com/a/1190000011682410
2017-10-23T18:06:45+08:00
2017-10-23T18:06:45+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>SpringMVC 表单验证</h2>
<p>本章节内容很丰富,主要有基本的表单操作,数据的格式化,数据的校验,以及提示信息的国际化等实用技能。<br>首先看效果图<br><img src="/img/bVXdld?w=936&h=444" alt="图片描述" title="图片描述"><br>然后项目目录结构图<br><img src="/img/bVXbhj?w=219&h=167" alt="20171007154550995" title="20171007154550995"></p>
<p>接下来用代码重点学习SpringMVC的表单操作,数据格式化,数据校验以及错误提示信息国际化。请读者将重点放在UserController.java,User.java,input.jsp三个文件中。<br>maven 项目必不可少的pom.xml文件。里面有该功能需要的所有jar包。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.springmvc</groupId>
<artifactId>springmvc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!-- 若不配置,打包时会提示错误信息
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project springmvc: Compilation failure:
提示 未结束的字符串文字 ,若字符串后面加上空格后可以打包成功,但会乱码。
原因是:maven使用的是默认的compile插件来进行编译的。complier是maven的核心插件之一,然而complier插件默认只支持编译Java 1.4
-->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<spring.version>4.1.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring end -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<!-- 缺少jsp-api 则提示 javax.servlet.jsp.JspException cannot be resolved to a type -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- JSR 303 start -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.1.Final</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
<!-- JSR 303 end -->
</dependencies>
</project> </code></pre>
<p>SpringMVC的核心配置文件</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 配置自定扫描的包 -->
<context:component-scan base-package="com.itdragon.springmvc" />
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 配置注解驱动 -->
<mvc:annotation-driven />
<!-- 配置视图 BeanNameViewResolver 解析器
使用视图的名字来解析视图
通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高
-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="100"></property>
</bean>
<!-- 配置直接跳转的页面,无需经过Controller层
http://localhost:8080/springmvc/index
然后会跳转到 WEB-INF/views/index.jsp 页面
-->
<mvc:view-controller path="/index" view-name="index"/>
<mvc:default-servlet-handler/>
<!-- 配置国际化资源文件 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
</beans> </code></pre>
<p>以上是准备工作。下面开始核心代码介绍。<br>数据的校验思路:<br>第一步,在实体类中指定属性添加校验注解(如@NotEmpty),<br>第二步,在控制层目标方法实体类参数添加注解@Valid,<br>第三步,在返回页面加上<form:errors path="xxx"></form:errors>显示提示错误信息</p>
<p>数据格式化思路:只需要在实体类中加上注解即可。</p>
<p>信息国际化思路:<br>第一步,在SpringMVC配置文件中配置国际化资源文件<br>第二步,创建文件i18n_zh_CN.properties文件<br>第三步,在i18n_zh_CN.properties文件配置国际化信息(要严格按照SpringMVC的语法)<br>UserController.java,两个重点知识。一个是SpringMVC的rest风格的增删改查。另一个是@Valid注解用法。具体看代码。</p>
<pre><code class="java">import java.util.Map;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.itdragon.springmvc.crud.dao.PositionDao;
import com.itdragon.springmvc.crud.dao.UserDao;
import com.itdragon.springmvc.crud.orm.User;
@Controller
public class UserController {
@Autowired
private UserDao userDao;
@Autowired
private PositionDao positionDao;
private static final String INPUT = "input"; // 跳转到编辑页面
private static final String LIST = "list"; // 跳转到用户列表页面
@ModelAttribute
public void getUser(@RequestParam(value="id",required=false) Integer id,
Map<String, Object> map){
if(id != null){
map.put("user", userDao.getUserById(id));
}
}
// 更新用户,用put请求方式区别get请求方式,属于SpringMVC rest 风格的crud
@RequestMapping(value="/user", method=RequestMethod.PUT)
public String updateUser(User user){
userDao.save(user);
return "redirect:/users";
}
// 点击编辑跳转编辑页面
@RequestMapping(value="/user/{id}", method=RequestMethod.GET)
public String input(@PathVariable("id") Integer id, Map<String, Object> map){
map.put("user", userDao.getUserById(id));
map.put("positions", positionDao.queryAllPositions());
return INPUT;
}
// 通过id删除用户
@RequestMapping(value="/delete/{id}", method=RequestMethod.GET)
public String delete(@PathVariable("id") Integer id){
userDao.deleteUserById(id);
return "redirect:/users";
}
/**
* 新增用户,若保存成功则跳转到用户列表页面,若失败则跳转到编辑页面
* @param user 用 @Valid 注解修饰后,可实现数据校验的逻辑
* @param result 数据校验结果
* @param map 数据模型
* @return
*/
@RequestMapping(value="/user", method=RequestMethod.POST)
public String save(@Valid User user, Errors result, Map<String, Object> map){
if(result.getErrorCount() > 0){
for(FieldError error : result.getFieldErrors()){
System.out.println(error.getField() + " : " + error.getDefaultMessage());
}
map.put("positions", positionDao.queryAllPositions());
return INPUT;
}
userDao.save(user);
return "redirect:/users";
}
@RequestMapping(value="/user", method=RequestMethod.GET)
public String input(Map<String, Object> map){
map.put("positions", positionDao.queryAllPositions());
map.put("user", new User());
return INPUT;
}
// 跳转用户列表页面
@RequestMapping("/users")
public String list(Map<String, Object> map){
map.put("users", userDao.queryAllUsers());
return LIST;
}
} </code></pre>
<p>User.java,两个重点知识。一个是数据的格式化(包括日期格式化和数值格式化)。另一个是使用 JSR 303 验证标准数据校验。<br>数据格式化,由于前端传给后台的是字符串,对于比较特殊的属性,比如Date,Float类型就需要进行数据格式化<br><strong> @NumberFormat 数值格式化 </strong><br>可以格式化/解析的数字类型:Short、Integer、Long、Float、Double、BigDecimal、BigInteger。<br>属性参数有:pattern="###,###.##"(重点)。<br>style= org.springframework.format.annotation.NumberFormat.Style.NUMBER(CURRENCY / PERCENT)。其中Style.NUMBER(通用样式,默认值);Style.CURRENCY(货币样式);Style.PERCENT(百分数样式)</p>
<p><strong> @DateTimeFormat 日期格式化 </strong> <br>可以格式化/解析的数字类型:java.util.Date 、java.util.Calendar 、java.long.Long。<br>属性参数有:pattern="yyyy-MM-dd hh:mm:ss"(重点)。<br>iso=指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用ISO模式,默认值),ISO.DATE(yyyy-MM-dd),ISO.TIME(hh:mm:ss.SSSZ),ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ);style=指定用于格式化的样式模式,默认“SS”,优先级: pattern 大于 iso 大于 style,后两个很少用。</p>
<p>数据校验<br>空检查 <br><strong> @Null </strong> 验证对象是否为null<br><strong> @NotNull </strong> 验证对象是否不为null, 无法查检长度为0的字符串<br><strong> @NotBlank </strong> 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,<br>且会去掉前后空格 <br><strong> @NotEmpty </strong> 检查约束元素是否为NULL或者是EMPTY<br>Booelan检查 <br><strong> @AssertTrue </strong> 验证 Boolean 对象是否为 true<br><strong> @AssertFalse </strong> 验证 Boolean 对象是否为 false<br>长度检查 <br><strong> @Size(min=, max=) </strong> 验证对象(Array,Collection,Map,String)值是否在给定的范围之内<br><strong> @Length(min=, max=) </strong> 验证对象(CharSequence子类型)长度是否在给定的范围之内<br>日期检查 <br><strong> @Past </strong> 验证 Date 和 Calendar 对象是否在当前时间之前<br><strong> @Future </strong> 验证 Date 和 Calendar 对象是否在当前时间之后<br><strong> @Pattern </strong> 验证 String 对象是否符合正则表达式的规则<br>数值检查 <br><strong> @Min </strong> 验证 Number 和 String 对象是否大等于指定的值<br><strong> @Max </strong> 验证 Number 和 String 对象是否小等于指定的值<br><strong> @DecimalMax </strong> 被标注的值必须不大于约束中指定的最大值. 这个约束的参数<br>是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度<br><strong> @DecimalMin </strong> 被标注的值必须不小于约束中指定的最小值. 这个约束的参数<br>是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度<br><strong> @Digits </strong> 验证 Number 和 String 的构成是否合法<br><strong> @Digits(integer=,fraction=) </strong> 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度<br><strong> @Range(min=, max=) </strong> 检查数字是否介于min和max之间<br><strong> @CreditCardNumber </strong> 信用卡验证<br><strong> @Email </strong> 验证是否是邮件地址,如果为null,不进行验证,算通过验证<br><strong> @ScriptAssert(lang= ,script=, alias=) </strong> 通过脚本验证</p>
<p>其中有几点需要注意:<br>空判断注解</p>
<pre><code>String name @NotNull @NotEmpty @NotBlank
null false false false
"" true false false
" " true true false
"ITDragon!" true true true</code></pre>
<p>数值检查:建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null</p>
<pre><code class="java">import java.util.Date;
import javax.validation.constraints.DecimalMin;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.NumberFormat;
public class User {
private Integer id;
@NotEmpty
private String account;
@Email
@NotEmpty
private String email;
private Integer sex;
private Position position;
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date createdDate;
@NumberFormat(pattern="###,###.#")
@DecimalMin("2000")
private Double salary;
public User() {
}
public User(Integer id, String account, String email, Integer sex,
Position position, Date createdDate, Double salary) {
this.id = id;
this.account = account;
this.email = email;
this.sex = sex;
this.position = position;
this.createdDate = createdDate;
this.salary = salary;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getSex() {
return sex;
}
public void setSex(Integer sex) {
this.sex = sex;
}
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "User [id=" + id + ", account=" + account + ", email=" + email
+ ", sex=" + sex + ", position=" + position + ", createdDate="
+ createdDate + ", salary=" + salary + "]";
}
} </code></pre>
<p>input.jsp,SpringMVC 表单标签知识点详解 <a href="https://link.segmentfault.com/?enc=%2B8J9h%2BelRtgTBzNXIkKOrQ%3D%3D.1agNcbTdS3IqtX1%2F995cpTHIPJ40zyE%2FGER6IVlz%2BFioX4jVhC03ArPWSx2o7fXf" rel="nofollow">http://www.cnblogs.com/liukem...</a></p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@page import="java.util.HashMap"%>
<%@page import="java.util.Map"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 表单操作</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<!--
1. 使用 form 标签可以更快速的开发出表单页面, 而且可以更方便的进行表单值的回显。
step1 导入标签 taglib prefix="form" uri="http://www.springframework.org/tags/form"
step2 和普通的form用法差不多。path 相当于 普通的form的name,form:hidden 隐藏域,form:errors 提示错误信息。
2. 使用form 标签需要注意:
通过 modelAttribute 属性指定绑定的模型属性, 该数据模型必须是实例化过的。
若没有 modelAttribute 指定该属性,则默认从 request 域对象中读取 command 的表单 bean (如果该属性值也不存在,则会发生错误)。
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'command' available as request attribute
-->
<div class="container">
<div class="row">
<div class="col-sm-6">
<div class="panel panel-info" style="margin-top:10px;">
<div class="panel-heading">
<h3 class="panel-title">修改或创建用户信息</h3>
</div>
<div class="panel-body">
<form:form action="${pageContext.request.contextPath }/user" method="POST"
modelAttribute="user" class="form-horizontal" role="form">
<c:if test="${user.id == null }">
<!-- path 属性对应 html 表单标签的 name 属性值 -->
<div class="form-group">
<label class="col-sm-2 control-label">Account</label>
<div class="col-sm-10">
<form:input class="form-control" path="account"/>
<form:errors style="color:red" path="account"></form:errors>
</div>
</div>
</c:if>
<c:if test="${user.id != null }">
<form:hidden path="id"/>
<input type="hidden" name="_method" value="PUT"/>
<%-- 对于 _method 不能使用 form:hidden 标签, 因为 modelAttribute 对应的 bean 中没有 _method 这个属性 --%>
<%--
<form:hidden path="_method" value="PUT"/>
--%>
</c:if>
<div class="form-group">
<label class="col-sm-2 control-label">Email</label>
<div class="col-sm-10">
<form:input class="form-control" path="email"/>
<form:errors style="color:red" path="email"></form:errors>
</div>
</div>
<!-- 这是SpringMVC 不足之处 -->
<%
Map<String, String> genders = new HashMap();
genders.put("1", "Male");
genders.put("0", "Female");
request.setAttribute("genders", genders);
%>
<div class="form-group">
<label class="col-sm-2 control-label">Sex</label>
<div class="col-sm-10">
<form:radiobuttons path="sex" items="${genders }" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Position</label>
<div class="col-sm-10">
<form:select class="form-control" path="position.id" items="${positions}" itemLabel="level" itemValue="id">
</form:select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Date</label>
<div class="col-sm-10">
<form:input class="form-control" path="createdDate"/>
<form:errors style="color:red" path="createdDate"></form:errors>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">Salary</label>
<div class="col-sm-10">
<form:input class="form-control" path="salary"/>
<form:errors style="color:red" path="salary"></form:errors>
</div>
</div>
<input class="btn btn-success" type="submit" value="Submit"/>
</form:form>
</div>
</div>
</div>
</div>
</div>
</body>
</html> </code></pre>
<p>i18n国际化文件</p>
<pre><code class="xml">#语法:实体类上属性的注解.验证目标方法的modleAttribute 属性值(如果没有默认为实体类首字母小写).注解修饰的属性
#以第一个为例:User实体类中 属性account用了NotEmpty注解修饰,表示不能为空。所以前缀是NotEmpty
#验证的目标方法 public String save(@Valid User user, ...) User被注解@Valid 修饰,但没有被modleAttribute修饰。所以中间是user
#后缀就是被注解修饰的属性名 account
NotEmpty.user.account=用户名不能为空
Email.user.email=Email地址不合法
#typeMismatch 数据类型不匹配时提示
typeMismatch.user.createdDate=不是一个日期
#required 必要参数不存在时提示
#methodInvocation 调用目标方法出错的时提示 </code></pre>
<p>其他文件,Position 实体类</p>
<pre><code class="java">public class Position {
private Integer id;
private String level;
public Position() {
}
public Position(Integer id, String level) {
this.id = id;
this.level = level;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
@Override
public String toString() {
return "Position [id=" + id + ", level=" + level + "]";
}
}
模拟用户操作的dao,UserDao.java
[java] view plain copy
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import com.itdragon.springmvc.crud.orm.Position;
import com.itdragon.springmvc.crud.orm.User;
@Repository
public class UserDao {
private static Map<Integer, User> users = null;
@Autowired
private PositionDao positionDao;
// 模拟数据库查询数据
static{
users = new HashMap<Integer, User>();
users.put(1, new User(1, "ITDragon", "11@xl.com", 1, new Position(1, "架构师"), new Date(), 18888.88));
users.put(2, new User(2, "Blog", "22@xl.com", 1, new Position(2, "高级工程师"), new Date(), 15555.55));
users.put(3, new User(3, "Welcome", "33@xl.com", 0, new Position(3, "中级工程师"), new Date(), 8888.88));
users.put(4, new User(4, "To", "44@xl.com", 0, new Position(4, "初级工程师"), new Date(), 5555.55));
users.put(5, new User(5, "You", "55@xl.com", 1, new Position(5, "java实习生"), new Date(), 2222.22));
}
// 下一次存储的下标id
private static Integer initId = 6;
public void save(User user){
if(user.getId() == null){
user.setId(initId++);
}
user.setPosition(positionDao.getPositionById(user.getPosition().getId()));
users.put(user.getId(), user);
}
public Collection<User> queryAllUsers(){
return users.values();
}
public User getUserById(Integer id){
return users.get(id);
}
public void deleteUserById(Integer id){
users.remove(id);
}
} </code></pre>
<pre><code class="java">import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Repository;
import com.itdragon.springmvc.crud.orm.Position;
@Repository
public class PositionDao {
private static Map<Integer, Position> positions = null;
static{
positions = new HashMap<Integer, Position>();
positions.put(1, new Position(1, "架构师"));
positions.put(2, new Position(2, "高级工程师"));
positions.put(3, new Position(3, "中级工程师"));
positions.put(4, new Position(4, "初级工程师"));
positions.put(5, new Position(5, "java实习生"));
}
// 模拟查询所有数据
public Collection<Position> queryAllPositions(){
return positions.values();
}
// 模拟通过id查询数据
public Position getPositionById(Integer id){
return positions.get(id);
}
} </code></pre>
<p>用户列表页面的list.jsp</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 表单操作</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<script src="https://code.jquery.com/jquery.js"></script>
<script type="text/javascript">
$(function(){
$(".delete").click(function(){
var msg = confirm("确定要删除这条数据?");
if (true == msg) {
$(this).onclick();
} else {
return false;
}
});
})
</script>
</head>
<body>
<!-- 用于删除的form -->
<form action="" method="POST" id="deleteForm">
<input type="hidden" name="_method" value="DELETE"/>
</form>
<div class="container">
<div class="row">
<div class="col-sm-9">
<c:if test="${empty requestScope.users }">
没有任何员工信息.
</c:if>
<c:if test="${!empty requestScope.users }">
<div class="table-responsive">
<table class="table table-bordered">
<caption>用户信息表 <a href="user" class="btn btn-default" >Add Account</a></caption>
<thead>
<tr>
<th>用户编码</th>
<th>账号名</th>
<th>邮箱</th>
<th>性别</th>
<th>职位</th>
<th>薪水</th>
<th>时间</th>
<th>编辑</th>
<th>删除</th>
</tr>
</thead>
<tbody>
<c:forEach items="${requestScope.users }" var="user">
<tr>
<td>${user.id }</td>
<td>${user.account }</td>
<td>${user.email }</td>
<td>${user.sex == 0 ? 'Female' : 'Male' }</td>
<td>${user.position.level }</td>
<td>${user.salary }</td>
<td><fmt:formatDate value="${user.createdDate }" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td><a href="user/${user.id}">Edit</a></td>
<td><a class="delete" href="delete/${user.id}">Delete</a></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</c:if>
</div>
</div>
</div>
</body>
</html> </code></pre>
<p>注意事项<br>javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint '<br>使用hibernate validator出现上面的错误, 需要 注意<br>@NotNull 和 @NotEmpty 和@NotBlank 区别<br>@NotEmpty 用在集合类上面<br>@NotBlank 用在String上面<br>@NotNull 用在基本类型上<br>如果在基本类型上面用NotEmpty或者NotBlank 会出现上面的错,笔者将@NotEmpty用到了Date上,导致出了这个问题。若还有问题,还继续在这里补充。</p>
<p>以上便是SpringMVC的表单操作,其中包含了常用知识,如数据的格式化,数据的校验,提示信息国际化,Form标签的用法。</p>
SpringMVC 视图解析器
https://segmentfault.com/a/1190000011682174
2017-10-23T17:57:34+08:00
2017-10-23T17:57:34+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>SpringMVC 视图解析器</h2>
<p>还记得SpringMVC 快速入门中,dispatcher-servlet.xml 配置的视图解析器么。它是SpringMVC 的核心知识点。本章节比较简单,明白视图解析器的工作原理,然后配置自定义的视图解析器和使用重点向跳转页面。</p>
<p>SpringMVC的配置文件,dispatcher-servlet.xml。这里配置了直接跳转的页面 mvc:view-controller 即不经过Controller层,直接根据视图解析器跳转页面。还配置了视图解析器 BeanNameViewResolver 优先级为666。其中jsp最为常见的解析器是 InternalResourceViewResolver 器优先级是int类型的最大值。也就是说优先级最低。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 配置自定扫描的包 -->
<context:component-scan base-package="com.itdragon.springmvc" />
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 配置注解驱动 -->
<mvc:annotation-driven />
<!-- 配置视图 BeanNameViewResolver 解析器
使用视图的名字来解析视图
通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高
-->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="666"></property>
</bean>
<!-- 配置直接跳转的页面,无需经过Controller层
http://localhost:8080/springmvc/index
然后会跳转到 WEB-INF/views/index.jsp 页面
-->
<mvc:view-controller path="/index" view-name="index"/>
</beans> </code></pre>
<p>自定义CustomViewResolver java类 实现 View 接口。</p>
<pre><code class="java">import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.View;
@Component
public class CustomViewResolver implements View{
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("CustomViewResolver order 越小优先级越高! 所以优先于 InternalResourceViewResolver");
}
} </code></pre>
<p>语法知识点说明类 ViewResolverController</p>
<pre><code class="java">import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ViewResolverController {
@RequestMapping("/testRedirect")
public String testRedirect() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^重定向到apistudy.jsp页面,地址栏URL改变");
return "redirect:apiStudy/testModelAndView";
}
@RequestMapping("/testForward")
public String testForward() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^转发到apistudy.jsp页面,地址栏URL不变");
return "forward:apiStudy/testModelAndView";
}
@RequestMapping("/testCustomViewResolver")
public String testCustomViewResolver() {
System.out.println("^^^^^^^^^^^^^^^^^^^^^进入到自定义的视图解析器中,返回值必须是类名首字母小写");
return "customViewResolver";
}
} </code></pre>
<p>WEB-INF/views/index.jsp</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 快速入门</title>
</head>
<body>
<h2>视图解析器</h2>
<a href="testRedirect">Test Redirect</a>
<br/><br/>
<a href="testForward">Test Forward</a>
<br/><br/>
<a href="testCustomViewResolver">Test Custom View Resolver</a>
<br/><br/>
</body>
</html> </code></pre>
<p>运行的效果图<br><img src="/img/bVXbdG?w=672&h=264" alt="20170929183915599" title="20170929183915599"><br>视图解析器工作流程:<br>首先,不管目标方法的返回值是 String, Map, ModelMap, Model, 还是 ModelAndView。 SpringMVC 都会在内部将他们装配成ModelAndView对象,<br>然后,借助视图解析器 ViewResolver,得到最终的逻辑视图对象View。最终的物理视图可以是jsp,excel,表单 等各种表现形式的视图。</p>
<p>到这里SpringMVC 的视图解析器就介绍完了,下一章是重难点,SpringMVC Form表单的crud操作。</p>
SpringMVC RequestMapping 详解
https://segmentfault.com/a/1190000011682058
2017-10-23T17:51:42+08:00
2017-10-23T17:51:42+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>SpringMVC RequestMapping 详解</h2>
<p>RequestMapping这个注解在SpringMVC扮演着非常重要的角色,可以说是随处可见。它的知识点很简单。今天我们就一起学习SpringMVC的RequestMapping这个注解。文章主要分为两个部分:RequestMapping 基础用法和RequestMapping 提升用法。</p>
<h3>准备工作</h3>
<p>pom.xml 这里是需要的jar包和相关的配置。这里设置了maven-compiler-plugin的java版本为1.7,避免打包出错和中文乱码的问题。</p>
<pre><code class="html"><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.springmvc</groupId>
<artifactId>springmvc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<!-- 若不配置,打包时会提示错误信息
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2:compile (default-compile) on project springmvc: Compilation failure:
提示 未结束的字符串文字 ,若字符串后面加上空格后可以打包成功,但会乱码。
原因是:maven使用的是默认的compile插件来进行编译的。complier是maven的核心插件之一,然而complier插件默认只支持编译Java 1.4
-->
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<properties>
<spring.version>4.1.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring end -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project> </code></pre>
<p>web.xml 设置字符拦截器,避免中文乱码</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>springmvc</display-name>
<!-- 配置 DispatcherServlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定 SpringMVC 配置文件的位置和名称
若SpringMVC的配置文件名的格式和位置满足: /WEB-INF/servlet-name + "-servlet.xml"
则可以省略下面的init-param代码。这里的servlet-name是dispatcher。 即/WEB-INF/dispatcher-servlet.xml
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!-- servlet知识点:处理所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 字符集过滤器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app> </code></pre>
<p>SpringMVC的配置文件(dispatcher-servlet.xml)没有变,这里就不再贴出来了。可以去上一章找</p>
<h3>RequestMapping 基础用法</h3>
<p>核心类 ApiStudyController,这是重点需要看的java文件。里面主要介绍了@RequestMapping 的基础用法。<br>你需要重点学习的有: 获取请求参数值的@RequestParam注解;通过占位符获取参数值的@PathVariable注解;指定请求方式的method属性;用于将数据存储到作用域中返回给前端的Map,Model和ModelMap参数;以及如何使用POJO对象作为方法的参数。</p>
<pre><code class="java">import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping("apiStudy")
public class ApiStudyController {
private static final String SUCCESS = "apistudy";
private static final String RESULT_KEY = "result";
/**
* 常用知识点:在类或者方法上使用 @RequestMapping 注解
* 若没有修饰类,则访问路径是: http://ip:port/项目名/方法的@RequestMapping值
* 若类有修饰类,则访问路径是: http://ip:port/项目名/类的@RequestMapping值/方法的@RequestMapping值
*/
/**
* 方法中用map作为参数,可以将数据存储到request作用域中,放回到页面上。
* 同样用法的有 Model 类型 和 ModelMap 类型
*/
@RequestMapping("/testMapResult")
public String testMapResult(Map<String, Object> map, Model model, ModelMap modelMap){
String apiDocs = "Map,Model,ModelMap (常用方法) : 在方法中添加Map的参数,可以将数据放到request 作用域中!";
map.put(RESULT_KEY, apiDocs);
model.addAttribute("model", "Model");
modelMap.addAttribute("modelMap", "ModelMap");
return SUCCESS;
}
/**
* 常用知识点:使用 method 属性来指定请求方式
* 若用GET方式请求,会提示:HTTP Status 405 - Request method 'GET' not supported 错误信息
*/
@RequestMapping(value = "/testRequestMethod", method=RequestMethod.POST)
public String testRequestMethod(Map<String, Object> map) {
String apiDocs = "RequestMethod (常用方法) : 若设置只有POST请求才能进入,则用GET方式请求,会报405的错误。反之亦然!";
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
/**
* 常用知识点:使用注解 @PathVariable 映射 URL绑定占位,属于REST风格。
* 注意两点:
* 1. 严格用法: @PathVariable("arg") String arg; 前一个arg参数,必须要和占位参数{arg}保持一致。后面一个arg参数可以自定义。
* 2. 偷懒用法: @PathVariable String arg; 这里的参数名 arg 必须要和占位参数{arg}保持一致,不然会提示400的错误
*/
@RequestMapping("/testPathVariable/{arg}")
public String testPathVariable(@PathVariable("arg") String arg, Map<String, Object> map) {
String apiDocs = "PathVariable (常用方法) : 通过映射 URL绑定占位获取的值是 " + arg;
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
/**
* 常用知识点:使用注解 @RequestParam 来映射请求参数
* 该注解有三个参数,
* value 请求的参数名,
* required 请求的参数是否必填 ,默认是true,
* defaultValue 请求的参数默认值.
* 参数的类型建议是封装数据类型,因为float默认值是0.0 ,若该参数是非必填,则会报错 HTTP Status 500
*/
@RequestMapping("/testRequestParam")
public String testRequestParam(@RequestParam("account") String account,
@RequestParam(value="password", required=true) String password,
@RequestParam(value="price", required=false, defaultValue="0.0") float price,
Map<String, Object> map) {
String apiDocs = "RequestParam (常用方法) : 获取映射请求参数的值有 account : " + account + " password : " + password + " price : " + price;
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
/**
* 常用知识点:方法参数是POJO对象
* 前端的请求参数名一定要和POJO对象属性一致。支持级联
*/
@RequestMapping(value = "/testPojo", method = RequestMethod.POST)
public String testPojo(User user, Map<String, Object> map) {
map.put(RESULT_KEY, user);
return SUCCESS;
}
/**
* 不常用方法:params 和 headers
* @RequestMapping 注解中,除了常用的value和method外,还有两个较为常用的params和headers
* 他们可以是请求跟精确,制定那些参数的请求不接受,同时也可以指定那些参数的请求接收。
* params={param1,param2}
* param1 表示 请求必须包含名为param1的请求参数
* !param1 表示 请求不能包含名为param1的请求参数
* param1!=value1 表示请求包含param1的请求参数,但是其值不能是value1
*/
@RequestMapping(value="/testParamsAndHeaders", params={"itdragon"},
headers = { "Accept-Language=zh-CN,zh;q=0.8" })
public String testParamsAndHeaders(Map<String, Object> map) {
String apiDocs = "params,headers (了解用法) : 这里表示当请求参数中包含了itdragon的时候才能进来";
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
/**
* 不常用方法:ant风格
* ?匹文件名中一个字
* *匹文件名中任意字
* ** 匹多 层径
*/
@RequestMapping("/*/testAntUrl")
public String testAntUrl(Map<String, Object> map) {
String apiDocs = "Ant风格 (了解用法) : ?匹文件名中一个字 ; *匹文件名中任意字 ; ** 匹多 层径 ";
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
/**
* 不常用方法:@RequestHeader 注解获取请求头数据。
*/
@RequestMapping("/testRequestHeader")
public String testRequestHeader(@RequestHeader(value = "Accept-Language") String al,
Map<String, Object> map) {
String apiDocs = "@RequestHeader (了解用法) : 获取请求头数据的注解, 如Accept-Language 的值是 : " + al;
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
/**
* 不常用方法:@CookieValue: 映射一个 Cookie值
*/
@RequestMapping("/testCookieValue")
public String testCookieValue(@CookieValue("JSESSIONID") String sessionId,
Map<String, Object> map) {
String apiDocs = "@CookieValue(了解用法) : 映射一个 Cookie值的注解, 如JSESSIONID 的Cookie值是 : " + sessionId;
map.put(RESULT_KEY, apiDocs);
return SUCCESS;
}
} </code></pre>
<p>用于测试的前端页面 index.jsp</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 快速入门</title>
</head>
<body>
<h2>@RequestMapping 注解基本用法</h2>
<a href="helloworld">史上最丑的HelloWorld</a>
<br/><br/>
<a href="apiStudy/testMapResult">Map,Model,ModelMap的使用方法</a>
<br/><br/>
<a href="apiStudy/testRequestMethod">用GET请求方式测试POST方法</a>
<form action="apiStudy/testRequestMethod" method="POST">
<input type="submit" value="用POST请求方式测试POST方法"/>
</form>
<br/>
<a href="apiStudy/testPathVariable/itdragon">@PathVariable获取占位数据</a>
<br/><br/>
<a href="apiStudy/testRequestParam?account=itdragon&password=123456">@RequestParam获取请求参数值</a>
<br/><br/>
<form action="apiStudy/testPojo" method="post">
account: <input type="text" name="account" value="itdragon"/> <br>
level: <input type="text" name="position.level" value="架构师"/> <br>
salary: <input type="text" name="position.salary" value="88888.88"/> <br>
<input type="submit" value="测试方法参数是POJO"/>
</form>
<br/>
<hr/>
<a href="apiStudy/testParamsAndHeaders?itdragon=great">params 和 headers用法</a>
<br/><br/>
<a href="apiStudy/itdragon/testAntUrl">Ant风格URL请求</a>
<br/><br/>
<a href="apiStudy/testRequestHeader">@RequestHeader 注解获取请求头数据</a>
<br/><br/>
<a href="apiStudy/testCookieValue">@CookieValue 注解获取 Cookie值</a>
</body>
</html> </code></pre>
<p>方便查看结果的apistudy.jsp页面</p>
<pre><code class="jsp"><%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 快速入门</title>
</head>
<body>
<h2>@RequestMapping 注解基本用法</h2>
request 作用域 : <br/>
${requestScope.result} <br/>
${requestScope.model} <br/>
${requestScope.modelMap} <br/>
<hr/>
session 作用域 : <br/>
${sessionScope.result}
</body>
</html> </code></pre>
<p>最后是两个用于测试的POJO对象,分别是User和Position</p>
<pre><code class="java">public class User {
private Integer id;
private String account;
private String password;
private Position position;
public User() {
}
public User(Integer id, String account, String password) {
this.id = id;
this.account = account;
this.password = password;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Position getPosition() {
return position;
}
public void setPosition(Position position) {
this.position = position;
}
@Override
public String toString() {
return "User [id=" + id + ", account=" + account + ", password=" + password + ", position="
+ position + "]";
}
} </code></pre>
<pre><code class="java">public class Position {
private Integer id;
private String level;
private Double salary;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "Position [level=" + level + ", salary=" + salary
+ "]";
}
} </code></pre>
<p>测试的效果图<br><img src="/img/bVXbbu?w=1049&h=614" alt="20170928160321643" title="20170928160321643"></p>
<h3>RequestMapping 提升用法</h3>
<p>这里的知识点有:输出模型数据 ModelAndView类型,Map(Model,ModelMap),@ModelAttribute注解,@SessionAttributes注解的使用,和 原生的Servlet Api的使用。<br>ModelAndView数据类型:见名知意,即包含了模型数据,又包含了视图数据。设置模型数据有两种方式:modelAndView.addObject(String attributeName, Object attributeValue) 方分别表示Key和Value。modelAndView.addAllObjects(modelMap) 存的参数是一个Map(Model,ModelMap其实都是Map数据类型)。<br>设置视图数据常用方法也有两种方式:<br>ModelAndView modelAndView = new ModelAndView(viewName) 初始化ModelAndView的时候设置。<br>modelAndView.setViewName(viewName) 初始化后再she'zh<br>@ModelAttribute 注解:被该注解修饰的方法, 会在每个目标方法执行之前被 SpringMVC 调用。实际开发中其实用的并不是很多。在我们更新数据的时候,一般都会先查询,后更新。该注解就扮演督促查询的角色,在执行更新的方法前先执行该注解修饰的查询数据方法。<br>@SessionAttributes 注解:只能修饰在类上,将模型数据暂存到HttpSession中,从而使多个请求的数据共享。常用方法有@SessionAttributes(value={"obj1", "obj2"}, types={String.class, Obj1.class})。表示可以将数据类型是String或者是对象Obj1的模型数据放到HttpSession中。也可以将变量名是obj1,obj2的模型数据放到HttpSession中。用这个注解容易出一个问题。HttpSessionRequiredException 异常,原因在代码注释中。<br>原生的Servlet Api:如果发现不能引入javax.servlet.* 的文件,可能是因为项目里面中没有servlet-api.jar。<br>普通项目解决方法:右击项目工程名称 ---> Build Path ---> Configure Build Path... ---> 选择 Libraries 点击 Add External JARS ---> 在按照tomcat的里面下,找到lib目录,里面就有。<br>Maven项目解决方法:如果按照上面的方法处理,虽然引入文件,当打包的时候会提示"找不到符号",是因为servlet-api.jar 没有真正的加入到classpath下。最简单的方法就是在pom.xml加入servlet-api.jar。<br>看代码:</p>
<pre><code class="java">import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("apiStudy")
@SessionAttributes(value={"user"}, types={String.class})
public class ApiStudyController {
private static final String SUCCESS = "apistudy";
private static final String RESULT_KEY = "result";
/**
* @SessionAttributes 将数据存储到session中,达到多个请求数据共享的目的。
* 只能修饰在类上的注解
* 通过属性名value,指定需要放到会话中的属性
* 通过模型属性的对象类型types,指定哪些模型属性需要放到会话中
*/
/**
* 常用方法:ModelAndView 方法的返回值设置为 ModelAndView 类型。
* ModelAndView 顾名思义,是包含视图和模型信息的类型。
* 其数据存放在 request 域对象中.
*/
@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView() {
String viewName = SUCCESS; // 需要返回的视图名
String apiDocs = "ModelAndView(常用方法) : 之前学习的方法返回值是字符串,数据是通过Map返回到前端。现在可以通过ModelAndView类型直接完成。";
ModelAndView modelAndView = new ModelAndView(viewName);
modelAndView.addObject(RESULT_KEY, apiDocs); // 添加数据到model中
return modelAndView;
}
/**
* SpringMVC 确定目标方法 POJO 类型入参的过程
* 第一步: 确定一个 key
* 若方法形如 "testModelAttribute(User user)" , 则key为 user(POJO 类名第一个字母小写)
* 若方法形如"testModelAttribute(@ModelAttribute("userObj") User user)" ,则key为 userObj
* 第二步: 在 implicitModel 中查找 key 对应的对象
* 若 implicitModel 存在, 则作为入参传入
* 若 implicitModel 中不存在, 则检查当前Handler 是否使用 @SessionAttributes 注解
* 若使用了该注解, 且 @SessionAttributes 注解的 value 属性值中包含了 key, 则会从 HttpSession 中来获取 key 所对应的 value 值, 若存在则直接传入到目标方法的入参中. 若不存在则将抛出异常.
* 若 Handler 没有标识 @SessionAttributes 注解或 @SessionAttributes 注解的 value 值中不包含 key, 则通过反射来创建 POJO 类型的参数, 传入为目标方法的参数
* implicitModel? SpringMVC 会把 key 和 POJO 类型的对象保存到 implicitModel 中, 进而会保存到 request 中.
*/
@RequestMapping("/testModelAttribute")
public ModelAndView testModelAttribute(User user){
ModelAndView modelAndView = new ModelAndView(SUCCESS);
modelAndView.addObject(RESULT_KEY, "update : " + user); // 添加数据到model中
return modelAndView;
}
/**
* 常用方法:@ModelAttribute 修饰方法。 被该注解修饰的方法, 会在每个目标方法执行之前被 SpringMVC 调用
* 运行流程:
* 第一步: 在执行 testModelAttribute(User user) 方法前,会先执行被@ModelAttribute 注解修饰的方法 getUser()
* 第二步: 执行getUser() 后,将执行放入到Map中,其中的key 必须和目标方法User对象的首字母小写user
* 第三步: SpringMVC 从 Map 中取出 User 对象,然后把对象传入目标方法的参数.
*
* 未使用 @ModelAttribute testModelAttribute方法 打印的信息 :
* update : User [id=1, account=itdragon, password=null, position=null]
* 使用@ModelAttribute testModelAttribute方法 打印的信息 :
* update : User [id=1, account=itdragon, password=zhangdeshuai, position=null]
*/
@ModelAttribute
public void getUser(@RequestParam(value="id",required=false) Integer id,
Map<String, Object> map){
if(id != null){
//模拟从数据库中获取对象
User user = new User(1, "itdragon", "zhangdeshuai");
map.put("user", user); // 这里的key 一定是该对象User的首字母小写user
}
}
/**
* 常用方法:可以使用 Serlvet 原生的 API 作为目标方法的参数 具体支持以下类型
*
* HttpServletRequest
* HttpServletResponse
* HttpSession
* InputStream
* OutputStream
* Reader
* Writer
*/
@RequestMapping("/testServletAPI")
public void testServletAPI(HttpServletRequest request,
HttpServletResponse response, Writer out) throws IOException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
out.write("Hello Servlet API,和用Servlet一样(0——0) ; <br/>request : " + request + " ;<br/>response : " + response);
}
} </code></pre>
<p>一样的前端index.jsp</p>
<pre><code class="html"><%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>SpringMVC 快速入门</title>
</head>
<body>
<h2>@RequestMapping 注解提升用法</h2>
<a href="apiStudy/testModelAndView">ModelAndView的使用方法</a>
<br/><br/>
<form action="apiStudy/testModelAttribute" method="post">
<input type="hidden" name="id" value="1"/><!-- 隐藏域 id -->
account: <input type="text" name="account" value="itdragon"/> <br>
<input type="submit" value="testModelAttribute"/>
</form>
<br/>
<a href="apiStudy/testServletAPI">使用原生的Servlet API</a>
<br/><br/>
</body>
</html> </code></pre>
<p>两个实体类和查看结果的jsp页面没有变<br>效果图:<br><img src="/img/bVXbb2?w=1017&h=291" alt="20170929110522386" title="20170929110522386"></p>
SpringMVC 快速入门
https://segmentfault.com/a/1190000011681748
2017-10-23T17:41:46+08:00
2017-10-23T17:41:46+08:00
itdragon
https://segmentfault.com/u/itdragon
2
<h2>SpringMVC 快速入门</h2>
<h3>SpringMVC 简介</h3>
<p>SpringMVC是 Spring为展示层提供的基于Web MVC设计模式的请求驱动类型的轻量级Web框架,它的功能和Struts2一样。但比Struts2更方便,更高效。是目前主流的Web框架。</p>
<p>模拟一个简单的业务逻辑,页面请求后跳转到HelloWorld页面。<br>首先创建一个SpringMVC的Maven项目。(如果不会创建Maven项目可以参考:Eclipse+Maven创建webapp项目)<br>项目结构如下:<br><img src="/img/bVXa7r?w=296&h=562" alt="20170927172542458" title="20170927172542458"><br>第一步:配置pom.xml</p>
<pre><code class="xml"><project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.springmvc</groupId>
<artifactId>springmvc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<spring.version>4.1.3.RELEASE</spring.version>
</properties>
<dependencies>
<!-- spring begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring end -->
</dependencies>
</project> </code></pre>
<p>第二步:因为这是一个web项目,我们需要配置它的web.xml文件,就像配置servlet一样配置DispatcherServlet。拦截所有请求,但这样做会拦截静态资源,后面文章会有介绍如何处理。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>springmvc</display-name>
<!-- 配置 DispatcherServlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 指定 SpringMVC 配置文件的位置和名称
若SpringMVC的配置文件名的格式和位置满足: /WEB-INF/servlet-name + "-servlet.xml"
则可以省略下面的init-param代码。这里的servlet-name是dispatcher。 即/WEB-INF/dispatcher-servlet.xml
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<!-- servlet知识点:处理所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app> </code></pre>
<p>第三步:(核心知识点)创建一个dispatcher-servlet.xml 文件,该文件名可以自定义,也可以遵守Spring的规则(第二步中注释内容有提到)。先要配置自动扫描包,使配置bean的注解生效。然后配置MVC注解驱动,使SpringMVC的注解生效。最后要配置视图解析器,将方法的返回值拼接成跳转的URL路径。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.itdragon.springmvc" />
<!-- 配置视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>
<!-- 配置注解驱动 -->
<mvc:annotation-driven />
</beans> </code></pre>
<p>第四步:业务逻辑代码,这里就简单的打印和返回一个helloworld的字符串。让视图解析器配置成跳转的helloworld.jsp页面。</p>
<pre><code class="java">import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloWorldController {
/**
* 浏览器访问http://localhost:8080/springmvc/helloworld
* 通过@RequestMapping 注解匹配映射请求的 URL,找到对应的方法。
* 方法执行后返回 "helloworld"字符串 。通过 视图解析器 解析机制(prefix + returnVal + suffix)
* 请求返回跳转页面 /WEB-INF/views/helloworld.jsp
*/
@RequestMapping("/helloworld")
public String helloWorld() {
System.out.println("^^^^^^^^^^^^^^^^HelloWorld");
return "helloworld";
}
} </code></pre>
<p>万事俱备,项目打包编译,启动tomcat后。偶遇一个异常:<br>java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet<br>说的是DispatcherServlet类没有找到,但实际上是已经导入了jar包。其实是创建Maven项目后,需要手动把依赖jar都加进classpath下。解决方法如下:<br>项目右击-->properties-->Deployment Assembly-->add-->Java Build Path Entries-->导入所有依赖的Jar包,重新start tomcat即可。<br>再次启动tomcat后,没有问题。浏览器访问:<a href="https://link.segmentfault.com/?enc=a%2B0HTbK4ajw1TdLw3mJoJQ%3D%3D.2AhxlVZJeGcQ8HxfN7RpHrexSReQWbKG6By0TVzS6l0%3D" rel="nofollow">http://localhost</a>:8080/springmvc 回车。</p>
<p>控制台打印:</p>
<pre><code class="java">^^^^^^^^^^^^^^^^HelloWorld </code></pre>
<p>到这里,一个简单的SpringMVC项目就搭完了。</p>
Spring4 事务管理
https://segmentfault.com/a/1190000011379448
2017-09-27T13:04:46+08:00
2017-09-27T13:04:46+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>Spring4 事务管理</h2>
<p>本章是Spring4 教程中的最后一章,也是非常重要的一章。如果说学习IOC是知识的入门,那学习事务管理就是知识的提升。本章篇幅可能有一丢丢长,也有一丢丢难,需要读者细细品味。主要从三个方面开始:事务简介,基于注解的事务管理 和基于xml的事务管理。</p>
<hr>
<h3>准备环境</h3>
<p>mysql文件,两张表:一个用户表,字段有帐号和余额。一个商品表,字段有sku,售价和库存。</p>
<pre><code class="sql">DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`account` varchar(255) NOT NULL,
`balance` float DEFAULT NULL COMMENT '用户余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO user VALUES ('1', 'itdragon', '100'); </code></pre>
<pre><code class="sql">DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` bigint(20) NOT NULL,
`sku` varchar(255) NOT NULL COMMENT '商品的唯一标识',
`price` float NOT NULL COMMENT '商品价格',
`stock` int(11) NOT NULL COMMENT '商品库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO product VALUES ('1', 'java', '40', '10');
INSERT INTO product VALUES ('2', 'spring', '50', '10'); </code></pre>
<hr>
<h3>事务简介</h3>
<p>工作中应该经常听到:"这是一个事务,你要保证它数据的一致性,在这里加个注解吧!"。于是我们就稀里糊涂地用,好像也没出什么问题。<br>因为加上注解,说明该方法支持事务的处理。事务就是一系列的动作,这一系列的动作要么都成功,要么都失败。所以你才会觉得没出什么问题。管理事务是应用程序开发必不可少的技术,用来确保数据的完整性和一致性,特别是和钱有关系的事务。<br>事务有四个关键属性:<br><strong>原子性</strong>:一系列的动作,要么都成功,要么都失败。<br><strong>一致性</strong>:数据和事务状态要保持一致。<br><strong>隔离性</strong>:为了防止数据被破坏,每个事务之间都存在隔离性。<br><strong>持久性</strong>:一旦事务完成, 无论发生什么系统错误, 它的结果都不应该受到影响。</p>
<p>我们用例子更好地说明事务:<br>A给B转账,A出账500元,B因为某种原因没有成功进账。若A出账的500元不回滚到A账户余额中,就会出现数据的<strong>不完整性和不一致性</strong>的问题。<br>本章模拟用户购买商品的场景。商场的下单逻辑是:先发货后设置用户余额。如果用户余额充足,商品库存充足的情况,是没有什么问题的。但若余额不足却购买商品,库存减少了,扣除用户余额时会因为余额不足而抛出异常,到最后用户余额并没有减少,商品库存却减少了,显然是不合理的。现在我们用Spring的事务管理来解决这种问题。<br>我们是谁???<strong>万能的程序员</strong><br><img src="/img/bVVUMl?w=320&h=213" alt="图片描述" title="图片描述"></p>
<hr>
<h3>基于注解的事务管理</h3>
<p>核心文件 applicationContext.xml。既然用到注解,就需要配置自动扫描包<strong>context:component-scan</strong>,还需要配置<strong>JdbcTempalte</strong>。最后要<strong>配置事务管理器</strong>和启动事务注解 <strong>tx:annotation-driven</strong>。<br>JDBC配置的事务管理器的class指定路径是DataSourceTransactionManager,<br>Hibernate配置的事务管理器的class指定路径是HibernateTransactionMannger 。两个的用法都是一样,只是配置事务管理时class指定的路径不同罢了。这是因为 Spring 在不同的事务管理上定义了一个抽象层。我们无需了解底层的API,就可以使用Spring的事务管理。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<context:component-scan base-package="com.itdragon.spring"></context:component-scan>
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置 C3P0 数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置 Spirng 的 JdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 启用事务注解 如果配置的事务管理器的id就是transactionManager , 这里是可以省略transaction-manager -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans> </code></pre>
<p>接下来是事务的业务代码,所有类都放在了一个目录下,没别的原因,就是因为懒。<br><img src="/img/bVVUed?w=219&h=160" alt="20170927104230756" title="20170927104230756"></p>
<p>核心是消费事务类 PurchaseService。介绍事务注解@Transactional的语法。<br>其次是批量消费事务类BatchPurchaseService。用于配合PurchaseService测试事务的传播性。<br>然后是事务测试类TransactionTest。主要负责测试和详细解释事务语法。<br>最后是自定义异常类。是为了测试事务的回滚属性。<br>PurchaseService(<strong>重点,注解语法</strong>),测试时,将注解逐一放开。</p>
<pre><code class="java">import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PurchaseService {
@Autowired
private ShopDao shopDao;
/**
* 模拟用户购买商品,测事务回滚
* 最基本用法,直接在方法或者类上使用注解@Transactional。值得注意的是:只能在公共方法上使用
* 对应的测试方法是 basicTransaction()
*/
@Transactional
/**
* 事务的传播 propagation=Propagation.REQUIRED
* 常用的有两种 REQUIRED,REQUIRES_NEW
* 对应的测试方法是 propagationTransaction()
*/
// @Transactional(propagation=Propagation.REQUIRED)
/**
* 事务的隔离性
* 将事务隔离起来,减少在高并发的场景下发生 脏读,幻读和不可重复读的问题
* 默认值是READ_COMMITTED 只能避免脏读的情况。
* 不好演示,没有对应的测试方法。
*/
// @Transactional(isolation=Isolation.READ_COMMITTED)
/**
* 回滚事务属性
* 默认情况下声明式事务对所有的运行时异常进行回滚,也可以指定某些异常回滚和某些异常不回滚。(意义不大)
* noRollbackFor 指定异常不回滚
* rollbackFor 指定异常回滚
*/
// @Transactional(noRollbackFor={UserException.class, ProductException.class})
/**
* 超时和只读属性
* 超时:在指定时间内没有完成事务则回滚。可以减少资源占用。参数单位是秒
* 如果超时,则提示错误信息:
* org.springframework.transaction.TransactionTimedOutException: Transaction timed out
* 只读属性:指定事务是否为只读. 若事务只读数据则有利于数据库引擎优化事务。
* 因为该事务有修改数据的操作,若设置只读true,则提示错误信息
* nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
* 对应的测试方法是 basicTransaction()
*/
// @Transactional(timeout=5, readOnly=false)
public void purchase(String account, String sku) {
//1. 获取书的单价
float price = shopDao.getBookPriceBySku(sku);
//2. 更新数的库存
shopDao.updateBookStock(sku);
//3. 更新用户余额
shopDao.updateUserBalance(account, price);
// 测试超时用的
/*try {
Thread.sleep(6000);
} catch (InterruptedException e) {
}*/
}
} </code></pre>
<p>批量消费事务类BatchPurchaseService</p>
<pre><code class="java">import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BatchPurchaseService {
@Autowired
private PurchaseService purchaseService;
// 批量采购书籍,事务里面有事务
@Transactional
public void batchPurchase(String username, List<String> skus) {
for (String sku : skus) {
purchaseService.purchase(username, sku);
}
}
} </code></pre>
<p>事务测试类TransactionTest(<strong>重点,知识点说明</strong>)</p>
<pre><code class="java">import java.util.Arrays;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TransactionTest {
private ApplicationContext ctx = null;
private PurchaseService purchaseService = null;
private BatchPurchaseService batchPurchaseService = null;
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
purchaseService = (PurchaseService) ctx.getBean("purchaseService");
batchPurchaseService = (BatchPurchaseService) ctx.getBean("batchPurchaseService");
}
/**
* 用户买一本书
* 基本用法-事务回滚
* 把@Transactional 注释。假设当前用户余额只有10元。单元测试后,用户余额没有变,spring的库存却减少了。赚了!!!
* 把@Transactional 注释打开。假设当前用户余额只有10元。单元测试后,用户余额没有变,spring的库存也没有减少。这就是回滚。
* 回滚:按照业务逻辑,先更新库存,再更新余额。现在是库存更新成功了,但在余额逻辑抛出异常。最后数据库的值都没有变。也就是库存回滚了。
*/
@Test
public void basicTransaction() {
System.out.println("^^^^^^^^^^^^^^^^^@Transactional 最基本的使用方法");
purchaseService.purchase("itdragon", "spring");
}
/**
* 用户买多本书
* 事务的传播性 -大事务中,有小事务,小事务的表现形式
* 用@Transactional, 当前用户余额50,是可以买一本书的。运行结束后,数据库中用户余额并没有减少,两本书的库存也都没有减少。
* 用@Transactional(propagation=Propagation.REQUIRED), 运行结果是一样的。
* 把REQUIRED 换成 REQUIRES_NEW 再运行 结果还是一样。。。。。
* 为什么呢???? 因为我弄错了!!!!!
* 既然是事务的传播性,那当然是一个事务传播给另一个事务。
* 需要新增一个事务类批量购买 batchPurchase事务, 包含了purchase事务。
* 把 REQUIRED 换成 REQUIRES_NEW 运行的结果是:用户余额减少了,第一本书的库存也减少了。
* REQUIRED:如果有事务在运行,当前的方法就在这个事务内运行。否则,就启动一个新的事务,并在自己的事务内运行。大事务回滚了,小事务跟着一起回滚。
* REQUIRES_NEW:当前的方法必须启动新事务,并在自己的事务内运行。如果有事务在运行,应该将它挂起。大事务虽然回滚了,但是小事务已经结束了。
*/
@Test
public void propagationTransaction() {
System.out.println("^^^^^^^^^^^^^^^^^@Transactional(propagation) 事务的传播性");
batchPurchaseService.batchPurchase("itdragon", Arrays.asList("java", "spring"));
}
/**
* 测试异常不回滚,故意超买(不常用)
* 当前用户余额10元,买了一本价值40元的java书。运行结束后,余额没有少,java书的库存减少了(赚了!)。因为设置指定异常不回滚!
* 指定异常回滚就不测了。
*/
@Test
public void noRollbackForTransaction() {
System.out.println("^^^^^^^^^^^^^^^^^@Transactional(noRollbackFor) 设置回滚事务属性");
purchaseService.purchase("itdragon", "java");
}
} </code></pre>
<p>业务处理接口以及接口实现类</p>
<pre><code class="java">import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository("shopDao")
public class ShopDaoImpl implements ShopDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public float getBookPriceBySku(String sku) {
String sql = "SELECT price FROM product WHERE sku = ?";
/**
* 第二个参数要用封装数据类型,如果用float.class,会提示 Type mismatch affecting row number 0 and column type 'FLOAT':
* Value [40.0] is of type [java.lang.Float] and cannot be converted to required type [float] 错误
*/
return jdbcTemplate.queryForObject(sql, Float.class, sku);
}
@Override
public void updateBookStock(String sku) {
// step1 防超卖,购买前先检查库存。若不够, 则抛出异常
String sql = "SELECT stock FROM product WHERE sku = ?";
int stock = jdbcTemplate.queryForObject(sql, Integer.class, sku);
System.out.println("^^^^^^^^^^^^^^^^^商品( " + sku + " )可用库存 : " + stock);
if(stock == 0){
throw new ProductException("库存不足!再看看其他产品吧!");
}
// step2 更新库存
jdbcTemplate.update("UPDATE product SET stock = stock -1 WHERE sku = ?", sku);
}
@Override
public void updateUserBalance(String account, float price) {
// step1 下单前验证余额是否足够, 若不足则抛出异常
String sql = "SELECT balance FROM user WHERE account = ?";
float balance = jdbcTemplate.queryForObject(sql, Float.class, account);
System.out.println("^^^^^^^^^^^^^^^^^您当前余额 : " + balance + ", 当前商品价格 : " + price);
if(balance < price){
throw new UserException("您的余额不足!不支持购买!");
}
// step2 更新用户余额
jdbcTemplate.update("UPDATE user SET balance = balance - ? WHERE account = ?", price, account);
// step3 查看用于余额
System.out.println("^^^^^^^^^^^^^^^^^您当前余额 : " + jdbcTemplate.queryForObject(sql, Float.class, account));
}
} </code></pre>
<p>最后两个自定义的异常类</p>
<pre><code class="java">public class UserException extends RuntimeException{
private static final long serialVersionUID = 1L;
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
} </code></pre>
<pre><code class="java">public class ProductException extends RuntimeException{
private static final long serialVersionUID = 1L;
public ProductException() {
super();
}
public ProductException(String message) {
super(message);
}
} </code></pre>
<p>当用户余额10元不够买售价为50的书,书的库存充足的情况。测试basicTransaction()方法打印的结果:用户余额不减少,库存也不减少<br><img src="/img/bVVUeH?w=847&h=238" alt="20170926183628059" title="20170926183628059"></p>
<p>当用户余额50元准备购买两本总价为90的书,但余额只够买一本书,书的库存充足的情况,测试propagationTransaction()方法打印的结果:若用 REQUIRES_NEW则两本中可以买一本;若用REQUIRED则一本都买不了。(事务的传播性有7种,这里主要介绍常用的REQUIRED和REQUIRES_NEW)<br><img src="/img/bVVUeI?w=956&h=263" alt="20170927094402928" title="20170927094402928"><br><img src="/img/bVVUeJ?w=485&h=159" alt="20170927094445263" title="20170927094445263"></p>
<p>当用户余额10元不够买售价40元的书,书的库存充足的情况。测试noRollbackForTransaction()方法打印的结果:用户余额没有减少,但商品库存减少了,说明事务没有回滚。<br><img src="/img/bVVUeL?w=946&h=259" alt="20170926184345738" title="20170926184345738"><br><img src="/img/bVVUeU?w=514&h=172" alt="20170926184326602" title="20170926184326602"><br>细细品味后,其实也很简单。事务就是为了保证数据的一致性。出了问题就把之前修改过的数据回滚。</p>
<hr>
<h3>基于xml的事务管理</h3>
<p>如果你理解了基于注解的事务管理,那基于xml的事务管理就简单多了。由于篇幅已经太长了,这里我长话短说。<br>首先把上面的java类中的所有IOC注解,@Transactional注解和@Autowired去掉。被@Autowired修饰的属性,还需要另外生成setter方法。<br>然后配置applicationContext.xml文件。将启动事务注解的代码删掉。将之前用自动扫描包的IOC注解和@Autowired注解的代码都<strong>配置bean</strong>(IOC知识),然后 <strong>配置事务属性</strong>,最后 <strong>配置事务切入点</strong>(AOP知识),这是系列博客,不懂的可以看前面几章。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置 C3P0 数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置 Spirng 的 JdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
<bean id="shopDao" class="com.itdragon.spring.my.transactionxml.ShopDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="purchaseService" class="com.itdragon.spring.my.transactionxml.PurchaseService">
<property name="shopDao" ref="shopDao"></property>
</bean>
<bean id="batchPurchaseService" class="com.itdragon.spring.my.transactionxml.BatchPurchaseService">
<property name="purchaseService" ref="purchaseService"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase"
propagation="REQUIRES_NEW"
timeout="3"
read-only="false"/>
<tx:method name="batchPurchase"/>
</tx:attributes>
</tx:advice>
<!-- 配置事务切入点 -->
<aop:config>
<aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.PurchaseService.purchase(..))"
id="pointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>
</aop:config>
<aop:config>
<aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.BatchPurchaseService.batchPurchase(..))"
id="batchPointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="batchPointCut"/>
</aop:config>
</beans> </code></pre>
<p>代码亲测可用。有什么错误地方可以指出。</p>
<p>到这里Spring4 的教程也就结束了。感谢您的观看!!!</p>
Spring4 JDBC详解
https://segmentfault.com/a/1190000011357127
2017-09-26T09:35:01+08:00
2017-09-26T09:35:01+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>Spring4 JDBC详解</h2>
<p>在之前的<a href="https://segmentfault.com/a/1190000011327065">Spring4 IOC详解</a> 的文章中,并没有介绍使用外部属性的知识点。现在利用配置c3p0连接池的契机来一起学习。本章内容主要有两个部分:配置c3p0(重点)和 使用 Spring JDBC模版。</p>
<h3>准备环境</h3>
<p>导入spring-jdbc的jar包</p>
<pre><code class="xml"> <dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.6.RELEASE</version>
</dependency> </code></pre>
<p>创建数据表</p>
<pre><code class="sql">SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `itdragon`
-- ----------------------------
DROP TABLE IF EXISTS `itdragon`;
CREATE TABLE `itdragon` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; </code></pre>
<hr>
<h3>配置c3p0</h3>
<p>c3p0的外部属性文件(可以在无需重启系统的情况下修改系统环境变量):db.properties。<br>db.properties文件中,user 和 password 分别表示 mysql连接 的账号和密码。driverClass 是加载的驱动。<br>jdbcUrl 是连接数据库的路径。格式是 jdbc:mysql://ip:port/数据库名(jdbc:mysql://localhost:3306/spring)<br>如果ip地址是本地,port是3306 是可以简写为 jdbc:mysql:///+数据库名。<br>initPoolSize 是 池内初始的数据连接个数,maxPoolSize是最大连接个数。和线程池是一样的概念。<br>如果你对这个不是很了解,可以先看看<a href="https://link.segmentfault.com/?enc=EApW3tACuFxChhmpH1OWjQ%3D%3D.S%2BpwmoK13NBLfjMgCfFdwFi%2BB0HN18NVsd5bGXr7wVw3WbzjvAT%2Bguar4VOr0axRUNMGdQwPjspDaGvT7JO%2BtA%3D%3D" rel="nofollow">jdbc操作mysql数据库</a></p>
<pre><code class="xml">jdbc.user=root
jdbc.password=root
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql:///spring
jdbc.initPoolSize=5
jdbc.maxPoolSize=10 </code></pre>
<p>核心文件applicationContext.xml ,首先要指定导入的资源<strong>context:property-placeholder</strong> , location从类路径下加载外部属性文件db.properties。<br>配置一个c3p0的bean,和普通bean一样。只是赋值采用el表达式${}。再配置两个jdbc的模版bean就可以了。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/>
<!-- 配置 C3P0 数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean>
<!-- 配置 Spirng 的 JdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数 -->
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
</beans> </code></pre>
<p>值得注意的是,在配置JdbcTemplate是用属性注入的方式,当然也可以用构造注入。但在配置NamedParameterJdbcTemplate只能通过构造注入的方式。源码如下:</p>
<pre><code class="java">/* */ public JdbcTemplate(DataSource dataSource)
/* */ {
/* 166 */ setDataSource(dataSource);
/* 167 */ afterPropertiesSet();
/* */ }
/* */ public NamedParameterJdbcTemplate(DataSource dataSource)
/* */ {
/* 89 */ Assert.notNull(dataSource, "DataSource must not be null");
/* 90 */ this.classicJdbcTemplate = new JdbcTemplate(dataSource);
/* */ }</code></pre>
<hr>
<h3>Spring JDBC模版</h3>
<p>先温故一下Mysql的基本语法<br>新增:INSERT [INTO] 表名 [(列名1, 列名2, 列名3, ...)] VALUES (值1, 值2, 值3, ...);<br>修改:UPDATE 表名 SET 列名=新值 WHERE 更新条件;<br>删除:DELETE FROM 表名 WHERE 删除条件;<br>查询:SELECT 列名称 FROM 表名称 [查询条件];<br>操作Mysql的测试方法,JdbcTemplate只是一个 JDBC的小工具。很多功能还不能实现,比如级联操作。真正的数据处理,还是交给Hibernate等ORM框架。</p>
<pre><code class="java">import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
public class MyJDBCMain {
private ApplicationContext ctx = null;
private JdbcTemplate jdbcTemplate = null; // JdbcTemplate 只是一个 JDBC的小工具,不支持级联属性
private NamedParameterJdbcTemplate namedParameterJdbcTemplate = null; // 支持具名参数,提高代码的可读性。
// 构造块,在创建对象的时候调用
{
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
jdbcTemplate = (JdbcTemplate) ctx.getBean("jdbcTemplate");
namedParameterJdbcTemplate = (NamedParameterJdbcTemplate) ctx.getBean("namedParameterJdbcTemplate");
}
// 测试是否连上数据库连接池
@Test
public void connectionDataSource() {
System.out.println("----------- connectionDataSource ----------");
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
try {
// 若能打印com.mchange.v2.c3p0.impl.NewProxyConnection 说明连接成功。
System.out.println(dataSource.getConnection());
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println("^^^^^^^^^^^ connectionDataSource ^^^^^^^^^^^");
}
/**
* 直接对Mysql数据进行增,删,改,查,统计的操作
*/
@Test
public void crudMysqlOperation() {
System.out.println("----------- crudMysqlOperation ----------");
// 插入
String insertSql = "INSERT INTO ITDragon (account, password) VALUES (?,?)";
System.out.println("insert result : " + jdbcTemplate.update(insertSql, "itdragon", "pwdItdragon"));
// 修改
String updateSql = "UPDATE ITDragon SET password = ? WHERE account = ?";
System.out.println("update result : " + jdbcTemplate.update(updateSql, "passwordItdragon", "itdragon"));
// 查询
String querySql = "SELECT * FROM ITDragon";
List<Map<String, Object>> results = jdbcTemplate.queryForList(querySql);
System.out.println("query result : " + results);
// 删除
String deleteSql = "DELETE FROM ITDragon WHERE account = ?";
System.out.println("delete result : " + jdbcTemplate.update(deleteSql, "itdragon"));
// 统计
String countSql = "SELECT count(id) FROM ITDragon";
System.out.println("count result : " + jdbcTemplate.queryForObject(countSql, Long.class));
System.out.println("^^^^^^^^^^^ crudMysqlOperation ^^^^^^^^^^^");
}
/**
* 直接对Mysql数据进行批量的增,删,改的操作
*/
@Test
public void batchCrudMysqlOperation() {
System.out.println("----------- batchCrudMysqlOperation ----------");
// 批量插入
String insertSql = "INSERT INTO ITDragon (account, password) VALUES (?,?)";
List<Object[]> insertArgs = new ArrayList<>();
insertArgs.add(new Object[]{"itdragon", "pwdItdragon"});
insertArgs.add(new Object[]{"blog", "pwdBlog"});
System.out.println("batch insert result : " + jdbcTemplate.batchUpdate(insertSql, insertArgs));
// 查询
String querySql = "SELECT * FROM ITDragon";
List<Map<String, Object>> results = jdbcTemplate.queryForList(querySql);
System.out.println("query result : " + results);
// 批量修改
String updateSql = "UPDATE ITDragon SET password = ? WHERE account = ?";
List<Object[]> updateArgs = new ArrayList<>();
updateArgs.add(new Object[]{"passwordItdragon", "itdragon"});
updateArgs.add(new Object[]{"passwordBlog", "blog"});
System.out.println("batch udpate result : " + jdbcTemplate.batchUpdate(updateSql, updateArgs));
// 批量删除
String deleteSql = "DELETE FROM ITDragon WHERE account = ?";
List<Object[]> deleteArgs = new ArrayList<>();
deleteArgs.add(new Object[]{"itdragon"});
deleteArgs.add(new Object[]{"blog"});
System.out.println("batch delete result : " + jdbcTemplate.batchUpdate(deleteSql, deleteArgs));
// 统计
String countSql = "SELECT count(id) FROM ITDragon";
System.out.println("count result : " + jdbcTemplate.queryForObject(countSql, Long.class));
System.out.println("^^^^^^^^^^^ batchCrudMysqlOperation ^^^^^^^^^^^");
}
/**
* 对象的增删改查,改用 NamedParameterJdbcTemplate
* 要求:参数名要和类的属性名一样
* 好处:之前的参数用?表示,不直观,代码的可读性较差,出错率较高。
* 缺点:多敲几个字母
*/
@Test
public void crudObjectOperation() {
System.out.println("----------- crudObjectOperation ----------");
// 插入
String insertSql = "INSERT INTO ITDragon (account, password) VALUES (:account,:password)";
ITDragon itDragon = new ITDragon();
itDragon.setAccount("itdragon");
itDragon.setPassword("pwdItdragon");
SqlParameterSource paramSource = new BeanPropertySqlParameterSource(itDragon);
System.out.println("insert object result : " + namedParameterJdbcTemplate.update(insertSql, paramSource));
// 查询
String querySql = "SELECT * FROM ITDragon";
// 使用 RowMapper 指定映射结果集的行
RowMapper<ITDragon> rowMapper = new BeanPropertyRowMapper<>(ITDragon.class);
List<ITDragon> results = namedParameterJdbcTemplate.query(querySql, rowMapper);
System.out.println("query object result : " + results);
// 更新和删除 也是update方法,这里不做过多的描述
System.out.println("^^^^^^^^^^^ crudObjectOperation ^^^^^^^^^^^");
}
} </code></pre>
<pre><code class="java">public class ITDragon {
private Integer id;
private String account;
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "ITDragon [id=" + id + ", account=" + account + ", password="
+ password + "]";
}
}</code></pre>
<pre><code class="java">----------- connectionDataSource ----------
com.mchange.v2.c3p0.impl.NewProxyConnection@16ba2b8
^^^^^^^^^^^ connectionDataSource ^^^^^^^^^^^
----------- crudMysqlOperation ----------
insert result : 1
update result : 1
query result : [{id=4, account=itdragon, password=passwordItdragon}]
delete result : 1
count result : 0
^^^^^^^^^^^ crudMysqlOperation ^^^^^^^^^^^
----------- batchCrudMysqlOperation ----------
batch insert result : [I@3c9dd8
query result : [{id=10, account=itdragon, password=pwdItdragon}, {id=11, account=blog, password=pwdBlog}]
batch udpate result : [I@c094f6
batch delete result : [I@1917d6d
count result : 0
^^^^^^^^^^^ batchCrudMysqlOperation ^^^^^^^^^^^
----------- crudObjectOperation ----------
insert object result : 1
query object result : [ITDragon [id=12, account=itdragon, password=pwdItdragon]]
^^^^^^^^^^^ crudObjectOperation ^^^^^^^^^^^ </code></pre>
<p>从打印的结果可以看出,如果插入,修改,删除一条数据,成功就返回1。如果是批量操作,则返回的是一个int[] 数组。<br>好了!到这里,Spring4 的JDBC就讲完了。有关jdbc的事务,就放到下一章介绍。喜欢的话可以点个赞哦!</p>
<p>一点点成长,一点点优秀。如果有什么建议和疑问可以留言。<br><img src="/img/bVVOAP?w=400&h=400" alt="20170925181846790" title="20170925181846790"></p>
Spring4 AOP详解
https://segmentfault.com/a/1190000011342620
2017-09-25T11:35:28+08:00
2017-09-25T11:35:28+08:00
itdragon
https://segmentfault.com/u/itdragon
6
<h2>Spring4 AOP详解</h2>
<p>第一章<a href="https://segmentfault.com/a/1190000011322952">Spring 快速入门</a>并没有对Spring4 的 AOP 做太多的描述,是因为AOP切面编程概念不好理解。所以这章主要从三个方面详解AOP:AOP简介(了解),基于注解的AOP编程(重点)和基于xml的AOP编程。</p>
<hr>
<h3>AOP简介</h3>
<h4>什么是AOP</h4>
<p>AOP(Aspect Oriented Programming)面向切面编程,是对传统的OOP(ObjectOriented Programming)面向对象编程的补充。</p>
<h4>AOP的作用</h4>
<p>如果A,B,C三个方法都要在执行前做验证操作,执行后做日志打印操作。肿么办?<br><img src="/img/bVVKPB?w=490&h=599" alt="20170925103224577" title="20170925103224577"></p>
<p>排版好丑。。。。。。</p>
<h4>AOP专业术语</h4>
<p><strong>切面(Aspect)</strong>: A,B,C,方法执行前都要调用的验证逻辑和执行后都要调用的日志逻辑,这两层业务逻辑就是切面。<br><strong>通知(Advice):</strong> 有五种通知,执行前,执行后,执行成功后,执行抛出异常后,环绕通知。就是切面执行的方法。<br><strong>目标(Target)</strong>: 被通知的对象,这里就是A,B,C三个方法。<br><strong>连接点(Joinpoint)</strong>:连接点是一个应用执行过程中能够插入一个切面的点。<br><strong>切点(pointcut)</strong>:每个类都拥有多个连接点,即连接点是程序类中客观存在的事务。AOP 通过切点定位到特定的连接点<br>打个比方:一天,三位侠客(被通知的对象Target)来我府上做客,被大门(切面Aspect)拦住,门前有五个保安(负责通知的Advice),因为其中一位侠客会降龙十八掌(满足被通知的一个条件Joinpoint),其中一位保安告知他:"你可以进去了"。另外两个侠客因为武艺超群(满足被通知的统一标准poincut)也都进去了。</p>
<hr>
<h3>基于注解的AOP编程</h3>
<p>基于注解的编程,需要依赖AspectJ框架(java中最流行的aop框架)。<br>第一步:导入AspectJ的jar包,该框架只有Spring 2.0以上才支持。</p>
<pre><code class="xml"><dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.2.2.RELEASE</version>
</dependency></code></pre>
<p>第二步:核心文件applicationContext.xml,里面需要配置自动扫描包(用于IOC注解)和配置启用AspectJ注解</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 自动扫描的包 -->
<context:component-scan base-package="com.itdragon.spring.*" ></context:component-scan>
<!-- 使 AspectJ 的注解起作用 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans> </code></pre>
<p>第三步:切面类,该类有什么特点?首先它必须是IOC的bean,还要声明它是AspectJ切面,最后还可以定义切面的优先级Order(非必填)<br>通知有五种注解<br><strong>@Before</strong> :前置通知的注解,在目标方法执行前调用<br><strong>@After</strong>:后置通知的注解, 在目标方法执行后调用,即使程序抛出异常都会调用<br><strong>@AfterReturning</strong>:返回通知的注解, 在目标方法成功执行后调用,如果程序出错则不会调用<br><strong>@AfterThrowing</strong>:异常通知的注解, 在目标方法出现指定异常时调用<br><strong>@Around</strong>:环绕通知的注解,很强大(相当于前四个通知的组合),但用的不多,<br>还有为了简化开发的重用切入点@Pointcut,以及抽象表达"*"</p>
<pre><code class="java">public interface Calculator {
public int add(int a, int b);
public int division(int a, int b);
} </code></pre>
<pre><code class="java">import org.springframework.stereotype.Repository;
@Repository("calculator")
public class CalculatorImp implements Calculator {
@Override
public int add(int a, int b) {
System.out.println("add 方法执行了 ----> " + (a + b));
return (a + b);
}
@Override
public int division(int a, int b) {
System.out.println("division 方法执行了 ----> " + (a / b));
return (a / b);
}
} </code></pre>
<pre><code class="java">import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @Order(n) : 切面的优先级,n越小,级别越高
* @Aspect:声明该类是一个切面
* @Component:切面必须是 IOC 中的 bean
*/
@Order(2)
@Aspect
@Component
public class LoggerAspect {
/**
* 前置通知的注解,在目标方法执行前调用
* execution最基础的表达式语法。
* 注意点:
* 1. 方法里面不能有行参,及add(int a, int b) 这是会报错的。
* 2. int(方法的返回值),add(方法名) 可以用 * 抽象化。甚至可以将类名抽象,指定该包下的类。
* 3. (int, int) 可以用(..)代替,表示匹配任意数量的参数
* 4. 被通知的对象(Target),建议加上包的路径
*/
@Before("execution(int com.atguigu.spring.my.aop.CalculatorImp.add(int , int))")
public void beforeAdvice(JoinPoint joinPoint) {
/**
* 连接点 joinPoint:add方法就是连接点
* getName获取的是方法名,是英文的,可以通过国际化转换对应的中文比较好。
*/
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("@Before 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
/**
* 后置通知的注解, 在目标方法执行后调用,即使是程序出错都会调用
* 这里将 方法的返回值 和 CalculatorImp类下所有的方法,以及方法的形参 都抽象了
*/
@After("execution(* com.atguigu.spring.my.aop.CalculatorImp.*(..))")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("@After 后置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
/**
* 重用切入点定义:声明切入点表达式。该方法里面不建议添加其他代码
*/
@Pointcut("execution(* com.atguigu.spring.my.aop.CalculatorImp.*(..))")
public void declareExecutionExpression(){}
/**
* 返回通知的注解, 在目标方法成功执行后调用,如果程序出错则不会调用
* returning="result" 和 形参 result 保持一致
*/
@AfterReturning(value="declareExecutionExpression()", returning="result")
public void afterRunningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("@AfterReturning 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
}
/**
* 异常通知的注解, 在目标方法出现指定异常时调用
* throwing="exception" 和 形参 exception 保持一致 , 且目标方法出了Exception(可以是其他异常)异常才会调用。
*/
@AfterThrowing(value="declareExecutionExpression()", throwing="exception")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
System.out.println("@AfterThrowing 异常通知 : 方法名 【 " + methodName + " 】and exception is " + exception);
}
} </code></pre>
<pre><code class="java">import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Order(1)
@Aspect
@Component
public class AroundAspect {
/**
* 环绕通知,很强大,但用的不多。 用环绕通知测试Order的优先级看的不明显(这里是笔者的失误)
* 环绕通知需要用ProceedingJoinPoint 类型的参数
*/
@Around("execution(* com.atguigu.spring.my.aop.CalculatorImp.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
try {
System.out.println("@Around 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
result = joinPoint.proceed();
System.out.println("@Around 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
} catch (Throwable e) {
e.printStackTrace();
System.out.println("@Around 异常通知 : 方法名 【 " + methodName + " 】and exception is " + e);
}
System.out.println("@Around 后置通知 : 方法名 【 " + methodName + " 】and args are " + args);
return result;
}
} </code></pre>
<pre><code class="java">import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator calculator = (Calculator) ctx.getBean("calculator");
calculator.add(11, 12);
calculator.division(21, 3); // 测试时,将被除数换成0,可以测试@AfterReturning , @After 和 @AfterThrowing
ctx.close();
}
} </code></pre>
<p>第四步:执行看结果。这里没有做环绕通知的打印。将被除数设置为零,可以测试 返回通知,后置通知 和 异常通知。</p>
<pre><code class="java">@Before 前置通知 : 方法名 【 add 】and args are [11, 12]
add 方法执行了 ----> 23
@After 后置通知 : 方法名 【 add 】and args are [11, 12]
@AfterReturning 返回通知 : 方法名 【 add 】and args are [11, 12] , result is 23
division 方法执行了 ----> 7
@After 后置通知 : 方法名 【 division 】and args are [21, 3]
@AfterReturning 返回通知 : 方法名 【 division 】and args are [21, 3] , result is 7 </code></pre>
<p>很简单对吧,用到的注解其实并不是很多。<br>以上代码有一个不足之处,就是测试Order优先级的时候,效果不明显。AroundAspect的优先级高于LoggerAspect,从打印的日志中发现,只有AroundAspect的前置通知在LoggerAspect前面打印,其他通知均在后面。<br>因为博客和课堂不同,如果把每个知识点都单独写出来,篇幅可能太长。笔者尽可能将所有知识点都写在一起,学A知识的同时将B,C,D的知识一起学习。但难免会有一些不听话的知识点。所以请各位读者见谅。<br><img src="/img/bVVKTe?w=250&h=233" alt="20170925095325141" title="20170925095325141"></p>
<hr>
<h3>基于xml的AOP编程</h3>
<p>上一篇文章讲到了基于xml的IOC设置bean,篇幅较长,内容较复杂。但配置AOP不同,它简单了很多。<br>第一步:核心文件applicationContext.xml,<br>首先是配置三个bean,方便是两个切面类,和一个方法类。<br>然后配置AOP,<br><strong>aop:config</strong>:注明开始配置AOP了,<br><strong>aop:pointcut</strong>:配置切点重用表达式,expression的值是具体的表达式,id 该aop:pointcut的唯一标识,<br><strong>aop:aspect</strong>:配置切面,ref的值引用相关切面类的bean,order设置优先级(也可以不设置)。<br>五种通知的配置:aop:before,aop:after,aop:after-returning,aop:after-throwing,aop:around。method的值就是对应的方法,poincut-ref的值要引用 aop:pointcut 的id。其中有两个比较特殊:aop:after-returning 要多配置一个returning,其中returning的值要和对应方法的形参保持一致。同理aop:after-throwing 也要多配置一个throwing,其中throwing的值也要和对应方法的形参保持一致。不然执行程序会报错。</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<bean id="calculator" class="com.atguigu.spring.my.xml.CalculatorImp"></bean>
<bean id="loggerAspect" class="com.atguigu.spring.my.xml.LoggerAspect"></bean>
<bean id="aroundAspect" class="com.atguigu.spring.my.xml.AroundAspect"></bean>
<!-- AOP配置 -->
<aop:config>
<!-- 配置切点表达式 类似注解的重用表达式-->
<aop:pointcut expression="execution(* com.atguigu.spring.my.xml.CalculatorImp.*(..))"
id="pointcut"/>
<!-- 配置切面及通知 method的值就是 loggerAspect类中的值-->
<aop:aspect ref="loggerAspect" order="2">
<aop:before method="beforeAdvice" pointcut-ref="pointcut"/>
<aop:after method="afterAdvice" pointcut-ref="pointcut"/>
<aop:after-returning method="afterRunningAdvice" pointcut-ref="pointcut" returning="result"/>
<aop:after-throwing method="afterThrowingAdvice" pointcut-ref="pointcut" throwing="exception"/>
</aop:aspect>
<aop:aspect ref="aroundAspect" order="1">
<!-- <aop:around method="aroundAdvice" pointcut-ref="pointcut"/> -->
</aop:aspect>
</aop:config>
</beans> </code></pre>
<p>第二步:下面几个类,就是脱去了所有注解的外衣,采用通过配置的xml,实现AOP编程。</p>
<pre><code class="java">public interface Calculator {
public int add(int a, int b);
public int division(int a, int b);
} </code></pre>
<pre><code class="java">public class CalculatorImp implements Calculator {
@Override
public int add(int a, int b) {
System.out.println("add 方法执行了 ----> " + (a + b));
return (a + b);
}
@Override
public int division(int a, int b) {
System.out.println("division 方法执行了 ----> " + (a / b));
return (a / b);
}
} </code></pre>
<pre><code class="java">import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
public class LoggerAspect {
public void beforeAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("Before 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("After 后置通知 : 方法名 【 " + methodName + " 】and args are " + args);
}
public void afterRunningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("AfterReturning 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
}
public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
System.out.println("AfterThrowing 异常通知 : 方法名 【 " + methodName + " 】and exception is " + exception);
}
} </code></pre>
<pre><code class="java">import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.ProceedingJoinPoint;
public class AroundAspect {
public Object aroundAdvice(ProceedingJoinPoint joinPoint) {
Object result = null;
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
try {
System.out.println("@Around 前置通知 : 方法名 【 " + methodName + " 】and args are " + args);
result = joinPoint.proceed();
System.out.println("@Around 返回通知 : 方法名 【 " + methodName + " 】and args are " + args + " , result is " + result);
} catch (Throwable e) {
e.printStackTrace();
System.out.println("@Around 异常通知 : 方法名 【 " + methodName + " 】and exception is " + e);
}
System.out.println("@Around 后置通知 : 方法名 【 " + methodName + " 】and args are " + args);
return result;
}
} </code></pre>
<pre><code class="java">import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Calculator calculator = (Calculator) ctx.getBean("calculator");
calculator.add(11, 12);
calculator.division(21, 3); // 测试时,将被除数换成0,可以测试AfterReturning ,After 和 AfterThrowing
ctx.close();
}
} </code></pre>
<pre><code class="java">Before 前置通知 : 方法名 【 add 】and args are [11, 12]
add 方法执行了 ----> 23
After 后置通知 : 方法名 【 add 】and args are [11, 12]
AfterReturning 返回通知 : 方法名 【 add 】and args are [11, 12] , result is 23
Before 前置通知 : 方法名 【 division 】and args are [21, 3]
division 方法执行了 ----> 7
After 后置通知 : 方法名 【 division 】and args are [21, 3]
AfterReturning 返回通知 : 方法名 【 division 】and args are [21, 3] , result is 7 </code></pre>
<p>到这里,基于xml文件的AOP编程也讲完了。4不4很简单。</p>
Spring4 IOC详解
https://segmentfault.com/a/1190000011327065
2017-09-23T20:31:48+08:00
2017-09-23T20:31:48+08:00
itdragon
https://segmentfault.com/u/itdragon
3
<h2>Spring4 IOC详解</h2>
<p><a href="https://segmentfault.com/a/1190000011322952">上一章</a>对Spring做一个快速入门的教程,其中只是简单的提到了IOC的特性。本章便对Spring的IOC进行一个详解。主要从三个方面开始:基于xml文件的Bean配置,基于注解的Bean配置和IOC容器Bean的生命周期。</p>
<hr>
<h3>基于xml文件的Bean配置</h3>
<p>首先是applicationContext.xml文件,这可是核心文件。<br>配置一个bean,需要一个id去唯一标识它,用class指定Bean对象的路径,作用域默认是单例。<br>通过prototype进行属性赋值,name是属性名,value是值,也可以用ref引用对象,用list,map设置集合。<br>用SpEL表达式语言赋值,用p命名空间简化赋值,用继承和依赖简化代码。</p>
<pre><code><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 配置一个 bean
bean中有一个id,且id是唯一的。若不指名则为该类的类名并首字母小写。
property 中有一个name,其name就是生产了setter方法的属性,value便是其值。
-->
<!-- Bean 的作用域
singleton:单例,默认值,容器初始化创建bean实例,在整个生命周期内只创建一个bean。
prototype:原型,容器初始化不创建bean实例,每次请求的时候会创建一个新的bean
-->
<bean id="entity" class="com.itdragon.spring.my.Entity" scope="prototype">
<!-- 属性赋值
属性注入使用 <property> 元素, 使用 name 属性指定 Bean 的属性名称,value 属性或 <value> 子节点指定属性值
-->
<property name="intValue" value="1"></property>
<!-- 引用bean
细节:
1. 用ref 是引用外部bean
2. 也可以直接写在当前bean里面,这就是内部bean, 内部bean是不能被外部引用
-->
<property name="spELEntity" ref="spELEntity"></property>
<!-- 构造器注入
构造器注入在 <constructor-arg> 元素里声明属性, <constructor-arg> 中没有 name 属性,按照构造器参数顺序赋值
细节:
1. 也可以用index(按索引匹配入参),和type(按类型匹配入参)去指定赋值。实际上是没有必要的
2. 使用构造器注入的前提是 entity 被初始化。
3. 特殊字符,需要用 <![CDATA[内容]]> 这属于xml语法
-->
<constructor-arg value="1.1" />
<constructor-arg >
<value><![CDATA[<ITDragon>]]></value>
</constructor-arg>
<property name="listValue">
<list>
<value>欢迎阅读</value>
<value>ITDragon</value>
<value>的博客!</value>
</list>
</property>
<property name="mapValue">
<map>
<entry key="one" value="1"></entry>
<entry key="two" value="2"></entry>
</map>
</property>
</bean>
<!-- SpEL 表达式语言, #{…} 作为定界符, 操作和java相似-->
<bean id="spELEntity" class="com.itdragon.spring.my.SpELEntity">
<property name="intSpel" value="#{1}"></property>
<property name="floatSpel" value="#{entity.intValue + 0.2}"></property>
<property name="stringSpel" value="#{'Spring4基础教程'}"></property>
<property name="bSpel" value="#{2 >= 1}"></property>
</bean>
<!-- 使用 p 命名空间
给Entity 对象 的intValue 字段设置 为2 ,根据名称匹配SpEL对象
自动装配 autowire (不常用)
byName(根据名称自动装配): 必须将目标 Bean 的名称和属性名设置的完全相同.反之不能
byType(根据类型自动装配): 若 IOC 容器中有多个与目标 Bean 类型一致的 Bean. 在这种情况下, 不能执行自动装配.
-->
<bean id="entity2" class="com.itdragon.spring.my.Entity"
p:intValue="2" autowire="byName" />
<!-- 继承 依赖
如果 被继承的bean 不想被继承,则要加上 abstract="true"
依赖:如果被依赖的bean,没有实例化,则会报错。如果有多个依赖需用","分开
-->
<bean id="entity3" class="com.itdragon.spring.my.Entity"
parent="entity" depends-on="spELEntity" />
<!-- 使用外部属性文件 在Spring操作数据库的时候再讲 -->
</beans> </code></pre>
<p>实体类Entity和SpELEntity</p>
<pre><code>import java.util.List;
import java.util.Map;
public class Entity {
private int intValue;
private float floatValue;
private String stringValue;
private SpELEntity spELEntity;
private Map<String, Object> mapValue;
private List<Object> listValue;
public Entity() {
}
public Entity(float floatValue, String stringValue) {
this.floatValue = floatValue;
this.stringValue = stringValue;
}
public int getIntValue() {
return intValue;
}
public void setIntValue(int intValue) {
this.intValue = intValue;
}
public float getFloatValue() {
return floatValue;
}
public void setFloatValue(float floatValue) {
this.floatValue = floatValue;
}
public String getStringValue() {
return stringValue;
}
public void setStringValue(String stringValue) {
this.stringValue = stringValue;
}
public SpELEntity getSpELEntity() {
return spELEntity;
}
public void setSpELEntity(SpELEntity spELEntity) {
this.spELEntity = spELEntity;
}
public Map<String, Object> getMapValue() {
return mapValue;
}
public void setMapValue(Map<String, Object> mapValue) {
this.mapValue = mapValue;
}
public List<Object> getListValue() {
return listValue;
}
public void setListValue(List<Object> listValue) {
this.listValue = listValue;
}
@Override
public String toString() {
return "Entity [intValue=" + intValue + ", floatValue=" + floatValue
+ ", stringValue=" + stringValue + ", spELEntity=" + spELEntity
+ ", mapValue=" + mapValue + ", listValue=" + listValue + "]";
}
} </code></pre>
<pre><code>public class SpELEntity {
private int intSpel;
private float floatSpel;
private boolean bSpel;
private String stringSpel;
public int getIntSpel() {
return intSpel;
}
public void setIntSpel(int intSpel) {
this.intSpel = intSpel;
}
public float getFloatSpel() {
return floatSpel;
}
public void setFloatSpel(float floatSpel) {
this.floatSpel = floatSpel;
}
public boolean isbSpel() {
return bSpel;
}
public void setbSpel(boolean bSpel) {
this.bSpel = bSpel;
}
public String getStringSpel() {
return stringSpel;
}
public void setStringSpel(String stringSpel) {
this.stringSpel = stringSpel;
}
@Override
public String toString() {
return "SpELEntity [intSpel=" + intSpel + ", floatSpel=" + floatSpel
+ ", bSpel=" + bSpel + ", stringSpel=" + stringSpel + "]";
}
} </code></pre>
<p>测试Main方法。首先要创建一个容器,然后通过bean的id获取到实例,这样就可以开始相关的操作。其中entity,entity2,entity3对应三块不同的知识点。</p>
<pre><code>import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
/**
* ClassPathXmlApplicationContext:从 类路径下加载配置文件,建议用该方法
* FileSystemXmlApplicationContext: 从文件系统中加载配置文件
*/
// 1. 创建 Spring 的 IOC 容器
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
/**
* entity 属性注入,构造器注入,对象引用,集合,SpEL,(重点)
* entity2 自动装配和P命名空间
* entity3 继承和依赖,作用域
*/
// 2. 从 IOC 容器中获取 bean 的实例
Entity entity = (Entity) ctx.getBean("entity");
// 3. 使用 bean
System.out.println(entity.toString());
// System.out.println(ctx.getBean("entity") == ctx.getBean("entity")); 使用 prototype 打印的是false
ctx.close();
}
} </code></pre>
<pre><code>Entity [intValue=1, floatValue=1.1, stringValue=<ITDragon>, spELEntity=SpELEntity [intSpel=1, floatSpel=1.2, bSpel=true, stringSpel=Spring4基础教程], mapValue={one=1, two=2}, listValue=[欢迎阅读, ITDragon, 的博客!]] </code></pre>
<hr>
<h3>基于注解的Bean配置</h3>
<p>相对于xml的配置,注解的方式显得异常简单。主要分两个步骤<br>第一步:在applicationContext.xml文件设置扫描包的范围</p>
<pre><code><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-4.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 配置自动扫描指定目录下的包
resource-pattern="xxx/*.class" 属性过滤特定的类
-->
<context:component-scan base-package="com.itdragon.spring.my" >
<!-- annotation 是针对指定的类 和 assignable 是针对所有继承或者扩展该类的类-->
<!-- context:exclude-filter 只排除expression里面的内容
<context:exclude-filter type="annotation" expression=""/>
-->
<!-- context:include-filter 只包含expression里面的内容
需配合 use-default-filters="false"(默认是true) 一起使用
<context:include-filter type="annotation" expression=""/>
-->
</context:component-scan>
</beans> </code></pre>
<p>第二步:在对象上用注解。这四几个注解的作用一样,只是为了结构清晰,取的名字不同罢了。<br>使用方法很简单:直接在类上加注解即可。无参数的情况,bean的id默认是小写字母开头的类名。也可以指定参数@Commponent("指定参数"),那bean的id就是指定参数。<br>@Component: 基本注解, 标识了一个受 Spring 管理的组件<br>@Respository: 标识持久层组件<br>@Service: 标识服务层(业务层)组件<br>@Controller: 标识表现层组件</p>
<pre><code>public interface AnnoRepository {
public void hello();
} </code></pre>
<pre><code>import org.springframework.stereotype.Repository;
@Repository
public class AnnoRepositoryImp implements AnnoRepository{
@Override
public void hello() {
System.out.println("AnnoRepository : hello!");
}
} </code></pre>
<pre><code>import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AnnoService {
@Autowired
private AnnoRepository annoRepository;
public void hello() {
System.out.println("AnnoService : hello!");
annoRepository.hello();
}
} </code></pre>
<pre><code>import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
@Controller
public class AnnoController {
@Autowired
private AnnoService annoService;
public void execut() {
System.out.println("AnnoController : hello !");
annoService.hello();
}
} </code></pre>
<p>这里还有一个注解Autowired, <context:component-scan> 元素会自动组件装配被Autowired修饰的对象。<br>测试类:虽然applicationContext.xml中没有annoController的bean配置,但我们有注解!</p>
<pre><code>import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
AnnoController annoController = (AnnoController) ctx.getBean("annoController");
annoController.execut();
ctx.close();
}
} </code></pre>
<pre><code>AnnoController : hello !
AnnoService : hello!
AnnoRepository : hello! </code></pre>
<p>有没有觉得基于注解的bean配置比基于xml的bean配置简单很多。</p>
<hr>
<h3>IOC容器Bean的生命周期</h3>
<p>step1 实例化,通过构造器创建 Bean 实例<br>step2 赋值,为 Bean 的属性设置值<br>step3 init-method,调用 Bean 的初始化方法(init-method)<br>step4 destroy-method,当容器关闭时, 调用 Bean 的销毁方法(destroy-method)</p>
<p>以上代码都是笔者亲测可用的,不要嫌麻烦,麻烦是学不好的,如果有什么问题和建议可以留言,我会及时处理。<a href="https://link.segmentfault.com/?enc=CW%2Bms8nMbEVqylKLjxCqcw%3D%3D.kyFCb9W30%2FZL0I6lNCziw9JZlKAyRA3smlD20YZwagp2rPJ2QnmcYx9MXMo7E2EOr%2B9HpKKrW70eIfuTza5Gkw%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=nPrEs%2BbTbO5uTwxuetqqrQ%3D%3D.l1imRWywIZn0JzFFKa%2FzV%2FUTiaDrWD1OBCFCzFBvS9aGywkarKgtzckfu%2F3Xnlmxl5Z%2FhQhYNYhnpnbBSmVyjw%3D%3D" rel="nofollow">http://blog.csdn.net/qq_19558...</a></p>
<p><img src="/img/bVVGQe?w=237&h=227" alt="20170923190228262" title="20170923190228262"></p>
Spring4 快速入门
https://segmentfault.com/a/1190000011322952
2017-09-23T12:27:46+08:00
2017-09-23T12:27:46+08:00
itdragon
https://segmentfault.com/u/itdragon
5
<h2>Spring4 快速入门</h2>
<h3>1 Spring简介</h3>
<h4>1.1 Spring是什么?</h4>
<p><strong>Spring</strong> 是一个 <strong>IOC</strong> 和 <strong>AOP</strong> 容器的开源框架,为简化企业级应用而生。<br><strong>IOC(Inversion of Control)</strong>控制反转,不再是等待容器返回资源,而是主动让容器推送资源。<br><img src="/img/bVVFLQ?w=311&h=521" alt="20170923102752554" title="20170923102752554"></p>
<p>其中DI(Dependency Injection)依赖注入,就是IOC的一种表现方式。说白了,就是利用xml解析+java反射机制技术,读取配置文件给对象赋值罢了(也就是下面的第四步)。<br>详解可参考:<a href="https://segmentfault.com/a/1190000011327065">Spring4 IOC详解</a><br><strong>AOP(Aspect Oriented Programming)</strong>面向切面编程,暂时不说!!!<a href="https://segmentfault.com/a/1190000011342620">Spring4 AOP详解</a></p>
<h4>1.2 Spring 做什么?</h4>
<p>Spring就像一个管家,帮你管理事务。传统的应用,应用层(Struts2)和事务层(Service)联系很紧密,通过Spring管理之间的关系,减低其耦合性。说白了,Spring的出现就是为了解决现有问题,使开发更快捷,更健壮。若是越用越麻烦,谁还用他。另外,一定要好好学习Spring,他可是有一统天下的野心。有针对Struts2的SpringMVC,有针对Hibernate/mybatis的SpringData,以及为了简化开发的 Spring boot 和 Spring Cloud。你害怕了么?</p>
<hr>
<h3>2 入门代码</h3>
<p>按照国际惯例,我们先来一个Hello World得意。<br>第一步:创建maven web项目,不清楚的可以访问:<a href="https://link.segmentfault.com/?enc=gUvCrp94WHgdGf3aROK8tw%3D%3D.1%2FlT%2B8ojJGqBq3zWOq6ipEiHEgIkxk86k%2FO6OkCZm2KvqfYIhBzgqBZPFrN45PW9ookXbpV1AmMvI55uG5Fenw%3D%3D" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=FMthpTnNRjLTeWNLKFfwlQ%3D%3D.OhuHvmITZeuByZVHOG%2BVBnIXErTxog7eegYTvfh%2BIHGSSkFeL8TBFQY8ivFjE8clwQktRhb8nvgjdL9mVlIbHA%3D%3D" rel="nofollow">http://blog.csdn.net/qq_19558...</a><br>第二步:导包。maven导包的好处,就是可以自己控制自己,把需要的最佳的依赖导入,从而减少包的冲突。<br><img src="/img/bVVFL0?w=140&h=115" alt="20170923111840148" title="20170923111840148"></p>
<pre><code><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>spring</groupId>
<artifactId>helloworld</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.2.1.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<!-- spring start -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring end -->
</dependencies>
</project> </code></pre>
<p>第三步:HelloWorld对象</p>
<pre><code>package com.spring.hello;
public class HelloWorld {
private String hello;
public void setHello(String hello) {
this.hello = hello;
}
public void helloWorld(){
System.out.println("Spring say :"+hello);
}
} </code></pre>
<p>第四步:配置xml文件,要严格按照语法来写,便于Spring底层代码解析xml</p>
<pre><code><?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 配置一个 bean -->
<bean id="helloWorld" class="com.spring.hello.HelloWorld">
<!-- 为属性赋值 -->
<property name="hello" value="Hello World"></property>
</bean>
</beans> </code></pre>
<p>第五步:体验一把依赖注入的快感</p>
<pre><code>import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class HelloTest {
public static void main(String[] args) {
//1. 创建Spring 的IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
//2. 从容器中获取 Bean 其实就是new 的过程
HelloWorld helloWorld = (HelloWorld) ctx.getBean("helloWorld");
// 也可以是,但不建议 HelloWorld helloWorld = ctx.getBean(HelloWorld.class);
//3. 执行函数
helloWorld.helloWorld();
}
} </code></pre>
<p>运行结果如果打印以下内容,则说明完成</p>
<pre><code>十一月 23, 2015 12:01:46 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@edbe39: startup date [Mon Nov 23 12:01:46 CST 2015]; root of context hierarchy
十一月 23, 2015 12:01:46 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [beans.xml]
Spring say :Hello World </code></pre>
<p>那么一个简单的spring HelloWorld就做好了,是不是很简单!如果你觉得很简单,那么你已经打开了Spring的大门。如果你觉得很难理解,没事!多看敲几遍。原文链接:<a href="https://link.segmentfault.com/?enc=l%2F8emtWXbdWwmfRdHvok6A%3D%3D.EuTwguIRocEQvKvzxNu74V5WVCpDkVQlbNpwjXjJpFdBFNwQjnbI3LvrrJZUtvZ6" rel="nofollow"></a><a href="https://link.segmentfault.com/?enc=KGvAWs8sU5VC5tRAmbOJoQ%3D%3D.RA1pfhJ5ab%2FmSi%2FxG0cgOK5F3FErhijzEi2QRZKQ1XuHDrgr0Nh3HylceyoyC%2BOZ" rel="nofollow">http://write.blog.csdn.net/po...</a><br>每天都在进步,每天都在成长,如果有什么问题和建议可以留言,我会及时处理。如果你觉得不错,可以点个赞哦!</p>
<hr>
<p>----------------------- 以上内容写于 2015-11-23 10:49 ,现在从csdn搬家过来, 以下是2017-10-24 补充内容 -----------------------</p>
<hr>
<p>其实以上内容并不完全是 spring4.x 的方法 =。=|||<br>在spring 2.x 和 spring 3.x 的时代:我们通常采用基本配置一般用xml方式,业务开发用注解方式。其实spring 3.x 已经提供了java编程方式。而在spring 4.x的时候就已经开始推荐使用java编程时。原因和具体的使用方法后续有时间补充介绍<br>使用起来很简单,两个注解:</p>
<ol>
<li>@Configuration 修饰在类上,告知程序这是一个配置类。你可以理解为一个xml文件。</li>
<li>@Bean 修改在一个getXXX() 方法上,告知程序它已经实例化了。你可以理解为是一个bean。</li>
</ol>
初识Queue队列
https://segmentfault.com/a/1190000011318194
2017-09-22T21:41:33+08:00
2017-09-22T21:41:33+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>架构师入门笔记三 初识Queue队列</h2>
<h3>wait和notify模拟Queue</h3>
<h4>wait/notify 基础知识</h4>
<p>线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理,就不能成为一个整体,线程之间的通信就成为整体的必用方法之一。<br>使用 wait/notify 方法注意点:<br>1)wait 和 notify 必须要配合 synchronized 关键字使用<br>2)wait方法是释放锁的, notify方法不释放锁。</p>
<h4>wait/notify 模拟BlockingQueue</h4>
<p>BlockingQueue:是一个队列,并且支持阻塞的机制,阻塞的放入和得到数据。我们要实现 LinkedBlockingQueue 下面两个简单的方法put 和 take<br>put(an object):把一个object 加到BlockingQueue里,如果BlockingQueue没有空间,则调用此方法的线程会被阻断,直到BlockingQueue里面有空间再继续。<br>take:取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状直到BlockingQueue有新数据被加入。</p>
<pre><code>import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class MyQueue {
//1 需要一个承装元素的集合
private LinkedList<Object> list = new LinkedList<Object>();
//2 需要一个计数器 AtomicInteger (原子性)
private AtomicInteger count = new AtomicInteger(0);
//3 需要制定上限和下限
private final int minSize = 0;
private final int maxSize ;
//4 构造方法
public MyQueue(int size){
this.maxSize = size;
}
//5 初始化一个对象 用于加锁
private final Object lock = new Object();
//put(anObject): 把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.
public void put(Object obj){
synchronized (lock) {
while(count.get() == this.maxSize){
try {
lock.wait(); // 当Queue没有空间时,线程被阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//1 加入元素
list.add(obj);
//2 计数器累加
count.incrementAndGet();
//3 新增元素后,通知另外一个线程(唤醒),队列多了一个元素,可以做移除操作了。
lock.notify();
System.out.println("新加入的元素为:" + obj);
}
}
//take: 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入.
public Object take(){
Object ret = null;
synchronized (lock) {
while(count.get() == this.minSize){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//1 做移除元素操作
ret = list.removeFirst();
//2 计数器递减
count.decrementAndGet();
//3 移除元素后,唤醒另外一个线程,队列少元素了,可以再添加操作了
lock.notify();
}
return ret;
}
public int getSize(){
return this.count.get();
}
public static void main(String[] args) {
final MyQueue mq = new MyQueue(5);
mq.put("a");
mq.put("b");
mq.put("c");
mq.put("d");
mq.put("e");
System.out.println("当前容器的长度:" + mq.getSize());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
mq.put("f");
mq.put("g");
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
Object o1 = mq.take();
System.out.println("移除的元素为:" + o1);
Object o2 = mq.take();
System.out.println("移除的元素为:" + o2);
}
},"t2");
t1.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
t2.start();
}
} </code></pre>
<p>从打印的信息可以看出,当t2 线程移除数据后,t1线程才开始加入数据</p>
<hr>
<h3>并发Queue</h3>
<h4>Queue 简介</h4>
<p>Queue的主要实现如下图所示。<br>图片描述</p>
<p>Queue主要分两类,一类是高性能队列 ConcurrentLinkedQueue; 一类是阻塞队列 BlockingQueue</p>
<h4>ConcurrentLinkedQueue接口</h4>
<p>ConcurrentLinkedQueue:是一个适合高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能。通常ConcurrentLinkedQueue性能好于BlockingQueue。它是一个基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最后加入的。该队列不允许null元素。<br>ConcurrentLinkedQueue 重要方法:<br>add() 和 offer() 都是加入元素,该队列中无区别<br>poll() 和 peek() 都是取头元素节点,区别在于前者会删除元素,后者不会</p>
<pre><code>import java.util.concurrent.ConcurrentLinkedQueue;
public class MyConcurrentLinkedQueue {
public static void main(String[] args) throws Exception {
//高性能无阻塞无界队列:ConcurrentLinkedQueue
ConcurrentLinkedQueue<String> q = new ConcurrentLinkedQueue<String>();
q.offer("a");
q.offer("b");
q.offer("c");
q.offer("d");
q.add("e");
System.out.println(q.poll()); // 打印结果:a (从头部取出元素,并从队列里删除)
System.out.println(q.size()); // 打印结果:4 (执行poll 后 元素减少一个)
System.out.println(q.peek()); // 打印结果:b (a 被移除了,首元素就是b)
System.out.println(q.size()); // 打印结果:4 (peek 不移除元素)
}
} </code></pre>
<h4>BlockingQueue接口</h4>
<p>ArrayBlockingQueue: 基于数组的阻塞队列实现。在内部,维护了一个定长数组,以便缓存队列中的数据对象。其内部没有实现读写分离,也就意味着生产和消费不能完全并行。长度是需要定义的,可以指定先进先出或者先进后出,是一个有界队列。</p>
<pre><code>import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class MyArrayBlockingQueue {
public static void main(String[] args) throws Exception {
ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5); // 可以尝试 队列长度由3改到5
array.offer("offer 方法 插入数据成功返回true 否则返回false");
array.offer("3秒后插入数据", 3, TimeUnit.SECONDS);
array.put("put 方法 若超出长度就会阻塞等待");
array.add("add 方法 在超出长度时会提示错误信息 java.lang.IllegalStateException"); // java.lang.IllegalStateException: Queue full
System.out.println(array.offer("true or false", 3, TimeUnit.SECONDS));
System.out.println(array);
}
} </code></pre>
<p>LinkedBlockingQueue:基于列表的阻塞队列。同ArrayBlockingQueue类似,其内部维护了一个数据缓冲队列(该队列由一个链表构成)。它之所以能够高效的处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。是一个无界队列<br>用法和 ArrayBlockingQueue 差不多 。区别在于,LinkedBlockingQueue是无界队列,初始化的时候,可以设置一个长度,也可以不设置。<br>SynchronousQueue:一种没有缓冲的队列,生存者生产的数据直接会被消费者获取并消费。</p>
<p>以上三个队列,用于什么场景呢?举个坐地铁例子: <br>在人少的时候,直接刷卡进站,无需等待,这用SynchronousQueue。 <br>上班高峰期,人很多,刷卡的时候需要排队,这用LinkedBlockingQueue无界队列。<br>放假高峰期,人满人患,这时候就要用有界队列ArrayBlockingQueue。如果采用LinkedBlockingQueue 无界队列的话,进来的人太多会影响地铁站正常工作了,所以人太多就不让进,等下次。</p>
<p>PriorityBlockingQueue:基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口),在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁,是一个无界队列。<br>传入队列的对象:Task</p>
<pre><code>public class Task implements Comparable<Task>{
private int id ;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Task task) {
return this.id > task.id ? 1 : (this.id < task.id ? -1 : 0);
}
public String toString(){
return this.id + "," + this.name;
}
} </code></pre>
<p>PriorityBlockingQueue 排序:</p>
<pre><code>import java.util.concurrent.PriorityBlockingQueue;
public class MyPriorityBlockingQueue {
public static void main(String[] args) throws Exception{
PriorityBlockingQueue<Task> q = new PriorityBlockingQueue<Task>();
// 由大到小的设置
Task t1 = new Task();
t1.setId(1);
t1.setName("id为1");
Task t2 = new Task();
t2.setId(4);
t2.setName("id为4");
Task t3 = new Task();
t3.setId(9);
t3.setName("id为9");
Task t4 = new Task();
t4.setId(16);
t4.setName("id为16");
Task t5 = new Task();
t5.setId(5);
t5.setName("id为5");
// 故意打乱顺序进入队列
q.add(t3);
q.add(t4);
q.add(t1);
q.add(t2);
q.add(t5);
System.out.println("初始队列容器:" + q);
System.out.println("第一个元素:" + q.take()); // 执行take后排序(取值后排序输出)
System.out.println("执行take方法后容器:" + q);
}
} </code></pre>
<p>DelayQueue:带有延迟时间的Queue,其中的元素只有指定的延迟时间到了,才能够从队列中获取到该元素,DelayQueue中的元素必须实现Delayed接口,DelayQueue是一个没有大小限制的队列,应用场景很多,比如对缓存超时的数据进行移除,任务超时处理,空闲连接的关闭等等<br>摘录网上代码,一个网吧上网的案例:</p>
<pre><code>import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class Wangmin implements Delayed {
private String name;
//身份证
private String id;
//截止时间
private long endTime;
//定义时间工具类
private TimeUnit timeUnit = TimeUnit.SECONDS;
public Wangmin(String name,String id,long endTime){
this.name=name;
this.id=id;
this.endTime = endTime;
}
public String getName(){
return this.name;
}
public String getId(){
return this.id;
}
/**
* 用来判断是否到了截止时间
*/
@Override
public long getDelay(TimeUnit unit) {
//return unit.convert(endTime, TimeUnit.MILLISECONDS) - unit.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS);
return endTime - System.currentTimeMillis();
}
/**
* 相互批较排序用
*/
@Override
public int compareTo(Delayed delayed) {
Wangmin w = (Wangmin)delayed;
return this.getDelay(this.timeUnit) - w.getDelay(this.timeUnit) > 0 ? 1:0;
}
} </code></pre>
<pre><code>import java.util.concurrent.DelayQueue;
public class WangBa implements Runnable {
private DelayQueue<Wangmin> queue = new DelayQueue<Wangmin>();
public boolean yinye =true;
public void shangji(String name,String id,int money){
Wangmin man = new Wangmin(name, id, 1000 * money + System.currentTimeMillis());
System.out.println("网名"+man.getName()+" 身份证"+man.getId()+"交钱"+money+"块,开始上机...");
this.queue.add(man);
}
public void xiaji(Wangmin man){
System.out.println("网名"+man.getName()+" 身份证"+man.getId()+"时间到下机...");
}
@Override
public void run() {
while(yinye){
try {
Wangmin man = queue.take();
xiaji(man);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String args[]){
try{
System.out.println("网吧开始营业");
WangBa siyu = new WangBa();
Thread shangwang = new Thread(siyu);
shangwang.start();
siyu.shangji("路人甲", "123", 1);
siyu.shangji("路人乙", "234", 10);
siyu.shangji("路人丙", "345", 5);
}
catch(Exception e){
e.printStackTrace();
}
}
} </code></pre>
初识线程关键字
https://segmentfault.com/a/1190000011315132
2017-09-22T17:24:29+08:00
2017-09-22T17:24:29+08:00
itdragon
https://segmentfault.com/u/itdragon
0
<h2>架构师入门笔记一 初识线程关键字</h2>
<p>本章节主要介绍线程的关键字 synchronized,volatile 的含义,使用方法,使用场景,以及注意事项。</p>
<h3>线程安全</h3>
<p>首先我们要了解线程安全的概念:当多个线程访问某一个类(对象或方法)时,这个类始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。<br>要如何确保类能表现出正确的行为,这就需要关键字出马!</p>
<hr>
<h3>关键字</h3>
<h4>synchronized</h4>
<p>synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区"<br>其工作原理:当一个线程想要执行synchronized修饰的方法,必须经过以下三个步骤</p>
<ol>
<li>step1 尝试获得锁</li>
<li>step2 如果拿到锁,执行synchronized代码体内容</li>
<li>step3 如果拿不到锁,这个线程就会不断地尝试获得这把锁,直到拿到为止。这个过程可能是多个线程同时去竞争这把锁(锁竞争的问题)。</li>
</ol>
<p>注*(多个线程执行的顺序是按照CPU分配的先后顺序而定的,而并非代码执行的先后顺序)</p>
<p>使用方法介绍:<br>synchronized 重入<br>在使用synchronized时,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。</p>
<pre><code>public class MySyncReentrant {
/**
* 重入调用
*/
private synchronized void method1() {
System.out.println("^^^^^^^^^^^^^method1");
method2();
}
private synchronized void method2() {
System.out.println("-----------------------method2");
method3();
}
private synchronized void method3() {
System.out.println("********************method3");
}
public static void main(String[] args) {
final MySyncReentrant mySyncReentrant = new MySyncReentrant();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
mySyncReentrant.method1();
}
}, "reentrant");
thread.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
SunClass sunClass = new SunClass();
sunClass.sunMethod();
}
});
thread2.start();
}
/**
* 有父子继承关系的类,如果都使用了synchronized关键字,也是线程安全的。
*/
static class FatherClass {
public synchronized void fatherMethod(){
System.out.println("fatherMethod....");
}
}
static class SunClass extends FatherClass{
public synchronized void sunMethod() {
System.out.println("sunMethod....");
this.fatherMethod();
}
}
} </code></pre>
<p>synchronized 代码块:<br>如果被修饰的方法执行需要很长时间,线程之间等待的时间就会很长,所以将synchronized 修饰在代码块上是可以优化执行时间。(这也叫减少锁的粒度)<br>synchronized (this) {} , 可以是 this(对象锁) class(类锁) Object lock = new Object(); 任何对象锁。</p>
<pre><code>import java.util.concurrent.atomic.AtomicInteger;
public class CodeBlockLock {
// 对象锁
private void thisLock () {
synchronized (this) {
System.out.println("this 对象锁!");
}
}
// 类锁
private void classLock () {
synchronized (CodeBlockLock.class) {
System.out.println("class 类锁!");
}
}
// 任何对象锁
private Object lock = new Object();
private void objectLock () {
synchronized (lock) {
System.out.println("object 任何对象锁!");
}
}
// 字符串锁,注意String常量池的缓存功能
private void stringLock () {
synchronized ("string") { // new String("string")
try {
for(int i = 0; i < 3; i++) {
System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 字符串锁改变
private String strLock = "lock";
private void changeStrLock () {
synchronized (strLock) {
try {
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !");
strLock = "changeLock";
Thread.sleep(5000);
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final CodeBlockLock codeBlockLock = new CodeBlockLock();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.thisLock();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.classLock();
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.objectLock();
}
});
thread1.start();
thread2.start();
thread3.start();
// 如果字符串锁,用new String("string") t4,t5线程是可以获取锁的,如果直接使用"string" ,若锁不释放,t5线程一直处理等待中
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.stringLock();
}
}, "t4");
Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.stringLock();
}
}, "t5");
thread4.start();
thread5.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 字符串变了,锁也会改变,导致t7线程在t6线程未结束后变开始执行,但一个对象的属性变了,不影响这个对象的锁。
Thread thread6 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.changeStrLock();
}
}, "t6");
Thread thread7 = new Thread(new Runnable() {
@Override
public void run() {
codeBlockLock.changeStrLock();
}
}, "t7");
thread6.start();
thread7.start();
}
} </code></pre>
<p>运行结果:</p>
<pre><code>this 对象锁!
class 类锁!
object 任何对象锁!
thread : t4 stringLock !
thread : t4 stringLock !
thread : t4 stringLock !
thread : t5 stringLock !
thread : t5 stringLock !
thread : t5 stringLock !
thread : t6 changeLock start !
thread : t7 changeLock start !
thread : t6 changeLock end !
thread : t7 changeLock end ! </code></pre>
<p>注* 给String的常量加锁,容易会出现死循环的情况。 如果加锁的字符串变了,锁也会变。若一个对象的属性变了,是不影响这个对象的锁。static + synchronized 一起使用 是类级别的锁<br>synchronized 异常:<br>synchronized 遇到异常后,自动释放锁,让其他线程调用。如果第一个线程在执行任务时,因为异常导致业务逻辑未能正常执行。后续的线程执行的任务也都是异常的。所以在编写代码时一定要考虑周全</p>
<hr>
<h4>同步与异步</h4>
<p>同步的概念就是共享,其目标就是为了线程安全(线程安全的两个特性:原子性和可见性),A线程获取对象的锁,若B线程想要执行synchronized方法,就需要等待,这就是同步。<br>异步的概念就是独立,A线程获取对象的锁,若B线程想要执行非synchronized方法,是无需等待的,这就是异步。可以参考ajax请求。</p>
<pre><code>public class SyncAndAsyn {
private synchronized void syncMethod() {
try {
System.out.println(Thread.currentThread().getName() + " synchronized method!");
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 若次方法也加上了 synchronized,就必须等待t1线程执行完后,t2才能调用
private void asynMethod() {
System.out.println(Thread.currentThread().getName() + " asynchronized method!");
}
public static void main(String[] args) {
final SyncAndAsyn syncAndAsyn = new SyncAndAsyn();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
syncAndAsyn.syncMethod();
}
}, "t1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
syncAndAsyn.asynMethod();
}
}, "t2");
thread1.start();
thread2.start();
}
} </code></pre>
<hr>
<h4>volatile</h4>
<p>volatile关键字不具备synchronized关键字的原子性(同步)其主要作用就是使变量在多个线程中可见。<br>原理图:<br><img src="/img/bVVDSa?w=500&h=391" alt="图片描述" title="图片描述"></p>
<p>在java中,每一个线程都会有一块工作内存区,其中存放着所有线程共享的<strong>主内存中的变量值的拷贝</strong>。当线程执行时,他在自己的工作内存区中操作这些变量。<br>为了存取一个共享的变量,一个线程通常先获取锁定并去清除它的内存工作区,把这些共享变量从所有线程的共享内存区中正确的装入到他自己的所在的工作内存区中。当线程解锁时保证该工作内存区中变量的值写回到共享内存中。<br>而<strong>volatile的作用就是强制线程到主内存里面去读取变量</strong>,而不去线程工作内存区里面读,从而实现了多个线程间的变量可见。也就是满足线程安全的可见性。<br>可见性:(被volatile修饰的变量,线程执行引擎是直接从主内存中读取变量的值)</p>
<pre><code>public class VolatileThread extends Thread{
// 如果不加 volatile,会导致 "thread end !" 一直没有打印,
private volatile boolean flag = true;
@Override
public void run() {
System.out.println("thread start !");
while (flag) {
}
System.out.println("thread end !");
}
public static void main(String[] args) {
VolatileThread thread = new VolatileThread();
thread.start();
try { // 等线程启动了,再设置值
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.setFlag(false);
System.out.println("flag : " + thread.isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
} </code></pre>
<p>volatile 不具备原子性:(多线程之间不是同步的,存在线程安全,从下面的例子中可以得知:如果是同步的,最后一次打印绝对是1000*10 。为了弥补这个问题,可以考虑使用atomic类的系类对象)</p>
<pre><code>import java.util.concurrent.atomic.AtomicInteger;
/**
* volatile关键字不具备synchronized关键字的原子性(同步)
*/
public class VolatileNoAtomic extends Thread{
// 多次执行程序,会发现最后打印的结果不是1000的整数倍.中途打印不是1000的整数倍,可能是因为System.out打印的延迟造成的
// private static volatile int count;
private static AtomicInteger count = new AtomicInteger(0); // 不会出现以上的情况
private static void addCount(){
for (int i = 0; i < 1000; i++) {
// count++ ;
count.incrementAndGet();
}
System.out.println(count);
}
public void run(){
addCount();
}
public static void main(String[] args) {
VolatileNoAtomic[] arr = new VolatileNoAtomic[10];
for (int i = 0; i < 10; i++) {
arr[i] = new VolatileNoAtomic();
}
// 执行10个线程
for (int i = 0; i < 10; i++) {
arr[i].start();
}
}
} </code></pre>
<p>其实atomic类 并非完美,它也只能保证自己方法是原子性,若要保证多次操作也是原子性,就需要synchronized的帮忙(若不用synchronized修饰,打印的结果中会出现非10倍数的信息,需多次执行才能模拟出来)</p>
<pre><code>import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicUse {
private static AtomicInteger count = new AtomicInteger(0);
//多个addAndGet在一个方法内是非原子性的,需要加synchronized进行修饰,保证4个addAndGet整体原子性 .
/**synchronized*/
public int multiAdd(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.addAndGet(1);
count.addAndGet(2);
count.addAndGet(3);
count.addAndGet(4); //+10
return count.get();
}
public static void main(String[] args) {
final AtomicUse au = new AtomicUse();
List<Thread> ts = new ArrayList<Thread>();
for (int i = 0; i < 100; i++) {
ts.add(new Thread(new Runnable() {
@Override
public void run() {
System.out.println(au.multiAdd());
}
})); // 添加100个线程
}
for(Thread t : ts){
t.start();
}
}
} </code></pre>
<p>以上便是初识线程关键字的内容,方便自己以后查阅,也希望对读者有些帮助。</p>