mirror of https://github.com/halo-dev/halo
Complete event queue publishing
parent
d41c32da43
commit
27a8460ce1
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
// }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue