Complete event queue publishing

pull/146/head
johnniang 2019-04-21 22:52:32 +08:00
parent d41c32da43
commit 27a8460ce1
13 changed files with 389 additions and 89 deletions

View File

@ -113,9 +113,9 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPaths(FILE_PROTOCOL + haloProperties.getWorkDir() + "templates/", "classpath:/templates/");
configurer.setDefaultEncoding("UTF-8");
if (haloProperties.isProductionEnv()) {
configurer.getConfiguration().setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
}
// if (haloProperties.isProductionEnv()) {
// configurer.getConfiguration().setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
// }
return configurer;
}

View File

@ -0,0 +1,86 @@
package run.halo.app.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.EventListener;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Event queue dispatcher.
*
* @author johnniang
* @date 19-4-20
*/
@Slf4j
@Component
public class ApplicationEventQueuePublisher {
private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
private final ApplicationListenerManager listenerManager;
private final ExecutorService executorService;
public ApplicationEventQueuePublisher(ApplicationListenerManager listenerManager) {
this.listenerManager = listenerManager;
this.executorService = Executors.newSingleThreadExecutor();
executorService.execute(new EventQueueConsumer());
}
public void publishEvent(Object event) {
try {
events.put(event);
} catch (InterruptedException e) {
log.warn("Failed to put event to the queue", e);
// Ignore this error
}
}
@PreDestroy
protected void destroy() {
log.info("Shutting down all event queue consumer");
this.executorService.shutdownNow();
}
@SuppressWarnings("unchecked")
private class EventQueueConsumer implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
// Take an event
Object event = events.take();
// Get listeners
List<EventListener> listeners = listenerManager.getListeners(event);
// Handle the event
listeners.forEach(listener -> {
if (listener instanceof ApplicationListener && event instanceof ApplicationEvent) {
ApplicationEvent applicationEvent = (ApplicationEvent) event;
// Fire event
((ApplicationListener) listener).onApplicationEvent(applicationEvent);
}
});
log.info("Event queue consumer has been shut down");
} catch (InterruptedException e) {
log.warn("Failed to take event", e);
} catch (Exception e) {
log.error("Failed to handle event", e);
}
}
}
}
}

View File

@ -0,0 +1,85 @@
package run.halo.app.event;
import cn.hutool.core.lang.Assert;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import run.halo.app.utils.ReflectionUtils;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Application listener manager.
*
* @author johnniang
* @date 19-4-21
*/
@Slf4j
@Component
public class ApplicationListenerManager {
/**
* Listener Map.
*/
private final Map<String, List<EventListener>> listenerMap = new ConcurrentHashMap<>();
public ApplicationListenerManager(ApplicationContext applicationContext) {
// TODO Need to refactor
// Register all listener on starting up
applicationContext.getBeansOfType(ApplicationListener.class).values().forEach(this::register);
log.debug("Initialized event listeners");
}
public List<EventListener> getListeners(@Nullable Object event) {
if (event == null) {
return Collections.emptyList();
}
// Get listeners
List<EventListener> listeners = listenerMap.get(event.getClass().getTypeName());
// Clone the listeners
return listeners == null ? Collections.emptyList() : new LinkedList<>(listeners);
}
public synchronized void register(@NonNull ApplicationListener<?> listener) {
// Get actual generic type
Type actualType = resolveActualGenericType(listener);
if (actualType == null) {
return;
}
// Add this listener
listenerMap.computeIfAbsent(actualType.getTypeName(), (key) -> new LinkedList<>()).add(listener);
}
public synchronized void unRegister(@NonNull ApplicationListener<?> listener) {
// Get actual generic type
Type actualType = resolveActualGenericType(listener);
if (actualType == null) {
return;
}
// Remove it from listener map
listenerMap.getOrDefault(actualType.getTypeName(), Collections.emptyList()).remove(listener);
}
@Nullable
private Type resolveActualGenericType(@NonNull ApplicationListener<?> listener) {
Assert.notNull(listener, "Application listener must not be null");
log.debug("Attempting to resolve type of Application listener: [{}]", listener);
ParameterizedType parameterizedType = ReflectionUtils.getParameterizedType(ApplicationListener.class, ((ApplicationListener) listener).getClass());
return parameterizedType == null ? null : parameterizedType.getActualTypeArguments()[0];
}
}

View File

@ -1,6 +1,9 @@
package run.halo.app.event;
import org.springframework.context.ApplicationEvent;
import run.halo.app.model.enums.LogType;
import run.halo.app.model.params.LogParam;
import run.halo.app.utils.ValidationUtils;
/**
* @author johnniang
@ -8,12 +11,28 @@ import org.springframework.context.ApplicationEvent;
*/
public class LogEvent extends ApplicationEvent {
private final LogParam logParam;
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
* @param logParam
*/
public LogEvent(Object source) {
public LogEvent(Object source, LogParam logParam) {
super(source);
// Validate the log param
ValidationUtils.validate(logParam);
this.logParam = logParam;
}
public LogEvent(Object source, String logKey, LogType logType, String content) {
this(source, new LogParam(logKey, logType, content));
}
public LogParam getLogParam() {
return logParam;
}
}

View File

@ -0,0 +1,30 @@
package run.halo.app.event;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import run.halo.app.model.entity.Log;
import run.halo.app.service.LogService;
/**
* Log event listener.
*
* @author johnniang
* @date 19-4-21
*/
@Component
public class LogEventListener implements ApplicationListener<LogEvent> {
private final LogService logService;
public LogEventListener(LogService logService) {
this.logService = logService;
}
@Override
public void onApplicationEvent(LogEvent event) {
// Convert to log
Log logToCreate = event.getLogParam().convertTo();
// Create log
logService.create(logToCreate);
}
}

View File

@ -1,12 +1,13 @@
package run.halo.app.model.entity;
import run.halo.app.model.enums.LogType;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import run.halo.app.model.enums.LogType;
import run.halo.app.utils.ServletUtils;
import javax.persistence.*;
@ -57,5 +58,16 @@ public class Log extends BaseEntity {
public void prePersist() {
super.prePersist();
id = null;
if (logKey == null) {
logKey = "";
}
// Get ip address
ipAddress = ServletUtils.getRequestIp();
if (ipAddress == null) {
logKey = "";
}
}
}

View File

@ -6,6 +6,13 @@ package run.halo.app.model.enums;
* @author johnniang
*/
public enum LogType implements ValueEnum<Integer> {
POST_PUBLISHED(0),
POST_EDITED(1),
POST_DELETED(5),
LOGGED_IN(2),
LOGGED_OUT(3),
LOGIN_FAILED(4),
;
private final Integer value;

View File

@ -0,0 +1,33 @@
package run.halo.app.model.params;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import run.halo.app.model.dto.base.InputConverter;
import run.halo.app.model.entity.Log;
import run.halo.app.model.enums.LogType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* @author johnniang
* @date 19-4-21
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LogParam implements InputConverter<Log> {
@Size(max = 1023, message = "Length of log key must not be more than {max}")
private String logKey;
@NotNull(message = "Log type must not be null")
private LogType type;
@NotBlank(message = "Log content must not be blank")
@Size(max = 1023, message = "Log content must not be more than 1023")
private String content;
}

View File

@ -13,6 +13,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.event.ApplicationEventQueuePublisher;
import run.halo.app.event.LogEvent;
import run.halo.app.exception.AlreadyExistsException;
import run.halo.app.exception.NotFoundException;
import run.halo.app.model.dto.CategoryOutputDTO;
@ -20,6 +22,7 @@ import run.halo.app.model.dto.TagOutputDTO;
import run.halo.app.model.dto.post.PostMinimalOutputDTO;
import run.halo.app.model.dto.post.PostSimpleOutputDTO;
import run.halo.app.model.entity.*;
import run.halo.app.model.enums.LogType;
import run.halo.app.model.enums.PostStatus;
import run.halo.app.model.params.PostQuery;
import run.halo.app.model.vo.ArchiveMonthVO;
@ -66,12 +69,15 @@ public class PostServiceImpl extends AbstractCrudService<Post, Integer> implemen
private final CommentService commentService;
private final ApplicationEventQueuePublisher eventQueuePublisher;
public PostServiceImpl(PostRepository postRepository,
TagService tagService,
CategoryService categoryService,
PostTagService postTagService,
PostCategoryService postCategoryService,
CommentService commentService) {
CommentService commentService,
ApplicationEventQueuePublisher eventQueuePublisher) {
super(postRepository);
this.postRepository = postRepository;
this.tagService = tagService;
@ -79,6 +85,7 @@ public class PostServiceImpl extends AbstractCrudService<Post, Integer> implemen
this.postTagService = postTagService;
this.postCategoryService = postCategoryService;
this.commentService = commentService;
this.eventQueuePublisher = eventQueuePublisher;
}
@Override
@ -214,13 +221,36 @@ public class PostServiceImpl extends AbstractCrudService<Post, Integer> implemen
return createOrUpdate(postToUpdate, tagIds, categoryIds, this::update);
}
@Override
public Post create(Post post) {
Post createdPost = super.create(post);
// Log the creation
LogEvent logEvent = new LogEvent(this, createdPost.getId().toString(), LogType.POST_PUBLISHED, createdPost.getTitle());
eventQueuePublisher.publishEvent(logEvent);
return createdPost;
}
@Override
public Post update(Post post) {
Post updatedPost = super.update(post);
// Log the creation
LogEvent logEvent = new LogEvent(this, updatedPost.getId().toString(), LogType.POST_EDITED, updatedPost.getTitle());
eventQueuePublisher.publishEvent(logEvent);
return updatedPost;
}
private PostDetailVO createOrUpdate(@NonNull Post post, Set<Integer> tagIds, Set<Integer> categoryIds, @NonNull Function<Post, Post> postOperation) {
Assert.notNull(post, "Post param must not be null");
Assert.notNull(postOperation, "Post operation must not be null");
// Check url
long count;
if (post.getId() != null) {
boolean isUpdating = post.getId() != null;
if (isUpdating) {
// For updating
count = postRepository.countByIdNotAndUrl(post.getId(), post.getUrl());
} else {

View File

@ -1,72 +0,0 @@
package run.halo.app.utils;
/**
* <pre>
*
* </pre>
*
* @author : RYAN0UP
* @date : 2018/7/12
*/
public class CommentUtil {
// /**
// * 获取组装好的评论
// *
// * @param commentsRoot commentsRoot
// * @return List
// */
// public static List<Comment> getComments(List<Comment> commentsRoot) {
// if (CollectionUtils.isEmpty(commentsRoot)) {
// return Collections.emptyList();
// }
//
// final List<Comment> commentsResult = new ArrayList<>();
//
// for (Comment comment : commentsRoot) {
// if (comment.getCommentParent() == 0) {
// commentsResult.add(comment);
// }
// }
//
// for (Comment comment : commentsResult) {
// comment.setChildComments(getChild(comment.getCommentId(), commentsRoot));
// }
// // 集合倒序,最新的评论在最前面
// Collections.reverse(commentsResult);
// return commentsResult;
// }
//
// /**
// * 获取评论的子评论
// *
// * @param id 评论编号
// * @param commentsRoot commentsRoot
// * @return List
// */
// private static List<Comment> getChild(Long id, List<Comment> commentsRoot) {
// Assert.notNull(id, "comment id must not be null");
//
// if (CollectionUtils.isEmpty(commentsRoot)) {
// return null;
// }
//
// final List<Comment> commentsChild = new ArrayList<>();
// for (Comment comment : commentsRoot) {
// if (comment.getCommentParent() != 0) {
// if (comment.getCommentParent().equals(id)) {
// commentsChild.add(comment);
// }
// }
// }
// for (Comment comment : commentsChild) {
// if (comment.getCommentParent() != 0) {
// comment.setChildComments(getChild(comment.getCommentId(), commentsRoot));
// }
// }
// if (commentsChild.size() == 0) {
// return null;
// }
// return commentsChild;
// }
}

View File

@ -0,0 +1,46 @@
package run.halo.app.utils;
import cn.hutool.extra.servlet.ServletUtil;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
/**
* Servlet utilities.
*
* @author johnniang
* @date 19-4-21
*/
public class ServletUtils {
private ServletUtils() {
}
/**
* Gets current http servlet request.
*
* @return an optional http servlet request
*/
@NonNull
public static Optional<HttpServletRequest> getCurrentRequest() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.filter(requestAttributes -> requestAttributes instanceof ServletRequestAttributes)
.map(requestAttributes -> ((ServletRequestAttributes) requestAttributes))
.map(ServletRequestAttributes::getRequest);
}
/**
* Gets request ip.
*
* @return ip address or null
*/
@Nullable
public static String getRequestIp() {
return getCurrentRequest().map(ServletUtil::getClientIP).orElse(null);
}
}

View File

@ -99,7 +99,6 @@ public class ContentArchiveController {
*
* @param url post slug url.
* @param cp comment page number
* @param request request
* @param model model
* @return template path: theme/{theme}/post.ftl
*/
@ -110,14 +109,8 @@ public class ContentArchiveController {
Model model) {
Post post = postService.getBy(PostStatus.PUBLISHED, url);
postService.getNextPost(post.getCreateTime()).ifPresent(nextPost -> {
log.debug("Next post: [{}]", nextPost);
model.addAttribute("nextPost", nextPost);
});
postService.getPrePost(post.getCreateTime()).ifPresent(prePost -> {
log.debug("Pre post: [{}]", prePost);
model.addAttribute("prePost", prePost);
});
postService.getNextPost(post.getCreateTime()).ifPresent(nextPost -> model.addAttribute("nextPost", nextPost));
postService.getPrePost(post.getCreateTime()).ifPresent(prePost -> model.addAttribute("prePost", prePost));
List<Category> categories = postCategoryService.listCategoryBy(post.getId());

View File

@ -0,0 +1,31 @@
package run.halo.app.event;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.context.ApplicationListener;
import run.halo.app.utils.ReflectionUtils;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Objects;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.*;
/**
* Log event listener test.
*
* @author johnniang
* @date 19-4-21
*/
@Slf4j
public class LogEventListenerTest {
@Test
public void getGenericTypeTest() {
ParameterizedType parameterizedType = ReflectionUtils.getParameterizedType(ApplicationListener.class, LogEventListener.class);
Type[] actualTypeArguments = Objects.requireNonNull(parameterizedType).getActualTypeArguments();
Type actualTypeArgument = actualTypeArguments[0];
assertThat(actualTypeArgument.getTypeName(), equalTo(LogEvent.class.getTypeName()));
}
}