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

2023年6月14日 星期三

Java GraphQL 實作練習 - 不使用 SpringBoot,只使用一般 Spring MVC

這篇文要來紀錄一下 Java 的 GraphQL 純 Spring MVC 實作練習,
因為網路上的許多範例都是使用 SpringBoot 來實作,
所以在這裡紀錄一下使用純 Spring MVC 、不使用 SpringBoot 的方法。

在這裡想要實作的情境是:
我們有 Author、和 Book 兩種資料,
Author 和 Book 都有 id、name 屬性,
Author 還有 bookIdList 屬性,是一個 Array,紀錄著此 Author 撰寫的 Book 的 id。


我們要實作兩個 query 和一個 mutation,分別是:
Author  getAuthorById(id) :
用 Author id 查 Author。
回傳的 bookList 可以給一個 prefix 參數,它可以為回傳的 book name 前面加上 prefix
(這只是用來練習如何在 Java GraphQL server 端取得 Query 的參數)。

Book  getBookById(id) :
用 Book id 查 Book,

Author updateAuthorName(id, name):
修改特定 id 的 Author 的 name。

GraphQL 的 Schema 設計如下,其中 Date 是我們自訂的 type,之後會建立 GraphQLScalarType 類別來解析它:

scalar Date

type Book {
	id: ID!
	name: String
    publishDate: Date	
}

type Author {
	id: ID!
	name: String
	bookList(prefix: String): [Book]
}

type Query {
  getAuthorById(id: ID!): Author
  getBookById(id: ID!): Book
}

type Mutation {
    updateAuthorName(id: ID!, name: String!): Author
}

Query Request 的例子如下:

query {
  getAuthorById(id: 3) {
    id
    name
    bookList(prefix: "test") {
        id
        name
        publishDate
    }
  }
  
  getBookById(id: 33) {
    id
    name
    publishDate
  }
}

Mutation Request 的例子如下:

mutation {
    updateAuthorName(id: 3, name: "Author 333") {
        id
        name
    }
}

以下開始實作,這裡使用 Opoen JDK 11,
先建立一個 archetype 為 maven-archetype-webapp 的 Java Dynamic Web Application Maven 專案,可以參考這裡
首先先來看一下專案的結構設計,如下圖所示:

接著開始說明,

因為我們要使用 Spring MVC 和 GraphQL 的 library,所以在 pom.xml 加上以下 dependency:

pom.xml (部份內容):

<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-webmvc</artifactId>
	    <version>5.1.3.RELEASE</version>
	</dependency>

	<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-context-support</artifactId>
	    <version>5.1.3.RELEASE</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
	<dependency>
	    <groupId>javax.servlet</groupId>
	    <artifactId>javax.servlet-api</artifactId>
	    <version>3.0.1</version>
	    <scope>provided</scope>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
	<dependency>
	    <groupId>org.aspectj</groupId>
	    <artifactId>aspectjweaver</artifactId>
	    <version>1.9.6</version>
	</dependency>
    
    <!-- https://mvnrepository.com/artifact/com.graphql-java/graphql-java -->
	<dependency>
	    <groupId>com.graphql-java</groupId>
	    <artifactId>graphql-java</artifactId>
	    <version>20.2</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/com.graphql-java-kickstart/graphql-java-tools -->
	<dependency>
	    <groupId>com.graphql-java-kickstart</groupId>
	    <artifactId>graphql-java-tools</artifactId>
	    <version>13.0.3</version>
	</dependency>

接著在 web.xml 裡設定把流量導向 Spring MVC:

/src/main/webapp/WEB-INF/web.xml:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/mvc-config.xml</param-value>
		</init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name> 
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

設定 Spring MVC 的設定檔:

src/main/webapp/WEB-INF/mvc-config.xml:

<beans xmlns="http://www.springframework.org/schema/beans"  xmlns:security="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:task="http://www.springframework.org/schema/task"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:util="http://www.springframework.org/schema/util"
	xmlns:oxm="http://www.springframework.org/schema/oxm"
	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/tx
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/task
        http://www.springframework.org/schema/task/spring-task-3.0.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
        http://www.springframework.org/schema/cache 
        http://www.springframework.org/schema/cache/spring-cache.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util-3.0.xsd
        http://www.springframework.org/schema/oxm 
        http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd
        http://www.springframework.org/schema/security
        http://www.springframework.org/schema/security/spring-security-4.2.xsd"       
        >
                           	
	<context:component-scan base-package="com.*"/>
	<aop:aspectj-autoproxy/>
	<mvc:annotation-driven/>
</beans>

說明:把 GraphQL 的 Schema 設定在 schema.graphqls 中。

/src/main/resources/schema.graphqls:

scalar Date

type Book {
	id: ID!
	name: String
    publishDate: Date
}

