JDBCTemplate в контроллере Spring

Мне нужно сделать некоторые отчеты, которые не являются строго списком сущностей. Запрос в более свободной форме, жестко закодированный SQL, возвращаемый как произвольный JSON.

Я пытаюсь использовать для этого JdbcTemplate и использовать код, который я нашел в Интернете (например, здесь), чтобы построить это.

Все это находится внутри класса ReportViewController.

Мой код (с ведением журнала/отладкой, который будет актуален позже):

private JdbcTemplate jdbcTemplate;
private DataSource dataSource;
private UUID uuid;


public ReportViewController() {
    this.uuid = UUID.randomUUID();
}


public void setDataSource(DataSource dataSource) {
    System.out.println("Setting data source");
    this.dataSource = dataSource;
    if (this.dataSource == null) {
        System.out.println("failed to set my dataSource properly");
    }
    System.out.println("uuid: " + this.uuid);
}

@RequestMapping(value = "/report/{reportId}", method = RequestMethod.POST)
@ResponseBody
public String runNewUntypedSearch(@PathVariable Integer reportId, WebRequest request, HttpSession session) {
    //View v = View.findView(reportId); 

    System.out.println("uuid: " + this.uuid);
    if (this.dataSource == null) {
        System.out.println("lost my dataSource somewhere along the way");
    }

    this.jdbcTemplate = new JdbcTemplate(this.dataSource);

    List<Map<String, Object>> l = null;
    try {
        l = this.jdbcTemplate.queryForList("select 1 as one");

        JSONSerializer s = new JSONSerializer();
        return s.serialize(l);

    } catch (Exception e) {
        System.out.println("querying failed -- " + e.toString());
        return "";
    }
} 

Моя проблема заключалась в том, что, несмотря на то, что консоль указывает, что она вызывает setDataSource(), когда я запускаю NewUntypedSearch(), эти свойства равны нулю.

Итак, подозревая нечестную игру с точки зрения объектов, с которыми я разговариваю, я заставляю этот контроллер создавать UUID в своем конструкторе и помещать его как часть своего журнала, как вы видите выше.

Вывод консоли частично показывает:

Setting data source
hash: bc679ef1-fbdc-4ef9-9457-dd79889f973e
...
hash: bbe2b03a-92ff-4517-add8-8d9ff6480cc8
lost my dataSource somewhere along the way

... и затем Spring отвечает браузеру "внутренней ошибкой" и трассировкой стека, которая указывает на нулевой указатель, где я пытаюсь создать экземпляр своего jdbcTemplate.

Это приводит меня к выводу, что любой объект, которому велят установить свой dataSource, не является тем же экземпляром, который подключается в качестве метода сопоставления URL. «Настоящий» экземпляр, на котором работает сервер, не вызывал getDataSource().

Может ли это быть? Почему это не работает?

РЕДАКТИРОВАТЬ:

web.xml (отредактировано для удаления имени моего клиента):

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.4" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">


    <display-name>myProjectName</display-name>

    <description>Roo generated application</description>


    <!-- Enable escaping of form submission contents -->
    <context-param>
        <param-name>defaultHtmlEscape</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:META-INF/spring/applicationContext*.xml</param-value>
    </context-param>

    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>



    <filter>
        <filter-name>HttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>



    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter-mapping>
        <filter-name>HttpMethodFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>



    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Creates the Spring Container shared by all Servlets and Filters -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Handles Spring requests -->
    <servlet>
        <servlet-name>myProjectName</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring/webmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>myProjectName</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>90</session-timeout>
    </session-config>

    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/uncaughtException</location>
    </error-page>

    <error-page>
        <error-code>404</error-code>
        <location>/resourceNotFound</location>
    </error-page>
</web-app>

applicationContext.xml (отредактировано для удаления имени клиента и комментария, созданного Roo)

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd         http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd         http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">


    <context:property-placeholder location="classpath*:META-INF/spring/*.properties"/>


    <context:spring-configured/>


    <context:component-scan base-package="com.myProjectName">
        <context:exclude-filter expression=".*_Roo_.*" type="regex"/>
        <context:exclude-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
    </context:component-scan>


    <bean class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" id="dataSource">
        <property name="driverClassName" value="${database.driverClassName}"/>
        <property name="url" value="${database.url}"/>
        <property name="username" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
        <property name="testOnBorrow" value="true"/>
        <property name="testOnReturn" value="true"/>
        <property name="testWhileIdle" value="true"/>
        <property name="timeBetweenEvictionRunsMillis" value="1800000"/>
        <property name="numTestsPerEvictionRun" value="3"/>
        <property name="minEvictableIdleTimeMillis" value="1800000"/>
        <property name="validationQuery" value="SELECT 1"/>
    </bean>
    <bean class="org.springframework.orm.jpa.JpaTransactionManager" id="transactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <tx:annotation-driven mode="aspectj" transaction-manager="transactionManager"/>
    <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory">
        <property name="persistenceUnitName" value="persistenceUnit"/>
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <bean class="org.springframework.mail.javamail.JavaMailSenderImpl" id="mailSender">
        <property name="host" value="${email.host}"/>
    </bean>
    <bean class="org.springframework.mail.SimpleMailMessage" id="templateMessage">
        <property name="from" value="${email.from}"/>
        <property name="subject" value="${email.subject}"/>
    </bean>
</beans>

webmvc-config.xml (отредактировано, чтобы удалить имя моего клиента):

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd     http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

    <!-- The controllers are autodetected POJOs labeled with the @Controller annotation. -->
    <context:component-scan base-package="com.myProjectName" use-default-filters="false">
        <context:include-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
    </context:component-scan>


    <!-- Turns on support for mapping requests to Spring MVC @Controller methods
         Also registers default Formatters and Validators for use across all @Controllers -->
    <mvc:annotation-driven conversion-service="applicationConversionService"/>

    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources -->
    <mvc:resources location="/, classpath:/META-INF/web-resources/" mapping="/resources/**"/>

    <!-- Allows for mapping the DispatcherServlet to "/" by forwarding static resource requests to the container's default Servlet -->
    <mvc:default-servlet-handler/>

    <!-- register "global" interceptor beans to apply to all registered HandlerMappings -->
    <mvc:interceptors>
        <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
        <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" p:paramName="lang"/>
    </mvc:interceptors>

    <!-- selects a static view for rendering without the need for an explicit controller -->
    <mvc:view-controller path="/login"/>
    <mvc:view-controller path="/admin" view-name="admin"/>
    <mvc:view-controller path="/" view-name="index"/>
    <mvc:view-controller path="/uncaughtException"/>
    <mvc:view-controller path="/resourceNotFound"/>
    <mvc:view-controller path="/dataAccessFailure"/>

    <!-- Resolves localized messages*.properties and application.properties files in the application to allow for internationalization. 
        The messages*.properties files translate Roo generated messages which are part of the admin interface, the application.properties
        resource bundle localizes all application specific messages such as entity names and menu items. -->
    <bean class="org.springframework.context.support.ReloadableResourceBundleMessageSource" id="messageSource" p:basenames="WEB-INF/i18n/messages,WEB-INF/i18n/application" p:fallbackToSystemLocale="false"/>

    <!-- store preferred language configuration in a cookie -->
    <bean class="org.springframework.web.servlet.i18n.CookieLocaleResolver" id="localeResolver" p:cookieName="locale"/> 

    <!-- resolves localized <theme_name>.properties files in the classpath to allow for theme support -->
    <bean class="org.springframework.ui.context.support.ResourceBundleThemeSource" id="themeSource"/>

    <!-- store preferred theme configuration in a cookie -->
    <bean class="org.springframework.web.servlet.theme.CookieThemeResolver" id="themeResolver" p:cookieName="theme" p:defaultThemeName="standard"/>

    <!-- This bean resolves specific types of exceptions to corresponding logical - view names for error views. 
         The default behaviour of DispatcherServlet - is to propagate all exceptions to the servlet container: 
         this will happen - here with all other types of exceptions. -->
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver" p:defaultErrorView="uncaughtException">
        <property name="exceptionMappings">
            <props>
                <prop key=".DataAccessException">dataAccessFailure</prop>
                <prop key=".NoSuchRequestHandlingMethodException">resourceNotFound</prop>
                <prop key=".TypeMismatchException">resourceNotFound</prop>
                <prop key=".MissingServletRequestParameterException">resourceNotFound</prop>
            </props>
        </property>
    </bean>

    <!-- allows for integration of file upload functionality -->
    <bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver"/>
  <bean class="com.mavenmanagement.web.ApplicationConversionServiceFactoryBean" id="applicationConversionService"/> 
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver" id="tilesViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.tiles2.TilesView"/>
  </bean>
    <bean class="org.springframework.web.servlet.view.tiles2.TilesConfigurer" id="tilesConfigurer">
    <property name="definitions">
      <list>
        <value>/WEB-INF/layouts/layouts.xml</value>
        <!-- Scan views directory for Tiles configurations -->
        <value>/WEB-INF/views/**/views.xml</value>
      </list>
    </property>
  </bean>
</beans>

Я вижу, что фильтрация при сканировании аннотаций блокирует @Controllers на уровне applicationContext и включает их на уровне webmvc-config. Так что, я думаю, это не объясняет двойную реализацию.


person Dan Ray    schedule 27.01.2012    source источник
comment
Использование JdbcTemplate из вашего веб-слоя — это очень плохо. Прочтите: en.wikipedia.org/wiki/Разделение_концернов   -  person Sean Patrick Floyd    schedule 27.01.2012
comment
Прекратите называть переменные списка l !   -  person Xorty    schedule 27.01.2012
comment
В стороне: вы можете ввести JdbcTemplates напрямую вместо DataSources. Я думаю, что было бы даже безопасно делиться ими между потоками, но если нет, то область прототипа позаботится об этом.   -  person millimoose    schedule 27.01.2012
comment
@Inerdial, да, вы можете вводить JdbcTemplate напрямую, и это потокобезопасно. Нет, вам не нужен prototype прицел, который совсем не поможет.   -  person Tomasz Nurkiewicz    schedule 27.01.2012
comment
Я попытался внедрить JdbcTemplate в свой метод, и, похоже, это сработало, но запрос завершился неудачно с исключением: не указан источник данных. Так что я не думаю, что это все мое решение.   -  person Dan Ray    schedule 31.01.2012


Ответы (2)


Прежде всего - в приложении Spring MVC есть два контекста приложения - основной, загружаемый через web.xml и ContextLoaderListener, и дочерний контекст, созданный каждым DispatcherServlet (обычно один).

Ваш компонент контроллера создается в обоих контекстах, вы устанавливаете источник данных в контроллере, созданном в основном контексте, но Spring MVC использует тот, который создан в дочернем контексте.

Очевидно, у вас должна быть только одна копия в дочернем контексте.

  • Если вы используете старую XML-конфигурацию, определение bean-компонента контроллера должно быть помещено в контекст *-servlet.xml, а не в applicationContext.xml (если вы придерживаетесь именования по умолчанию).

  • Если вы используете сканирование компонентов, убедитесь, что основной контекст не сканирует пакеты или классы контроллера, аннотированные @Controller.

Если вы уверены, что создается только один экземпляр, он должен работать. Обратите внимание, что контроллер может получить доступ к DataSource (дочерний контекст может получить доступ к основному контексту), но не наоборот.

Примечание 1

В подобных случаях достаточно напечатать:

System.out.println(this);

В контроллере. Значение по умолчанию toString() должно давать разные результаты для разных экземпляров.

Заметка 2

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

person Tomasz Nurkiewicz    schedule 27.01.2012
comment
Я вам скажу, это мой первый Java-проект, я создал его с помощью Roo, и я действительно не очень понимаю, что происходит в файлах конфигурации XML. Но я думаю, ясно, что этот объект (и, возможно, все приложение? Yikes.) создается дважды. Я отредактирую свой вопрос, включив в него файлы конфигурации, не могли бы вы взглянуть? - person Dan Ray; 31.01.2012
comment
Кроме того, Ру не дал мне файл *-servlet.xml. Он создал файлы web.xml и applicationContext.xml. - person Dan Ray; 31.01.2012
comment
О, я только что обнаружил файл webmvc-config.xml... И он настраивает сканирование аннотаций... - person Dan Ray; 31.01.2012

Подсказки от @Tomasz Nurkiewicz и @Inerdial в комментариях к вопросу заставили меня думать совершенно по-другому, то есть намного проще и блестяще работает, и все же я нигде не видел, чтобы это было задокументировано.

В нижней части applicationContext.xml я создаю экземпляр объекта JdbcTemplate:

   <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> 
    <property name="dataSource" ref="dataSource"/> 
   </bean>

Затем я просто @AutoWire подключаю его к контроллеру, где он мне нужен:

@Autowired
private JdbcTemplate jdbcTemplate;

И твой дядя Боба, теперь я могу поговорить с полностью настроенным объектом шаблона в this.jdbcTemplate.

Почему этот класс контроллера создавался дважды, потому что я запускал его из applicationContext.xml, чтобы попытаться настроить его источник данных. Затем версия, созданная аннотацией @Controller, была создана, но не настроена. Так что проблема была там.

Я также последовал другому совету в комментариях и переместил эту функцию запроса в класс типа модели и вызвал ее из моего контроллера представления. Да, очевидно, что взаимодействие с БД в контроллере представления — это мерзость.

person Dan Ray    schedule 31.01.2012
comment
На самом деле это задокументировано в документации Spring как лучшая практика: [docs.spring.io/spring/docs/4.0.0.RC1/spring-framework-reference/ - person mcanti; 05.01.2014