文章目录
一、概述
在上一篇文章中,我们主要分析了代码生成包的工具类,和一部分搜索引擎包中的工具类。经过小组成员讨论,决定继续由我分析关于搜索引擎包下的几个工具类。
本次要分析的几个工具类都与搜索引擎Lucene有关,位于下面的包下
经过这一部分的分析,我希望能更好地掌握有关Lucene搜索引擎的相关知识
除了这一部分以外,本篇博客还将分析项目中关于日志打印的工具类。
希望经过相关的学习讨论,能更好地掌握关于日志打印方面的内容
二、代码分析
2.1 IKAnalyzer5x类
本类是Lucene的IK分词器,继承了Analyzer类。而Analyzer类拥有构建分词器,分析文章中的关键词。
首先我们关注引入的包
本类中引入了下面两个类,都是由lucene搜索引擎提供的
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.Tokenizer;
接下来我们看到类的定义
该类继承了Analyzer类
public class IKAnalyzer5x extends Analyzer
接下来看构造方法
在本类中,一共有两个构造方法。
默认的构造方法将useSmart属性置为false,使用细粒度切分算法
在有参的构造方法中,可以传入true,使IK分词器使用智能切分
/** * IK分词器Lucene Analyzer接口实现类 * * 默认细粒度切分算法 */public IKAnalyzer5x(){ this(false); } /** * IK分词器Lucene Analyzer接口实现类 * * @param useSmart 当为true时,分词器进行智能切分 */public IKAnalyzer5x(boolean useSmart){ super(); this.useSmart = useSmart; }
接下来是这个类剩余的一些方法
获取是否使用智能切分算法,设置useSmart属性的值
private boolean useSmart; public boolean useSmart() { return useSmart; } public void setUseSmart(boolean useSmart) { this.useSmart = useSmart; }
重写createComponents,用于构造分词组件
/** * 重写createComponents * 重载Analyzer接口,构造分词组件 */@Override protected TokenStreamComponents createComponents(String fieldName) { Tokenizer _IKTokenizer = new IKTokenizer5x(this.useSmart()); return new TokenStreamComponents(_IKTokenizer); }
2.2 IKTokenizer5x类
IKTokenizer5x是一个中文分词器,与上面的类相似,都提供了相关的分词方法
首先我们看到这个类的引入
import org.apache.lucene.analysis.Tokenizer; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import org.apache.lucene.analysis.tokenattributes.OffsetAttribute; import org.apache.lucene.analysis.tokenattributes.TypeAttribute; import org.wltea.analyzer.core.IKSegmenter; import org.wltea.analyzer.core.Lexeme; import java.io.IOException;
引入了apache相关的分词器依赖,同时又引入了Java IO流的相关类
接下来我们看到这个类的属性,由于这个类继承了Tokenizer类,具体属性的解释在注释中有。
//IK分词器实现 private IKSegmenter _IKImplement; //词元文本属性 private final CharTermAttribute termAtt; //词元位移属性 private final OffsetAttribute offsetAtt; /*词元分类属性(该属性分类参考org.wltea.analyzer.core.Lexeme中的分类常量)*/private final TypeAttribute typeAtt; //记录最后一个词元的结束位置 private int endPosition;
接下来我们看到这个类的构造方法,在这个构造方法中,调用了addAttribute方法对属性进行初始化。而这个addAttribute方法,是apache官方在lucene搜索引擎的AttributeSource类下的一个方法
/*IK构造器*/public IKTokenizer5x(boolean useSmart){ super(); offsetAtt = addAttribute(OffsetAttribute.class); termAtt = addAttribute(CharTermAttribute.class); typeAtt = addAttribute(TypeAttribute.class); _IKImplement = new IKSegmenter(input, useSmart); }
通过查看addAttribute方法我们发现,这个方法底层利用Java的反射机制创建相应的对象。
public final <T extends Attribute> T addAttribute(Class<T> attClass) { AttributeImpl attImpl = attributes.get(attClass); if (attImpl == null) { if (!(attClass.isInterface() && Attribute.class.isAssignableFrom(attClass))) { throw new IllegalArgumentException( "addAttribute() only accepts an interface that extends Attribute, but " + attClass.getName() + " does not fulfil this contract." ); } addAttributeImpl(attImpl = this.factory.createAttributeInstance(attClass)); } return attClass.cast(attImpl); }
接下来我们看到下一个方法,这个方法的主要作用是用来进行中文分词,在分词的过程中设置词元的各种属性。这个方法的具体作用已经放在注释中说明了。值得一提的是,这个方法中的clearAttributes方法,与上面的方法一样,都调用了AttributeSource类的方法。接下来我们看一下这个方法做了什幺
@Override public final boolean incrementToken() throws IOException { /*清除所有的词元属性*/clearAttributes(); Lexeme nextLexeme = _IKImplement.next(); if(nextLexeme != null){ /*将Lexeme转成Attributes*//*设置词元文本*/termAtt.append(nextLexeme.getLexemeText()); /*设置词元长度*/termAtt.setLength(nextLexeme.getLength()); /*设置词元位移*/offsetAtt.setOffset(nextLexeme.getBeginPosition(), nextLexeme.getEndPosition()); /*记录分词的最后位置*/endPosition = nextLexeme.getEndPosition(); /*记录词元分类*/typeAtt.setType(nextLexeme.getLexemeTypeString()); /*返会true告知还有下个词元*/return true; } /*返会false告知词元输出完毕*/return false; }
在这个方法中,利用for循环,使用state=state.next进行迭代前进,调用clear方法将对应的状态清除。
public final void clearAttributes() { for (State state = getCurrentState(); state != null; state = state.next) { state.attribute.clear(); } }
接下来我们看到这个类的最后两个方法。这两个方法分别代表重置分词器,以及分词结束后执行的方法。在end方法中,重置了分词器的偏移。这两个方法重写了父类中的reset和end方法
@Override public void reset() throws IOException { super.reset(); _IKImplement.reset(input); } @Override public final void end() { //设置最终偏移 int finalOffset = correctOffset(this.endPosition); offsetAtt.setOffset(finalOffset, finalOffset); }
2.3 QueryUtil类
这个类的内容比较简单,主要是对要查询的字符串进行一些基本的操作。
首先我们看到这个类引入的包,可以看到,这个类同样与lucene搜索引擎相关,引入了如Analyzer,Query等类。
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.Query;
接下来我们看到这个类的主体部分,可以看到,这个类主要包含两个方法,一个是queryStringFilter方法,另一个是query方法。两个方法都是静态方法,方便调用。
在queryStringFilter方法中,使用字符串的replace方法将/与\重置为空格。这个方法的主要作用是过滤非法字符。
在query方法中,首先调用了BooleanQuery的setMaxClauseCount方法,这个方法用于设置每个 BooleanQuery 允许的最大子句数。通过阅读源码可知,这个值得默认值为1024。接下来调用了queryStringFilter方法过滤非法字符。之后构建一个MultiFieldQueryParser对象,并设置默认的条件为or,最后返回一个Query对象。
public class QueryUtil { private static String queryStringFilter(String query) { return query.replace("/", " ").replace("\\", " "); } public static Query query(String query, Analyzer analyzer, String... fields) throws ParseException { BooleanQuery.setMaxClauseCount(32768); //过滤非法字符 query = queryStringFilter(query); MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer); parser.setDefaultOperator(QueryParser.Operator.OR); return parser.parse(query); } }
2.4 DBLog类
上面我们提到了搜索引擎,接下来我们开始分析本项目中与日志相关的类。
日志记录在一个系统中是非常重要的,在本项目中,使用Slf4j作为日志记录的工具
在这个类中,首先我们关注引入的包,可以看到引入了Slf4j类,还引入了BlockingQueue类,LinkedBlockingQueue类作为暂时存储的队列
import com.sdu.nurse.security.api.vo.log.LogInfo; import com.sdu.nurse.security.gate.service.LogService; import lombok.extern.slf4j.Slf4j; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
接下来我们关注方法头,这个方法继承了Java的Thread类,说明这个方法可多线程运行,并且加上了@Slf4j注解
@Slf4j public class DBLog extends Thread{ }
由于这个类继承了Thread类,下面我们关注这个类重写的run方法。
在这个类中首先新建了一个LogInfo的缓冲队列,用于保存日志信息,接下来通过for-each循环读取缓冲队列的数据。在finally语句块中,如果缓冲区不为空,则将缓冲区中的内容清除。
@Override public void run() { // 缓冲队列 List<LogInfo> bufferedLogList = new ArrayList<LogInfo>(); while (true) { try { bufferedLogList.add(logInfoQueue.take()); logInfoQueue.drainTo(bufferedLogList); if (bufferedLogList != null && bufferedLogList.size() > 0) { // 写入日志 for(LogInfo log:bufferedLogList){ logService.saveLog(log); } } } catch (Exception e) { e.printStackTrace(); // 防止缓冲队列填充数据出现异常时不断刷屏 try { Thread.sleep(1000); } catch (Exception eee) { } } finally { if (bufferedLogList != null && bufferedLogList.size() > 0) { try { bufferedLogList.clear(); } catch (Exception e) { } } } } }
接下来我们关注LogService对象,这个类在后面会说明,目前只需要知道,这个类是用于调用一些日志的相关服务即可,这里有get和set方法,其中set方法直接返回一个DBLog对象,便于链式调用。
public LogService getLogService() { return logService; } public DBLog setLogService(LogService logService) { if(this.logService==null) { this.logService = logService; } return this; } private LogService logService;
最后我们关注以下几个方法
第一个方法加上了synchronized关键字,说明这是一个线程同步的方法,通过getInstance方法放回一个本类的一个实例对象
第二个方法中,调用了父类(Thread类)中的构造方法,传入了字符串(代表线程名)
第三个方法用于写入对应的日志信息
public static synchronized DBLog getInstance() { if (dblog == null) { dblog = new DBLog(); } return dblog; } private DBLog() { super("CLogOracleWriterThread"); } public void offerQueue(LogInfo logInfo) { try { logInfoQueue.offer(logInfo); } catch (Exception e) { log.error("日志写入失败", e); } }
2.5 LogService接口和LogServiceImpl类
这两个类相比前几个类来说比较简单。分别是日志服务的接口和接口的实现类
首先我们看到接口,这个接口中只有一个方法,就是保存日志信息的方法,里面传入了一个LogInfo参数
public interface LogService { void saveLog(LogInfo info); }
接下来我们关注实现类,可以看到,这个实现类上加上了@Component注解,并且自动注入了一个WebClient.Builder对象,最后log.debug函数在控制台上输出对应的日志
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @Component @Slf4j public class LogServiceImpl implements LogService { @Autowired private WebClient.Builder webClientBuilder; @Override public void saveLog(LogInfo info) { Mono<Void> mono = webClientBuilder.build().post().uri("http://nurse-admin/api/log/save").body(BodyInserters.fromValue(info)).retrieve().bodyToMono(Void.class); // 输出结果 log.debug(String.valueOf(mono.block())); } }
这里特别说明一下这个@Slf4j注解,这个注解是lombok提供的一个注解,加上这个注解之后,可以自动生成以下方法
比如原本的类是
@Slf4j public class LogExample { }
相当于自动生成如下代码,可以省略不必要的代码
public class LogExample { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class); }
因此在上面的类中,我们可以使用log这个变量调用对应的方法。而表面上log变量并没有出现在类中。
这一部分内容,我们同样可以再lombok的官方文档上查询到
三、总结
在本篇博客中,我主要对老年健康管理系统中的几个工具类进行了相关的分析,总的来说,通过本次分析,我获得了不少知识,并在这个过程中,通过小组讨论与查阅资料,解决了很多问题。希望在未来的项目实训中,我也能吸收这个项目的优点,争取用在项目中。
Be First to Comment