顯示具有 javaEE 標籤的文章。 顯示所有文章
顯示具有 javaEE 標籤的文章。 顯示所有文章

2024年8月27日 星期二

No XML for Java EE Spring Application

這裡來紀錄一下如何不使用 XML 配置檔改用程式的方式來設定 Java EE + Spring 的 Web Application。

首先來看一下專案的檔案結構如下圖:




因為不用 XML 了的關係,所以我們這次範例專案中不會特別放 Java EE 的 web.xml 和 Spring 的 Config XML。

pom.xml (設定了一些要用到的 Dependency) :
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>my.test</groupId>
  <artifactId>javaee-spring-no-xml-test</artifactId>
  <version>0.1</version>
  <packaging>war</packaging>

  <name>javaee-spring-no-xml-test Maven Webapp</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
	<dependency>
	    <groupId>javax.servlet</groupId>
	    <artifactId>javax.servlet-api</artifactId>
	    <version>4.0.1</version>
	</dependency>
	
    <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->  
    <dependency>
    	<groupId>org.springframework</groupId>
    	<artifactId>spring-webmvc</artifactId>
    	<version>5.3.18</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-jdbc</artifactId>
	    <version>5.3.18</version>
	</dependency>

  </dependencies>

  <build>
    <finalName>javaee-spring-no-xml-test</finalName>
    <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-war-plugin</artifactId>
          <version>3.2.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

src/main/com/config/WebInitializer.java (Java EE Web Application 的啟動設定,取代 web.xml) :
package com.config;

import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.context.support.XmlWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

//Servlet Container (例如 Tomcat) 會在 Web 應用啟動時尋找實作 WebApplicationInitializer 的 Class 並執行 onStartup()
public class WebInitializer implements WebApplicationInitializer {
	
	@Override
	public void onStartup(ServletContext servletContext) {
		
		//可以用 AnnotationConfigWebApplicationContext 設定 Class Config
		//或是用 XmlWebApplicationContext 設定 XML Config 
		//來建立 Spring Application 上下文 (Context)
		AnnotationConfigWebApplicationContext springWebApplicationContext = new AnnotationConfigWebApplicationContext();
		springWebApplicationContext.register(SpringApplicationConfig.class); //這裡可以指定 Spring Class Config
//		XmlWebApplicationContext springWebApplicationContext = new XmlWebApplicationContext();
//		springWebApplicationContext.setConfigLocation("classpath:application-config.xml"); //這裡可以指定 Spring XML Config
		
		DispatcherServlet dispatcherServlet = new DispatcherServlet(springWebApplicationContext);
		
		ServletRegistration.Dynamic springDispatcherServlet = servletContext.addServlet("springDispatcherServlet", dispatcherServlet);
		springDispatcherServlet.setLoadOnStartup(1);
		springDispatcherServlet.addMapping("/");
		/** 上述 code 相當於 web.xml 中的以下 DispatcherServlet 設定
		 * <servlet>
				<servlet-name>springDispatcherServlet</servlet-name>
				<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
				<init-param>
					<param-name>contextConfigLocation</param-name>
					<param-value>/xx/xx/application-config.xml</param-value>
					</init-param>
				<load-on-startup>1</load-on-startup>
			</servlet>
			<servlet-mapping>
				<servlet-name>springDispatcherServlet</servlet-name> 
				<url-pattern>/</url-pattern>
			</servlet-mapping>
		 */
	}
}
src/main/com/config/SpringApplicatoinConfig.java (Spring Config 設定,取代 Spring XML Config File) :
package com.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = "com.*")
public class SpringApplicationConfig {
	
	@Bean
	public InternalResourceViewResolver setupViewResolver() {
		InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
		viewResolver.setPrefix("/WEB-INF/view/");
		viewResolver.setSuffix(".jsp");
		
		return viewResolver;
		
		/** 上述 code 相當於 Spring Config XML 的如下 InternalResourceViewResolver 設定
		 * <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 
		        <property name="prefix">
		            <value>/WEB-INF/view/</value>
		        </property>
		        <property name="suffix">
		            <value>.jsp</value>
		        </property>
		    </bean>
		 */
	}
}


