@inponomarev
Иван Пономарёв, КУРС/МФТИ
Как автоматически тестировать компоненты, которые жёстко связаны со своими зависимостями через статические поля/методы?
— Да никак!
Увеличивает Coupling
Нарушает Single Responsibility Principle
Невозможно нормально тестировать
// вынесли весь "wiring" из классов в фабричный метод
public static Controller makeController() {
DataSource dataSource =
new DataSource(ConnectionPool.getInstance());
ConferenceDao conferenceDao = new ConferenceDao(dataSource);
SpeakerDao speakerDao = new SpeakerDao(dataSource);
TalkDao talkDao = new TalkDao(dataSource);
return new Controller(conferenceDao, speakerDao, talkDao);
}
Мы отвязались от «wiring» и можем создавать компоненты по отдельности, что позволяет тестировать.
Однако в фабричном методе много повторяющихся действий, фабрика должна «знать» про все компоненты.
Вообще этот процесс можно автоматизировать!
Google Guice
JBoss Seam Framework
PicoContainer
Spring
Пример на базе тренинга Евгения Борисова «Spring Ripper»
Позволяет понять логику и внутреннее устройство Spring Framework
public class RobotLecturer {
//«магическим» образом пусть тут появится то, что нужно!
@InjectByType
private Lecture lecture;
@InjectByType
private Speaker speaker;
@InjectByType
private SlideShow slideShow;
...
}
//Читать лекции просто
public void lecture() {
lecture.getSlides().forEach(
slide -> {
slideShow.show(slide.getText());
speaker.speak(slide.getComment());
}
);
}
//Main-метод
public static void main(String[] args)
throws ReflectiveOperationException {
RobotLecturer lecturer =
new ObjectFactory().createObject(RobotLecturer.class);
lecturer.lecture();
}
@RequiredArgsConstructor
public class InjectByTypeAnnotationObjectConfigurator
implements ObjectConfigurator {
//передадим сюда через конструктор ObjectFactory
private final ObjectFactory factory;
@Override
public void configure(Object t) throws ... {
for (Field field : t.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(InjectByType.class)) {
field.setAccessible(true);
//Мы же умеем по типу создавать объект?
field.set(t, factory.createObject(field.getType()));
}
}
}
}
public class ObjectFactory {
//Правда, тут напрашивается DI?
private final Reflections scanner =
new Reflections("edu.phystech");
private final List<ObjectConfigurator> configurators =
new ArrayList<>();
public <T> T createObject(Class<? extends T> type) throws ... {
//Находим реализацию запрошенного типа
type = resolveImpl(type);
//Создаём объект (с помощью конструктора по умолчанию, TODO)
T t = type.newInstance();
//Конфигурируем
configure(t);
....
}
private <T> void configure(T t) throws ... {
for (ObjectConfigurator configurator : configurators) {
configurator.configure(t);
}
}
private <T> Class<? extends T> resolveImpl(Class<? extends T> type){
if (type.isInterface()) {
Set<Class<? extends T>> classes =
scanner.getSubTypesOf((Class<T>) type);
if (classes.size() != 1) {
throw new RuntimeException(
"0 or more than one implementation found for type "
+ type + " please update your config");
}
type = classes.iterator().next();
}
return type;
}
public class RobotLecturer {
//«магическим» образом пусть тут появится то, что нужно!
@InjectByType
private Lecture lecture;
@InjectByType
private Speaker speaker;
@InjectByType
private SlideShow slideShow;
...
}
//Читать лекции просто
public void lecture() {
lecture.getSlides().forEach(
slide -> {
slideShow.show(slide.getText());
speaker.speak(slide.getComment());
}
);
}
//Main-метод
public static void main(String[] args)
throws ReflectiveOperationException {
RobotLecturer lecturer =
new ObjectFactory().createObject(RobotLecturer.class);
lecturer.lecture();
}
@RequiredArgsConstructor
public class InjectByTypeAnnotationObjectConfigurator
implements ObjectConfigurator {
//передадим сюда через конструктор ObjectFactory
private final ObjectFactory factory;
@Override
public void configure(Object t) throws ... {
for (Field field : t.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(InjectByType.class)) {
field.setAccessible(true);
//Мы же умеем по типу создавать объект?
field.set(t, factory.createObject(field.getType()));
}
}
}
}
public class ObjectFactory {
//Правда, тут напрашивается DI?
private final Reflections scanner =
new Reflections("edu.phystech");
private final List<ObjectConfigurator> configurators =
new ArrayList<>();
public <T> T createObject(Class<? extends T> type) throws ... {
//Находим реализацию запрошенного типа
type = resolveImpl(type);
//Создаём объект (с помощью конструктора по умолчанию, TODO)
T t = type.newInstance();
//Конфигурируем
configure(t);
....
}
private <T> void configure(T t) throws ... {
for (ObjectConfigurator configurator : configurators) {
configurator.configure(t);
}
}
private <T> Class<? extends T> resolveImpl(Class<? extends T> type){
if (type.isInterface()) {
Set<Class<? extends T>> classes =
scanner.getSubTypesOf((Class<T>) type);
if (classes.size() != 1) {
throw new RuntimeException(
"0 or more than one implementation found for type "
+ type + " please update your config");
}
type = classes.iterator().next();
}
return type;
}
//Конструктор ObjectFactory
public ObjectFactory() throws ReflectiveOperationException {
Set<Class<? extends ObjectConfigurator>> classes =
scanner.getSubTypesOf(ObjectConfigurator.class);
for (Class<? extends ObjectConfigurator> aClass : classes) {
try {
Constructor<? extends ObjectConfigurator> constructor =
aClass.getConstructor(ObjectFactory.class);
//инжектим себя через конструктор, по необходимости
configurators.add(constructor.newInstance(this));
} catch (NoSuchMethodException e){
configurators.add(aClass.newInstance());
}
}
....//продолжение следует
@Retention(RUNTIME)
public @interface InjectRandomInt {
int min();
int max();
}
public class InjectRandomIntObjectConfigurator
implements ObjectConfigurator {
@Override
public void configure(Object t) throws IllegalAccessException {
Class<?> type = t.getClass();
for (Field field : ReflectionUtils.getAllFields(type)) {
InjectRandomInt annotation =
field.getAnnotation(InjectRandomInt.class);
if (annotation != null) {
int min = annotation.min();
int max = annotation.max();
int value = ThreadLocalRandom.current().nextInt(min, max+1);
field.setAccessible(true);
field.set(t, value);
}
}
}
}
Почему конструктор не годится для действий, включающих в себя инъектированные объекты?
Инъекция может происходить после конструктора. Поэтому нужно специальное действие, вызываемое после конструктора и инъекции!
public class RobotLecturer {
@InjectByType
private Lecture lecture;
@InjectRandomInt(min = 1, max = 3)
private int repeat;
@PostConstruct
public void init() {
//Место, где можно использовать все инжектированные значения
for (int i = 0; i < repeat; i++)
speaker.speak("Всем привет");
}
...
}
public <T> T createObject(Class<? extends T> type) throws ... {
....
//Конфигурируем
configure(t);
//Запускаем методы PostConstruct
invokeInitMethods(type, t);
....
}
private <T> void invokeInitMethods(Class<? extends T> type, T t)
throws ... {
for (Method method : type.getMethods()) {
if (method.isAnnotationPresent(PostConstruct.class)) {
method.invoke(t);
}
}
}
@Retention(RUNTIME)
public @interface Benchmark {
}
[[[BENCHMARK method speak
Speaking: blah-blah-blah
Time: 107100ns]]]
public class BenchmarkProxyConfigurator
implements ProxyConfigurator {
@Override
public <T> T wrapWithPoxy(T t, Class<? extends T> type) {
boolean isProxyNeeded = type.isAnnotationPresent(Benchmark.class)
|| !ReflectionUtils.getAllMethods(type, method ->
method.isAnnotationPresent(Benchmark.class)).isEmpty();
if (isProxyNeeded) {
return (T) Proxy.newProxyInstance(type.getClassLoader(),
type.getInterfaces(),
(proxy, method, args) -> {
Method classMethod = type.getMethod(method.getName(),
method.getParameterTypes());
return invoke(t, type, method, args, classMethod);
});
}
return t;
}}
private Object invoke(Object t, Class type, Method method,
Object[] args, Method classMethod) throws ... {
if (classMethod.isAnnotationPresent(Benchmark.class)
|| type.isAnnotationPresent(Benchmark.class)) {
System.out.printf("[[[BENCHMARK method %s%n", method.getName());
long start = System.nanoTime();
Object retVal = method.invoke(t, args);
long end = System.nanoTime();
System.out.printf("Time: %dns]]]%n", end - start);
return retVal;
} else {
return method.invoke(t, args);
}
}
DI-контейнер реализует следующие этапы «настройки» объектов:
создание
конфигурация (injections)
инициализация (postconstruct)
проксирование
DI-паттерн повторяет сам себя: многие детали DI-контейнера удобно настраивать через DI!
"Perhaps one of the hardest parts of explaining Spring is classifying exactly what it is" — Pro Spring 5, 5th ed., p. 1
DI
AOP
Тестирование
Интеграция с огромным количеством технологий
Очень развитый (и продолжающий активно развиваться)
Было:
public class Main {
public static void main(String[] args) throws ... {
RobotLecturer lecturer = new ObjectFactory()
.createObject(RobotLecturer.class);
lecturer.lecture();
}
}
Стало:
@ComponentScan("edu.phystech.robotlecturer")
public class Main {
public static void main(String[] args) {
ApplicationContext ctx =
new AnnotationConfigApplicationContext(Main.class);
RobotLecturer lecturer = ctx.getBean(RobotLecturer.class);
lecturer.lecture();
}
}
В нашем примере —
RobotLecturer
,
FirstLecture
,
SpeakerImpl
,
SlideShowImpl
.
Spring beans — это переиспользуемые программные компоненты.
Годится любой класс, как наш, так и из сторонней библиотеки.
Разновидности конфигураций Spring:
Annotation-based
XML-based
Groovy-based
Мы будем рассматривать только annotation-based, как наиболее употребимую в настоящее время и практичную.
В огромном количестве тьюториалов (и проектов) ещё встречается XML-конфигурация.
Classpath Scanning: ищем проаннотированные классы в заданных пакетах.
@Component
@Service
@Controller
@Repository
Фабричные методы
@Configuration → @Bean
@ComponentScan("edu.phystech.robotlecturer")
Атрибуты аннотации:
String[] basePackages
— базовые пакеты для сканирования в поисках аннотированных компонент.
Class<?>[] basePackageClasses
— как типобезопасная альтернатива, можно указать классы. Пакеты каждого из указанных классов будут просканированы.
Плюсы: удобно.
Минусы: классы должны быть проаннторированы как @Component
, @Service
и т. п., что не всегда возможно для сторонних библиотек.
@Configuration
classКласс конфигурации должен быть либо явно указан через конструктор AnnotationConfigApplicationContext
, либо доступен через сканирование пакетов (тогда нужно аннотировать класс как @Configuration
).
Чтобы прописывать в классе бины, надо пользоваться @Bean
.
@Configuration
class — пример@Configuration
public class AppConfig{
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public Color randomColor(){
ThreadLocalRandom random = ThreadLocalRandom.current();
return new Color(random.nextInt(256),
random.nextInt(256), random.nextInt(256));
}
...
}
SCOPE_SINGLETON
— по умолчанию. Создаётся один при первом запросе и всюду впрыскивается единственный экземпляр.
SCOPE_PROTOTYPE
— создаётся новый при каждом запросе.
Есть ещё всякие, и можно создавать свои.
По умолчанию все синглетоны создаются при поднятии контейнера (чтобы fail fast, и чтобы избежать задержек при работе приложения).
Для конкретного бина это поведение можно изменить при помощи аннотации @Lazy
(см. документацию).
Ленивая инициализация — не такая хорошая идея, как кажется на первый взгляд.
Каждый бин получает имя (id).
По умолчанию, имя вычисляется из имени класса (SpeakerImpl
→ "speakerImpl"
) или фабричного метода (getSpeaker
→ "speaker"
).
Имя бина можно задать явно в параметре аннотации @Component
и других (например: @Service("bestSpeaker")
).
Constructor
Setter
Field
Lookup method
@Component
@RequiredArgsConstructor
public class RobotLecturer {
//автоматически будут переданы в конструктор
private final Lecture lecture;
private final Speaker speaker;
private final SlideShow slideShow;
Внешне может быть незаметен (особенно с Lombok).
Хорош для создания иммутабельных объектов.
Много параметров конструктора? — А точно столько надо?
@Autowired
void setLecture(Lecture lecture) {
//сеттер будет автоматически вызван после конструирования
this.lecture = lecture;
}
Хорош в ситуации, когда компонента сама себе способна предоставить зависимость "по умолчанию".
@Component
public class RobotLecturer {
//будут установлены через рефлексию после конструирования
@Autowired
private Lecture lecture;
@Autowired
private Speaker speaker;
@Autowired
private SlideShow slideShow;
Не плодит в классе сеттеры и конструкторы, но в целом сильно связывает код и считается не очень удачной практикой.
Хотя, в классах тестов — это ровно то, что нужно.
Проблема бинов с разным жизненным циклом: SCOPE_PROTOTYPE
не спасает.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Bar {...}
@Component
public class Foo {
@Autowired
private Bar bar;
public void bar(){
//заинжектированный экземпляр bar всегда один и тот же
}
}
@Component
public abstract class Foo {
@Lookup
abstract Bar getBar();
public void bar(){
Bar b = getBar();
//теперь в b будет всякий раз новое (ну или одно и то же,
//если убрать SCOPE_PROTOTYPE, хотя где тогда смысл?)
}
}
Foo foo = ctx.getBean(Foo.class);
foo.bar(); foo.bar(); ...
— Как, мы инстанцируем абстрактный класс?! — Нет, мы же инстанцируем обёртку, на самом деле.
@Component
public class Foo {
//главное -- чтобы не был приватным
@Lookup
Bar getBar(){
return null;
};
public void bar(){
//не null!
Bar b = getBar();
...
}
}