type Author {
	id: ID!
	name: String
	bookList(prefix: String): [Book]
}

type Query {
  getAuthorById(id: ID!): Author
}

type Mutation {
    updateAuthorName(id: ID!, name: String!): Author
}

再來建好需要的 Bean 。

/src/java/com/bean/Author.java:

package com.bean;

import java.util.List;

public class Author {
	private int id;
	private String name;
	private List<Integer> bookIdList;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public List<Integer> getBookIdList() {
		return bookIdList;
	}

	public void setBookIdList(List<Integer> bookIdList) {
		this.bookIdList = bookIdList;
	}
	
	
}

/src/java/com/bean/Book.java:

package com.bean;

import java.util.Calendar;

public class Book {
	private int id;
	private String name;
    Calendar publishDate;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
    
    public Calendar getPublishDate() {
		return publishDate;
	}

	public void setPublishDate(Calendar publishDate) {
		this.publishDate = publishDate;
	}
}

建好查詢資料和修改資料的 DAO (這邊沒有使用真正的資料庫,只是建假的資料來模擬)。

/src/main/java/com/dao/AuthorDAO.java:

package com.dao;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Repository;

import com.bean.Author;

@Repository
public class AuthorDAO {
	private static List<Author> authorList;
	
	public AuthorDAO() {
		authorList  = new ArrayList<Author>();
		
		//Set mock data to simulate data in database.
		Author author = new Author();
		author.setId(1);
		author.setName("Author 1");
		author.setBookIdList(List.of(1, 11));
		authorList.add(author);
		
		author = new Author();
		author.setId(2);
		author.setName("Author 2");
		author.setBookIdList(List.of(2, 22));
		authorList.add(author);
		
		author = new Author();
		author.setId(3);
		author.setName("Author 3");
		author.setBookIdList(List.of(3, 33));
		authorList.add(author);
	}
	
	public Author getAuthorById(int id) {
		return authorList.stream().filter((Author author) -> {
			return author.getId() == id;
		}).findAny().orElse(null);
	}
	
	public Author updateAuthorName(int id, String name) {		
		Author author = getAuthorById(id);
		if (author != null) {
			author.setName(name);
		}
		return author;
	}
}

/src/main/java/com/dao/BookDAO.java:

package com.dao;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.stereotype.Repository;

import com.bean.Author;
import com.bean.Book;

@Repository
public class BookDAO {
	private static List<Book> bookList;
	
	public BookDAO() {
		bookList = new ArrayList<Book>();
		
		//Set mock data to simulate data in database.
		Book book = new Book();
		book.setId(1);
		book.setName("Book 1");
		book.setPublishDate(getCalendarByStr("2019-01-01T01:00:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(11);
		book.setName("Book 11");
		book.setPublishDate(getCalendarByStr("2019-01-01T01:01:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(2);
		book.setName("Book 2");
		book.setPublishDate(getCalendarByStr("2019-01-01T02:00:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(22);
		book.setName("Book 22");
		book.setPublishDate(getCalendarByStr("2019-01-01T02:02:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(3);
		book.setName("Book 3");
		book.setPublishDate(getCalendarByStr("2019-01-01T03:00:00+0800"));
		bookList.add(book);
		
		book = new Book();
		book.setId(33);
		book.setName("Book 33");
		book.setPublishDate(getCalendarByStr("2019-01-01T03:03:00+0800"));
		bookList.add(book);
	}
	
	public Book getBookById(int id) {
		return bookList.stream().filter((Book book) -> {
			return book.getId() == id;
		}).findAny().orElse(null);
	}
	
	public List<Book> getBookListByAuthor(Author author) {
		if (author == null || author.getBookIdList() == null || author.getBookIdList().size() == 0) {
			return new ArrayList<Book>();
		}
		
		return author.getBookIdList().stream().map((Integer bookId) -> {
			return bookList.stream().filter((Book book) -> {
				return book.getId() == bookId;
			}).findAny().orElse(null);
		}).filter((Book book) -> {
			return book != null;
		})
		.collect(Collectors.toList());
	}
    
    Calendar getCalendarByStr(String dateStr) {
    	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
    	Calendar date = Calendar.getInstance();
    	try {
			date.setTime(sdf.parse(dateStr));
		} catch (ParseException e) {
			e.printStackTrace();
		}
        return date;
	}
}

我們這邊建一個 GraphqlController.java 來對應至GraphQL server 會收到的 request body JSON,
等一下在 Spring MVC 的 controller 可以直接把 GraphQL request 的值用 GraphqlController.java 來接,
這邊說明一下,Client 端要向 GraphQL 發起請求的時候,可以使用 HTTP GET 及 HTTP POST 的方式:

  1. HTTP GET:
    將參數直接串在網址後面,例如以下例子,不過要注意有些限制,像是無法使用 variables、mutation (雖然官方這樣說,不過其實如果 GraphQL server 是像這篇例子一樣自己實做的話,還是可以自己去設計取得 variables, mutation 實作行為,例如 query=mutation {...}) 等,可參考 Making a GraphQL requests using the GET method
    https://xxx/xxx?query=...&operationName=...
    
  2. HTTP POST:
    將參數 (query、operationName, variables 等) 直接放在 HTTP Body 中 (相當於 Postman 的 raw 格式),例如:
    {
      "query": "...",
      "operationName": "...",
      "variables": { "myVariable": "someValue", ... }
    } 

詳細可以參考這裡的說明: HTTP Methods, Headers, and Body

而 GraphqlController.java 我們把它設計成可對應 HTTP POST Body  的 JSON (有 query, operationName, variables 等屬性)。

/src/main/java/com/query/GraphqlRequestQuery.java:

package com.query;

import java.util.Map;

public class GraphqlRequestQuery {
	String query;
	String operationName;	
	Map<String, Object> variables = new HashMap<>();

	public String getQuery() {
		return query;
	}

	public void setQuery(String query) {
		this.query = query;
	}

	public String getOperationName() {
		return operationName;
	}

	public void setOperationName(String operationName) {
		this.operationName = operationName;
	}

	public Map<String, Object> getVariables() {
		return variables;
	}

	public void setVariables(Map<String, Object> variables) {
		this.variables = variables;
	}
}


建立各個 Resolver 和 Mutation,這會分別對應到 GraphQL 的 query 和 mutation,
說明都寫在注釋裡。

QueryResolver.java 對應到 GraphQL query,裡面會實作 getAuthorById() 和 getBookById() 方法

/src/main/java/com/resolver/QueryResolver.java:

package com.resolver;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bean.Author;
import com.bean.Book;
import com.dao.AuthorDAO;
import com.dao.BookDAO;

import graphql.kickstart.tools.GraphQLQueryResolver;

@Component //對應至 GraphQL request 的 query
public class QueryResolver implements GraphQLQueryResolver {
	
	@Autowired
	AuthorDAO authorDAO;
	
	@Autowired
	BookDAO bookDAO;
	
	//對應至 GraphQL request query 的 getAuthorById
	public Author getAuthorById(int id) {
        return authorDAO.getAuthorById(id);
    }
	
	//對應至 GraphQL request query 的 getBookById
	public Book getBookById(int id) {
        return bookDAO.getBookById(id);
    }
} 

AuthorResolver.java 對應到 GraphQL query 回傳 Author 的 bookList 屬性實作方法,
因為在 QueryResolver.java 中 getAuthorById() 回傳的 Author 裡面找不到 bookList 屬性 (Author.java 只有 bookIdList 和 getBookIdList() 的 getter ,並且 bookIdList 是 int[],也不是 Book[] ),
所以我們必須為 Author 的 bookList 屬性實作值的取得方法。

/src/main/java/com/resolver/AuthorResolver.java:

package com.resolver;

import java.util.List;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bean.Author;
import com.bean.Book;
import com.dao.BookDAO;

import graphql.kickstart.tools.GraphQLResolver;

@Component //對應至 GraphQL request query 中,Author 屬性值的 Resolver 獲取方式 
public class AuthorResolver implements GraphQLResolver<Author> {
	
	@Autowired
	BookDAO bookDAO;
	
	//對應至 GraphQL request query 中,Author 屬性 bookList 值的 Resolver 獲取方式 
	List<Book> bookList(Author author, String prefix) { //可獲取 Parent (也就是 Author) 的資料和傳入參數 (此例為 prefix)
		List<Book> bookList = bookDAO.getBookListByAuthor(author);
		
		if (StringUtils.isNotBlank(prefix)) {			
			bookList = bookList.stream().map((Book book) -> {
				Book renamedBook = new Book();
				renamedBook.setId(book.getId());
				renamedBook.setName(prefix + "-" + book.getName());
				renamedBook.setPublishDate(book.getPublishDate());
				return renamedBook;
			}).collect(Collectors.toList());
		}
		
		return bookList;
	}
}

MutationResolver.java 對應至 GraphQL mutation,裡面會實作 updateAuthorName

/src/main/java/com/resolver/MutationResolver.java:

package com.resolver;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.bean.Author;
import com.dao.AuthorDAO;

import graphql.kickstart.tools.GraphQLMutationResolver;

@Component //對應至 GraphQL request 的 mutation
public class MutationResolver implements GraphQLMutationResolver{
	
	@Autowired
	AuthorDAO authorDAO;
	
	//對應至 GraphQL request mutation 的 updateAuthorName
	public Author updateAuthorName(int id, String name) {
		return authorDAO.updateAuthorName(id, name);
	}
}

接下來我們要來處理自訂的 Date Scalar , 我們這邊建立一個 DateScalarBuilder.java 來回傳自訂實作的 GraphQLScalarType,關於 parseValue() 和 parseLiteral() 的差異可以參考官方這篇 Scalars in GraphQL 和 Stackoverflow 這篇 what's the difference between parseValue and parseLiteral in  GraphQLScalarType 別人的回答
DateScalarBuilder.java:

package com.scalar;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import graphql.language.StringValue;
import graphql.schema.Coercing;
import graphql.schema.GraphQLScalarType;

public class DateScalarBuilder {
	
	private DateScalarBuilder() {
		
	}
	
	public static GraphQLScalarType getDateScalar() {
		String dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ";
		
		return GraphQLScalarType.newScalar()
				.name("Date")
				.description("A custom scalar that handles date.")
		        .coercing(new Coercing<Calendar, String>() {
		            @Override
		            public String serialize(Object dataFetcherResult) {
                    	//實作從 Java 物件 (此例是 Calendar) 轉成要回傳 graphql response 的 String
		            	Calendar date = (Calendar) dataFetcherResult;
		            	SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		                return sdf.format(date.getTime()) ;
		            }

		            @Override
		            public Calendar parseValue(Object input) {
                    	//實作從 Graphql 得到的 String 轉成 Java 物件 (此例是 Calendar)
                        //如果 input 使用了 inline input 的方式,就會使用 parseValue() 來解析
		            	//例如:
		            	/*
		            	 * Request:
		            	 * query myQuery {		            	 * 
		            	 * 	getBookById(id: 1) {
		            	 *  	id
		            	 *  	name 
		            	 * 	}
		            	 * } 
		            	 */
		            	String dateStr = (String) input;
		            	SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		            	Calendar date = Calendar.getInstance();
		            	try {
							date.setTime(sdf.parse(dateStr));
						} catch (ParseException e) {
							e.printStackTrace();
						}
		                return date;
		            }

		            @Override
		            public Calendar parseLiteral(Object input) {
                    	//實作從 Graphql 得到的 String 轉成 Java 物件 (此例是 Calendar)
                        //如果 input 使用了 variables 的方式,就會使用 parseLiteral() 來解析
		            	//例如:
		            	/*
		            	 * Request:
		            	 * query myQuery($id: ID) { 
		            	 * 
		            	 * 	getBookById(id: $id) {
		            	 *  	id
		            	 *  	name 
		            	 * 	}
		            	 * }
		            	 * 
		            	 * Variables:
		            	 * {
		            	 * 	"id": 1
		            	 * }
		            	 */
		            	String dateStr = ((StringValue) input).getValue();
		            	SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
		            	Calendar date = Calendar.getInstance();
		            	try {
							date.setTime(sdf.parse(dateStr));
						} catch (ParseException e) {
							e.printStackTrace();
						}
		                return date;
		            }
		        })
		        .build();
	}
}

GraphqlController.java 是 SpringMVC 的 Controller 設定,
這裡我們設定了 /graphql 為處理 GraphQL request 的 url,
限制只接收 HTTP POST 且 Mime type 為 application/json 格式的 request。

接著讀取 GraphQL Schema 、設定要用的 Resolver 、 Scalar,並配合接收到的 GraphQL request
產生並回傳查詢結果。

src/java/com/controller/GraphqlController.java:

package com.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import graphql.ExecutionInput;
import graphql.ExecutionResult;
import graphql.GraphQL;
import graphql.kickstart.tools.SchemaParser;
import graphql.schema.GraphQLSchema;

import com.scalar.DateScalarBuilder;
import com.query.GraphqlRequestQuery;
import com.resolver.AuthorResolver;
import com.resolver.MutationResolver;
import com.resolver.QueryResolver;

@Controller
public class GraphqlController {
	
	@Autowired
	private QueryResolver queryResolver;
	
	@Autowired
	private AuthorResolver authorResolver;
	
	@Autowired
	private MutationResolver mutationResolver;
	
	@RequestMapping(value="/graphql", method = {RequestMethod.POST}, produces = "application/json")
	public @ResponseBody Map<String, Object> myGraphql(@RequestBody GraphqlRequestQuery requestQuery) {
		
		//讀取 GraphQL Schema、設定各 Resolver
		GraphQLSchema graphQLSchema = SchemaParser.newParser()
									  .file("schema.graphqls")
                                      .scalars(DateScalarBuilder.getDateScalar())
									  .resolvers(queryResolver
											     , authorResolver
											     , mutationResolver)
									  .build()
									  .makeExecutableSchema();

		GraphQL graphQL = GraphQL.newGraphQL(graphQLSchema).build();
		
		//將 GraphQL request 的 query (也可視需要取出 operationName, variables) 值取出,準備進行查詢
		ExecutionInput executionInput = ExecutionInput.newExecutionInput()
										.query(requestQuery.getQuery())
                                        .operationName(requestQuery.getOperationName())
                                        .variables(requestQuery.getVariables())
										.build();
		ExecutionResult executionResult = graphQL.execute(executionInput);	
		
		//executionResult.toSpecification() 會回傳一個 Map,
		//裡面包含了查詢結果 data 、 errors、 extensions 等資料
		return executionResult.toSpecification();
	}
}

最後展示一下查詢結果:

  1. 查詢:
    query {
      getAuthorById(id: 3) {
        id
        name
        bookList(prefix: "test") {
            id
            name
            publishDate
        }
      },
      getBookById(id: 33) {
        id
        name
        publishDate
      }
    }
    
    結果:
    {
        "data": {
            "getAuthorById": {
                "id": "3",
                "name": "Author 3",
                "bookList": [
                    {
                        "id": "3",
                        "name": "test-Book 3"
                        "publishDate": "2019-01-01T03:00:00+0800"
                    },
                    {
                        "id": "33",
                        "name": "test-Book 33"
                        "publishDate": "2019-01-01T03:03:00+0800"
                    }
                ]
            },
            "getBookById": {
                "id": "33",
                "name": "Book 33"
                "publishDate": "2019-01-01T03:03:00+0800"
            }
        }
    }
    
  2. 查詢:
    mutation {
        updateAuthorName(id: 3, name: "Author 3 - changed") {
            id
            name
            bookList(prefix: "test") {
                id
                name
                publishDate
            }
        }
    }
    
    結果:
    {
        "data": {
            "updateAuthorName": {
                "id": "3",
                "name": "Author 3 - changed",
                "bookList": [
                    {
                        "id": "3",
                        "name": "test-Book 3"
                        "publishDate": "2019-01-01T03:00:00+0800"
                    },
                    {
                        "id": "33",
                        "name": "test-Book 33"
                        "publishDate": "2019-01-01T03:03:00+0800"
                    }
                ]
            }
        }
    }
    

下載分享:

MyGraphQL.7z

參考資料:

  1. HTTP Methods, Headers, and Body
  2. Making a GraphQL requests using the GET method
  3. GraphQL x Spring WebMVC
  4. Defining a schema
  5. Think in GraphQL :: 2019 iT 邦幫忙鐵人賽

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 。


參考:

2022年10月15日 星期六

避免在用 Maven 建置 war 檔時, minify-maven-plugin 最小化過的檔案在 package phase 被 maven-war-plugin 用 source code 蓋掉

當我們在使用 Maven 的 maven-war-plugin 建置 war 專案時常會發現,
如果有使用一些 javascript/css 最小化 (minify) 的 plugin,
例如 minify-maven-plugin,
minify-maven-plugin 會先執行把 js/css 檔做最小化,
然後 maven-war-plugin 會在 package phase 時從專案把未被最小化的 source code 放到
輸出資料夾中 (包括 war 檔),此時未被最小化的 source code 
會把己被 minify-maven-plugin 最小化的檔案給覆蓋掉,
造成最後輸出資料夾、war 檔裡的檔案沒有成功被最小化。

以下分享一個解決的方法,
可以使用 minify-maven-plugin 的 <webappTargetDir> 設定
把最小化過的檔案放在另一個資料夾下,
例如:   ${basedir}/target/minify (等同  ${project.build.directory}/minify) 資料夾中 ("minify" 資料夾名可自行決定),

<webappTargetDir>${project.build.directory}/minify</webappTargetDir>

然後再使用 maven-war-plugin 的 <webResource> 設定將 ${basedir}/target/minify 資料夾中的
最小化檔案放回至專案輸出資料夾中 (也包括 war 檔裡)

<webResource>
    <directory>${project.build.directory}/minify</directory>
</webResource>

如以下範例:

<plugin>
	<groupId>com.samaxes.maven</groupId>
	<artifactId>minify-maven-plugin</artifactId>
	<version>1.7.6</version>
	<executions>
	  <execution>
		<id>default-minify</id>
		<configuration>
		  <skipMerge>true</skipMerge>
		  <nosuffix>true</nosuffix>
		  <webappSourceDir>${basedir}/WebContent</webappSourceDir>
		  <webappTargetDir>${project.build.directory}/minify</webappTargetDir>
		  <cssSourceDir>./</cssSourceDir>
		  <cssSourceIncludes>
			<cssSourceInclude>**/*.css</cssSourceInclude>
		  </cssSourceIncludes>
		  <cssSourceExcludes>
			<cssSourceExclude>**/*.min.css</cssSourceExclude>
		  </cssSourceExcludes>			  
		  <jsSourceDir>./</jsSourceDir>
		  <jsSourceIncludes>
			<jsSourceInclude>**/*.js</jsSourceInclude>
		  </jsSourceIncludes>
		  <jsSourceExcludes>
			<jsSourceExclude>**/*.min.js</jsSourceExclude>
			<jsSourceExclude>**/node_modules/**/*.js</jsSourceExclude>
			<jsSourceExclude>**/webpack.config.js</jsSourceExclude>				
		  </jsSourceExcludes>
		  <jsEngine>CLOSURE</jsEngine>
		</configuration>
		<phase>prepare-package</phase>
		<goals>
		  <goal>minify</goal>
		</goals>
	  </execution>
	</executions>
</plugin>
<plugin>
	<artifactId>maven-compiler-plugin</artifactId>
	<version>3.8.0</version>
	<configuration>
		<rules>
			<requireJavaVersion>
				<version>11</version>
			</requireJavaVersion>
		</rules>
	  <source>11</source>
	  <target>11</target>        
	  <release>11</release>
	</configuration>
</plugin>
<plugin>
	<artifactId>maven-war-plugin</artifactId>
	<version>3.2.1</version>
	<configuration>
	  <warSourceDirectory>WebContent</warSourceDirectory>
	  <webResources>
		<webResource>
			<directory>${project.build.directory}/minify</directory>
		</webResource>			
	  </webResources>
	  <archive>
		<addMavenDescriptor>false</addMavenDescriptor>
	  </archive>		  
	</configuration>
</plugin>

參考:

  1. How to get maven to build a war with minified files using yuicompressor-maven-plugin
  2. yuicompressor maven plugin and maven-war-plugin
  3. Maven内置属性、POM属性 - Ruthless - 博客园
  4. Adding and Filtering External Web Resources
  5. Minification of JS and CSS Assets with Maven

2020年10月27日 星期二

Maven - 使用 profile 功能為不同環境設置不同內容的檔案 - 以 web.xml, proxool.xml 為例

 Maven 可以使用 profile 的功能來設定多個環境,

並對不同環境使用不同的設定,非常方便,

舉個例來說:

通常程式專案可能會有幾個開發階段,

例如開發、測試、正式上線等,

而在不同的階段,我們可能會想使用不同的設定,

例如 Java EE Web 專案的 web.xml 可能在不同的階段會想使用不同的內容設定、

或者是 DB connection pool, Proxool 的 proxool.xml 可能在開發階段想連本地端或測試機的資料庫 (Database),但在正式上線時想連正式機的資料庫等。


這篇文主要是來紀錄使用 Maven 的 profile 功能,來為一個 Jave EE Web 專案使用

不同的 web.xml 和 proxool.xml。

我們希望在使用 maven 產生 war 檔

(命令列指令:

mvn clean install -P{active 的 profile},例如:

mvn clean install -Pdev)

時,能夠依不同的 profile 使用不同的設定。


在這篇文的例子中,先說明幾個比較特別的地方:

  1. 此篇文的 Java EE Web 專案,是由沒有使用 Maven 的舊 Java EE Web 專案轉成的有使用 Maven 的新專案,故檔案結構跟一開始就使用 Maven + webapp archetype 不一樣。
  2. 此篇文使用 Maven 的 maven-war-plugin 來處理 war 檔的發佈,其中 web.xml 可以用 <webXml> 參數來設置。
    而 proxool.xml 用 <webResource> 參數來設置。


如果舊專案是用 Eclipse 產生出來的,可以對專案按右鍵,選 

Configure --> Convert to Maven Project

來將其轉成 Maven 專案,如下圖:



檔案結構如圖所示,其中我們在專案的根目錄下 (也是 pom.xml 所在的位置),建立了一個 config 資料夾,並在其中建立了三個不同環境的資料夾 (dev, demo, production),在各個環境的資料夾中都放置了一份 web.xml 和 proxool.xml,如下圖所示:






首先在 pom.xml 中設定 profile,例如在這我們分三個階段,開發 (dev)、測試 (demo)、正式上線 (production):

<profiles>
  	<profile>			
		<id>dev</id> 
		<properties>
			<profiles.active>dev</profiles.active>	
			<jdk>11</jdk>			
		</properties>
		<activation>
			<activeByDefault>true</activeByDefault>
		</activation>
	</profile>
	<profile>			
		<id>demo</id>
		<properties>
			<profiles.active>demo</profiles.active>			
			<jdk>11</jdk>			
		</properties>		
	</profile>
	<profile>			
		<id>production</id>
		<properties>
			<profiles.active>production</profiles.active>			
			<jdk>11</jdk>			
		</properties>		
	</profile>
</profiles>

接著來設定 web.xml 和 proxool.xml,我們使用了 maven-war-plugin (詳細參數) 來處理,
首先設定 warSourceDirectory 到 WebContent 資料夾 (平常預設是 ${basedir}/src/main/webapp),
然後用 webXml 設定 web.xml 所在的位置 (相對 pom.xml 所在的路徑),
最後用 webResource 設定 proxool.xml :
<plugin>
	<artifactId>maven-war-plugin</artifactId>
	<version>3.2.1</version>
	<configuration>
		<warSourceDirectory>WebContent</warSourceDirectory>
		<webXml>config/${profiles.active}/web.xml</webXml>
		<webResources>
			<webResource>
 				<directory>config/${profiles.active}</directory>
			  	<includes>
			  		<include>proxool.xml</include>
			  	</includes>
			  	<targetPath>WEB-INF</targetPath>
			</webResource>
		</webResources>
	</configuration>
</plugin>
在這邊,${profiles.active} 代表要發佈時所要使用的 profile,即 dev, demo 或 production,
profile 可以在
mvn clean install -P{要的 profile} 
指令中指定,例如如要使用 demo,指令就為:
mvn clean install -Pdemo,
如果沒有在命令中指定 profile (即 -P 參數),就會使用 profile 裡有設定 activeByDefault = true 的 profile (例如此篇文就是設定 dev)
<activation>
	<activeByDefault>true</activeByDefault>
</activation>

在 webResource 設定裡, targetPath 的位置是相對於 warSourceDirectory 的位置,
在這篇文中,就是 WebContent 資料夾

因為在這篇文中,我們沒有把 web.xml 和 proxool.xml 放在專案的 resource directory 之中 (config 資料夾不在 resource directory 裡),
也沒有放在 warSourceDirectory 的設定資料夾中,
也就是 web.xml 和 proxool.xml 不會被發佈到 war 檔裡面,
所以不需要特別對其做 resource excludes 的動作,
例如 resource 的 exclude 
或 <warSourceExcludes> (值為相對於 warSourceDirectory 設定的位置)


參考資料:

Maven - 引用本地端的 jar

這篇文主要來紀錄我使用 Maven 引用本地端 jar 檔的經驗。

在使用 Maven 時,如果想要加入到 library 的 jar 檔在 Maven 的 Repository 找不到、

例如:

  1. 因為某些授權的原因,jar 檔的作者不願意將之放到 Maven Repository 上面,
    或就是沒有人把其 jar 檔放到 Maven Repository 上面。
  2. 可能 jar 檔是來自於合作的第三方,例如跟公司合作的夥伴提供的專屬 jar 檔。
  3. 或是本來原本官方的 jar 檔,有被自己以手動的方式做二次修改的 jar 檔 ,可能是為了相容問題或修正某些官方 bug,例如:Proxool + MS SQL Server DB Driver的相容问题 (參考:
    JAVA+ Proxool+ SQLserver 2008 “signer information does not match signer information of other classe...
    Proxool+SQLserver2008(sqljdbc4.jar) integration problem
  4. 自己寫的,例如公司內部用的 jar 檔,或是想要把舊專案轉成 Maven 時,因年代已久無法考證 jar 檔來原,所以乾脆直接繼續拿來用的 jar 檔。

這時我們就必須要引用本地端 (local) 的 jar 檔,

Maven 大概有幾種作法:

  1. 在 pom.xml 使用 <scope>system</scope> 和 <systemPath> 來引用本地 jar 檔檔案路徑
  2. 將本地 jar 檔放到 local repository (使用命令列指令)
  3. 將本地 jar 檔放到 local repository (在 pom.xml 中加入設定)


首先先講我在實務上希望的需求是:

  1. 能夠在 pom.xml 中容易地看出哪些是自訂的 local jar dependency。
  2. 能夠看得出及容易找出 local jar 檔放在哪裡。
  3. 能夠在不下命令列指令的情況下成功地匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 

再來在以下分述這三種作法及我實務上的例子,及我最後的選擇偏好:


1. 在 pom.xml 使用 <scope>system</scope> 和 <systemPath> 來引用本地 jar 檔檔案路徑

第一種方法 (稱為 System Dependencies) 在使用上非常簡單,直接在 pom.xml 上指定 local jar 檔的檔案路徑,

例如:

<dependency>
        <groupId>myGroupId</groupId>
        <artifactId>myArtifactId</artifactId>
        <version>1.0.0</version>
	<systemPath>D:\myJar.jar</systemPath>
	<scope>system</scope>
</dependency>

其中 <systemPath> 中也可使用用一些 Maven 自己的參數,例如表示專案路徑的 ${basedir}

此方法有幾個缺點:

  1. Maven 官網說 <scope>system</scope> 的用法 (System Dependencies)已經 deprecated 了 (來源)
  2. 在要使用 maven 指令部屬專案時,例如要產生 war 檔時, mvn clean install 產生出的 war 檔裡並不會把用 System Dependencies 設定的 local jar 檔包進生產的專案中。
  3. 如果使用 Eclipse Maven plugin + Tomcat 來開發,用 System Dependencies 設定的 local jar 檔不會被部屬至 server 上 (例如:  C:\Users\xxxUser\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\xxProject\WEB-INF\lib)


2. 將本地 jar 檔放到 local repository (使用命令列指令)

此方法是使用 Maven 的指令直接將本地端的 jar 檔放進 local repository 中,命令如下 (大括弧 {} 中的值可自由填寫):

mvn install:install-file -Dfile={本地端 jar 檔的檔案位置} -DgroupId={自訂的 group id} -DartifactId={自訂的 artifact id} -Dversion={自訂的 version}

例如:

mvn install:install-file -Dfile=D:\myJar-1.0.0.jar -DgroupId=myGroupId -DartifactId=myJar

-Dversion=1.0.0

注意 jar 檔的檔名要改成符合 maven 格式,就是 artifactId-version 這種格式。

執行完命令後,可以到自己本地上的 local respository (通常位置在C:/Users/xxxUser/.m2/repository) 去看是否有成功匯入 jar 檔。

最後在 pom.xml 中加入引用 jar 檔的設定如下:

<dependency>
          <groupId>myGroupId</groupId>
          <artifactId>myArtifactId</artifactId>
          <version>1.0.0</version>
</dependency>


不過個人認為這種方法有一個缺點,

就是如果今天我們換了一台電腦或換了一個開發者要開發時,

必須要對 local jar 檔再一次執行上述的命令例指令以匯入 jar 檔 local repository,

並且單看 pom.xml 很難辨認出哪個 dependency 是使用了自訂的 local jar 檔。

對比我上述的需求,此方法:

  1. 無法在 pom.xml 中容易地看出哪些是自訂的 local jar dependency (除非把命令列指令存起來,再對照去找 local jar 檔位置)。
  2. 單看 pom.xml 看不出 local jar 檔放在哪裡 (除非例如把要下的命令列指令放到 project 中的某一個 file 上)。
  3. 需下命令列指令來匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 


3. 將本地 jar 檔放到 local repository (在 pom.xml 中加入設定)
補充:不確定是不是 Maven 3 版本以上才需要,發現除了本地 jar 檔,還需要再放一個本地 pom 檔 (除了附檔名,檔名跟本地 jar 檔一樣)

此方法是我目前最喜歡的方法,此方法跟第二種方法一樣,

都是把本地 jar 檔放到 local repository 中,不過差在不是執行命令列指令,

而是把要的設定寫在 pom.xml 中。

首先我們要在 pom.xml 中,設定自訂的 repository,例如:

<repository>
	<id>localLibRepository</id>
	<name>localLibRepository</name>
	<url>file:${basedir}/localLibRepository</url>
</repository>

其中 ${basedir} 為 Maven 的參數,代表專案的根目錄位置,

localLibRepository 為我自己建立的資料夾,用來存放 local jar。


設定好後,接著要來把 local jar 檔放到相應的位置去,

首先像上述第二種方法一樣,把 local jar 檔命名成 maven 的格式,例如: 

myArtifactId-1.0.0.jar

然後仿照 Maven Repository 的檔案路徑格式,即 

{repositoryPath}/{groupId}/{artifactId}/{version}/{jarFileName}

的規則建立資料夾 (如同 java 的 package 一樣,如 com.xxx.yyyy,應為 com, xxx, yyy 三個資料夾),並將 local jar 檔放到正確的位置上,例如:

D:\myProject\localLibRepository\myGroupId\myArtifactId\1.0.0\myArtifactId-1.0.0.jar

接著在同一個位置增加 pom 檔,例如:

D:\myProject\localLibRepository\myGroupId\myArtifactId\1.0.0\myArtifactId-1.0.0.pom

內容範例如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>myGroupId</groupId>
  <artifactId>myArtifactId</artifactId>
  <version>1.0.0</version>
</project>

最後如第二種方法一樣,在 pom.xml中引入 local jar 檔的 dependency,例如:

<dependency>
          <groupId>myGroupId</groupId>
          <artifactId>myArtifactId</artifactId>
          <version>1.0.0</version>
</dependency>

之所以我最後選擇這個方法的原因是,它完全符合我的需求:

  1. 比較能夠在 pom.xml 中容易地看出哪些是自訂的 local jar dependency
    (對照 pom.xml 中自訂 repository 的設定,打開 repository 的資料夾即可得知)。
  2. 能夠看得出及容易找出 local jar 檔放在哪裡 (看 pom.xml 中即可得知自訂 repository 的位置)。
  3. 能夠在不下命令列指令的情況下成功地匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 


參考資料:

  1. 3 ways to add local jar to maven project
  2. Adding dependencies to local .jar files in pom.xml
  3. How to include local jar files in Maven project [duplicate]
  4. Maven私服上传pom和jar实操