Archive

Archive for March, 2017

Log4jdbc Spring Boot Starter

March 27th, 2017 No comments

Logging SQL as it’s executed is a fairly common desire when developing applications. Perhaps an ORM (such as Hibernate) is being used, and you want to see the actual SQL being executed. Or maybe you’re tracking down a performance problem and need to know if it’s in the application or the database, so step #1 is finding out what query is executing and for how long.

Solving this problem once and for all (at least for Spring Boot applications), I created Log4jdbc Spring Boot Starter. It’s a very simple yet powerful way to log SQL queries (and more, such as timing information). And unlike other solution, the queries logged are ready to run – the ‘?’ parameters are replaced with their values. This means you can copy and paste the query from the log and run them unmodified in the SQL query tool of your choice, saving a lot of time.

For background, my motivation for this work is a result of a Spring Boot / Hibernate application I have in progress. I started by using spring.jpa.properties.hibernate.show_sql=true but that only logs queries with ‘?’ place holders. To log the values, add spring.jpa.properties.hibernate.type=trace. At least now I had the query and the values for it, but to run it in my query tool (I need to EXPLAIN the query), I had to replace each ‘?’ with the value – and I had over 20 place holders. That got old fast.

There are other approaches to log queries, such as the one described in Display SQL to Console in Spring JdbcTemplate. I’m not a fan of this approach because it only works for queries made through JdbcTemplate (so Hibernate queries aren’t logged, for example) and it’s an awfully lot of code to include and therefore have to maintain in each project.

I discovered Log4jdbc but it’s a bit of a pain to setup in a Spring Boot application because it:

  • doesn’t use the Spring Environment (application.properties)
  • needs setup to wrap the DataSource’s in the Log4jdbc DataSourceSpy

Wanting to solve this problem precisely once and never again, I created Log4jdbc Spring Boot Starter.

To use it, just add to your project:

  1. <dependency>
  2.   <groupId>com.integralblue</groupId>
  3.   <artifactId>log4jdbc-spring-boot-starter</artifactId>
  4.   <version>[INSERT VERSION HERE]</version>
  5. </dependency>

Then turn on the logging levels as desired in application.properties, for example:


logging.level.jdbc.sqlonly=DEBUG

When no logging is configured (all loggers are set to fatal or off), log4jdbc returns the original Connection.

See the Log4jdbc Spring Boot Starter project page for more information.

Categories: Uncategorized Tags:

Working around HHH-9663: Orphan removal does not work for OneToOne relations

March 23rd, 2017 No comments

HHH-9663 means that orphan removal doesn’t work for OneToOne relationships. For example, given File and FileContent as below (taken from the bug report):

  1. package pl.comit.orm.model;
  2.  
  3. import javax.persistence.Entity;
  4. import javax.persistence.FetchType;
  5. import javax.persistence.Id;
  6. import javax.persistence.OneToOne;
  7.  
  8. @Entity
  9. public class File {
  10.  
  11. 	private int id;
  12.  
  13. 	private FileContent content;
  14.  
  15. 	@Id
  16. 	public int getId() {
  17. 		return id;
  18. 	}
  19.  
  20. 	public void setId(int id) {
  21. 		this.id = id;
  22. 	}
  23.  
  24. 	@OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
  25. 	public FileContent getContent() {
  26. 		return content;
  27. 	}
  28.  
  29. 	public void setContent(FileContent content) {
  30. 		this.content = content;
  31. 	}
  32. }
  1. package pl.comit.orm.model;
  2.  
  3. import javax.persistence.Entity;
  4. import javax.persistence.Id;
  5.  
  6. @Entity
  7. public class FileContent {
  8.  
  9. 	private int id;
  10.  
  11. 	@Id
  12. 	public int getId() {
  13. 		return id;
  14. 	}
  15.  
  16. 	public void setId(int id) {
  17. 		this.id = id;
  18. 	}
  19. }
  1. package pl.comit.orm.dao;
  2.  
  3. import javax.persistence.EntityManager;
  4. import javax.persistence.PersistenceContext;
  5.  
  6. import org.springframework.stereotype.Repository;
  7. import org.springframework.transaction.annotation.Transactional;
  8.  
  9. import pl.comit.orm.model.File;
  10. import pl.comit.orm.model.FileContent;
  11.  
  12. @Repository
  13. public class Dao {
  14.  
  15. 	@PersistenceContext
  16. 	private EntityManager entityManager;
  17.  
  18. 	@Transactional
  19. 	public void assureCreatedTaskAndNote(int fileId, int contentId) {
  20. 		FileContent content = entityManager.find(FileContent.class, contentId);
  21. 		if (content == null) {
  22. 			content = new FileContent();
  23. 			content.setId(contentId);
  24. 			entityManager.persist(content);
  25. 		}
  26.  
  27. 		File file = entityManager.find(File.class, fileId);
  28. 		if (file == null) {
  29. 			file = new File();
  30. 			file.setId(fileId);
  31. 			entityManager.persist(file);
  32. 		}
  33. 		file.setContent(content);
  34. 	}
  35.  
  36. 	@Transactional
  37. 	public void removeContent(int fileId) {
  38. 		File file = entityManager.find(File.class, fileId);
  39. 		file.setContent(null);
  40. 	}
  41.  
  42. 	public FileContent find(int contentId) {
  43. 		return entityManager.find(FileContent.class, contentId);
  44. 	}
  45. }

Running this as the main class will result in an exception:

  1. package pl.comit.orm;
  2.  
  3. import org.springframework.context.support.ClassPathXmlApplicationContext;
  4.  
  5. import pl.comit.orm.dao.Dao;
  6. import pl.comit.orm.model.FileContent;
  7.  
  8. public final class Application {
  9.  
  10. 	private static final String CFG_FILE = "applicationContext.xml";
  11.  
  12. 	public static void main(String[] args) {
  13. 		test(new ClassPathXmlApplicationContext(CFG_FILE).getBean(Dao.class));
  14. 	}
  15.  
  16. 	public static void test(Dao dao) {
  17. 		dao.assureCreatedTaskAndNote(1, 2);
  18. 		dao.removeContent(1);
  19. 		FileContent content = dao.find(2);
  20. 		if (content != null) {
  21. 			System.err.println("Content found: " + content);
  22. 		}
  23. 	}
  24. }

A workaround is to manually remove and detach the old referent, and then persist the new referent. Here’s an updated File.java:

  1. package pl.comit.orm.model;
  2.  
  3. import org.springframework.stereotype.Component;
  4.  
  5. import javax.annotation.PostConstruct;
  6. import javax.annotation.PreDestroy;
  7. import javax.persistence.Entity;
  8. import javax.persistence.FetchType;
  9. import javax.persistence.Id;
  10. import javax.persistence.OneToOne;
  11. import javax.persistence.PersistenceContext;
  12.  
  13. @Entity
  14. public class File {
  15.  
  16. 	private int id;
  17.  
  18. 	private FileContent content;
  19.  
  20. 	@Id
  21. 	public int getId() {
  22. 		return id;
  23. 	}
  24.  
  25. 	public void setId(int id) {
  26. 		this.id = id;
  27. 	}
  28.  
  29. 	@OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
  30. 	public FileContent getContent() {
  31. 		return content;
  32. 	}
  33.  
  34. 	public void setContent(FileContent content) {
  35. 		if(this.content != content){
  36. 			final oldContent = this.content;
  37. 			this.content = content;
  38. 			if(oldContent!=null){
  39. 				// Hibernate won't remove the oldContent for us, so do it manually; workaround HHH-9663
  40. 				WorkaroundHHH9663.entityManager.remove(oldContent);
  41. 				WorkaroundHHH9663.entityManager.detach(oldContent);
  42. 			}
  43. 		}
  44. 	}
  45.  
  46. 	// WORKAROUND https://hibernate.atlassian.net/browse/HHH-9663 "Orphan removal does not work for OneToOne relations"
  47. 	@Component
  48. 	public static class WorkaroundHHH9663 {
  49. 		@PersistenceContext
  50. 		private EntityManager injectedEntityManager;
  51.  
  52. 		private static EntityManager entityManager;
  53.  
  54. 		@PostConstruct
  55. 		public void postConstruct(){
  56. 			entityManager = injectedEntityManager;
  57. 		}
  58.  
  59. 		@PreDestroy
  60. 		public void preDestroy(){
  61. 			this.entityManager = null; // NOPMD
  62. 		}
  63. 	}
  64. 	// END WORKAROUND
  65. }

Note that no Dao changes were made, so if, for example, Spring Data was used instead of such a Dao, you wouldn’t have to modify anything else. And you can just remove this workaround code easily when a version of Hibernate because available with HHH-9963 fixed.
Finally, yes, this approach does exhibit a bit of code smell (the use of the static variable in this way and the entity having a container managed component aren’t exactly best practices), but, it’s a workaround – hopefully just a temporary one.

Categories: Uncategorized Tags:

Spring Boot, HTTPS required, and Elastic Beanstalk health checks

March 9th, 2017 2 comments

Spring Boot can be very easily configured to require HTTPS for all requests. In application.properties, simply set

security.require-ssl=true

And that works great – until you’re running the Spring Boot application on AWS Elastic Beanstalk with both HTTP and HTTPS listeners:

In that case, Elastic Beanstalk’s health check is always done over HTTP. The configuration page even says as much (there is no option to change it to be over HTTPS):

Since Spring Boot will redirect all non-secure HTTP requests to HTTPS, the health check will see an HTTP 302 redirect and therefore fail.

To workaround this issue (and in my opinion, AWS shortcoming), Spring Boot needs to be configured to allow insecure requests to the health check URL. To do so, we’ll define a new URL (/aws-health) that proxies Actuator’s health URL but responds over http and https.

In your security configuration class (named WebSecurityConfiguration below as an example) which extends WebSecurityConfigurerAdapter, add the following to the existing implementation of configure(HttpSecurity) (or create that method if it doesn’t already exist):

  1. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  2.  
  3. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  4. import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
  5.  
  6. public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
  7.   @Override
  8.   protected void configure(final HttpSecurity http) throws Exception {
  9.     // Elastic Beanstalk health checks only happen over HTTP, so as a workaround
  10.     // create a new URL (/aws-health) that forwards to the Actuator health check, see InsecureHealthController
  11.     // That URL is set to respond over any channel (not just secure, aka https, ones)
  12.     // see https://candrews.integralblue.com/2017/03/spring-boot-https-required-and-elastic-beanstalk-health-checks/
  13.     http.requiresChannel()
  14.       .antMatchers("/aws-health").requires(ChannelDecisionManagerImpl.ANY_CHANNEL)
  15.       .anyRequest().requiresSecure();
  16.   }
  17. }

Now create the controller:

  1. import java.io.IOException;
  2.  
  3. import javax.servlet.ServletException;
  4. import javax.servlet.http.HttpServletRequest;
  5. import javax.servlet.http.HttpServletRequestWrapper;
  6. import javax.servlet.http.HttpServletResponse;
  7.  
  8. import org.springframework.boot.actuate.autoconfigure.ManagementServerProperties;
  9. import org.springframework.stereotype.Controller;
  10. import org.springframework.web.bind.annotation.RequestMapping;
  11. import org.springframework.web.bind.annotation.RequestMethod;
  12. import org.springframework.web.util.UriComponentsBuilder;
  13.  
  14. /**
  15.  * Elastic Beanstalk health checks only happen over HTTP, so as a workaround
  16.  * create a new URL (/aws-health) that forwards to the Actuator health check
  17.  * That URL is set to respond over any channel (not just secure, aka https, ones) in {@link WebSecurityConfiguration}
  18.  * 
  19.  * @see https://candrews.integralblue.com/2017/03/spring-boot-https-required-and-elastic-beanstalk-health-checks/
  20.  */
  21. public class InsecureHealthController {
  22.   private final ManagementServerProperties management;
  23.  
  24.   public InsecureHealthController(ManagementServerProperties management) {
  25.     this.management = management;
  26.   }
  27.  
  28.   @RequestMapping(value="/health", method=RequestMethod.GET)
  29.   public void health(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException{
  30.     final String healthUrl = UriComponentsBuilder.fromPath(management.getContextPath()).path("/aws-health").toUriString();
  31.     final HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(request){
  32.       @Override
  33.         public boolean isSecure() {
  34.           return true;
  35.         }
  36.       };
  37.       request.getRequestDispatcher(healthUrl).forward(requestWrapper, response);
  38.     }
  39. }
Categories: Uncategorized Tags: