Add Performance Monitor Aspect To Spring Boot Application

Add Aspect

package com.lob.proj.config;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.interceptor.PerformanceMonitorInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@Aspect
public class AspectConfig {

	@Pointcut("execution(public * com.lob.proj.api.CustomerApi.*(..))")
	public void monitor() {
	}

	@Bean
	PerformanceMonitorInterceptor performanceMonitorInterceptor() {
		return new PerformanceMonitorInterceptor(false);
	}

	@Bean
	Advisor performanceMonitorAdvisor(PerformanceMonitorInterceptor performanceMonitorInterceptor) {
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		pointcut.setExpression("com.lob.proj.config.AspectConfig.monitor()");
		return new DefaultPointcutAdvisor(pointcut, performanceMonitorInterceptor);
	}
}

Add Logging level

logging.level.org.springframework.aop.interceptor.PerformanceMonitorInterceptor=TRACE

Kubernetes CronJob To Delete Expired Files

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: delete-older-data-files
spec:
  schedule: "0 * * * *"
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      ttlSecondsAfterFinished: 3600
      template:
        spec:
          securityContext: 
            fsGroup: 53134
            runAsUser: 1001
          containers:
          - name: delete-older-data-files
            image: docker.repo1.uhc.com/busybox:1.32.0
            command: ["/bin/sh", "-c"]
            args:
              - echo starting;
                find /app/data/ -mindepth 1 -type f -mmin +120 -print -delete;
                find /app/data/imports -mindepth 1 -empty -type d -mmin +120 -print -delete;
                find /app/data/exports -mindepth 1 -empty -type d -mmin +120 -print -delete;
                echo done;
            volumeMounts:
            - name: app-data-volume
              mountPath: "/app/data"
          restartPolicy: OnFailure
          volumes:
          - name: app-data-volume
            persistentVolumeClaim:
              claimName: app-data

find /path/to/directory/ -mindepth 1 -mtime +5 -delete

References

MapStruct Mapping List To Individual Fields And Vice Versa

public class Lookup {

	private String name;
	private String description;

	private String param1;
	private String param2;
	private String param3;
	private String param4;

	public int paramsCount() {
		int result = 0;
		
		if (isNotEmpty(param4)) {
			result = 4;
		} else if (isNotEmpty(param3)) {
			result = 3;
		} else if (isNotEmpty(param2)) {
			result = 2;
		} else if (isNotEmpty(param1)) {
			result = 1;
		}		
		return result;
	}

	private boolean isNotEmpty(String param42) {
		return param42 != null && param42.trim().length() > 0;
	}

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	public String getParam1() {
		return param1;
	}
	public void setParam1(String param1) {
		this.param1 = param1;
	}
	public String getParam2() {
		return param2;
	}
	public void setParam2(String param2) {
		this.param2 = param2;
	}
	public String getParam3() {
		return param3;
	}
	public void setParam3(String param3) {
		this.param3 = param3;
	}
	public String getParam4() {
		return param4;
	}
	public void setParam4(String param4) {
		this.param4 = param4;
	}

	@Override
	public String toString() {
		return "Lookup [name=" + name + ", description=" + description + ", param1=" + param1 + ", param2=" + param2
				+ ", param3=" + param3 + ", param4=" + param4 + "]";
	}
}

import java.util.List;

public class LookupModel {
	
	private String name;
	private String description;
	private List<String> params;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}
	public List<String> getParams() {
		return params;
	}
	public void setParams(List<String> params) {
		this.params = params;
	}
	
	@Override
	public String toString() {
		return "LookupModel [name=" + name + ", description=" + description + ", params=" + params + "]";
	}
}

Approach #1

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;

@Mapper
public interface LookupMapper {
	
	LookupMapper INSTANCE = Mappers.getMapper( LookupMapper.class );

	@Mapping(source = "source", target = "param1", qualifiedByName = "param1")
	@Mapping(source = "source", target = "param2", qualifiedByName = "param2")
	@Mapping(source = "source", target = "param3", qualifiedByName = "param3")
	@Mapping(source = "source", target = "param4", qualifiedByName = "param4")
	Lookup toLookup(LookupModel source);

	@Named("param1")
    default String lookupModelToParam1(LookupModel source) {
		return getParam(0, source);
    }

