Статьи

Оптимизация вашего приложенияContext

Есть проблема с Spring, она была там некоторое время, и я сталкивался с ней в ряде проектов. Это не имеет ничего общего с Spring или с парнями в Spring, это зависит от таких пользователей Spring, как вы и я. Позвольте мне объяснить… В старые времена Spring 2 вам приходилось вручную настраивать свой контекст приложения, вручную создавая файл конфигурации XML, содержащий все определения вашего компонента. Недостатком этого метода было то, что создание этих XML-файлов занимало много времени, а затем у вас возникла головная боль при обслуживании этого все более сложного файла. Кажется, я помню, что в то время он назывался «Spring Config Hell». С другой стороны, по крайней мере, у вас была центральная запись всего, что было загружено в контекст. Склоняясь к требованию и популярному представлению о том, что аннотации являются @Service способом, Spring 3 представил целый ряд классов стереотипов, таких как @Service , @Component , @Controller и @Repository а также добавление в XML-файл конфигурации <context:component-scan/> element. С точки зрения программирования это значительно упростило задачу и стало чрезвычайно популярным способом построения контекстов Spring.

Однако есть и обратная сторона в использовании аннотаций Spring с @Component @Controller и @Repository всего с помощью @Service , @Component , @Controller или @Repository что особенно проблематично в больших кодовых @Repository . Проблема в том, что ваш контекст загрязняется вещами, которые просто не нужны, и это проблема, потому что:

  • Вы израсходуете ненужное пространство для перманентного использования, что приводит к риску возникновения «ошибок из-под перманентного пространства»
  • Вы излишне используете свое пространство кучи.
  • Ваше приложение может занять гораздо больше времени для загрузки.
  • Нежелательные объекты могут «просто делать вещи», особенно если они многопоточные, имеют метод start() или реализуют InitializingBean .
  • Нежелательные объекты могут просто остановить работу вашего приложения …

В небольших приложениях я думаю, что на самом деле не имеет значения, есть ли у вас пара дополнительных объектов в контексте Spring, но, как я уже говорил выше, это может быть особенно проблематично, если ваше приложение является большим, интенсивно использует процессор или требует много памяти. На этом этапе стоит разобраться в этой ситуации, и для этого вам необходимо сначала выяснить, что именно вы загружаете в контекст Spring.

Один из способов сделать это – просто включить отладку в пакете com.springsource , добавив что-то вроде следующего в ваши свойства log4j:

1
log4j.logger.com.springsource=DEBUG

Добавив вышеперечисленное к своим свойствам log4j (в данном случае log4j 1.x), вы получите много информации о вашем контексте Spring – и я имею в виду много. Это действительно только то, что вам нужно сделать, если вы один из ребят в Spring и работаете над исходным кодом Spring.

Еще один более краткий подход – добавить в приложение класс, который будет сообщать именно то, что загружается в контекст Spring. Затем вы можете просмотреть отчет и внести соответствующие изменения.

Пример кода этого блога состоит из одного класса, и это то, что я написал два или три раза раньше, работая над разными проектами для разных компаний. Он опирается на несколько функций Spring; а именно, что Spring может вызывать метод в вашем классе после загрузки контекста, а интерфейс ApplicationContext Spring содержит несколько методов, которые расскажут вам все о его внутренностях.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
@Service
public class ApplicationContextReport implements ApplicationContextAware, InitializingBean {
 
  private static final String LINE = "====================================================================================================\n";
  private static final Logger logger = LoggerFactory.getLogger("ContextReport");
 
  private ApplicationContext applicationContext;
 
  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.applicationContext = applicationContext;
  }
 
  @Override
  public void afterPropertiesSet() throws Exception {
    report();
  }
 
  public void report() {
 
    StringBuilder sb = new StringBuilder("\n" + LINE);
    sb.append("Application Context Report\n");
    sb.append(LINE);
 
    createHeader(sb);
    createBody(sb);
    sb.append(LINE);
 
    logger.info(sb.toString());
  }
 
  private void createHeader(StringBuilder sb) {
 
    addField(sb, "Application Name: ", applicationContext.getApplicationName());
    addField(sb, "Display Name: ", applicationContext.getDisplayName());
 
    String startupDate = getStartupDate(applicationContext.getStartupDate());
    addField(sb, "Start Date: ", startupDate);
 
    Environment env = applicationContext.getEnvironment();
    String[] activeProfiles = env.getActiveProfiles();
    if (activeProfiles.length > 0) {
      addField(sb, "Active Profiles: ", activeProfiles);
    }
  }
 
  private void addField(StringBuilder sb, String name, String... values) {
 
    sb.append(name);
    for (String val : values) {
      sb.append(val);
      sb.append(", ");
    }
    sb.setLength(sb.length() - 2);
    sb.append("\n");
  }
 
  private String getStartupDate(long startupDate) {
 
    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    return df.format(new Date(startupDate));
  }
 
  private void createBody(StringBuilder sb) {
    addColumnHeaders(sb);
    addColumnValues(sb);
  }
 
  private void addColumnHeaders(StringBuilder sb) {
    sb.append("\nBean Name\tSimple Name\tSingleton\tFull Class Name\n");
    sb.append(LINE);
  }
 
  private void addColumnValues(StringBuilder sb) {
    String[] beanNames = applicationContext.getBeanDefinitionNames();
 
    for (String name : beanNames) {
      addRow(name, sb);
    }
  }
 
  private void addRow(String name, StringBuilder sb) {
    Object obj = applicationContext.getBean(name);
 
    String fullClassName = obj.getClass().getName();
    if (!fullClassName.contains("org.springframework")) {
 
      sb.append(name);
      sb.append("\t");
      String simpleName = obj.getClass().getSimpleName();
      sb.append(simpleName);
      sb.append("\t");
      boolean singleton = applicationContext.isSingleton(name);
      sb.append(singleton ? "YES" : "NO");
      sb.append("\t");
      sb.append(fullClassName);
      sb.append("\n");
    }
  }
}

Первое, что следует отметить, – эта версия кода реализует интерфейс Spring InitializingBean . Spring будет проверять этот интерфейс, когда загружает класс в контекст. Если он найдет его, он вызовет метод AfterPropertiesSet() .

Это не единственный способ заставить Spring вызывать ваш класс при запуске, см. « Три @PostConstruct жизненного цикла Spring Bean» и использование аннотации @PostConstruct JSR-250 для @PostConstruct InitializingBean

Следующее, что следует отметить, это то, что этот класс отчетов реализует интерфейс ApplicationContextAware Spring. Это еще один полезный интерфейс рабочей лошадки Spring, который обычно не требуется использовать ежедневно. Смысл этого интерфейса в том, чтобы предоставить вашему классу доступ к ApplicationContext вашего приложения. Он содержит единственный метод: setApplicationContext(...) , который вызывается Spring для внедрения ApplicationContext в ваш класс. В этом случае я просто сохраняю аргумент ApplicationContext в качестве переменной экземпляра.

Генерация основного отчета выполняется методом report() (вызываемым afterPropertiesSet() ). Все, что делает метод report() – это создает класс StringBuilder() а затем добавляет много информации. Я не буду подробно останавливаться на каждой строке, так как этот код довольно линейный и очень скучный. addColumnValues(...) происходит в форме метода addColumnValues(...) вызываемого createBody(...) .

1
2
3
4
5
6
7
  private void addColumnValues(StringBuilder sb) {
    String[] beanNames = applicationContext.getBeanDefinitionNames();
 
    for (String name : beanNames) {
      addRow(name, sb);
    }
  }

Этот метод вызывает applicationContext.getBeanDefinitionNames() чтобы получить массив, содержащий имена всех bean-компонентов, загруженных этим контекстом. Получив эту информацию, я перебираю массив, вызывая applicationContext.getBean(...) для каждого имени компонента. Получив сам компонент, вы можете добавить сведения о его классе в StringBuilder в виде строки в отчете.

Создав отчет, нет смысла писать собственный код для обработки файлов, который сохранит содержимое StringBuilder на диск. Этот вид кода был написан много раз прежде. В этом случае я решил использовать Log4j (через slf4j), добавив строку этого логгера в код Java выше:

1
  private static final Logger logger = LoggerFactory.getLogger("ContextReport");

… И добавив следующее в мой XML-файл log4j:

01
02
03
04
05
06
07
08
09
10
11
12
<appender name="fileAppender" class="org.apache.log4j.RollingFileAppender">
      <param name="Threshold" value="INFO" />
      <param name="File" value="/tmp/report.log"/>
      <layout class="org.apache.log4j.PatternLayout">
         <param name="ConversionPattern" value="%d %-5p  [%c{1}] %m %n" />
      </layout>
   </appender>
 
    <logger name="ContextReport" additivity="false">
  <level value="info"/>   
       <appender-ref ref="fileAppender"/>
    </logger>

Обратите внимание, что если вы используете log4j 2.x, XML будет другим, но это выходит за рамки этого блога.

Здесь следует отметить, что я использую RollingFileAppender , который записывает файл в /tmp именем report.log – хотя этот файл, очевидно, может находиться где угодно.

Другая конфигурация, на которую следует обратить внимание – это ContextReport Logger. Это направляет весь вывод его журнала в fileAppender и, из-за атрибута fileAppender additivity="false" , только fileAppender и больше fileAppender .

Единственный другой фрагмент конфигурации, который нужно запомнить, – это добавить пакет report в component-scan Spring, чтобы Spring обнаружил аннотацию @Service и загрузил класс.

1
<context:component-scan base-package="com.captaindebug.report" />

Чтобы доказать, что это работает, я также создал тестовый пример JUnit, как показано ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml" })
public class ApplicationContextReportTest {
 
  @Autowired
  private ApplicationContextReport instance;
 
  @Test
  public void testReport() {
 
    System.out.println("The report should now be in /tmp");
  }
 
}

При этом используется SpringJUnit4ClassRunner и аннотация @ContextConfiguration для загрузки файла приложения в реальном времени servlet-context.xml . Я также включил аннотацию @WebAppConfiguration чтобы сообщить Spring, что это веб-приложение.

Если вы запустите тест JUnit, вы получите report.log который содержит что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
2014-01-26 18:30:25,920 INFO   [ContextReport]
====================================================================================================
Application Context Report
====================================================================================================
Application Name:
Display Name: org.springframework.web.context.support.GenericWebApplicationContext@74607cd0
Start Date: 2014-01-26T18:30:23.552+0000
 
Bean Name       Simple Name     Singleton       Full Class Name
====================================================================================================
deferredMatchUpdateController   DeferredMatchUpdateController   YES     com.captaindebug.longpoll.DeferredMatchUpdateController
homeController  HomeController  YES     com.captaindebug.longpoll.HomeController
DeferredService DeferredResultService   YES     com.captaindebug.longpoll.service.DeferredResultService
SimpleService   SimpleMatchUpdateService        YES     com.captaindebug.longpoll.service.SimpleMatchUpdateService
shutdownService ShutdownService YES     com.captaindebug.longpoll.shutdown.ShutdownService
simpleMatchUpdateController     SimpleMatchUpdateController     YES     com.captaindebug.longpoll.SimpleMatchUpdateController
applicationContextReport        ApplicationContextReport        YES     com.captaindebug.report.ApplicationContextReport
the-match       Match   YES     com.captaindebug.longpoll.source.Match
theQueue        LinkedBlockingQueue     YES     java.util.concurrent.LinkedBlockingQueue
BillSykes       MatchReporter   YES     com.captaindebug.longpoll.source.MatchReporter
====================================================================================================

Отчет содержит заголовок, который содержит такую ​​информацию, как Display Name и Start Date за которой следует основной текст. Тело – это таблица, разделенная табуляцией, которая содержит следующие столбцы: имя компонента, простое имя класса, независимо от того, является ли компонент одноэлементным или прототипным, и полное имя класса.

Теперь вы можете использовать этот отчет для определения классов, которые вы не хотите загружать в свой Spring Spring. Например, если вы решили, что не хотите загружать экземпляр com.captaindebug.longpoll.source.MatchReporter , то у вас есть следующие варианты.

Во-первых, это, вероятно, тот случай, BillSykes бин BillSykes загрузился, потому что он находится в неправильном пакете. Это обычно происходит, когда вы пытаетесь организовать структуры проекта вдоль линий типа класса, например, объединяя все службы в пакет service и все контроллеры в пакет controller ; следовательно, включение сервисного модуля в ваше приложение загрузит ВСЕ классы обслуживания, даже те, которые вам не нужны, и это может вызвать проблемы. Как правило, лучше организовать по функциональному принципу, как описано в разделе Как вы организовываете подмодули Maven? ,

К сожалению, реорганизация всего вашего проекта особенно затратна и не принесет большого дохода. Другой, более дешевый способ решения этой проблемы – внести коррективы в context:component-scan Spring context:component-scan Element context:component-scan Element и исключить те классы, которые вызывают проблемы.

1
2
3
<context:component-scan base-package="com.captaindebug.longpoll" />
        <context:exclude-filter type="regex" expression="com\.captaindebug\.longpoll\.source\.MatchReporter"/>
    </context:component-scan>

… или все классы из любого данного пакета:

1
2
3
<context:component-scan base-package="com.captaindebug.longpoll" />
        <context:exclude-filter type="regex" expression="com\.captaindebug\.longpoll\.source\..*"/>
    </context:component-scan>

Использование exclude-filter является полезным методом, но об этом много написано вместе с его аналогом: include-filter, и поэтому полное объяснение этой конфигурации XML выходит за рамки этого блога, хотя, возможно, я расскажу об этом в более поздняя дата.

  • Код для этого блога доступен на GitHub в рамках длинного опроса по адресу: https://github.com/roghughe/captaindebug/tree/master/long-poll

Ссылка: Оптимизация вашего ApplicationContext от нашего партнера JCG Роджера Хьюза в блоге Captain Debug’s Blog .