Комментарии 23
Возник вопрос — а зачем?
Вы используете спринг. Можно создавать бины через Spring Java Configuration, то есть напрямую, через параметры конструктора.
Код получится чище и понятней, без reflection. Все равно конфигурацию часто выносят в properties, и тут спринг справится и без велосипеда.
Вы используете спринг. Можно создавать бины через Spring Java Configuration, то есть напрямую, через параметры конструктора.
Код получится чище и понятней, без reflection. Все равно конфигурацию часто выносят в properties, и тут спринг справится и без велосипеда.
В статье приводится пример как можно совмещать два варианта инжекции свойств: с помощью аннотирования классов и с помощью извлечения свойств, например, из спрингового Environment.
Да, бины можно создавать по-разному. Инжектировать свойства тоже можно по-разному. Я привожу еще раз преимущества аннотаций:
Если у вас есть, скажем, абстрактный сервис
а для ServiceB queueType по дефолту неплохо было бы задать как ARRAY_BLOCKING_QUEUE и размер ограничить 1000 элементами (queueSize = 1000), то написав
вы получаете возможность конфигурировать дефолтные проперти декларативным способом.
Кроме того, у конфигураций с использованием аннотаций имеется еще один весьма серьезный плюс: все свойства являются или примитивами, или инстансами энамов, или аннотациями или массивами из всех вышеперечисленных. Иногда бывает, что разработчики, когда делают билдер свойств для какого-то бина, применяют комплексные объекты в качестве свойств, что противоречит здравому смыслу:
Например, вот такой билдер
плох только потому, что содержит ссылку на весьма сложный объект с состоянием. Такой контейнер свойств нельзя сериализовать.
Вариант с применением аннотаций дает полную гарантию, что вы не получите несериализуемого объекта в принципе.
Чем, на мой взгляд, плох вариант «протягивания» свойств в бины через параметры конструктора:
1. У мало-мальски реального сервиса много параметров, поэтому конструктор получается громоздкий: желание избавить пользователя от вызова конструктора с большим числом параметров приводит к написанию кучи перегруженных конструкторов, что само по себе уже — недостаток в архитектуре.
2. Если у конструктора большое число параметров, то создавать бины приходится примерно так (я привожу пример с Spring):
Это красиво?
Добавьте новое свойство, поменяйте их порядок и вы получите изменение во всех классах конфигураций, в которых создается этот бин.
Конечно, вы можете возразить: давайте напишем свой контейнер. Однако написание своего контейнера — это проблема с тем, что я уже озвучивал выше: кому-то может прийти в голову использовать там тяжеловесные объекты.
Теперь что касается рефлекшена: речь идет о том, что данный код будет помещен в библиотеку, покроется тестами и его можно использовать без каких-либо проблем. Загляните вглубь Spring и вы увидите не только рефлешн, но вещи и похуже, вроде модификаций байт-кода.
Можно создавать бины через Spring Java Configuration, то есть напрямую, через параметры конструктора.
Да, бины можно создавать по-разному. Инжектировать свойства тоже можно по-разному. Я привожу еще раз преимущества аннотаций:
Если у вас есть, скажем, абстрактный сервис
@A(queueType = LINKED_BLOCKING_QUEUE)
public class ServiceA {
}
а для ServiceB queueType по дефолту неплохо было бы задать как ARRAY_BLOCKING_QUEUE и размер ограничить 1000 элементами (queueSize = 1000), то написав
@A(queueType=ARRAY_BLOCKING_QUEUE, queueSize=1000)
public class ServiceB extends ServiceA {
}
вы получаете возможность конфигурировать дефолтные проперти декларативным способом.
Кроме того, у конфигураций с использованием аннотаций имеется еще один весьма серьезный плюс: все свойства являются или примитивами, или инстансами энамов, или аннотациями или массивами из всех вышеперечисленных. Иногда бывает, что разработчики, когда делают билдер свойств для какого-то бина, применяют комплексные объекты в качестве свойств, что противоречит здравому смыслу:
Например, вот такой билдер
public class MyBeanParametersBuilder {
int x;
int y;
ThreadPoolExecutor executor;
public MyBeanParametersBuilder setX(int x) {
this.x = x;
return this;
}
public MyBeanParametersBuilder setY(int y) {
this.y = y;
return this;
}
public MyBeanParametersBuilder setExecutor(ThreadPoolExecutor) {
this.executor = executor;
return this;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public ThreadPoolExecutor getExecutor() {
return executor;
}
}
плох только потому, что содержит ссылку на весьма сложный объект с состоянием. Такой контейнер свойств нельзя сериализовать.
Вариант с применением аннотаций дает полную гарантию, что вы не получите несериализуемого объекта в принципе.
Чем, на мой взгляд, плох вариант «протягивания» свойств в бины через параметры конструктора:
1. У мало-мальски реального сервиса много параметров, поэтому конструктор получается громоздкий: желание избавить пользователя от вызова конструктора с большим числом параметров приводит к написанию кучи перегруженных конструкторов, что само по себе уже — недостаток в архитектуре.
2. Если у конструктора большое число параметров, то создавать бины приходится примерно так (я привожу пример с Spring):
@Configuration
public class MyConfiguration {
@Value("${myService.property1}")
int property1;
@Value("${myService.property2}")
String property2;
// very, very long list of properties
@Bean
public MyService myService() {
return new MyService(
property1,
property2,
property3,
property4,
property5,
property6,
property7,
...,
property24
);
}
}
Это красиво?
Добавьте новое свойство, поменяйте их порядок и вы получите изменение во всех классах конфигураций, в которых создается этот бин.
Конечно, вы можете возразить: давайте напишем свой контейнер. Однако написание своего контейнера — это проблема с тем, что я уже озвучивал выше: кому-то может прийти в голову использовать там тяжеловесные объекты.
Теперь что касается рефлекшена: речь идет о том, что данный код будет помещен в библиотеку, покроется тестами и его можно использовать без каких-либо проблем. Загляните вглубь Spring и вы увидите не только рефлешн, но вещи и похуже, вроде модификаций байт-кода.
В простых случаях конструктор с 1-2 параметрами вполне подойдет. Для сложных случаях давным-давно придумали наследование определений docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/beans.html#beans-child-bean-definitions
Если XML принципиально не хотите, то вот stackoverflow.com/questions/23266175/bean-definition-inheritance-with-annotations
Если XML принципиально не хотите, то вот stackoverflow.com/questions/23266175/bean-definition-inheritance-with-annotations
Для сложных случаях давным-давно придумали наследование определений docs.spring.io/spring-framework/docs/current/spring-framework-reference/html/beans.html#beans-child-bean-definitions
Может быть я неясно выразился в предыдущем ответе: речь здесь не о конфигурировании бинов на уровне какого-либо IoC. Речь здесь о том, как с помощью декларативного подхода с помощью аннотаций позволять задавать дефолты для контейнера свойств, соблюдать принцип «свойство сервиса — это не сложный объект».
В описываемых мной случаях вы прямо в коде сервиса видите что наследник от другого сервиса по-умолчанию использует ArrayBlockingQueue. В вашем случае это задается где-то совершенно наверху, а не там, где это должен увидеть программист: непосредственно в классе бина.
Еще раз повторюсь: речь не о Spring и не о том, как можно с помощью Spring «втянуть» свойства.
Если XML принципиально не хотите, то вот stackoverflow.com/questions/23266175/bean-definition-inheritance-with-annotations
Второй случай вы привели ну совершенно не прочитав мой пост: я писал о финальных полях бина. Вы же приводите пример когда в бин сетается свойство после его создания. Разве это одно и то же?
Речь здесь о том, как с помощью декларативного подхода с помощью аннотаций позволять задавать дефолты для контейнера свойств
Почему бы не использовать стандартные методы спринга? Вы же используете спринг в ваших примерах.
соблюдать принцип «свойство сервиса — это не сложный объект».
У вас этого как раз и нет. Ваши сервисы знают о выбранном способе параметризации (DemoService<P extends CommonServiceParams & DemoServiceParams> extends AbstractService).
там, где это должен увидеть программист: непосредственно в классе бина
Если прямо в классе нужно использовать ArrayBlockingQueue (например, это критично для конкретно этой реализации), то пусть оно и будет прямо в коде. Если же нет, то ему там делать нечего — конфигурация будет происходить вне кода класса, а сам класс должен использовать интерфейсы.
финальных полях бина
Я бы задался вопросом — а нужны ли финальные поля? Сервисы создаются 1 раз при старте контейнера.
Spring-way в данном случае — это сеттеры.
Почему бы не использовать стандартные методы спринга? Вы же используете спринг в ваших примерах.
Я третий раз говорю одно и то же: статья не о том, как Spring позволяет инжектировать свойства в бины.
У вас этого как раз и нет. Ваши сервисы знают о выбранном способе параметризации (DemoService<P extends CommonServiceParams & DemoServiceParams> extends AbstractService).
P — это не свойство, а тип контейнера свойств. Получив в конструкторе в качестве единственного параметра этот контейнер, класс просетывает финальные поля для свойств.
Если прямо в классе нужно использовать ArrayBlockingQueue (например, это критично для конкретно этой реализации), то пусть оно и будет прямо в коде. Если же нет, то ему там делать нечего — конфигурация будет происходить вне кода класса, а сам класс должен использовать интерфейсы.
Зачем в коде задавать явно ArrayBlockingQueue? Речь идет о том, чтобы пользователь имел свободу выбора. В классе потомка задется свойство, которое (все еще) можно переопределить.
Я бы задался вопросом — а нужны ли финальные поля? Сервисы создаются 1 раз при старте контейнера.
Spring-way в данном случае — это сеттеры.
Если вы не видите разницы между финальным полем и не понимаете в чем преимущество использования финальных полей для хранения настроек сервиса, когда куча потоков обращается к их значениям очень и очень часто, то не вижу смысла вести бессмысленную дискуссию «финальное поле/не финальное поле».
Я третий раз говорю одно и то же: статья не о том, как Spring позволяет инжектировать свойства в бины.
Я вам в третий раз говорю — какую бизнес-цель вы решаете? Если сделать удобной конфигурацию различных сервисов, то тут можно поспорить о реализации через аннотации.
P — это не свойство, а тип контейнера свойств. Получив в конструкторе в качестве единственного параметра этот контейнер, класс просетывает финальные поля для свойств.
Вы вносите новую сущность, от которой зависят ваши сервисы. Если конфигурацию делать стандартными средствами спринга, то таких зависимостей нет.
Зачем в коде задавать явно ArrayBlockingQueue? Речь идет о том, чтобы пользователь имел свободу выбора. В классе потомка задется свойство, которое (все еще) можно переопределить.
Ваши аннотации установлены непосредственно на класс. Для изменения параметров нужно открыть код и поправить аннотацию. Точно так же можно открыть код и поправить list = new LinkedList() на list = new ArrayList()
Если вы не видите разницы между финальным полем и не понимаете в чем преимущество использования финальных полей для хранения настроек сервиса, когда куча потоков обращается к их значениям очень и очень часто, то не вижу смысла вести бессмысленную дискуссию «финальное поле/не финальное поле».
:) Я так понимаю, вы уже делали тестирование производительности и точно знаете, что в вашем случае финальные поля дают выигрыш X% по сравнению с нефинальными? Можно посмотреть цифры? ;)
Во многом согласен с аргументами, но вот тут, по-моему, довольно распространенная ошибка:
Дело совсем не в производительности, а в видимости. Почитайте про safe publication в Java Memory Model.
Я так понимаю, вы уже делали тестирование производительности и точно знаете, что в вашем случае финальные поля дают выигрыш X% по сравнению с нефинальными?
Дело совсем не в производительности, а в видимости. Почитайте про safe publication в Java Memory Model.
1) Spring инициализирует бины в одном потоке
2) A call to start() on a thread happens-before any actions in the started thread. docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5
Следовательно, если запускать потоки после создания контекста, то проблем с видимостью не будет.
2) A call to start() on a thread happens-before any actions in the started thread. docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.5
Следовательно, если запускать потоки после создания контекста, то проблем с видимостью не будет.
Ага, это правда. Но проблема в том, что не всегда есть контроль над потоками, в которых вызываются сервисы, поэтому возможность объявить поля final — это, действительно, очень хорошо с точки зрения видимости. Другими словами — это не всегда критично, но является очень приятным бонусом, если можно обойтись final полями.
По поводу сериализации билдеров.
Если так уж хочется, ну сделайте .withExecutor(EXECUTOR_FROM_ENUM) и не надо готовый объект передавать.
Создавайте его в .build()
Если так уж хочется, ну сделайте .withExecutor(EXECUTOR_FROM_ENUM) и не надо готовый объект передавать.
Создавайте его в .build()
Речь совершенно не об этом: создать как раз можно что угодно, если вы параметризуете бин неким произвольным объектом. Хорошо, если разработчик параметризующего контейнера понимает это. А если нет?
В моем случае вы вынуждаете его использовать только «хорошие» типы свойств и если на проекте есть подобная практика, то всегда есть внутренняя уверенность, что никто не будет передавать в свойства кэш объемом в 1 ГиБ, и ведь это хорошо, не так ли?
В моем случае вы вынуждаете его использовать только «хорошие» типы свойств и если на проекте есть подобная практика, то всегда есть внутренняя уверенность, что никто не будет передавать в свойства кэш объемом в 1 ГиБ, и ведь это хорошо, не так ли?
Так ведь никто не помешает разработчику задать левые свойства в property файле. Проверять параметры можно прямо в сеттерах либо в @PostConstruct через checkArgument или assert.
Опять же, в билдер не обязательно передавать готовый Executor или кэш. Пусть в билдере будет метод .withCacheSize() в котором и будет проверяться аргумент. Так работает Guava Cache, к примеру.
Опять же, в билдер не обязательно передавать готовый Executor или кэш. Пусть в билдере будет метод .withCacheSize() в котором и будет проверяться аргумент. Так работает Guava Cache, к примеру.
Как раз «левые» свойства в property в моем случае никто не сможет задать. И пример с Guava Cache по-моему совсем неудачный: попробуйте задать для него weigher через .properties. Вообще CacheBuilder, если уж на то пошло, как раз-таки и является примером плохого контейнера для сервиса. Но Guava кэш, к счастью, не сервис, а его строительный блок.
Все кажется немного запутанным. В Java есть объекты; их состояние описывается свойствами. Если надо создать объект с необходимыми свойствами — создайте его. Пользователь должен иметь возможность взять класс и создать нужный ему экземпляр множеством способов: через поля, через XML, через файл свойств. Если нужны неизменяемые значения — пусть будет final private объявленный в начале. Или abstract Config getConfg() который переопределят потомки. Аннотации — это прикольно, но они хорошо работают, когда их можно применят на любом классе и они универсальны для всех его экземпляров, а если у вас они применимы только к небольшой группе объектов, то проще воспользоваться наследованием.
Пользователь должен иметь возможность взять класс и создать нужный ему экземпляр множеством способов: через поля, через XML, через файл свойств.
Для этого в рассматриваемом случае есть конструктор с параметром: bounds у парараметра — только интерфейсы, поэтому никто не мешает написать свой контейнер, имплементирующий их. И пусть он берет данные из XML, из .properties, из БД и вообще откуда угодно.
Аннотации здесь нужны для:
- Возможности задать дефолты для каждого класса потомка, если это требуется: аннотация, появляющаяся на классе-потомке говорит программисту какие будут дефолты еще до того, как он будет использовать какой-нибудь IoC, прямо в библиотеке
- Типы свойств будут всегда простыми объектами или массивами их: это гарантируют интерфейсы аннотаций
- Возможности создавать конфигурацию бинов например на Groovy
Последний случай выглядел бы примерно так:
@CommonServiceParameters(threadCount=8)
@HttpServerParameters(host = "0.0.0.0", port = 80)
class HttpServer extends EmbeddedHttpServer {
}
@CommonServiceParameters(threadCount = 4)
@DataStorageParameters(dir = "/ssd/data", ttl = 1, timeUnit = DAYS)
class DataStorage extends EmbeddedDataStorage {
}
def conf = new Configuration(
new HttpServer(),
new DataStorage()
).start();
подразумевается, что Configuration знает как вызвать методы, помеченные @PostConstruct, @PreDestroy. Для этого требуется написать небольшую библиотеку.
Т.е. для простых приложений и для несложного IoC можно все сделать без фреймворков CDI, Spring, или Guice: только Groovy-файл с аннотациями.
Есть несколько вещей, которые мне в таком решении не нравятся.
- сервис работает напрямую с environment, это нехорошо, это лишняя связанность, которая может неприятными моментами всплыть например в тестах
- конфигурация «хранится» в аннотациях, на мой взгляд, это неудобно. Намного удобнее использовать для этого бины — они для этого как-то лучше предназначены
- если очень хочется делать дефолтные настройки для сервисов аннотациями, можно сделать еще дополнительные к бинам аннотации, которые просто будут транслироваться в имя->значение в environment, который в свою очередь будут писаться в бины тем же самым спингом
сервис работает напрямую с environment, это нехорошо, это лишняя связанность, которая может неприятными моментами всплыть например в тестах
Для тестов можно сделать TestBeanConfig implements P1, P2,… P3.
Моей целью было показать как можно конфигурировать бины не только с помощью аннотацииями, но и из произвольных контейнеров.
Учитывая что Environment — сущность, неразрывно связанная с контекстом docs.spring.io/spring/docs/current/javadoc-api/org/springframework/context/support/AbstractApplicationContext.html#getEnvironment--, а PropertySource-конфигурирующие бины должны быть статическими, я не вижу здесь никакой лишней связанности. Вот если бы был бы одного уровня с «бизнес»-бинами, тогда ваш агрумент имел бы смысл.
конфигурация «хранится» в аннотациях, на мой взгляд, это неудобно. Намного удобнее использовать для этого бины — они для этого как-то лучше предназначены
См. вышеприведенный пример на Groovy без использования какого-либо фреймворка с IoC.
Я уже писал об этом в комментариях, но напишу еще: преимущество аннотаций в том, что они
1) не дают конфигурировать бины сложными объектами
2) обеспечивают (в моем решении) то, что уже на стадии конструктора конфигурация задана и все поля сервиса можно сделать финальными
3) конфигурация всегда сериализуема (это обеспечивается первым пунктом)
4) можно задавать дефолты в том месте, где они должны появляться: на уровне классов-потомков, причем в декларативном виде, а не где-то в коде
5) можно строить контексты на скриптовых языках типа Groovy не прибегая к помощи Spring, CDI, Guice и т.д.
Для тестов можно сделать TestBeanConfig implements P1, P2,… P3
Не совсем понимаю, как это избавит от необходимости передавать в конструктор бина environment, в то время как бину нужны только несколько параметров возможно в виде бина или вообще передаваемые параметрами в конструктор? А аннотацию создать и передать в конструктор не так-то просто. Обычно, я избавляю сервисы от знаний о том, откуда берется конфигурация и считаю это хорошим тоном — говорить о связанности можно при любой попытке использовать в одном классе другой, но одно дело, если это будет специальный «локальный» бин (или может быть даже без него) и совсем другое, если это будет (ненужная в данном случае) спринговая абстракция.
не дают конфигурировать бины сложными объектами
Я понимаю этот аргумент, но, если честно, не уверен, что это хороший способ вводить ограничения. Ведь у аннотаций специфические ограничения, которые не всегда хорошо применимы к конфигурации. Если ограничения на параметры вроде сложности или сериализуемости критичны — всегда можно сделать валидацию, хотя в общем случае объекты приходящие из .properties не обладают особой сложностю, особенно если использовать спринговые механизмы для конфигурирования.
… все поля сервиса можно сделать финальными
Аналогичный результат, если в конструктор передается просто бин или набор нужных параметров.
можно задавать дефолты в том месте, где они должны появляться...
Опять же — я не говорил, что сама идея конфигурирования аннотациями плоха — я лишь выражаю опасения по поводу некоторых нюансов реализации. Дефолтные значения вполне можно задавать аннотациями — просто на мой взгляд не стоит об этом сервису ничего знать. По поводу последнего пункта тоже никаких противоречий с тем, что я написал — нет, даже проще, когда схема не завязана на Configurator который вызывает какие-то методы сервисов, а сервисы принимают простые и понятные параметры никак не связанные с тем откуда они пришли.
Хочу сказать еще следующее. Я отлично понимаю, что это хорошее решение, например, для корпоративного приложения, где есть некоторые задачи, которые этот подход хорошо решает, я сам не раз делал решения под конкретный продукт, потому что они бывает лучше подходят для конкретного места, но, мне кажется, что я бы не выбрал ваш подход как общее решение для своего проекта по вышеобозначенным причинам.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Конфигурирование бинов с помощью аннотаций: решение проблемы с отсутствием наследования для интерфейсов аннотаций