Getting rid of null parameters with a simple spring aspect
What is the most hated and at the same time the most popular exception in the world?
I bet it’s the NullPointerException.
NullPointerException can mean anything, from simple “ups, I didn’t think that can be null” to hours and days of debugging of third-party libraries (try using Dozer for complicated transformations, I dare you).
The funny thing is, it’s trivial to get rid of all the NullPointerExceptions in your code. This triviality is a side effect of a technique called “Design by Contract”.
I won’t go into much details about the theory, you can find everything you need on Wikipedia, but in the nutshell Design by Contract means:
- each method has a precondition (what it expects before being called)
- each method has a postcondition (what it guarantees, what is returned)
- each class has an constraint on its state (class invariant)
So at the beginning of each method you check whether preconditions are met, at the end, whether postconditions and invariant are met, and if something’s wrong you throw an exception saying what is wrong.
Using Spring’s internal static methods that throw appropriate exceptions (IllegalArgumentException), it can look something like this:
import static org.springframework.util.Assert.notNull; import static org.springframework.util.StringUtils.hasText; public class BranchCreator { public Story createNewBranch(Story story, User user, String title) { verifyParameters(story, user, title); Story branch = //... the body of the class returnig an object verifyRetunedValue(branch); return branch; } private void verifyParameters(Story story, User user, String title) { notNull(story); notNull(user); hasText(title); } private void verifyRetunedValue(Story branch) { notNull(branch); } }
You can also use Validate class from apache commons instead of spring’s notNull/hasText.
Usually I just check preconditions and write tests for postconditions and constraints. But still, this is all boiler plate code. To move it out of your class, you can use many Design by Contract libraries, for example
SpringContracts, or Contract4J. Either way you end up checking the preconditions on every public method.
And guess what? Except for Data Transfer Objects and some setters, every public method I write expects its parameters NOT to be null.
So to save us some writing of this boiler plate ocde, how about adding a simple aspect that will make it impossible in the whole application, to pass null to anything other than DTOs and setters? Without any additional libraries (I assume you are already using Spring Framework), annotations, and what else.
Why would I like to not allow for nulls in parameters? Because we have method overloading in modern languages. Seriously, how often do you want to see something like this:
Address address = AddressFactory.create(null, null, null, null);
And this is not much better either
Microsoft.Office.Interop.Excel.Workbook theWorkbook = ExcelObj.Workbooks.Open(openFileDialog.FileName, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing);
The solution
So here is a simple solution: you add one class to your project and a few lines of spring IoC configuration.
The class (aspect) looks like this:
import org.aspectj.lang.JoinPoint; import static org.springframework.util.Assert.notNull; public class NotNullParametersAspect { public void throwExceptionIfParametersAreNull(JoinPoint joinPoint) { for(Object argument : joinPoint.getArgs()) { notNull(argument); } } }
And the spring configuration is here (remember to change the namespace to your project).
<aop:config proxy-target-class='true'> <aop:aspect ref='notNullParametersAspect'> <aop:pointcut expression='execution(public * eu.solidcraft.*..*.*(..)) && !execution(public * eu.solidcraft.*..*Dto.*(..)) && !execution(public * eu.solidcraft.*..*.set*(..))' id='allPublicApplicationOperationsExceptDtoAndSetters'> <aop:before method='throwExceptionIfParametersAreNull' pointcut-ref='allPublicApplicationOperationsExceptDtoAndSetters'></aop:before> </aop:pointcut> <task:annotation-driven> <bean class='eu.solidcraft.aspects.NotNullParametersAspect' id='notNullParametersAspect'></bean> </task:annotation-driven> </aop:aspect> </aop:config>
The ‘&&’ is no error, it’s just && condition escaped in xml. If you do not understand aspectj pointcut definition syntaxt, here is a little cheat sheet.
And here is a test telling us that the configuration is succesfull.
public class NotNullParametersAspectIntegrationTest extends AbstractIntegrationTest { @Resource(name = 'userFeedbackFacade') private UserFeedbackFacade userFeedbackFacade; @Test(expected = IllegalArgumentException.class) public void shouldThrowExceptionIfParametersAreNull() { //when userFeedbackFacade.sendFeedback(null); //then exception is thrown } @Test public void shouldNotThrowExceptionForNullParametersOnDto() { //when UserBookmarkDto userBookmarkDto = new UserBookmarkDto(); userBookmarkDto.withChapter(null); StoryAncestorDto ancestorDto = new StoryAncestorDto(null, null, null, null); //then no exception is thrown } }
AbstractIntegrationTest is a simple class that starts the spring test context. You can use AbstractTransactionalJUnit4SpringContextTests with @ContextConfiguration(..) instead.
The catch
Ah yes, there is a catch. Since spring AOP uses either J2SE dynamic proxies basing on an interface or aspectj CGLIB proxies, every class will either need an interface (for simple proxy based aspect weaving) or a constructor without any parameters (for cglib weaving). The good news is that the constructor can be private.
Reference: Getting rid of null parameters with a simple spring aspect from our JCG partner Jakub Nabrdalik at the Solid Craft blog.
Nice. I prefer using the AspectJ compiler with before-constructor and before- & after-method execution advice along with @NotNull (and any other JSR 303 validation annotations).
Curious: why did you use ? NotNullParametersAspect doesn’t use any annotations, so the could be defined regularly under the element. Am I missing something there?