User activity logging: Spring

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.

 

 

6 thoughts on “User activity logging: Spring

  1. 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?

  2. 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

Leave a Reply

Your email address will not be published. Required fields are marked *