src/main/com/controller/TestController.java (測試用的 Controller) :
package com.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class TestController {
	
	@ResponseBody
	@RequestMapping("/test")
	public String test() {
		return "HIHI";
	}
	
	@RequestMapping("/hello")
	public String hello() {
		return "hello";
	}
	
}

src/main/webapp/WEB-INF/view/hello.jsp (測試用的 JSP View) : 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
Say hello!
</body>
</html>

可以自己跑一個 Tomcat 起來試,應該可以成功存取以下 url

  1. http://localhost:8080/index.jsp (直接存取 webapp 下的 JSP Resource)
  2. http://localhost:8080/hello (會觸發 TestController.hello(),Request 會導向 webapp/WEB-INF/view/hello.jsp)
  3. http://localhost:8080/test (會觸發 TestController.test())

源碼下載分享:
javaee-spring-no-xml-test.7z


參考資料:

  1.  NO XML - Spring MVC Java-based configuration in 9 steps || Spring Annotation || Part 3

2024年7月17日 星期三

JavaEE - 使用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 在 filter 中取得 request 和 response 的內容

 JavaEE 中,如果 HttpServletResponse 的內容一經使用 OutputStream 等方式被讀取的話,
因為資料流只能被讀取一次,
會讓之後在輸入內容到前端時發生不預期的錯誤,
所以不能只接讀取,
如果要獲得 HttpServletRequest, HttpServletResponse 的內容的話,例如拿來做 log 紀錄等用途時會需要,
這時可以使用 Spring 的
ContentCachingRequestWrapper
和
ContentCachingResponseWrapper
這兩個工具來幫忙,
以下用一個 Filter 實作來做範例:

package xxx.api.filter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

@WebFilter(filterName = "apiFilter", urlPatterns = "/xxx/xxx.do")
public class ApiFilter extends OncePerRequestFilter {

	private Logger logger = LoggerFactory.getLogger("xxxLogger");
	
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		
		Instant tic = Instant.now();
		
		ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
		
		filterChain.doFilter(requestWrapper, responseWrapper); //不用原來的 Request 和 Response,改把用 Wrapper 封裝的 Request 和 Response 放到 filterChain 中繼續傳遞
		
		long durationMillis = Duration.between(tic, Instant.now()).toMillis();
		
		String requestBody = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
		
		byte[] responseByteArr = responseWrapper.getContentAsByteArray();
		String responseStr = new String(responseByteArr, StandardCharsets.UTF_8);
		
		String loggerMsg = requestWrapper.getRemoteAddr() +
						   "|" + requestWrapper.getMethod() +
						   "|" + requestWrapper.getRequestURI() +
						   "|" + StringUtils.defaultIfBlank(requestWrapper.getQueryString(), "")  +
						   "|" + responseWrapper.getStatus() +
						   "|" + durationMillis +
						   "|" + requestBody +
						   "|" + responseStr +
					       "|" + responseByteArr.length;
		logger.info(loggerMsg);
		
		responseWrapper.copyBodyToResponse(); //要記得執行這段,不然 HttpServletResponse 會沒有資料輸出
	}

}

說明:
ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 會把
HttpServletRequest 和 HttpServletResponse 包起來,內部用了 ByteArrayInputStream 和
ByteArrayOutputStream 等取得 HttpServletRequest 和 HttpServletResponse 的內容,
可以重覆取得而不用怕影響到 HttpServletRequest 和 HttpservletResponse 的輸入、輸出流。

這邊要注意的是,我們在 Filter 的最後要記得呼叫

responseWrapper.copyBodyToResponse()

不然 HttpServletResponse 會沒有資料輸出。

參考資料:

  1. 【Spring Boot】第16課-使用 Filter 擷取請求與回應

2024年2月15日 星期四

Java EE Spring JDBC tmeplate 執行 PostgreSQL 的 Function

這篇要來紀錄如何使用 Java EE Spring JDBC template 來執行
PostgreSQL 的 Function。

首先我們先建立一個測試用的 Table:

兩個 PostgreSQL Function,如以下 SQL 語法:
CREATE TABLE IF NOT EXISTS public.testTable
(
    id integer NOT NULL,
	title text NOT NULL,
	PRIMARY KEY(id) 
)
並且塞入一些資料:
INSERT INTO public.testTable(id, title) VALUES(1, '111');
INSERT INTO public.testTable(id, title) VALUES(2, '222');
INSERT INTO public.testTable(id, title) VALUES(3, '333');
然後建立三個 Function,以下三個 Function 的作用基本一樣,都是接受傳入的 param_id 變數,然後進行
SELECT * FROM testTable WHERE id = param_id
的查詢,不同的是傳回的 type 不同,呼叫 Functoin 的方式也有所不同,
可以注意到的是,有兩個 Function 的名稱一樣 (都取名為 queryUsingReturnedRefcursor),不過 Function 簽名不同 (傳入參數不同),這是為了演示 PostgreSQL 能夠支持同名 Function 故意取名的,類似 Java 的 Function Overload:
  1. queryUsingReturnedSetof(param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedSetof(param_id integer) RETURNS SETOF testTable AS $$
      SELECT * FROM testTable WHERE id = param_id;
    $$ LANGUAGE sql;
    
    說明:
    此 Function 傳回值 type 為 SETOF,相當於傳回 Table 的資料行集合,呼叫 Function 的方式為如下 SQL 語法:
    SELECT * FROM queryUsingReturnedSetof(2);
  2. queryUsingReturnedRefcursor(param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedRefcursor(param_id integer) RETURNS refcursor AS $$
    DECLARE ref refcursor;
    BEGIN
      OPEN ref FOR SELECT * FROM testTable WHERE id = param_id;
      RETURN ref; 
    END;
    $$ LANGUAGE plpgsql;
    
    說明:
    此 Function 傳回值 type 為 refcursor,為一個 cursor ,不是 Table 的資料行集合,直接呼叫的話只會得到一個 cursor ,必須要配合 FETCH 指令才能遍歷每一行的資料,例如以下語法:
    SELECT queryUsingReturnedRefcursor(2); FETCH ALL IN "<unnamed portal 6>";
    其中,<unnamed portal 6> 是被回傳的 cursor 名稱,不過在實際上每次執行 Function 後回傳的 cursor name 是會變化的,每次都不一樣,
    而執行完 Function 後, cursor 就消失了,導致執行 FETCH 時會有
    ERROR: cursor "<unnamed portal xxx>" does not exist 
    的錯誤,所以上面那樣的語法沒辦法正確地得到查詢結果資料行,
    要解決上面的問題,可以看下一個 Functoin 的說明,
    其想法是把想要的 cursor name 傳進 Function 以指定回傳的 cursor name。

    不過如果是用 Java 去呼叫的話,是可以成功執行並得到資料行的,
    之後可以看到我們可以用 Java 去指定 cursor name。
  3. queryUsingReturnedRefcursor(ref refcursor, param_id integer):
    CREATE OR REPLACE FUNCTION queryUsingReturnedRefcursor(ref refcursor, param_id integer) RETURNS refcursor AS $$
    BEGIN
      OPEN ref FOR SELECT * FROM testTable WHERE id = param_id;
      RETURN ref; 
    END;
    $$ LANGUAGE plpgsql;
    
    說明:
    此 Function 傳回值 type 為 refcursor,跟上一個 Fucntion 一樣,只差在多傳進一個指定的 cursor name,這樣我們就可以用已知的 cursor name 來取得資料行,呼叫方式如下:
    SELECT queryUsingReturnedRefcursor('xxx_cur', 2); FETCH ALL IN "xxx_cur";
    其中 xxx_cur 可以隨意更換成想要的 cursor name。
接下來是 Java EE Spring 的部份了。
下圖為測試專案的檔案結構,紅框處的是主要專案檔案:






因為重要的地方主要是 DAO 使用 JdbcTemplate 的部份,
為了簡單起見,所以這裡只展示 DAO 的部份,也就是 com.dao.MyDAO.java,
其他 Java EE Spring、Data Source 、使用到的 lib (使用 Maven POM 檔設定) 等的設定可以參考文章最後的源碼下載分享

MyDAO.java :
package com.dao;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
import org.springframework.stereotype.Repository;

@Repository
public class MyDAO {

	@Autowired
	@Qualifier("postgresql_JdbcTemplate")
	private JdbcTemplate postgresql_JdbcTemplate;
	
	public List<Map> queryUsingReturnedSetof(int id) {
		List<Map> dataList = postgresql_JdbcTemplate.query("SELECT * FROM queryUsingReturnedSetof(?);", new RowMapper<Map>() {		
	        @Override
	        public Map mapRow(ResultSet rs, int rowNumber) throws SQLException {
	        	Map<String, Object> map = new HashMap<>();
	        	
	        	map.put("id", rs.getInt("id"));
	        	map.put("title", rs.getString("title"));
	        	
	            return map;
	        }
	    }, id);
		
		//for test
		for (Map data : dataList) {
			System.out.println(data.get("id") + ", " + data.get("title"));
		}
		
		return dataList;
	}
	
	public List<Map> queryUsingReturnedRefcursor(int id) {
		String customCursorName = "xxx_cur";
		
		SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(postgresql_JdbcTemplate)
				                        .withProcedureName("queryUsingReturnedRefcursor")
				                        .declareParameters(new SqlParameter("id", Types.INTEGER))
				                        .withoutProcedureColumnMetaDataAccess()
				                        .returningResultSet(customCursorName, new RowMapper<Map>() {
				                	        @Override
				                	        public Map mapRow(ResultSet rs, int rowNumber) throws SQLException {
				                	        	Map<String, Object> map = new HashMap<>();
				                	        	
				                	        	map.put("id", rs.getInt("id"));
				                	        	map.put("title", rs.getInt("title"));
				                	        	
				                	            return map;
				                	        }
				                	    });
		
		Map returnedMapContainsCursorName = simpleJdbcCall.execute(new MapSqlParameterSource().addValue("id", id));
		List<Map> dataList = (List) returnedMapContainsCursorName.get(customCursorName);
		
		//for test
		for (Map data : dataList) {
			System.out.println(data.get("id") + ", " + data.get("title"));
		}
		
		return dataList;
	}
}

說明:
queryUsingReturnedSetof(int id) 對應到 PostgreSQL 中的 queryUsingReturnedSetof(param_id integer) 這個 Function。

queryUsingReturnedRefcursor(int id) 對應到 PostgreSQL 中的 queryUsingReturnedRefcursor(param_id integer) 這個 Function,可以看到它可以讓我們指定一個回傳的 refCursor name,
simpleJdbcCall.execute() 並不直接回傳 data list 資料,而是一個 Map,
就像上面我們直接用 Sql 語法查詢那樣回傳的 refCursor 的資料,
我們需要再用指定的 refCursor name 當作 Key 從 Map 中才能獲取真正的 data list 資料。

源碼下載分享:

2023年5月28日 星期日

建立 Java Dynamic Web Application JavaEE 的 Maven 專案

在 Eclipse 新增 Java Dynamic Web Application,
對 Project 按右鍵,選 
Configure --> Convert to Maven Project
(會有 webContent 資料夾)

或

直接用 Maven 指令產生專案 
(我要加雙引號包住參數,不然會有
The goal you specified requires a project to execute but there is no POM in this directory
的錯誤訊息):
mvn archetype:generate "-DarchetypeGroupId=org.apache.maven.archetypes" "-DarchetypeArtifactId=maven-archetype-webapp" "-DarchetypeVersion=1.4" "-DgroupId=你專案要取的groupId" "-DartifactId=你專案要取的artifactId" "-Dversion=0.1" "-DinteractiveMode=false"

或

在 Eclipse 新增 Maven project,
Archetype 選 maven-archetype-webapp
(不會有 webContent 資料夾)

要 build 成 war 檔時,下載 maven 後,
到專案目錄下 (有 pom.xml 的位置),
執行命令列模式 (command line) 的
mvn clean install。 
如果 consle印出 "Build Success",
應可在專案裡的 "target" 資料夾找到 build 出來的程式,
應有完整已 compile 的 web 專案 (包括 lib),
即簡單佈署用的 war 檔。

如果想在 Eclipse 裡面 build ,可以使用 plugin,
在 pom.xml 加上

<plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.4</version>
 </plugin>

要 build 時,在 Eclipse 對專案按右鍵,選
Run Configuratoins --> 選 Maven,
Goals 欄位填 "clean install" ,再按 Run 。


參考: