目录
前缀树:Trie、字典树、查找树
查找效率高;消耗内存大;用于字符串检索、词频统计、字符串排序等;
实现敏感词过滤器:
1、定义前缀树;
2、根据敏感词,初始化前缀树;
3、编写过滤敏感词的方法;
private class TrieNode{
private boolean isKeywordEnd = false;
//key为下一个字符;value为下一个节点
private Map<Character,TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
public void addSubNode(Character c, TrieNode node){
subNodes.put(c,node);
}
public TrieNode getSubNode(Character c){
return subNodes.get(c);
}
}
根据敏感词,初始化前缀树:
@PostConstruct
public void init(){
try(
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is))
){
String keyword;
while ((keyword = reader.readLine()) != null){
//添加到前缀树
this.addKeyword(keyword);
}
}catch (IOException e){
logger.error("加载敏感词失败:"+e.getMessage());
}
}
//在sensitive-words.txt中定义了敏感词
public void addKeyword(String keyword){
TrieNode tempNode = rootNode;//当前节点
for(int i= 0; i<= keyword.length()-1; i++){//遍历将节点加入字典树中
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);//得到当前节点的下级节点
if(subNode == null){//判断其是否为空;为空就新建一个;不为空就不用新建了
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
tempNode = subNode;//指向下一个节点
if(i == keyword.length()-1){//最后一个字符了,设置最后的标志位为true
tempNode.setKeywordEnd(true);
}
}
}
编写过滤敏感词的方法:
//过滤敏感词,输入的是待过滤的字符串;输出的是过滤后(替换)的字符串
public String filter(String txt){
if(StringUtils.isBlank(txt)){
return null;
}
//
int left = 0;
int right = 0;
TrieNode tempNode = rootNode;
StringBuilder sb = new StringBuilder();
while (right <= txt.length()-1){
char c = txt.charAt(right);
if(isSymbol(c)){//跳过符号
if(tempNode == rootNode){
sb.append(c);
left++;
}
right++;
continue;
}
tempNode =tempNode.getSubNode(c);
if(tempNode == null){
sb.append(txt.charAt(left));
left++;
right = left;
tempNode = rootNode;
}else if(tempNode.isKeywordEnd()){
sb.append(REPLACEMENT);//替换
left = ++right;
tempNode = rootNode;
}else {//检查下一个字符
right++;
}
}
sb.append(txt.substring(left));
return sb.toString();
}
以上定义在一个工具类中;在需要过滤的地方调用filter方法即可:
调用:
发布帖子
使用jQuery发送AJAX请求
//添加帖子
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if (user == null) {
return CommunityUtil.getJSONString(403, "你还没有登录哦!");
//403代表没有权限的意思
//异步的Json格式的数据
//使用jQuery发送AJAX请求,网页能够将增量更新呈现在页面上,而不需要刷新整个页面
}
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 报错的情况,将来统一处理.
return CommunityUtil.getJSONString(0, "发布成功!");
}
server层:
//加入一条帖子
public int addDiscussPost(DiscussPost post) {
if (post == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 先转义HTML标记
// post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
// post.setContent(HtmlUtils.htmlEscape(post.getContent()));
// 再过滤敏感词
post.setTitle(sensitiveFilter.filter(HtmlUtils.htmlEscape(post.getTitle())));
post.setContent(sensitiveFilter.filter(HtmlUtils.htmlEscape(post.getContent())));
// post.setTitle(sensitiveFilter.filter(post.getTitle()));
// post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
mapper(dao层)
int insertDiscussPost(DiscussPost discussPost);
xml文件:
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
发布帖子使用jQuery发送AJAX请求:index.js页面:
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
// 获取标题和内容
var title = $("#recipient-name").val();
var content = $("#message-text").val();
// 发送异步请求(POST)
$.post(
CONTEXT_PATH + "/discuss/add",
{"title":title,"content":content},
function(data) {
data = $.parseJSON(data);
// 在提示框中显示返回消息
$("#hintBody").text(data.msg);
// 显示提示框
$("#hintModal").modal("show");
// 2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
// 刷新页面
if(data.code == 0) {
window.location.reload();
}
}, 2000);
}
);
}
帖子详情
DiscussPostMapper、DiscussPostService、DiscussPostController
Index.html:在帖子标题上增加访问详情页面的链接
Discuss_detail.html:处理静态资源的访问路径;复用index.html的header区域;显示标题、作者、发布时间、帖子正文等内容;复用了分页功能;
DiscussPostMapper:(一个帖子discussPostId的全部内容)
//帖子详情
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
//我们希望显示用户头像,用户名;而在帖子表中得到的是用户id;有两种方法
//1、mapper中实现查询的时候,写一个关联查询,关联到user表中;查到后结果需要做处理;mybatis是支持的
//这种方式效率较高:一次查到了所有信息;缺点:数据有冗余,不需要的时候也查询到了
//2、可以先查到userid,用userService来查到用户,通过model将用户发送给模板,这样也可以
//还要支持评论区的分页功能:page用来接收、整理分页的条件,只要是实体类型bean在条件中作为一个参数的
// 话,那么最终springMvc都会把这个bean存到model中;所以在页面上就可以通过model获得这个配置
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
// 评论分页信息
page.setLimit(5);
page.setPath("/discuss/detail/" + discussPostId);
page.setRows(post.getCommentCount());
// 评论: 给帖子的评论
// 回复: 给评论的评论
// 评论列表
List<Comment> commentList = commentService.findCommentsByEntity(
ENTITY_TYPE_POST, post.getId(), page.getOffset(), page.getLimit());
// 评论VO列表
List<Map<String, Object>> commentVoList = new ArrayList<>();
if (commentList != null) {
for (Comment comment : commentList) {
// 评论VO
Map<String, Object> commentVo = new HashMap<>();
// 评论
commentVo.put("comment", comment);
// 作者
commentVo.put("user", userService.findUserById(comment.getUserId()));
// 回复列表
List<Comment> replyList = commentService.findCommentsByEntity(
ENTITY_TYPE_COMMENT, comment.getId(), 0, Integer.MAX_VALUE);
// 回复VO列表
List<Map<String, Object>> replyVoList = new ArrayList<>();
if (replyList != null) {
for (Comment reply : replyList) {
Map<String, Object> replyVo = new HashMap<>();
// 回复
replyVo.put("reply", reply);
// 作者
replyVo.put("user", userService.findUserById(reply.getUserId()));
// 回复目标
User target = reply.getTargetId() == 0 ? null : userService.findUserById(reply.getTargetId());
replyVo.put("target", target);
replyVoList.add(replyVo);
}
}
commentVo.put("replys", replyVoList);
// 回复数量
int replyCount = commentService.findCommentCount(ENTITY_TYPE_COMMENT, comment.getId());
commentVo.put("replyCount", replyCount);
commentVoList.add(commentVo);
}
}
model.addAttribute("comments", commentVoList);
return "/site/discuss-detail";
//对于显示帖子详情(先不管评论),模板中需要处理:1首页添加链接,需要能够访问到这个模板 2帖子详情页面:用户信息及帖子详情
//加上评论的处理:首页的处理:1、index评论数量:直接从帖子处获取,在首页帖子列表discussPosts
// 2、帖子详情页面:根据传入进去的数据参数都修改一遍,
// 如:先取得comments,对其中每一个元素commentVo:取user,replys,comment等等,将这些数据传到前端模板页面进行显示
//在模板中评论区域可以复用首页的分页逻辑
}
public DiscussPost findDiscussPostById(int id) {
return discussPostMapper.selectDiscussPostById(id);
}
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
显示评论、添加评论(事务)
需要用到事务的知识:添加评论与修改帖子评论数据要么都成功,要么都失败;
事务管理:声明式事务,做配置就可以;编程式事务;当前事务在整个范围内,并不是局部,加个注解就可以了。
对于discuss_post表,它冗余的存储了comment_count字段,加快了查询速度(由于经常查询)
显示评论:
数据层- 根据实体查询一页评论数据。- 根据实体查询评论的数量。
业务层- 处理查询评论的业务。-处理查询评论数量的业务。
表现层- 显示帖子详情数据时-同时显示该帖子所有的评论数据。
添加评论:
数据层- 增加评论数据。- 修改帖子的评论数量。
业务层- 处理添加评论的业务:先增加评论、再更新帖子的评论数量。
表现层- 处理添加评论数据的请求。- 设置添加评论的表单
1、显示评论,在显示详情的时候写了表现层,显示帖子详情数据时,同时显示该帖子所有的评论数据。
2、显示评论,业务层:
public List<Comment> findCommentsByEntity(int entityType, int entityId, int offset, int limit) {
return commentMapper.selectCommentsByEntity(entityType, entityId, offset, limit);
}
public int findCommentCount(int entityType, int entityId) {
return commentMapper.selectCountByEntity(entityType, entityId);
}
2、显示评论,数据层:
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
int selectCountByEntity(int entityType, int entityId);
3、xml:
<sql id="selectFields">
id, user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<sql id="insertFields">
user_id, entity_type, entity_id, target_id, content, status, create_time
</sql>
<select id="selectCommentsByEntity" resultType="Comment">
select <include refid="selectFields"></include>
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
order by create_time asc
limit #{offset}, #{limit}
</select>
<select id="selectCountByEntity" resultType="int">
select count(id)
from comment
where status = 0
and entity_type = #{entityType}
and entity_id = #{entityId}
</select>
4、添加评论,表现层:处理添加评论数据的请; 设置添加评论的表单。
@RequestMapping(path = "/add/{discussPostId}",method = RequestMethod.POST)
public String addComment(@PathVariable("discussPostId") int discussPostId, Comment comment){
comment.setUserId(hostHolder.getUser().getId());//如果用户没有登陆,这种异常后面会做统一的异常处理
comment.setStatus(0);
comment.setCreateTime(new Date());
commentService.addComment(comment);
return "redirect:/discuss/detail/" + discussPostId;
}
5、添加评论,业务层:先增加评论、再更新帖子的评论数量:(事务)
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
//事务方法:添加评论,更新帖子评论数量为一个事务
if (comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 添加评论
// comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(HtmlUtils.htmlEscape(comment.getContent())));
int rows = commentMapper.insertComment(comment);
// 更新帖子评论数量
if (comment.getEntityType() == ENTITY_TYPE_POST) {
int count = commentMapper.selectCountByEntity(comment.getEntityType(), comment.getEntityId());
discussPostService.updateCommentCount(comment.getEntityId(), count);
}
return rows;
}
6、添加评论,数据层 :增加评论数据; 修改帖子的评论数量;
int insertComment(Comment comment);
<insert id="insertComment" parameterType="Comment">
insert into comment(<include refid="insertFields"></include>)
values(#{userId},#{entityType},#{entityId},#{targetId},#{content},#{status},#{createTime})
</insert>
业务层中引入了discussPostService中的:对应的dicussMapper及xml
public int updateCommentCount(int id, int commentCount) {
return discussPostMapper.updateCommentCount(id, commentCount);
}
int updateCommentCount(int id, int commentCount);
<update id="updateCommentCount">
update discuss_post set comment_count = #{commentCount} where id = #{id}
</update>
私信列表
私信列表: 1、查询当前用户的会话列表;2、每个会话只显示一条最新的私信。 支持分页显示(复用)。
私信详情: 查询某个会话所包含的私信。 支持分页显示。
私信列表:MessageController:
// 私信列表
@RequestMapping(path = "/letter/list", method = RequestMethod.GET)
public String getLetterList(Model model, Page page) {
// Integer.valueOf("abc");//故意写一个错误,用来测试异常处理
User user = hostHolder.getUser();
// 分页信息
page.setLimit(5);
page.setPath("/letter/list");
page.setRows(messageService.findConversationCount(user.getId()));
// 会话列表
List<Message> conversationList = messageService.findConversations(
user.getId(), page.getOffset(), page.getLimit());
//得到会话列表后,取出每一个会话的相关的信息,加入到map中:conversations
List<Map<String, Object>> conversations = new ArrayList<>();
if (conversationList != null) {
for (Message message : conversationList) {
Map<String, Object> map = new HashMap<>();
map.put("conversation", message);
map.put("letterCount", messageService.findLetterCount(message.getConversationId()));
map.put("unreadCount", messageService.findLetterUnreadCount(user.getId(), message.getConversationId()));
//取会话列表用户相关的信息:得到target
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
map.put("target", userService.findUserById(targetId));
conversations.add(map);
}
}
model.addAttribute("conversations", conversations);//将conversations加入模型中
// 查询未读消息总的数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
私信列表:MessageService
public List<Message> findConversations(int userId, int offset, int limit) {
return messageMapper.selectConversations(userId, offset, limit);
}
public int findConversationCount(int userId) {
return messageMapper.selectConversationCount(userId);
}
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetters(conversationId, offset, limit);
}
public int findLetterCount(String conversationId) {
return messageMapper.selectLetterCount(conversationId);
}
public int findLetterUnreadCount(int userId, String conversationId) {
return messageMapper.selectLetterUnreadCount(userId, conversationId);
}
私信列表:MessageMapper
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.
List<Message> selectConversations(int userId, int offset, int limit);
// 查询当前用户的会话数量.
int selectConversationCount(int userId);
// 查询某个会话所包含的私信列表.
List<Message> selectLetters(String conversationId, int offset, int limit);
// 查询某个会话所包含的私信数量.
int selectLetterCount(String conversationId);
// 查询未读私信的数量
int selectLetterUnreadCount(int userId, String conversationId);
massage—mapper:
<sql id="selectFields">
id, from_id, to_id, conversation_id, content, status, create_time
</sql>
<sql id="insertFields">
from_id, to_id, conversation_id, content, status, create_time
</sql>
<select id="selectConversations" resultType="Message">
select <include refid="selectFields"></include>
from message
where id in (
select max(id) from message
where status != 2
and from_id != 1
and (from_id = #{userId} or to_id = #{userId})
group by conversation_id
)
order by id desc
limit #{offset}, #{limit}
</select>
<select id="selectConversationCount" resultType="int">
select count(m.maxid) from (
select max(id) as maxid from message
where status != 2
and from_id != 1
and (from_id = #{userId} or to_id = #{userId})
group by conversation_id
) as m
</select>
<select id="selectLetters" resultType="Message">
select <include refid="selectFields"></include>
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
order by id desc
limit #{offset}, #{limit}
</select>
<select id="selectLetterCount" resultType="int">
select count(id)
from message
where status != 2
and from_id != 1
and conversation_id = #{conversationId}
</select>
<select id="selectLetterUnreadCount" resultType="int">
select count(id)
from message
where status = 0
and from_id != 1
and to_id = #{userId}
<if test="conversationId!=null">
and conversation_id = #{conversationId}
</if>
</select>
SQL:
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`from_id` int(11) DEFAULT NULL,
`to_id` int(11) DEFAULT NULL,
`conversation_id` varchar(45) NOT NULL,
`content` text,
`status` int(11) DEFAULT NULL COMMENT '0-未读;1-已读;2-删除;',
`create_time` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_from_id` (`from_id`),
KEY `index_to_id` (`to_id`),
KEY `index_conversation_id` (`conversation_id`)
) ENGINE=InnoDB AUTO_INCREMENT=355 DEFAULT CHARSET=utf8
辅助:
查询会话数量:(重复的会话算一个)
通过查询最大的消息id来查询每个会话中最新的一个会话:
私信详情页面:
MessageController:(显示一个conversationId中的所有内容)
//私信页面详情
@RequestMapping(path = "/letter/detail/{conversationId}", method = RequestMethod.GET)
public String getLetterDetail(@PathVariable("conversationId") String conversationId, Page page, Model model) {
// 分页信息
page.setLimit(5);
page.setPath("/letter/detail/" + conversationId);
page.setRows(messageService.findLetterCount(conversationId));
// 私信列表
List<Message> letterList = messageService.findLetters(conversationId, page.getOffset(), page.getLimit());
List<Map<String, Object>> letters = new ArrayList<>();
if (letterList != null) {
for (Message message : letterList) {
Map<String, Object> map = new HashMap<>();
map.put("letter", message);
map.put("fromUser", userService.findUserById(message.getFromId()));
letters.add(map);
}
}
model.addAttribute("letters", letters);
// 私信目标
model.addAttribute("target", getLetterTarget(conversationId));
//设置为已读
List<Integer> ids = getletterIds(letterList);
if(!ids.isEmpty()){
messageService.readMessage(ids);
}
return "/site/letter-detail";
}
private User getLetterTarget(String conversationId) {
String[] ids = conversationId.split("_");
int id0 = Integer.parseInt(ids[0]);
int id1 = Integer.parseInt(ids[1]);
if (hostHolder.getUser().getId() == id0) {
return userService.findUserById(id1);
} else {
return userService.findUserById(id0);
}
}
//得到所有未读的私信的id
private List<Integer> getletterIds(List<Message> letterList){
List<Integer> ids = new ArrayList<>();
if(letterList != null){
for(Message message : letterList){
if(hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0){
//0为未读
ids.add(message.getId());
}
}
}
return ids;
}
MessageService:
public List<Message> findLetters(String conversationId, int offset, int limit) {
return messageMapper.selectLetters(conversationId, offset, limit);
}
public int readMessage(List<Integer> ids){//对id集合:更新读的状态
return messageMapper.updateStatus(ids, 1);//0为未读,1为已读,2为无权限
}
MessageMapper:
//修改消息的状态
int updateStatus(List<Integer> ids, int status);
//一个用户,多个会话;一个会话是两个人之间的会话,一个会话,多条消息;
// 查询本用户的会话列表:包含对方的user信息,与用户会话的最后一条消息;
xml:
<update id="updateStatus">
update message set status = #{status}
where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
发送私信
发送私信:采用异步的方式发送私信。发送成功后刷新私信列表。
设置已读:访问私信详情时,将显示的私信设置为已读状态。(上面已实现)
//发送私信
@RequestMapping(path = "/letter/send",method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName, String content){
// Integer.valueOf("abc");//故意写一个错误,用来测试异常处理
//toName接收用户名,content内容
User target = userService.findUserByName(toName);
if(target == null){
return CommunityUtil.getJSONString(1, "目标用户不存在!");
}
Message message = new Message();
message.setFromId(hostHolder.getUser().getId());
message.setToId(target.getId());
if(message.getFromId() < message.getToId()){
message.setConversationId(message.getFromId() + "_" + message.getToId());
}else {
message.setConversationId(message.getToId() + "_" + message.getFromId());
}
message.setContent(content);
message.setCreateTime(new Date());
messageService.addMessage(message);
return CommunityUtil.getJSONString(0);//正常执行,返回状态0
}
public int addMessage(Message message){
message.setContent(sensitiveFilter.filter(HtmlUtils.htmlEscape(message.getContent())));
return messageMapper.insertMessage(message);
}
//新增消息
int insertMessage(Message message);
<insert id="insertMessage" parameterType="Message" keyProperty="id">
insert into message(<include refid="insertFields"></include>)
values(#{fromId},#{toId},#{conversationId},#{content},#{status},#{createTime})
</insert>
采用异步的方式发送私信。letter.js:
$(function(){
$("#sendBtn").click(send_letter);
$(".close").click(delete_msg);
});
function send_letter() {
$("#sendModal").modal("hide");
var toName = $("#recipient-name").val();
var content = $("#message-text").val();
$.post(
CONTEXT_PATH + "/letter/send",
{"toName":toName,"content":content},
function(data) {
data = $.parseJSON(data);
if(data.code == 0) {
$("#hintBody").text("发送成功!");
} else {
$("#hintBody").text(data.msg);
}
$("#hintModal").modal("show");
setTimeout(function(){
$("#hintModal").modal("hide");
location.reload();
}, 2000);
}
);
}
function delete_msg() {
// TODO 删除数据
$(this).parents(".media").remove();
}
项目中需要着重关注的技术点:
安全,性能,分布式
统一处理异常
放式一:需要将error文件夹放到templates目录下,error文件夹下有404,500两个文件;Spring自动集成好了,当遇到服务器出错时,自动返回500页面;当找不到网页时自动返回到404页面;可处理较简单的问题
方式二:使用spring的注解:@ControllerAdvice,可以记录一些日志信息;
这里实现所有的带有Controller注解的bean的异常处理:
@ControllerAdvice(annotations = Controller.class)//只需要扫描带有Controller注解的bean,即Collector组件
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler({Exception.class})//Exception是所以方法的父类;此处所有异常都用Exception来处理
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常: " + e.getMessage());
for (StackTraceElement element : e.getStackTrace()) {
logger.error(element.toString());
}
//记完日志后给浏览器一个响应;重定向到错误页面;注意:
// 浏览器访问服务器可能是普通的请求,返回网页;也可能是异步的请求,希望返回一个json
//先判断是普通请求,还是异步请求
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
//XMLHttpRequest表示一个异步请求
//XMLHttpRequest 用于在后台与服务器交换数据。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
response.setContentType("application/plain;charset=utf-8");//返回一个普通的字符串,先确保传入是json格式
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {
response.sendRedirect(request.getContextPath() + "/error");
}
}
}
统一记录日志
传统:将记录日志的方法封装到一个组件里,在不同的service中去调用;可以在每个业务中实现日志的记录;有很大的弊端:如有的时候需求在方法前记日志,有些在方法后记日志;有些是在报异常的时候才记日志。
记录日志实际上属于系统需求,不是业务需求;可以将记录日志的需求单独拿出来实现,而不是硬编码到业务方法中。
AOP:面向切面编程,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等。
AOP实现原理:AOP实现的关键在于AOP框架自动创建AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表;
AspectJ:1) AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法。2) AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件。
Spring AOP:1) Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器。2) Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点。3) Spring支持对AspectJ的集成。(实际应用中也只用到方法类型的连接点)
Spring AOP使用的动态代理,在每次运行时生成AOP代理对象。所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个代理对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP的动态代理主要有两种方式:JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。Spring AOP默认采用此种方式。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library)是一个代码生成的类库,可以在运行时动态生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
这里实现所有的service层的日志记录:
@Component
@Aspect
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.liu.community.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 用户[1.2.3.4],在[xxx],访问了[com.liu.community.service.xxx()].
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();//得到HttpServletRequest
String ip = request.getLocalAddr();//ip
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());//时间
//得到切入处的:getDeclaringTypeName类名,getName方法名;
String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
}
}
filter、interceptor、aspect、controllerAdvice
1、filter,这是java的过滤器,和框架无关的,是所有过滤组件中最外层的,从粒度来说是最大的。配置方式,有直接实现Filter+@component,@Bean+@configuration(第三方的filter);可以控制最初的http请求,但是更细一点的类和方法控制不了。
2、interceptor,spring框架的拦截器配置方式,@configuration+继承WebMvcConfigurationSupport类添加过滤器。可以控制请求的控制器和方法,但控制不了请求方法里的参数。
3、aspect,可以自定义切入的点,有方法的参数,但是拿不到http请求,可以通过其他方式如RequestContextHolder获得,粒度最小。加个注解用效果更佳。
4、controllerAdvice,是controller的增强,和ExceptionHandler一起用来做全局异常。