7 февраля 2012 г.

Проксирование запросов БД

Недавно меня попросили написать приложение, которое бы проксировало запросы к БД и возвращало результат выполнения этих запросов клиенту. Не знаю стоял ли за этой просьбой здравый смысл - возможно кто-то хотел скрыть от клиентских приложений параметры БД, а возможно просто не осилил работу с БД в своем языка (странно, да?). Но задача есть задача и вооружившись Java, Spring и Apache CXF за пару часов я набросал простенький веб-сервис для запросов. О том как я это сделал и будет данная статья.


Сначала в pom.xml пропишем нужные зависимости, а именно:
...
<properties>
    <cxf.version>2.4.6</cxf.version>
    <spring.version>3.1.0.RELEASE</spring-version>
    <dbcp.version>1.4</dbcp.version>
</properties>
...
<dependencies>
    <!-- Apache CXF dependencies -->
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-rt-frontend-jaxws</artifactId>
        <version>${cxf.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-rt-transports-http</artifactId>
        <version>${cxf.version}</version>
    </dependency>

    <!-- Spring Dependencies -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <!-- Datasource -->
    <dependency>
        <groupId>commons-dbcp</groupId>
        <artifactId>commons-dbcp</artifactId>
        <version>${dbcp.version}</version>
    </dependency>
</dependencies>
...
Прежде всего нам нужен сам веб-сервис. У него должен быть только один метод, который собственно и делает запрос. Возвращать он будет массив массивов строк (об этом - ниже). Таким образом получаем что-то вроде:
@WebService
public interface DBProxy {
    @WebMethod(operationName = "query")
    public String[][] query(@WebParam(name="queryText") String queryText);
}
Теперь займемся реализацией этого интерфейса:
@WebService(endpointInterface = "com.example.dbproxy.DBProxy")
public class DBProxyImpl implements DBProxy {
    private DBHelper helper;
    
    @Override
    public String[][] query(String queryText) {
        return helper.query(queryText);
    }

    public void setHelper(DBHelper newHelper) {
        this.helper = newHelper;
    } 
}
Тут появился интерфейс DBHelper. Как не сложно заметить за ним будет стоять класс, реализующий механику работы с БД. Почему нельзя было просто написать всю механику в методе query и не усложнять приложение? Потому что веб-сервис и механика БД - это разные задачи, а значит и реализованы должны быть в разных местах. Это придаст сделает приложение модульным и позволит менять механику хоть во время выполнения приложения (для этого тут и есть метод setHelper). Собственно вот как выглядит интерфейс работы с БД:
public interface DBHelper {
    String[][] query(String queryText);
}
А теперь самая длинная часть приложения - реализация самой механики. По соглашению с заказчиком первая строка полученного массива строк будет содержать названия колонок, полученных в результате выполнения запроса. Вторая строка - тип этих колонок в числовом виде. Последующие строки - собственно сам результат запроса. Таким образом получаем что-то вроде следующего:
public class JdbcDBHelper implements DBHelper {
    private JdbcTemplate db;
    private List<List<String>> result;
    private SqlRowSet rs;

    @Override
    public String[][] query(String queryText) {
        clearResult(); 
        
        getResultRowSet(queryText);
  
        addResultHeader();
  
        addQueryResult();

        return resultAsArrays();
    }
 
    private void clearResult() {
        result = new ArrayList<List<String>>();
    }
 
    private SqlRowSet getResultRowSet(String queryText) {
        rs = getRowSet(queryText);
        return rs;
    }

    private SqlRowSet getRowSet(String queryText) {
        return db.queryForRowSet(queryText);
    }

    private void addResultHeader() {
        addColumnNames();
        addColumnTypes();
    }

    private void addColumnNames() {
        List<String> columnNames = new ArrayList<String>();
        columnNames.addAll(Arrays.asList(rs.getMetaData().getColumnNames()));
        result.add(columnNames);
    }

    private void addColumnTypes() {
        List<String> columnTypes = new ArrayList<String>();
        for (int i = 1; i < rs.getMetaData().getColumnCount(); i++) {
            int columnType = rs.getMetaData().getColumnType(i);
            columnTypes.add(Integer.toString(columnType));
        }
        result.add(columnTypes);
    }
 
    private void addQueryResult() {
        while (rs.next()) {
            List<String> line = new ArrayList<String>();
            for (int i = 1; i < rs.getMetaData().getColumnCount(); i++) {
                if (rs.getObject(i) != null) {
                    line.add(rs.getObject(i).toString());
                } else {
                    line.add("null");
                }
            }
            result.add(line);
        }
    }

    private String[][] resultAsArrays() {
        String[][] converted = new String[result.size()][];

        for (int i = 0; i < result.size(); i++) {
            converted[i] = result.get(i).toArray(new String[result.get(i).size()]);
        }
        return converted;
    } 

    public void setDataSource(DataSource dataSource) {
        this.db = new JdbcTemplate(dataSource);
    }
}
Теперь добавим немного магии Spring, которая сделает из нашего набора классов работающее приложение:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

    <!-- Spring manage ServiceBean -->
    <bean id="dbproxyServ" class="com.example.dbproxy.DBProxyImpl"><
        <property name="helper" ref="dbHelper"/>
    </bean>

    <!-- JAX-WS Service Endpoint -->
    <jaxws:endpoint id="dbproxyService" implementor="#dbproxyServ" address="/dbproxyService" />

    <bean id="dbHelper" class="com.example.dbproxy.db.JdbcDBHelper">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- the DataSource (parameterized for configuration via a PropertyPlaceHolderConfigurer) -->
        <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
        <property name="url" value="my_url"/>
        <property name="username" value="my_user"/>
        <property name="password" value="my_pass"/>
    </bean>

</beans>
Последний штрих - добавим дескриптор развёртывания:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
        http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

    <display-name>Database proxy web service</display-name>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:application-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>CXFServlet</servlet-name>
        <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
          <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>CXFServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>
Всё. Теперь можно деплоить полученное приложение и радоваться технологиям, которые позволяют так быстро писать подобные приложения :)

Комментариев нет:

Отправить комментарий

Примечание. Отправлять комментарии могут только участники этого блога.