	@Named("param2")
    default String lookupModelToParam2(LookupModel source) {
		return getParam(1, source);
    }

	@Named("param3")
    default String lookupModelToParam3(LookupModel source) {
       return getParam(2, source);
    }

	@Named("param4")
    default String lookupModelToParam4(LookupModel source) {
       return getParam(3, source);
    }

	default String getParam(int index, LookupModel source) {
		if (source.getParams().size() > index) {
			return source.getParams().get(index);
		}
		return null;
	}

	@Mapping(source = "source", target = "params", qualifiedByName = "params")
	LookupModel toLookupModel(Lookup source);

	@Named("params")
	default List<String> lookupToParams(Lookup source) {
		return extractParams(source);
	}

	default List<String> extractParams(Lookup source) {
		List<String> result = new ArrayList<String>();

		int paramsCount = source.paramsCount();
		
		if (paramsCount > 0) {
			result.add(source.getParam1());
		}
		
		if (paramsCount > 1) {
			result.add(source.getParam2());
		}
		
		if (paramsCount > 2) {
			result.add(source.getParam3());
		}
		
		if (paramsCount > 3) {
			result.add(source.getParam4());
		}
		return result;
	}
}

Approach #2

@Mapper
public interface LookupMapper {
	
	LookupMapper INSTANCE = Mappers.getMapper( LookupMapper.class );

	Lookup toLookup(LookupModel source);

	@AfterMapping
	default void populateAdditionalParams(LookupModel source, @MappingTarget Lookup target) {

		int idx = 0;
		for (String param : source.getParams()) {
			idx++;
			if (idx == 1) {
				target.setParam1(param);
			} else if (idx == 2) {
				target.setParam2(param);
			}else if (idx == 3) {
				target.setParam3(param);
			}else if (idx == 4) {
				target.setParam4(param);
			}
		}
	}

	LookupModel toLookupModel(Lookup source);

	@AfterMapping
	default void populateAdditionalParams(Lookup source, @MappingTarget LookupModel target) {
		target.setParams(extractParams(source));
	}

	default List<String> extractParams(Lookup source) {
		List<String> result = new ArrayList<String>();

		int paramsCount = source.paramsCount();
		
		if (paramsCount > 0) {
			result.add(source.getParam1());
		}
		
		if (paramsCount > 1) {
			result.add(source.getParam2());
		}
		
		if (paramsCount > 2) {
			result.add(source.getParam3());
		}
		
		if (paramsCount > 3) {
			result.add(source.getParam4());
		}
		return result;
	}
}

Mapping Between JPA Entities And DTOs In Spring Boot Application

Source Code can be found here

Update Pom

Update your build.plugins

<plugin>
	            <groupId>org.apache.maven.plugins</groupId>
	            <artifactId>maven-compiler-plugin</artifactId>
	            <configuration>
	                <source>${java.version}</source>
	                <target>${java.version}</target>
	                <annotationProcessorPaths>
	                    <path>
	                        <groupId>org.mapstruct</groupId>
	                        <artifactId>mapstruct-processor</artifactId>
	                        <version>${mapstruct.version}</version>
	                    </path>
	                </annotationProcessorPaths>
	            </configuration>
	        </plugin>

Add mapstruct dependencies

		<dependency>
		    <groupId>org.mapstruct</groupId>
		    <artifactId>mapstruct</artifactId>
		    <version>${mapstruct.version}</version>
		</dependency>

Structure

Create DTOs (Models)

We have created the Models AddressModel and CustomerModel

Create Mapper

import java.util.List;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import com.org.lob.project.repository.entity.Address;
import com.org.lob.project.service.model.AddressModel;

@Mapper(componentModel = "spring")
public interface AddressMapper {

	AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

	List<AddressModel> toAddressModels(List<Address> source);

	List<Address> toAddresses(List<AddressModel> source);
}

import java.util.List;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import com.org.lob.project.repository.entity.Customer;
import com.org.lob.project.service.model.CustomerModel;

@Mapper(componentModel = "spring", uses = AddressMapper.class)
public interface CustomerMapper {

	CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);

	Customer toCustomer(CustomerModel source);

	CustomerModel toCustomerModel(Customer source);

	List<CustomerModel> toCustomerModels(List<Customer> source);
}

Generate sources

Execute the following before usage

mvn clean package

which would generate the sources as follows

you can add the generated-sources folder as source folder

Use Mapper in Service

import java.util.List;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import com.org.lob.project.exception.ProjectException;
import com.org.lob.project.repository.CustomerRepository;
import com.org.lob.project.repository.entity.Customer;
import com.org.lob.project.service.mapper.AddressMapper;
import com.org.lob.project.service.mapper.CustomerMapper;
import com.org.lob.project.service.model.CustomerModel;
import com.org.lob.project.service.model.CustomerSearchRequest;
import com.org.lob.project.service.specification.CustomerSpecification;

@Service
public class DefaultCustomerService implements CustomerService {

	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultCustomerService.class);

	private final CustomerRepository customerRepository;
	private final CustomerMapper customerMapper;
	private final AddressMapper addressMapper;

	public DefaultCustomerService(CustomerRepository customerRepository, CustomerMapper customerMapper, AddressMapper addressMapper) {
		this.customerRepository = customerRepository;
		this.customerMapper = customerMapper;
		this.addressMapper = addressMapper;
	}

	@Override
	public Optional<CustomerModel> getCustomerById(Long customerId) {
		LOGGER.debug("Fetching customer by id: {}", customerId);
		Optional<Customer> optionalCustomer = customerRepository.findById(customerId);
		return optionalCustomer.isPresent() ? Optional.of(customerMapper.toCustomerModel(optionalCustomer.get())) : Optional.empty();
	}

	@Override
	public CustomerModel create(CustomerModel customerModel) {
		try {
			LOGGER.debug("Creating a new customer with emailAddress: {}", customerModel.getEmailAddress());
			return customerMapper.toCustomerModel(customerRepository.save(customerMapper.toCustomer(customerModel)));
		} catch (DataIntegrityViolationException e) {
			LOGGER.error("Customer already exists with emailAddress: {}", customerModel.getEmailAddress());
			throw ProjectException.duplicateRecord("Customer already exists with same emailAddress " + customerModel.getEmailAddress());
		}
	}

	@Override
	public CustomerModel update(CustomerModel customerModel) {
		LOGGER.debug("Updating a customer with id: {}", customerModel.getId());
		Optional<Customer> optionalCustomer = customerRepository.findById(customerModel.getId());
		if (!optionalCustomer.isPresent()) {
			LOGGER.error("Unable to update customer by id {}", customerModel.getId());
			throw ProjectException.noRecordFound("Customer does not exists " + customerModel.getId());
		}
		Customer existingCustomer = optionalCustomer.get();
		existingCustomer.setAddresses(addressMapper.toAddresses(customerModel.getAddresses()));
		existingCustomer.setFirstName(customerModel.getFirstName());
		existingCustomer.setLastName(customerModel.getLastName());
		return customerMapper.toCustomerModel(customerRepository.save(existingCustomer));
	}
	
	@Override
	public List<CustomerModel> findByName(String name) {
		return customerMapper.toCustomerModels(customerRepository.findAllByFirstNameContainingOrLastNameContaining(name, name));
	}

	@Override
	public Optional<CustomerModel> findByEmail(String email) {
		Optional<Customer> optionalCustomer = customerRepository.findCustomerByEmailAddress(email);
		return optionalCustomer.isPresent() ? Optional.of(customerMapper.toCustomerModel(optionalCustomer.get())) : Optional.empty();
	}

	// Paging implementation of findAll
	@Override
	public Page<CustomerModel> findAll(Pageable pageable) {
		return new PageImpl<>( customerMapper.toCustomerModels(customerRepository.findAll(pageable).getContent()));
	}

	@Override
	public void deleteCustomer(Long customerId) {
		try {
			customerRepository.deleteById(customerId);
		} catch (EmptyResultDataAccessException e) {
			LOGGER.error("Unable to delete customer by id {}", customerId);
			throw ProjectException.noRecordFound("Customer does not exists " + customerId);
		}
	}

	@Override
	public Page<CustomerModel> search(CustomerSearchRequest request, Pageable pageable) {
		return new PageImpl<>(customerMapper.toCustomerModels(customerRepository.findAll(new CustomerSpecification(request), pageable).getContent()));
	}

	@Override
	public List<CustomerModel> findAllById(Iterable<Long> ids) {
		return customerMapper.toCustomerModels(customerRepository.findAllById(ids));
	}
}

References