Статьи

Избавление от нулевых параметров с простым пружинным аспектом

Какое самое ненавистное и в то же время самое популярное исключение в мире?

Бьюсь об заклад, это NullPointerException.

NullPointerException может означать что угодно, от простых «взлетов, я не думал, что это может быть нулевым» до часов и дней отладки сторонних библиотек (попробуйте использовать Dozer для сложных преобразований, я вас осмелюсь).

Самое смешное, что избавиться от всех исключений NullPointerException в вашем коде тривиально. Эта тривиальность является побочным эффектом техники, называемой « Проектирование по контракту ».

Я не буду вдаваться в подробности теории, вы можете найти все, что вам нужно, в Википедии, но в двух словах «Дизайн по контракту» означает:

  • у каждого метода есть предварительное условие (что он ожидает перед вызовом)
  • у каждого метода есть постусловие (что гарантирует, что возвращается)
  • каждый класс имеет ограничение на свое состояние (инвариант класса)

Таким образом, в начале каждого метода вы проверяете, выполняются ли предварительные условия, в конце — соблюдаются ли постусловия и инвариант, и если что-то не так, вы генерируете исключение, в котором говорится, что не так.

Используя внутренние статические методы Spring, которые генерируют соответствующие исключения (IllegalArgumentException), он может выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
}

Вы также можете использовать класс Validate из Apache Commons вместо Spring notNull / hasText.

Обычно я просто проверяю предварительные условия и пишу тесты для постусловий и ограничений. Но все же, это все код котельной плиты. Чтобы вывести его из своего класса, вы можете использовать множество библиотек Design by Contract, например
SpringContracts или Contract4J . В любом случае вы в конечном итоге проверяете предварительные условия для каждого открытого метода.

И угадай что? За исключением объектов передачи данных и некоторых сеттеров, каждый открытый метод, который я пишу, ожидает, что его параметры НЕ будут нулевыми.

Итак, чтобы сэкономить нам немного времени для написания этой статьи, как насчет добавления простого аспекта, который сделает невозможным во всем приложении передачу пустых значений чему-либо, кроме DTO и сеттеров? Без каких-либо дополнительных библиотек (я полагаю, вы уже используете Spring Framework), аннотаций и чего-то еще.

Почему я не хотел бы принимать значения NULL в параметрах? Потому что у нас есть перегрузка метода в современных языках. Серьезно, как часто вы хотите увидеть что-то вроде этого:

1
Address address = AddressFactory.create(null, null, null, null);

И это не намного лучше, либо

1
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);

Решение

Итак, вот простое решение: вы добавляете один класс в ваш проект и несколько строк весенней конфигурации IoC.

Класс (аспект) выглядит так:

01
02
03
04
05
06
07
08
09
10
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);
        }
    }
}

И весенняя конфигурация здесь (не забудьте изменить пространство имен для вашего проекта).

01
02
03
04
05
06
07
08
09
10
11
12
13
<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>

‘&&’ не является ошибкой, это просто условие && в xml. Если вы не понимаете синтаксис определения pointject аспекта, вот небольшая шпаргалка .

И вот тест, говорящий нам, что конфигурация успешна.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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 — это простой класс, который запускает контекст весеннего теста. Вместо этого вы можете использовать AbstractTransactionalJUnit4SpringContextTests с @ContextConfiguration (..).

Подвох

Ах да, есть подвох. Поскольку Spring AOP использует либо динамические прокси J2SE, основанные на интерфейсе, либо прокси CGLIB aspectj, каждому классу потребуется либо интерфейс (для простого аспектного ткачества на основе прокси), либо конструктор без каких-либо параметров (для ткачества cglib). Хорошей новостью является то, что конструктор может быть приватным.

Справка: избавление от нулевых параметров с простым аспектом пружины от нашего партнера по JCG Якуба Набрдалика из блога Solid Craft .