过滤敏感词
- 前缀树
名称: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);
}
}
发布帖子
异步请求:当前网页不刷新,访问服务器,服务器返回一些不是网页的结果,从结果中提炼的数据对网页进行局部刷新
- AJAX
异步的JavaScript与XML,网页能够将增量更新呈现在页面上,而不需要刷新整个页面
X代表XML,但是目前JSON字符串使用更加的方便普遍
DiscussPost实体类封装帖子信息
index页面上点击我要发布会调用jindex.js中方法,把数据传到服务器中;DiscussPostController创建实体类将从浏览器传过来的标题和内容封装进去,然后调用DiscussPostService处理:转义标签,过滤敏感词,添加帖子;添加帖子是通过DiscussPostMapper将帖子放入数据库
//index.js
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},//向服务器提交的数据,json字符串
function(data) { //回调函数,服务器响应的数据会传到data里
data = $.parseJSON(data);
// 在提示框中显示返回消息
$("#hintBody").text(data.msg);
// 显示提示框
$("#hintModal").modal("show");
// 2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
// ==0成功,刷新页面
if(data.code == 0) {
window.location.reload(); //重新加载页面
}
}, 2000);
}
);
//数据访问层,Mapper
// 增加帖子的方法,返回的是增加的行数,传入的是实体
int insertDiscussPost(DiscussPost discussPost);
//Mapper.xml
<!--id:对应的方法名,parameterType:方法的参数-->
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
//service层
// 添加帖子,转义标签,过滤敏感词
public int addDiscussPost(DiscussPost post) {
if (post == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 转义HTML标记 :<script>asd</script>,把标签去掉
// HtmlUtils.htmlEscape(String) 方法会把其中的标签转义成转义字符
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getContent()));
// 过滤敏感词
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);
}
//controller层
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if (user == null) {
//返回json字符串
return CommunityUtil.getJSONString(403, "你还没有登录哦!");
}
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, "发布成功!");
}
帖子详情
首页点击帖子(包含帖子信息和对应的user信息)会通过帖子id访问帖子详情,访问DiscussPostController的方法
<!--点帖子会从这里访问/discuss/detail/{discussPostId}-->
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}"></a>
//controller层
//访问时是/discuss/detail/{discussPostId} ,discussPostId为帖子id而不是userid,这个类路径/discuss
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者,从帖子中userID获得username
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
return "/site/discuss-detail";
}
//DiscussPostService
// 根据ID查询帖子
public DiscussPost findDiscussPostById(int id) {
return discussPostMapper.selectDiscussPostById(id);
}
// Mapper
DiscussPost selectDiscussPostById(int id);
// Mapper.xml
<!--id:对应的方法名,resultType:方法返回的参数-->
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
事务管理
- 事务:事务由N步数据库操作序列组成的逻辑执行单元,这系列操作要么全执行,要么全放弃执行
- 事务的特性(ACID):原子性、一致性、隔离性、持久性
- 事物的隔离性
常见的并发异常:第一类丢失更新、第二类丢失更新;脏读、不可重复读、幻读
常见的隔离级别:读取未提交的数据、读取已提交的数据、可重复读、串行化
第一类丢失更新:某一个事务的回滚,导致另外一个事务已更新的数据丢失了。
第二类丢失更新:某一个事务的提交,导致另外一个事务已更新的数据丢失了。
脏读:某一个事务,读取了另外一个事务未提交的数据。
不可重复读:某一个事务,对同一个数据前后读取结果不一样。
幻读:某一个事务,对同一个表前后查询的行数不一致。
实现机制
- 悲观锁(数据库)
共享锁(S锁):事务A对某数据加了共享锁之后,其他事务只能对该数据加共享锁,但不能加排它锁。(可读,不可写)
排他锁(X锁):事务A对某数据加了排他锁之后,其他事务对该数据既不能加共享锁,也不能加排它锁。(其他事务不可读,不可写) - 乐观锁(自定义)
版本号、时间戳等;在更新数据前,检查版本号是否发生变化。若变化则取消本次更新,否则就更新数据(版本号+1);
Spring事务管理
- 声明式事务(整个方法在一个事务之内,不能是局部进行事务管理)
通过XML配置,声明某方法的事务特征
通过注解,声明某方法的事务特征 @Transactional - 编程式事务(整个方法在一个事务之内,也可以是方法局部进行事务管理)
通过Transaction Template类管理事务,并通过它执行数据库操作。
//Service层进行事务管理
// isolation = Isolation.READ_COMMITTED :隔离级别
// propagation传播机制:业务方法A可能调用业务方法B,这两个方法可能都加上这个注解(管理事务),B的事务应该以谁为准;解决交叉的问题
// REQUIRED: 支持当前事务(外部事务,对于B来说A就是当前事务),如果不存在则创建新事务.
// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).
// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public Object save1() {
// 新增用户
User user = new User();
user.setUsername("alpha");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("alpha@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/99t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("Hello");
post.setContent("新人报道!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc"); //这是个错误,演示回滚
return "ok";
}
public Object save2() {
// 声明隔离级别
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
// 声明传播机制
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 这个方法实现事务管理
// TransactionCallback:回调接口
return transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
//回调方法,由Template底层自动调的,调用的时候会对方法进行事务的管理
public Object doInTransaction(TransactionStatus status) {
// 新增用户
User user = new User();
user.setUsername("beta");
user.setSalt(CommunityUtil.generateUUID().substring(0, 5));
user.setPassword(CommunityUtil.md5("123" + user.getSalt()));
user.setEmail("beta@qq.com");
user.setHeaderUrl("http://image.nowcoder.com/head/999t.png");
user.setCreateTime(new Date());
userMapper.insertUser(user);
// 新增帖子
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle("你好");
post.setContent("我是新人!");
post.setCreateTime(new Date());
discussPostMapper.insertDiscussPost(post);
Integer.valueOf("abc");
return "ok";
}
});
}
显示评论
- 数据层
根据实体查询一页评论数据。
根据实体查询评论的数量。 - 业务层
处理查询评论的业务,处理查询评论数量的业务。 - 表现层
显示帖子详情数据时,同时显示该帖子所有的评论数据。
//访问时是/discuss/detail/{discussPostId} ,这个类路径/discuss
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model, Page page) {//Page接收、整理分页条件
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
// 作者,从帖子中userID获得username
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列表,把评论对应的user找到和评论一起放入一个map中,以及帖子评论的评论
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()));
// 回复目标,==0
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";
//CommentService
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);
}
@Mapper
public interface CommentMapper {
// 根据entityType实体(帖子的评论、课程的评论等)查询,offset每页的起始行,limit每页显示的行数的限制
// 查询某一个帖子(entityType类型中id为entityId的帖子)的评论中从offset起的limit条评论
List<Comment> selectCommentsByEntity(int entityType, int entityId, int offset, int limit);
// 查询评论数据的条目数
int selectCountByEntity(int entityType, int entityId);
}
<!--Mapper.xml-->
<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>
添加评论
- 数据层
增加评论数据
修改帖子的评论数量 - 业务层
处理添加评论的业务:先增加评论、再更新帖子的评论数量(进行事务管理,要同步) - 表现层
处理添加评论数据的请求
设置添加评论的表单
共有三个回复的情况:1.直接在最下面对当前帖子回复(只有这一种会增加帖子回复的数量);2.对某一个评论回复;3.对某一个评论的评论回复(显示谁回复了谁)
@Controller
@RequestMapping("/comment")
public class CommentController {
@Autowired
private CommentService commentService;
@Autowired
private HostHolder hostHolder;
// 帖子详情路径(/discuss/detail/{discussPostId})中有帖子的id,{discussPostId}把id传过来,
// 一会添加完评论后重新回到帖子详情时需要用到这个id
@RequestMapping(path = "/add/{discussPostId}", method = RequestMethod.POST)
// comment中存当前用户的userid,评论创建的时间
// comment会从表单中自动接收content、entityType、entityId
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;
}
//Service层
// 进行事务管理,先增加评论、再更新帖子的评论数量这两件事要么一块完成,要么都不完成
// 如果判断不是帖子回复的类型,则不会增加帖子评论数量
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
public int addComment(Comment comment) {
if (comment == null) {
throw new IllegalArgumentException("参数不能为空!");
}
// 添加评论
// HTML标签的过滤,进行敏感词过滤
comment.setContent(HtmlUtils.htmlEscape(comment.getContent()));
comment.setContent(sensitiveFilter.filter(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;
}
添加评论和显示评论的过程
添加评论
- 帖子详情页面("/discuss/detail/{discussPostId}")的三种回复的提交方式(评论类型entityType不同,各自的entityId,content,以及是否有targetId)—>
- 都会访问DocummentController.addComment()方法,Comment实体接收表单传过来的信息—>
- 调用 commentService.addComment()方法【这是一个事务管理方法,先增加评论、再更新帖子的评论数量这两件事要么一块完成,要么都不完成。如果判断不是帖子回复的类型,则不会增加帖子评论数量】—>
- commentMapper.insertComment()把评论增加到相应的数据库中【如果是帖子回复更新帖子的评论数量,discussPostService.updateCommentCount(comment.getEntityId(), count);—>discussPostMapper.updateCommentCount(id, commentCount);】
显示评论
- 从首页点击帖子打开帖子详情页面("/discuss/detail/{discussPostId}")—>
- 访问DiscussPostController.getDiscussPost ()方法:
① post=discussPostService.findDiscussPostById(discussPostId) —> discussPostMapper.selectDiscussPostById(discussPostid); 找到帖子
② userService.findUserById(post.getUserId()); 找到帖子作者
③ List<Map<String, Object>> 评论VO列表,把评论对应的user找到和评论以及帖子评论的回复列表一起放入一个map中。
私信列表
- 私信列表
查询当前用户的会话列表,每个会话只显示一条最新的私信;
支持分页显示
未读消息数量用小红框显示在会话的上面,显示和某一个人总共有多少条私信往来 - 私信详情
查询某个会话所包含的私信
支持分页显示 - 设置已读
访问私信详情时,将显示的私信状态设置为已读状态。 - 发送私信
采用异步的方式发送私信AJAX
发送成功后刷新私信列表列表,或者会话列表(看是否点进去和某个用户的会话页面发送)
// 私信列表
@RequestMapping(path = "/letter/list", method = RequestMethod.GET)
// 虽然get请求,current会传过来装到page里
public String getLetterList(Model model, Page page) {
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()); //从浏览器点的第几页获取page.getOffset()
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()));
// 谁发给我的私信
int targetId = user.getId() == message.getFromId() ? message.getToId() : message.getFromId();
map.put("target", userService.findUserById(targetId));// targetID对应的用户信息
conversations.add(map);
}
}
model.addAttribute("conversations", conversations);
// 查询未读消息数量,总体未读数量
int letterUnreadCount = messageService.findLetterUnreadCount(user.getId(), null);
model.addAttribute("letterUnreadCount", letterUnreadCount);
return "/site/letter";
}
// 私信会话,具体的一个私信
@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);
}
}
// 找到未读信息
private List<Integer> getLetterIds(List<Message> letterList) {
List<Integer> ids = new ArrayList<>();
if (letterList != null) {
for (Message message : letterList) {
// 这条私信中我是接收方,并且私信状态为未读,把未读信息加到ids中
if (hostHolder.getUser().getId() == message.getToId() && message.getStatus() == 0) {
ids.add(message.getId());
}
}
}
return ids;
}
//发送私信
@RequestMapping(path = "/letter/send", method = RequestMethod.POST)
@ResponseBody
public String sendLetter(String toName, String content) {
User target = userService.findUserByName(toName); // 根据名字找到我发送给的这个用户user
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);
}
//对于Mapper
// 查询当前用户的会话列表,针对每个会话只返回一条最新的私信.
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);
// 新增消息
int insertMessage(Message message);
// 修改消息的状态
int updateStatus(List<Integer> ids, int status);
统一处理异常
针对表现层统一处理异常,业务层和数据层抛出的异常最终都会到表现层上。
- @ControllerAdvice
用于修饰类,表示该类是Controller的全局配置类。(对所有的controller错误进行统一处理)
在此类中,可以对controller进行三种全局配置:异常处理方案、绑定数据方案、绑定参数方案。 - @ExceptionHandler
用于修饰方法,该方法会在Controller出现异常后调用,用于处理捕获到的异常 - @ModelAttribute
用于修饰方法,该方法会在Controller执行前被调用,用于为Model对象绑定参数 - @DataBinder
用于修饰方法,该方法会在Controller执行前被调用,用于绑定参数的转换器
把error文件夹放在Template文件夹下,同时文件夹下的文件名字要为错误状态码名字
// controller全局配置类
// 这个组件只去扫描带有controller注解的bean
@ControllerAdvice(annotations = Controller.class)
public class ExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(ExceptionAdvice.class);
@ExceptionHandler({Exception.class})
// Exception:controller中发生的异常
public void handleException(Exception e, HttpServletRequest request, HttpServletResponse response) throws IOException {
logger.error("服务器发生异常: " + e.getMessage());
// 遍历把所有异常信息记下来
for (StackTraceElement element : e.getStackTrace()) {
logger.error(element.toString());
}
// 判断是普通请求还是异步请求
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) { // 这是异步请求
// application/plain:向浏览器返回的是一个普通的字符串(可以是json格式),浏览器得到后要人为的将字符串转化为js对象
// application/json:向浏览器返回一个字符串,浏览器自动把其转化为json对象
// charset=utf-8:声明字符集格式
response.setContentType("application/plain;charset=utf-8");
// 输出一个输出流
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(1, "服务器异常!"));
} else {
// <img th:src="@{/img/404.png}" > 传回去的是图片,不是网页
response.sendRedirect(request.getContextPath() + "/error");
}
}
统一记录日志
AOP:面向切面编程,是一种编程思想,是对面向对象(OOP)的补充。
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
AOP实现:
- AspectJ :语言级别的实现,扩展了java语言,定义了AOP语法;在编译器织如代码,它有一个专门的编译器,用来生成遵循java字节码规范的class文件
- Spring AOP:Spring AOP使用纯java实现,它不需要专门的编译过程,也不需要特殊的类加载器;在运行时织入代码,只支持方法类型的连接点;Spring支持AspectJ的集成。
Spring AOP实现
- JDK动态代理:java提供的动态的代理方式,可以在运行时创建接口的代理实例;Spring AOP默认采用这种方式,在接口的代理实例中织入代码。
- CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理对象;当目标对象不存在接口时,Apring AOP会采用这种方式,在子类实例中织入代码。
@Component
@Aspect // AOP切面 注解
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.nowcoder.community.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {//JoinPoint指代程序织入的目标组件调用的那个方法
// 用户[1.2.3.4],在[xxx],访问了[com.nowcoder.community.service.xxx()].
// 获得request对象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 从request中获取IP
String ip = request.getRemoteHost();
// 把当前时间格式化
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
// .前获得类名,.后获得方法名
String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
logger.info(String.format("用户[%s],在[%s],访问了[%s].", ip, now, target));
}