一、介绍
1.1、项目描述
bbs论坛系统是一个相对比较轻量级的java社区系统,上手简单、学习成本低、比较容易扩展,主要包括用户注册、登录、退出、发帖回帖、主页、贴子置顶删除等功能。其中还具有全局用户错误处理、语法直观,性能高超,功能能齐全的模板引擎和强大的搜索技术等特点。
1.2、项目特点
bbs论坛系统采用spring boot2,Spring Cache,Elastic Search,Beetl,BeetlSQL框架基于MVC机制开发的一套简易的社区论坛系统,可拿来即用。强大、灵活的搜索满足绝大部分社区用户的需求。支持MySQL、Oracle等主流数据库。
Elastic Search : 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是Lucene 那么简单,它不仅包括了全文搜索功能,还可以进行以下工作;分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。实时分析的分布式搜索引擎。可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。Elastic Search的两个基本特征就是储存和索引,Elasticsearch是面向文档的,是面向文档型数据库,即一条数据就是一个文档,它可以存储整个对象或文档。然而它不仅仅是存储,还会索引每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。这种理解数据的方式与以往完全不同,这也是Elasticsearch能够执行复杂的全文搜索的原因之一,同时具有强大的索引能力使得大大提升了搜索性能。因此搜索便是Elastic Search 最核心功能。后面的发帖模块的分析会给出Elastic Search的基本使用,也可翻阅官方api地址:https://es.xiaoleilu.com/
Beetl :java模板引擎,Beetl相对于其他java模板引擎,具有功能齐全,语法直观,性能超高,以及编写的模板容易维护等特点。使得开发和维护模板有很好的体验。Beetl的核心是GroupTemplate,是一个重量级对象,实际使用的时候建议使用单模式创建,创建GroupTemplate需要俩个参数,一个是模板资源加载器,一个是配置类,可参考:http://ibeetl.com/guide/#beetl
BeetlSQL:BeetSql是一个全功能DAO工具类似我们常用的ORM框架Mybatis、Hibernate 、JPA,但BeerlSQL同时具有Hibernate 和 Mybatis优点功能,适用于承认以SQL为中心,同时又需求工具能自动能生成大量常用的SQL的应用。使用大量内置SQL,SQL 模板基于Beetl实现,更容易写和调试,以及扩展可以针对单个表(或者视图)代码生成pojo类和sql模版,甚至是整个数据库。能减少代码编写工作量提高开发效率。其它可参考:http://ibeetl.com/guide/#beetlsql
对于大部分开发者这3个框架也许是我们用得最少的,因此这个社区论坛系统也是我们学习Elastic Search,Beetl,BeetlSQL这几个框架很好的一个demo
1.3 、项目介绍
此社区系统为单模块应用,未实行前后端分离,前后端交互是采用MVC ModelAndView 的那套机制与原理,使用ModelAndView类用来存储处理完后的结果数据,以及显示该数据的视图。业务处理器调用模型层处理完用户请求后,把结果数据存储在该类的model属性中,把要返回的视图信息存储在该类的view属性中,然后让该ModelAndView返回该Spring MVC框架对该对象进行解析,最后把结果数据显示在指定的页面上。
1.4、本地部署
. 环境要求:jdk1.8+ 、MySql5.5+
. 创建表database 数据库编码为 UTF-8
. 执行数据库脚本,执行 install-mysql.sql 文件,初始化数据
. git clone 项目 运行 BbsMain.class类
.项目访问路径:http://localhost:8084/bbs/bbs/index
.账号密码:admin/123456
.必须安装elastic search 作为全文搜索:
下载 https://www.elastic.co/downloads/past-releases/elasticsearch-5-4-3(更高级的版本目前暂时未验证通过),进入bin目录,调用elasticsearch 启动
.安装elastic search 分词:进入bin目录,运行 ./elasticsearch-plugin install analysis-smartcn
然后重新启动elasticsearch
注:本地部署测试只需要安装elasticsearch,然后cmd进入安装elasticsearch的bin目录调用elasticsearch启动即可进行发帖搜索等相关功能实践
二、项目后端源码分析
2.1、分析项目的思路
当我们熟悉一个项目时,我们首先应该要清楚的知道这个项目运用了哪些技术,其次我们就需要知道这个项目是怎么样进行数据交互的(即:前后端是怎样进行数据交互的),再深层次我们则可了解设计原理、思路以及其它相关技术的原理等等。(下面的登录模块详细介绍了其交互流程)
2.2 、登录模块
在首页index.html页面中引入了layout.html,如下:
在layout.html中有如下一段代码,其采用了ajax方式登录
在后台登录接口
核心代码有两行:
1、 BbsUser user =bbsUserService.getUserAccount(userName, password);通过用户名和密码检测获取用户信息
2、WebUtils.loginUser(request, response, user, true); 将用户信息set到cookie使户登陆状态维持,cookie设计为:des(私匙).encode(userId~time~maxAge~password~ip),并且为了保证安全同时将cookie指定为httpOnly
2.3、发帖模块
2.3.1、首先我们安装 Elasticsearch后安装Marvel,Marvel是Elasticsearch的管理和监控工具,在开发环境下免费使用。它包含了一个叫做Sense的交互式控制台,使用户方便的通过浏览器直接与Elasticsearch进行交互。安装Marvel不是必须的,但是它可以通过在你本地Elasticsearch集群中运行示例代码。Marvel是一个插件,可在Elasticsearch目录中运行以下命令来下载和安装:
./bin/plugin -i elasticsearch/marvel/latest
echo'marvel.agent.enabled: false'>> ./config/elasticsearch.yml 这个命令则是关闭监控
运行 Elasticsearch启动速度会有点慢耐心等待会,其索引地址默认端口为9200
web管理界面head 安装 进入bin目录下,打开cmd,进入dos界面 输入:plugin install mobz/elasticsearch-head 进行下载,注册服务进入bin目录下,打开cmd,进入dos界面 依次输入: service.bat install service.bat start 成功之后,再输入 services.msc 跳转到Service服务界面,可以直接查看es的运行状态!
访问http://localhost:9200/页面显示有东西则表示启动成功,接下来我们可以开始各种实验了我们在项目的pom文件中加入elasticsearch依赖
2.3.2、我们在项目的配置文件 application.properties 配置Elasticsearch的地址
#Elasticsearch
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
#Elasticsearch bbs索引地址
elasticsearch.bbs.content.url=http://127.0.0.1:9200/bbs/content/_search
2.3.3、Elasticsearch 模板配置,这个模板根据自己的需求自行配置下面以发帖为例简述其使用,当然这里只是最基本的一些使用,高级点的还有分布式集群、聚合以及索引的种类等想要了解更多的可参考官方文档或相关博客。
发帖模块算是整个论坛系统的核心模块之一,这里用spring AOP技术(此处使用的是AOP的环绕通知@Around 在这里其底层使用的是cglib动态代理)通过匹配持有指定注解@EsIndexs或@EsIndexType的方法实现了创建索对象引并且Elasticsearch数据保存。我们也以发帖模块为例来说明项目中如何来使用Elasticsearch
@EsIndexType和EsIndexs中用@Document标记,表示这个类为文档文档对象,@Document注解是Elasticsearch的常用的注解之一,其常用的属性有①indexName:对应索引库名称;②type:对应在索引库中的类型;④shards:分片数;replicas:副本数;indexStoreType:索引文件存储类型;createIndex:是否创建索引。另外Elasticsearch还有@Field,作用在成员变量,标记为文档的字段并制定映射属性。@Id:作用在成员变量,标记一个字段为id主键;一般id字段或是域不需要存储也不需要分词;type:字段的类型,取值是枚举FieldType;index:是否索引,布尔值类型,默认是true;store:是否存储,布尔值类型,默认值是false;analyzer:分词器名称,一般情况下Elasticsearch都会把该字段存储到Field域中,只是当store= false时,默认设置;那么给字段只存储在"_source"的Field域中,当store = true时,该字段的value会存储在一个跟_source平级的独立Field域中;同时也会存储在_source中,因此,我们在使用store时需注意以下两点:(1)source field在索引的mapping中disable了。这种情况下如果不将某个field定义成store=true,那些将无法在返回的查询结果中看到这个field;(2)__source的内容非常大我们想要在返回的_source document中解释出某个field的值的话,开销会很大(当然你也可以定义source filtering将减少network overhead),比例某个document中保存的是一本书,所以document中可能有这些field: title, date, content。假如我们只是想查询书的title 跟date信息,而不需要解释整个_source(非常大),这个时候我们可以考虑将title, date这些field设置成store=true。(3)虽然ield store可以减少查询的开销但其实这样也会加大disk的访问频率。假如你将_source中的10个field都定义store,那么在你查询这些field的时候会将会有10次disk seek的操作。而返回_source只有一次disk seek的操作。所以这个也是我们在定义的时候需要blance的。
基本用法:
@Field(index=true)表示是否索引;
@Field(analyzer="ik_max_word",searchAnalyzer="ik_max_word")表示是否分词,如果是分词就会按照分词的单词搜索,如果不是分词就按照整体搜索
@Field(store=true)是否存储,也就是页面上显示。
注:analyzer在Elasticsearch中扮演者分析器的角色,在全文搜索中,词是一个搜索单元,表示文本中的一个词,标记表示在文本字段中出现的词,由词的文本、在原始文本中的开始和结束偏移量、以及数据类型等组成。ElasticSearch 把文档数据写到倒排索引(Inverted Index)的结构中,倒排索引建立词(Term)和文档之间的映射,索引中的数据是面向词,而不是面向文档的。分析器(Analyzer)的作用就是分析(Analyse),用于把传入Lucene的文档数据转化为倒排索引,把文本处理成可被搜索的词。分析器由一个分词器(Tokenizer)和零个或多个标记过滤器(TokenFilter)组成,也可以包含零个或多个字符过滤器(Character Filter)。
在ElasticSearch引擎中,分析器的任务是分析(Analyze)文本数据,分析是分词,规范化文本的意思,其工作流程是:首先,字符过滤器对分析(analyzed)文本进行过滤和处理,例如从原始文本中移除HTML标记,根据字符映射替换文本等,过滤之后的文本被分词器接收,分词器把文本分割成标记流,也就是一个接一个的标记,然后,标记过滤器对标记流进行过滤处理,例如,移除停用词,把词转换成其词干形式,把词转换成其同义词等,最终,过滤之后的标记流被存储在倒排索引中;ElasticSearch引擎在收到用户的查询请求时,会使用分析器对查询条件进行分析,根据分析的结构,重新构造查询,以搜索倒排索引,完成全文搜索请求,可见,分析器扮演的是处理索引数据和查询条件的重要角色。在2.4版本中,ElasticSearch 预定义了7个分析器,并且支持用户根据预定义的字符过滤器,分词器和标记过滤器创建自定义的分析器,以满足用户多样性的文本分析需求。用户在创建索引时配置索引的分析,通过向ElasticSearch发送请求,在请求body的settings 配置节中设置索引的分析器,例如,为索引配置默认的分析器:
我们也可自定义分词如:
分词发生在文档创建和搜索的时候即读时分词和写时分词,读时分词发生在用户查询时,ElasticSearch会即时地对用户输入的关键词进行分词,分词结果只存在内存中,当查询结束时,分词结果也会随即消失。而写时分词发生在文档写入时,ElasticSearch会对文档进行分词后,将结果存入倒排索引,该部分最终会以文件的形式存储于磁盘上,不会因查询结束或者 ElasticSearch重启而丢失。ElasticSearch中处理分词的部分被称作分词器,英文是Analyzer,它决定了分词的规则。ElasticSearch自带了很多默认的分词器,比如Standard、Keyword、Whitespace等等,默认是Standard。当我们在读时或者写时分词时可以指定要使用的分词器。
发帖这部分代码使用的是aop的环绕通知首先获取索引的注解集合,EsIndexType索引类型,EsIndexs索引,然后获取获取索引的数据集合,调用原方法保存相关数据至数据库,最后组装指定索引类型的数据,保存索引
@Pointcut("@annotation(com.ibeetl.bbs.es.annotation.EsIndexType) || @annotation(com.ibeetl.bbs.es.annotation.EsIndexs)")
private void anyMethod(){//定义ES的切入点
}
@Around("anyMethod()")public Object simpleAop(ProceedingJoinPoint pjp) throws Throwable{
try {
Signature sig = pjp.getSignature();
MethodSignature msig = (MethodSignature) sig;//代理方法
Object target = pjp.getTarget();//代理类
Method method = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
EsIndexType esIndexType = method.getAnnotation(EsIndexType.class);
EsIndexs esIndexs = method.getAnnotation(EsIndexs.class);
//获取索引的注解集合
List types =new ArrayList<>();
if(esIndexs !=null){
types.addAll(Arrays.asList(esIndexs.value()));
}
if(esIndexType !=null){
types.add(esIndexType);
}
//获取索引的数据集合
List typeDatas =new ArrayList<>();
//当操作为删除时,需要提前获取id
for (EsIndexType index : types) {
if(index.operateType() == EsOperateType.DELETE) {
String key = index.key();
Map parameterNames =this.getParameterNames(pjp);
Integer id = (Integer)parameterNames.get(key);
if(id ==null) {
logger.error(target.getClass().getName()+"$"+msig.getName()+":未获取到主键,
无法更新索引");
}else {
BbsIndex bbsIndex =esService.createBbsIndex(index.entityType(), (Integer)id);
//创建Elasticsearch索引,可根据自己的规则创建唯一索引,这里的规则为:
//bbs_post、bbs_reply、bbs_topic 这三张表的主键id拼接
String md5Id = EsUtil.getEsKey(bbsIndex.getTopicId(),bbsIndex.getPostId()
,bbsIndex.getReplyId());
EsIndexTypeData data = new EsIndexTypeData(
index.entityType(), index.operateType(), md5Id);
typeDatas.add(data);
}
}
}
Object o = pjp.proceed();
//当操作为更新时,可以从返回值中获取id
for (EsIndexType index : types) {
if(index.operateType() != EsOperateType.DELETE) {
Integer id =null;
String key = index.key();
Map parameterNames =this.getParameterNames(pjp);
id = (Integer)parameterNames.get(key);
boolean resultErr =false;
if(id ==null) {
if(oinstanceof ModelAndView) {
ModelAndView modelAndView = (ModelAndView)o;
id = (Integer)modelAndView.getModel().get(key);
}else if(oinstanceof JSONObject) {
JSONObject json = (JSONObject)o;
id = json.getInteger(key);
resultErr =1 == json.getInteger("err")?true:false;
}
}
if(id ==null) {
if(!resultErr){
logger.error(target.getClass().getName()+"$"+msig.getName()+":未获取到主键,
无法更新索引");
}
}else {
EsIndexTypeData data =new EsIndexTypeData(index.entityType(), index.operateType(), id);
typeDatas.add(data);
}
}
}
for (EsIndexTypeData esIndexTypeData : typeDatas) {
esService.editEsIndex(esIndexTypeData.getEntityType(), esIndexTypeData.getOperateType(), esIndexTypeData.getId());
}
return o;
}catch (Exception e) {
throw e;
}
}
BbsIndexRepository类是 extends CrudRepository
现在Elasticsearch中已经存储了一些数据,我们可以根据业务需求开始进行搜索实践了
2.4、论坛首页select模块
如果不是通过关键字搜索则仅仅走数据库查询,如果是通过关键字查询则先查询索引即先查询Elasticsearch所存储的文裆值然后再通过相关对应值查询数据库。这里只分析下通过关键字搜索即索引查询。对PageQuery searcherKeywordPage =this.esService.getQueryPage(keyword,p);的getQueryPage()方法进行分析。当我们检索一个关键字帖子时我们只要执行HTTP GET请求并指出文档的“地址”——索引、类型和ID既可。根据这三部分信息,我们就可以返回原始JSON文档:
try {
HttpHeaders headers =new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
//渲染模板,bssContent.html为模板
Template template =beetlTemplate.getTemplate("/bssContent.html");
template.binding("pageSize", pageSize);
template.binding("pageNumber", pageNumber);
template.binding("keyword", keyword);
String esJson = template.render();
HttpEntity httpEntity =new HttpEntity(esJson,headers);
//使用restTemplate.postForObject()发送请求获取本地Elasticsearch服务上的值;
// elasticsearch.bbs.content.url为配置文件中配置的Elasticsearch bbs索引地址
String result =restTemplate.postForObject(env.getProperty("elasticsearch.bbs.content.url"),
httpEntity, String.class);
List indexObjectList =new ArrayList<>();
//使用ObjectMapper对含有任意key的JSON进行反序列化将多个实体放到Map中
JsonNode root =new ObjectMapper().readTree(result);
//获取Elasticsearch服务上的值的总条数
long total = root.get("hits").get("total").asLong();
//获取Elasticsearch服务上的值文本list
Iterator iterator = root.get("hits").get("hits").iterator();
//循环iterator
while(iterator.hasNext()) {
JsonNode jsonNode = iterator.next();
double score = jsonNode.get("_score").asDouble();
//循环获取值并序列化为BbsIndex
BbsIndex index =new ObjectMapper().convertValue(jsonNode.get("_source"), BbsIndex.class);
index.setContent(jsonNode.get("highlight").get("content").get(0).asText());
//获取数据库相关信息并set到indexObject对象中
if(index.getTopicId() !=null) {
IndexObject indexObject =null;
BbsTopic topic =bbsService.getTopic(index.getTopicId());
BbsUser user = topic.getUser();
BbsModule module = topic.getModule();
.......................................省略其他
最后通过ModelAndView渲染到页面
2.5、帖子删除
帖子删除同样用到了spring AOP环绕通知技术,这里AOP底层同样是cjlib动态代理,通过扫描注解获取索引的注解集合,通过id从数据库查早相应信息后用发帖时相同的规则MD5加密,删除数据库bbs_topic、bbs_reply、bbs_post表的相关数据,最后通过id删除Elasticsearch服务上存储的值
通过索引删除Elasticsearch对应的数据
2.6、回复帖子、置顶、精华模块
回复帖子置顶和设置精华功能模块实现与发帖和删帖类似,同样是spring AOP环绕通知,回帖对bbs_topic表的post_count(回帖总数)进行统计加以,用户和帖子关联表bbs_message对应数据插入,内容表bbs_post插入回帖数据同时更改用户表的等级等相关信息,Elasticsearch服务上对应索引的值也做相应更改
2.7、注册、退出
注册时password使用MD5加密,退出移除session
以上就是整个bbs论坛系统后端的分析,前端部分很基础,所有的请求都是ajax请求
三、学习目的
1、在分布式系统尤其一些电商核心业务交易系统的设计都需要实时大数据搜索分析,比如,商品中心的上千万的数据需要实时搜索,再到海量的在线订单实时查询都需要用到搜索,在一些DevOps的工具中都需要提供强大的实时搜索功能,elasticsearch就是一套很好的解决方案它可以帮助你用前所未有的速度去处理大规模数据。
2、beetl模板也是我们在web开发中经常使用的,他可生成特定格式的文档使用户界面与业务数据分离。