Hi,
In this article I’ll show you how you can log user activity in your spring application. We’ll use spring’s HandlerInterceptorAdapter to intercept requests and thus logging activities.
1. Create Activity entity
Create an entity that represents an activity. When user sends a request to an url we’ll save this object to our database (On Condition lol).
Activity.java
@Entity public class Activity extends BaseEntity{ private String userAgent; private String ip; private String expires; @OneToOne @JsonBackReference private User user; private String requestMethod; private String url; private Long totalVisitors; public String getUserAgent() { return userAgent; } public void setUserAgent(String userAgent) { this.userAgent = userAgent; } public String getExpires() { return expires; } public void setExpires(String expires) { this.expires = expires; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public String getRequestMethod() { return requestMethod; } public void setRequestMethod(String requestMethod) { this.requestMethod = requestMethod; } public Long getTotalVisitors() { return totalVisitors; } public void setTotalVisitors(Long totalVisitors) { this.totalVisitors = totalVisitors; } } @MappedSuperclass public abstract class BaseEntity { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Temporal(TemporalType.TIMESTAMP) private Date created; @Temporal(TemporalType.TIMESTAMP) private Date lastUpdated; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Date getCreated() { return created; } @PrePersist public void setCreated() { this.created = new Date(); } public void setCreated(Date created) { this.created = created; } public Date getLastUpdated() { if (lastUpdated == null) return created; return lastUpdated; } @PreUpdate public void setLastUpdated() { this.lastUpdated = new Date(); } public Date getTime() { if (this.lastUpdated != null) return this.lastUpdated; return this.created; } public String getReadableDateTime(Date date) { if (date == null) return ""; DateFormat dateFormat = DateUtils.getReadableDateTimeFormat(); return dateFormat.format(date); } public String getReadableDayMonth(Date date) { if (date == null) return ""; return new SimpleDateFormat("dd MMMM").format(date); } public String getReadableDateWithoutTime(Date date) { if (date == null) return ""; SimpleDateFormat sdf = new SimpleDateFormat("MMMM, dd yyyy"); return sdf.format(date); } @Override public String toString() { return "BaseEntity{" + "id=" + id + ", created=" + created + ", lastUpdated=" + lastUpdated + '}'; } }
2. Repository for that Entity
@Repository public interface ActivityRepository extends JpaRepository<Activity,Long> { Activity findFirstBy(); Activity findFirstByUserOrderByIdDesc(User user); Page<Activity> findByUser(User user, Pageable pageable); }
3. Activity Service
@Service public class ActivityServiceImpl implements ActivityService { private final ActivityRepository activityRepo; @Autowired public ActivityServiceImpl(ActivityRepository activityRepo) { this.activityRepo = activityRepo; } public Activity save(Activity activity) { if (activity.getId() == null) { // new activity (user logged in) Activity firstActivity = this.findFirst(); if (firstActivity != null) { long total = firstActivity.getTotalVisitors(); activity.setTotalVisitors(++total); firstActivity.setTotalVisitors(total); this.activityRepo.save(firstActivity); } } return this.activityRepo.save(activity); } @Override public Activity findFirst() { return this.activityRepo.findFirstBy(); } @Override public Activity findLast(User user) { return activityRepo.findFirstByUserOrderByIdDesc(user); } @Override public Page<Activity> findByUser(User user, int page, int size) { return this.activityRepo.findByUser(user, new PageRequest(page, size, Sort.Direction.DESC, SortAttributes.FIELD_ID)); } @Override public Activity findOne(long id) { return this.activityRepo.findOne(id); } @Override public List<Activity> findAll() { return this.activityRepo.findAll(); } @Override public void delete(Long id) { this.activityRepo.delete(id); } }
Okay, now that we’ve prepared our object. We can now execute some basic queries to read/write to database. Let’s jump into the main work. Create an interceptor to intercept request and do things.
4. Create Interceptor
@Component public class ActivityInterceptor extends HandlerInterceptorAdapter { private final ActivityService activityService; @Autowired public ActivityInterceptor(ActivityService activityService) { this.activityService = activityService; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userAgent = request.getHeader("User-Agent"); if (userAgent == null) userAgent = request.getHeader("user-agent"); String expires = response.getHeader("Expires"); Activity activity = new Activity(); activity.setIp(this.getClientIpAddress(request)); activity.setExpires(expires); activity.setRequestMethod(request.getMethod()); activity.setUrl(request.getRequestURI()); Matcher m = Pattern.compile("\(([^)]+)\)").matcher(userAgent); if (m.find()) { activity.setUserAgent(m.group(1)); } if (SecurityContextHolder.getContext().getAuthentication() != null && SecurityContextHolder.getContext().getAuthentication().isAuthenticated() && //when Anonymous Authentication is enabled !(SecurityContextHolder.getContext().getAuthentication() instanceof AnonymousAuthenticationToken)) { User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); activity.setUser(user); if (!activity.getUrl().contains("image") && !activity.getUrl().equals("/")) activity = activityService.save(activity); return super.preHandle(request, response, handler); } else if (activity.getUrl().equals("/")) { Activity existingActivity = this.activityService.findFirst(); if (existingActivity != null) { activity.setId(existingActivity.getId()); activity.setCreated(existingActivity.getCreated()); long totalVisitors = existingActivity.getTotalVisitors(); activity.setTotalVisitors(++totalVisitors); } else activity.setTotalVisitors(1L); activity = this.activityService.save(activity); } return super.preHandle(request, response, handler); } private String getClientIpAddress(HttpServletRequest request) { String xForwardedForHeader = request.getHeader("X-Forwarded-For"); if (xForwardedForHeader == null) { return request.getRemoteAddr(); } else { // As of https://en.wikipedia.org/wiki/X-Forwarded-For // The general format of the field is: X-Forwarded-For: client, proxy1, proxy2 ... // we only want the client return new StringTokenizer(xForwardedForHeader, ",").nextToken().trim(); } } }
What we’ve done here is that, if user is logged in, we’re recording all of the requests from loggedin user except “/” request and image requests.
If user isn’t logged in we’re recording pageviews for “/” request in the first row. We don’t need to record every request for users who aren’t logged in, right?
Okay now register this interceptor to spring in a configuration class that extends WebMvcConfigurerAdapter.
5. Register Interceptor
@Configuration public class AppConfig extends WebMvcConfigurerAdapter{ private final ActivityInterceptor activityInterceptor; @Autowired public AppConfig(ActivityInterceptor activityInterceptor) { this.activityInterceptor = activityInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { super.addInterceptors(registry); registry.addInterceptor(activityInterceptor); } }
Bamn! boy! you’re app will now track users activities.
Okay, you may not like the idea to record every requests forever. It may flood your database with, I don’t know, garbage!!?
So we’ll schedule a task to clean activities that are older than a month. Let’s make it quick.
6. Enable Scheduling
Add @EnableScheduling annotation to your main class. This will enable cron jobs across your application.
@SpringBootApplication @EnableWebSecurity @EnableScheduling public class HrsApplication { public static void main(String[] args) { SpringApplication.run(HrsApplication.class, args); } }
7. Create scheduler to execute task in a specific time (Here everyday at 12 am)
@Component public class ScheduledTasks { private final ActivityService activityService; @Autowired public ScheduledTasks(ActivityService activityService) { this.activityService = activityService; } @Scheduled(cron = "0 1 15 * * ?") void deleteActivitiesOlderThanAMonth() { Activity firstActivity = this.activityService.findFirst(); List<Activity> activityList = this.activityService.findAll(); for (Activity activity : activityList) { if (!activity.getId().equals(firstActivity.getId())) { // exclude first activity if (DateUtils.getDateDiff(activity.getCreated(), new Date(), TimeUnit.DAYS) >= 31) { this.activityService.delete(activity.getId()); System.out.println("Deleted activity! " + activity.getId()); } } } System.out.println("Deleted old user activities!"); } }
You may wonder what is those“0 1 15 * * ?” expressions inside @Scheduled(cron = “0 1 15 * * ?”). They are called regular expressions.
Here’s a useful quick explanation I found at stackoverflow:
* "0 0 * * * *" = the top of every hour of every day. * "*/10 * * * * *" = every ten seconds. * "0 0 8-10 * * *" = 8, 9 and 10 o'clock of every day. * "0 0/30 8-10 * * *" = 8:00, 8:30, 9:00, 9:30 and 10 o'clock every day. * "0 0 9-17 * * MON-FRI" = on the hour nine-to-five weekdays * "0 0 0 25 12 ?" = every Christmas Day at midnight
Cron expression is represented by six fields:
second, minute, hour, day of month, month, day(s) of week
(*) means match any */X means "every X" ? ("no specific value") - useful when you need to specify something in one of the two fields in which the character is allowed, but not the other. For example, if I want my trigger to fire on a particular day of the month (say, the 10th), but don't care what day of the week that happens to be, I would put "10" in the day-of-month field, and "?" in the day-of-week field.
hi, I used this tutorial but when i get all the activities from preHandler method,it is not saving ..it was returning null,please can you explain why
Hi, Thank you for your comment. But you need to be a little more specific. Can you please explain a bit with the code snippet so that I can understand in which part you are facing the problem?
This comment has been removed by the author.
Thanks a lot for your contribution. Would you like to share with me the dependancies I need in my pom.xml to be able to use your code
Hi,
You don’t need any dependency for this.
Could you please share the table design for this example?