Java Enterprise

Podstawy

Opisze po krótce etapy tworzenia przykładowej aplikacji webowej z wykorzystaniem Tomcat, do budowy której posłużę się servletami raz JSP. Przedstawiam opis wdrażania aplikacji bez wykorzystania IDE.

Kilka komend w bash'u:

mkdir myFirstWebApp
cd myFirstWebApp
mkdir WEB-INF
mkdir src
mkdir classes
touch index.html
touch test.jsp
cd WEB-INF
touch web.xml
cd ..

Zaczynamy od utworzenia drzewa projektu, które powinno wyglądać następująco:

├── classes
├── index.html
├── src
├── test.jsp
└── WEB-INF
    └── web.xml

Pliki index.html oraz test.jsp zostały utworzone dodatkowo dla lepszej ilustracji przykładu. W dalszej kolejności utworzona zostanie klasa serwletu.

cd src
mkdir cecherz
cd cecherz
touch Start.java

Oczekiwany efekt finalny to:

├── classes
├── index.html
├── src
│   └── cecherz
│       └── Start.java
├── test.jsp
└── WEB-INF
    └── web.xml

JSP i Servlety

Wypełniamy zawartością pliki index.html oraz test.jsp aby dobrze zobaczyć współdziałanie różnych elementów aplikacji:

index.html

<!DOCTYPE html>
<html>
	<body>
		<h1>Hello Kamil</h1>
		<a href="test.jsp">Test.jsp</a>
	</body>
</html>

test.jsp

<!DOCTYPE html>
<html>
	<body>
		<%
			for(int i = 0; i < 10; i++) {
				out.println("<h1>Hello Kamil</h1>");
			}
		%>
	</body>
</html>

W pliku test.jsp zaprezowany jest przykład użycia skrytletu, kod Javy zostaje osadzony pomiędzy znacznikami HTML.

W następnej kolejności tworzymy klasę servletu, warto zwrócić uwagę na nazwę pakietu i pliku zawierającego klasę. W celu zachowania porządku nazwa klasy i pliku javy jest identyczna.

Start.java

package cecherz;

import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class Start extends HttpServlet {
	public void doGet(HttpServletRequest request, HttpServletResponse response)
	throws IOException{
		PrintWriter out = response.getWriter();
		out.println("<html>");
		out.println("<body>");
		out.println("<h1>Hello Kamil</h1>");
		out.println("</body>");
		out.println("</html>");	
	}
}

Tworzymy możliwie najprostszą obsługę metody GET protokołu HTTP. Klient w widoku, po wysłaniu żądania zobaczy stronę HTML-a.

Wdrożenie aplikacji

Do obsługi serwletu konieczny będzie deskryptor wdrożenia plik web.xml, który będzie informacją dla serwera w jaki sposób poprzez ades URL odwołać się do strony utworzonej przez servlet.

<web-app>
	<servlet>
		<servlet-name>Start</servlet-name>
		<servlet-class>cecherz.Start</servlet-class>
	</servlet>
	
	<servlet-mapping>
		<servlet-name>Start</servlet-name>
		<url-pattern>/Start/</url-pattern>
	</servlet-mapping>
</web-app>

Przechodzimy do skompliwania klasy servletu. Nie intersują nas pliki .jsp, są one przetwarzane automatycznie na klasy Javy.

javac src/cecherz/Start.java -classpath "/usr/local/tomcat9/lib/servlet-api.jar" -d classes/

W celu prawidłowej kompilacji określamy parametr -classpath. W katalu instalacyjnym serwera aplikacyjnego wyszukujemy niezbędne skomplikowane klasy do obsługi servletów (API) w postaci archiwum .jar. Podajemy folder wyjściowy dla skompilowanego servletu Start, który odpowiadał będzie za obsługę żądań.

Aplikacja może zostać wstawiona na serwer. Zawartość projektu przenosimy do katalogu /usr/local/tomcat9/webapps (System operacyjny Fedora). Wcześniej kopijąc wraz z zawartością folder class do lokalizacji WEB-INF.

Na serwer pliki można przenieść również w postaci archiwum .war. Tworzymy je za pomocą polecenia:

jar -cvf test.war *

Dokumentacja, materiały dodatkowe:

Połączenie z bazą danych

Klasa ułatwiająca komunikację z bazami danych na poziomie Servletów.

Parametry odpowiedzialne za komunikację z bazą dobrze jest trzymać w osobnej klasie. W opisanym przypadku w zmiennych statycznych na sztywno zdefiniowano przykładowe dane. Łączymy się z bazą world z lokalnego servera. Klasa posiada settery do zmiany domyślnych wartości. Zmienna driver jest potrzebna na zewnątrz w celu zdefiniowania klasy sterownika, przykładowo:

getMethod() throws SQLException, ClassNotFoundException {
    Class.forName(Connector.getDriver()); 
}

Pozostałe parametry potrzebne są w celu poprawnego wykonania metody connectAndSetQuery odpowiada ona za poprawne połączenie z bazą i wykonanie zapytania SQL. Zwraca ona obiekt typu ResultSet (dokumentacja). Przykład użycia:

List<City> cities = null;

final String sqlQuery = "SELECT Name, Population FROM city ORDER BY Population DESC";
String cityName = null;
int cityPopulation = 0;
cities = new ArrayList<>();
ResultSet resultSet  = Connector.connectAndSetQuery(sqlQuery);
while (resultSet.next()) {
     cityName = resultSet.getString("Name");
     cityPopulation = resultSet.getInt("Population");
     City city = new City(cityName, cityPopulation);
     cities.add(city);
}
return cities;

Powyższy przykład opiera się o kurs javastart: link. Po wywołaniu zapytania pobieramy z obiektu ResultSet dane w tym wypadku kolumny City.Name oraz City.Population

  • Kod klasy Connector
package pl.cecherz.jdbc.utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Connector {

    private static String driver = "com.mysql.cj.jdbc.Driver";
    private static String dbHost = "jdbc:mysql://localhost:3306/";
    private static String dbName = "world";
    private static String[] dbOption = {
            "?useSSL=false",
            "&serverTimezone=UTC",
            "&allowPublicKeyRetrieval=true"
    };

    private static final String dbPath = dbHost + dbName + dbOption[0] + dbOption[1] + dbOption[2];
    private static String user = "root";
    private static String pass = "Pangeon66#";

    public static ResultSet connectAndSetQuery(String sqlQuery) throws SQLException {
        Connection conn = DriverManager.getConnection(
                Connector.dbPath,
                Connector.user,
                Connector.pass
        );
        var statement = conn.createStatement();
        return statement.executeQuery(sqlQuery);
    }
    public static void setDbHost(String dbHost) {
        Connector.dbHost = dbHost;
    }

    public static void setDbName(String dbName) {
        Connector.dbName = dbName;
    }

    public static void setDbOption(String[] dbOption) {
        Connector.dbOption = dbOption;
    }

    public static void setUser(String user) {
        Connector.user = user;
    }

    public static void setPass(String pass) {
        Connector.pass = pass;
    }

    public static String getDriver() {
        return driver;
    }

    public static void setDriver(String driver) {
        Connector.driver = driver;
    }
}

Pula połączeń

Aby zapewnić możliwość większej ilości jednoczesnych połączeń do bazy można skorzystać z dodatkowych bibliotek, zapewniających wielowątkową obsługę połączeń.

  • Kod klasy DbUtil
  • Zmodyfikowany kod źródłowy z serwisu javastart.pl

package pl.cecherz.jdbc.utils;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

public class DbUtil {
    private static DbUtil dbUtil;
    private HikariDataSource dataSource;
    private Map<String, String> db = new HashMap<>();
    {
        db.put("driver", "com.mysql.cj.jdbc.Driver");
        db.put("host", "jdbc:mysql://localhost:3306/");
        db.put("name", "world");
        db.put("opt", "?useSSL=false" +
                "&serverTimezone=UTC" +
                "&allowPublicKeyRetrieval=true");
        db.put("user", "root");
        db.put("pass", "Pangeon66#");
    }
    private DbUtil() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(db.get("host") + db.get("name") + db.get("opt"));
        config.setDriverClassName(db.get("driver"));
        config.setUsername(db.get("user"));
        config.setPassword(db.get("pass"));
        dataSource = new HikariDataSource(config);
    }
    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
    public void close() {
        dataSource.close();
    }
    public static DbUtil getInstance() {
        if(dbUtil == null) {
            dbUtil = new DbUtil();
        }
        return dbUtil;
    }
}

Metodę opisaną w poprzednim przykładzie w stworzonej przeze mnie klasie można zmodyfikować, tak by pola w klasie Connector stały się zbędne. W tym wypadku całą odpowiedzialność za połączenie przejmuje klasa DbUtil, za realizację puli połączeń odpowiadają zaś biblioteki HikariCP oraz SFL4j

public static ResultSet connectAndSetQueryHikari(String sqlQuery) throws SQLException {
        Connection conn = DbUtil.getInstance().getConnection();
        var statement = conn.createStatement();
        return statement.executeQuery(sqlQuery);
}