Статьи

Студент проекта: Веб-сервис Интеграция

Это часть проекта Студент . Другие посты: Клиент Webservice с Джерси , Сервер Webservice с Джерси , Бизнес-уровень , Постоянство с данными Spring и Тестирование интеграции данных Sharding .

Ранее мы успешно провели интеграционные тесты как для уровня персистентности / бизнес-уровня (с использованием встроенной базы данных H2), так и для уровня REST-сервера / клиента (с использованием сервера Jetty). Пора вязать все вместе.

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

Ограничения

  • Аутентификация пользователя — не было предпринято никаких усилий для аутентификации пользователей.
  • Шифрование — не было предпринято никаких усилий для шифрования сообщений.

Управляемый контейнером источник данных и JNDI

Управляемые контейнером источники данных имеют плохую репутацию для многих разработчиков, и я не знаю почему. Возможно, путаница с персистентностью, управляемой контейнером (CMP)?

В любом случае, идея источника данных, управляемого контейнером, проста. Вам не нужно выяснять, как поддерживать параметры подключения к базе данных в развернутой системе — не нужно изменять развернутое веб-приложение (что небезопасно) или читать файл из файловой системы (к которой вы, возможно, не сможете получить доступ) и т. Д. Вы просто передаёте проблему человеку, обслуживающему веб-сервер / сервер приложений, и получаете значение через JNDI.

Tomcat и Jetty требуют ручной настройки в XML-файле. Более продвинутые серверы приложений, такие как JBoss и GlassFish, позволяют вам настраивать источники данных через приятный графический интерфейс. Это не имеет большого значения, так как вы должны сделать это только один раз. Инструкция для Tomcat 7 и Jetty .

Ключевым моментом для Tomcat является то, что серверная библиотека находится в каталоге $ CATALINA_HOME / lib, и это может быть не там, где вы ожидаете. Например, в Ubuntu это / usr / share / tomcat7 / lib , а не / var / lib / tomcat7 / lib . Во-вторых, если файл META-INF / context.xml не извлечен из файла .war, вы должны поместить его в файл conf / Catalina / localhost / student-ws-webapp.xml (или как там у вас был назван файл .war. ). Последний переопределяет первый, поэтому обычно настраивают файл .war для запуска в среде разработки, а затем переопределяют конфигурацию в тестовой и рабочей средах.

META-INF / context.xml (Tomcat)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<Context>
 
    <Resource name="jdbc/studentDS"
        auth="Container"
        type="javax.sql.DataSource"
        driverClassName="org.postgresql.Driver"
        url="jdbc:postgresql:student"
        username="student"
        password="student"
        maxActive="20"
        maxIdle="10"
        maxWait="-1"
        factory="org.apache.commons.dbcp.BasicDataSourceFactory" />
 
</Context>

Стоит отметить, что передавать ключ шифрования через JNDI (как java.lang.String) тривиально. Это имеет те же преимущества, которые обсуждались ранее в отношении необходимости изменения развернутого веб-приложения или доступа к файловой системе сервера.

(Реализация немного сложнее, так как вы хотите, чтобы ваш фактический ключ шифрования требовал и ключа JNDI, и соли на основе файловой системы, но это легко сделать во время первоначального развертывания веб-приложения.)

Конфигурация JPA

Наш файл persistence.xml крайне минимален. Обычно нам нужны два постоянных модуля, один для транзакций JTA (производство) и один для транзакций не JTA (разработка и тестирование).

META-INF / persistence.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    version="1.0">
 
    <persistence-unit name="studentPU-local"
        transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <non-jta-data-source>jdbc/studentDS</non-jta-data-source>
    </persistence-unit>
</persistence>

Веб-конфигурация

Файл web.xml практически идентичен файлу, используемому при интеграционном тестировании. Основное отличие состоит в том, что он получает ссылку на ресурс для предоставленного контейнером источника данных.

WEB-INF / web.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
 
    <display-name>Project Student Webservice</display-name>
 
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </context-param>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            com.invariantproperties.sandbox.student.config.PersistenceJpaConfig
            com.invariantproperties.sandbox.student.config.BusinessApplicationContext
            com.invariantproperties.sandbox.student.webservice.config.RestApplicationContext
        </param-value>
    </context-param>
 
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
 
    <servlet>
        <servlet-name>REST dispatcher</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
        <init-param>
            <param-name>spring.profiles.active</param-name>
            <param-value>test</param-value>
        </init-param>
    </servlet>
 
    <servlet-mapping>
        <servlet-name>REST dispatcher</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
 
    <resource-ref>
        <description>Student Datasource</description>
        <res-ref-name>jdbc/studentDS</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
    </resource-ref>
</web-app>

Конфигурация пружины

Конфигурация Spring для персистентного уровня резко отличается от предыдущей из-за двух изменений. Стилистически мы можем использовать стандартный файл конфигурации вместо класса конфигурации, поскольку нам больше не нужно иметь дело со встроенной базой данных.

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

ApplicationContext-dao.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="UTF-8"?>
    xsi:schemaLocation="
 
 
 
 
 
 
 
 
 
 
 
    <context:property-placeholder location="WEB-INF/database.properties" />
 
    <context:annotation-config />
 
    <!-- we use container-based datasource -->
    <jee:jndi-lookup id="dataSource" jndi-name="${persistence.unit.dataSource}"
        expected-type="javax.sql.DataSource" />
 
    <bean name="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="persistenceUnitName" value="${persistence.unit.name}" />
        <property name="packagesToScan" value="${entitymanager.packages.to.scan}" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
    </bean>
 
    <bean name="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager" />
 
    <bean name="exceptionTranslation"
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
 
    <tx:annotation-driven transaction-manager="transactionManager"
        proxy-target-class="false" />
 
</beans>

Наш файл свойств содержит только имя источника данных, имя единицы сохраняемости и список сканируемых пакетов, которые содержат аннотации JPA.

WEB-INF / database.properties

1
2
3
4
# jpa configuration
entitymanager.packages.to.scan=com.invariantproperties.sandbox.student.domain
persistence.unit.dataSource=java:comp/env/jdbc/studentDS
persistence.unit.name=studentPU-local

Схема базы данных и безопасность

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

Важно написать и протестировать сценарии обновления и понижения, прежде чем приступить к работе. Нам всегда нужен способ изящного восстановления, если есть проблема.

Что еще более важно, наша схема базы данных всегда должна принадлежать другому пользователю, чем веб-приложение. Например, веб-приложение может использовать «студент-пользователь» для таблиц, созданных «студент-владелец». Это не позволит злоумышленнику, использующему инъекцию SQL, удалить или изменить таблицы.

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
--
-- for security this must run as student-owner, not student-user!
--
 
--
-- create an idempotent stored procedure that creates the initial database schema.
--
create or replace function create_schema_0_0_2() returns void as $$
declare
    schema_version_rec record;
    schema_count int;
begin
    create table if not exists schema_version (
        schema_version varchar(20) not null
    );
 
    select count(*) into schema_count from schema_version;
 
    case schema_count
        when 0 then
            -- we just created table
            insert into schema_version(schema_version) values('0.0.2');
        when 1 then
            -- this is 'create' so we only need to make sure it's current version
            -- normally we accept either current version or immediately prior version.
            select * into strict schema_version_rec from schema_version;
            if schema_version_rec.schema_version  '0.0.2' then
                raise notice 'Unwilling to run updates - check prior version';
                exit;
            end if;     
        else
            raise notice 'Bad database - more than one schema versions defined!';
            exit;
    end case;
 
    -- create tables!
 
    create table if not exists test_run (
        test_run_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        name varchar(80) not null,
        test_date timestamp not null,
        username varchar(40) not null
    );
 
    create table if not exists classroom (
        classroom_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    create table if not exists course (
        course_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    create table if not exists instructor (
        instructor_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null,
        email varchar(200) unique not null
    );
 
    create table if not exists section (
        section_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    create table if not exists student (
        student_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null,
        email varchar(200) unique not null
    );
 
    create table if not exists term (
        term_pkey int primary key,
        uuid varchar(40) unique not null,
        creation_date timestamp not null,
        test_run_pkey int references test_run(test_run_pkey),
        name varchar(80) not null
    );
 
    -- correction: need to define this!
    create sequence hibernate_sequence;
 
    -- make sure nobody can truncate our tables
    revoke truncate on classroom, course, instructor, section, student, term, test_run from public;
 
    -- grant CRUD privileges to student-user.
    grant select, insert, update, delete on classroom, course, instructor, section, student, term, test_run to student;
 
    grant usage on hibernate_sequence to student;
 
    return;
end;
$$ language plpgsql;
 
-- create database schema
select create_schema_0_0_2() is null;
 
-- clean up
drop function create_schema_0_0_2();

Интеграционное тестирование

Мы можем повторно использовать интеграционные тесты из службы веб-сервиса, но я не копировал их в этот проект, так как 1) я ненавижу дублирование кода, и было бы лучше перенести интеграционные тесты в отдельный проект, используемый и сервером, и веб-приложением, и 2) Jetty легко настроить для предоставления значений JNDI, но по какой-то причине документированный класс выдает исключение, и это не настолько важно (в настоящее время), чтобы тратить больше нескольких часов на исследование.

Исходный код

коррекция

Предоставленная схема игнорировала последовательность hibernate_sequence, необходимую для создания новых объектов. Он должен быть определен и читаем пользователем «студента».

Ссылка: студент проекта: интеграция Webservice от нашего партнера JCG Беара Джайлза в блоге Invariant Properties .