在Windows Azure平台中,應用程式除了可選擇完整的資料庫平台SQL Azure儲存資料外,也可選擇較為簡單的Windows Azure Storage Services來儲存資料,
Windows Azure Storage Services共提供三種儲存體
Using Windwos Azure Storage Services with Java
文/黃忠成
雲端儲存體
在Windows Azure平台中,應用程式除了可選擇完整的資料庫平台SQL Azure儲存資料外,也可選擇較為簡單的Windows Azure Storage Services來儲存資料,
Windows Azure Storage Services共提供三種儲存體:
- Table Storage Service儲存row/column(Table)類型的資料
 
- Blob Service儲存大型的二進位資料
 
- Queue Service佇列
 
接下來本文將探討如何於Java/Tomcat中與這三種儲存體互動。
Windows Azure SDK Eclipse Plug-In中的Storage Explorer
Windows Azure SDK的Eclipse Plug-In提供了一個相當方便的Storage Explorer工具,可以讓開發者於Eclipse直接連到Windows Azure Storage Services來
查看或變更資料,開Storage Explorer的方法很簡單,請點選Windows|Show Views|Other選單,就可開出圖1的畫面。
圖1
	
圖2
	
點選OK按鈕後,Storage Explorer就出現在右方,開發者可依據喜好來移動她。
圖3
	
進行下一步動作前,請記得確認Storage Emulator是在執行中的,如果沒有,可透過程式集來啟動。
圖4
	
圖5
	
一切正常的話,就可以點選Open按鈕來連結到Storage Emulator。
圖6
	
開發者可透過Storage Explorer來取得或是管理Table、Blob或是Queue等儲存體中的資料。
圖7
	
圖8
	
圖9
	
圖10
	
圖11
	
當然,開發者也可以直接連結到真正位於Windows Azure平台中的Storage Services,只要點選Windows|Perferences選單來添加Account即可。
圖12
	
圖13
	
圖14
	
使用Windows Azure SDK for Java Client Library
要在應用程式中存取Windows Azure Storage Services,開發者還需要至以下網址下載Windows Azure Libraries for Java。
http://www.windowsazure.com/en-us/develop/java/java-home/download-the-windows-azure-sdk-for-java/
圖15
	
紅框框起來的部分都要一一下載,慣用Maven的開發者也可於此網頁中找到取得的方法(這個較為快速且簡單)。
在取得這個Library後,就可以著手進行開發,本文以一個Web Project為例,第一步驟就是要引用Windows Azure Libraries for Java中的所有.jar。
圖16
	
圖17
	
圖18
	
使用Table Storage Service
Table Storage Servcie可以用來儲存Table型態的資料,是一種完全針對雲端運算環境所設計的儲存體。在Java中使用的方式很簡單,先新增一個StorageClient.java。
package com.azure.stroage.demo;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import com.microsoft.windowsazure.serviceruntime.RoleEnvironment;
import com.microsoft.windowsazure.services.blob.client.CloudBlobClient;
import com.microsoft.windowsazure.services.core.storage.CloudStorageAccount;
import com.microsoft.windowsazure.services.queue.client.CloudQueueClient;
import com.microsoft.windowsazure.services.table.client.CloudTableClient;
public class StorageClient {
          
           //for running outside worker role.
           static final String storageConnectionString =
                         "UseDevelopmentStorage=true;" +
                         "AccountName=devstoreaccount1;" +
                         "AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
          
           private static CloudStorageAccount Account = null;
           private static CloudTableClient _tableClient = null;
           private static CloudBlobClient _blobClient = null;
           private static CloudQueueClient _queueClient = null;
          
           public static CloudTableClient getCloudTableClient() {
                     if(_tableClient == null)
                                _tableClient = Account.createCloudTableClient();
                     return _tableClient;
           }
          
           public static CloudBlobClient getCloudBlobClient() {
                     if(_blobClient == null)
                                _blobClient = Account.createCloudBlobClient();
                     return _blobClient;
           }
          
           public static CloudQueueClient getCloudQueueClient() {
                     if(_queueClient == null)
                                _queueClient = Account.createCloudQueueClient();
                     return _queueClient;
           }
          
           static {
                     try {
                                Account = CloudStorageAccount.parse(RoleEnvironment.getConfigurationSettings().get("DataConnectionString"));
                                //Account = CloudStorageAccount.parse(storageConnectionString);
                     } catch (InvalidKeyException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                     } catch (URISyntaxException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                     }
                    
           }
}
StorageClient類別負責提供Table Storage、Blob、Queue的初始化程式碼,這裡有一段需特別注意。
Account = CloudStorageAccount.parse(RoleEnvironment.getConfigurationSettings().get("DataConnectionString"));
這一段是透過Windows Azure Worker Role的組態檔來取得Storage Services的連線參數,但這只能夠在Windows Azure Worker Role環境中執行,如果你想直接執行Web Project的話(比較快),需修改成下面這樣子。
Account = CloudStorageAccount.parse(storageConnectionString);
接著新增一個Customer.java,其是對應成Table Storage中的一筆資料。
package com.azure.stroage.demo;
import com.microsoft.windowsazure.services.table.client.TableServiceEntity;
public class Customer extends TableServiceEntity {
	
	int _age;
    
    public String getName() {
    	return this.partitionKey;
    }
    
    public void setName(String name) {
    	this.partitionKey = name;
    }
    
    public String getCustomerID() {
    	return this.rowKey;
    }
    
    public void setCustomerID(String id) {
    	this.rowKey = id;
    }
    
    public int getAge() {
    	return _age;
    }
    
    public void setAge(int age) {
    	_age = age;
    }
    
    public Customer(String id, String name) {
        setName(name);
        setCustomerID(id);
    }
    
    public Customer() {
    	
    }
}
為了方便使用,我把操作Table的程式碼封裝在Customers類別中。
package com.azure.stroage.demo;
import com.microsoft.windowsazure.services.core.storage.StorageException;
import com.microsoft.windowsazure.services.table.client.TableBatchOperation;
import com.microsoft.windowsazure.services.table.client.TableConstants;
import com.microsoft.windowsazure.services.table.client.TableOperation;
import com.microsoft.windowsazure.services.table.client.TableQuery;
import com.microsoft.windowsazure.services.table.client.TableQuery.Operators;
import com.microsoft.windowsazure.services.table.client.TableQuery.QueryComparisons;
public class Customers {
          
           public static final void createIfNotExists() throws StorageException {
                     StorageClient.getCloudTableClient().createTableIfNotExists("Customers");
           }
          
           public static final void insert(Customer entity) throws StorageException {
                     TableOperation operation = TableOperation.insert(entity);
                     StorageClient.getCloudTableClient().execute("Customers", operation);
           }
          
           public static final void insertEntitys(Customer[] entitys) throws StorageException {
                     TableBatchOperation batch = new TableBatchOperation();
                     for(Customer c : entitys)
                                batch.insert(c);
                     StorageClient.getCloudTableClient().execute("Customers", batch);
           }
          
           public static final void delete(Customer entity) throws StorageException {
                     TableOperation operation = TableOperation.delete(entity);
                     StorageClient.getCloudTableClient().execute("Customers", operation);
           }
          
           public static final void update(Customer entity) throws StorageException {
                     TableOperation operation = TableOperation.replace(entity);
                     StorageClient.getCloudTableClient().execute("Customers", operation);
           }
          
           public static final Iterable retrieveAll() {
                     TableQuery myQuery = TableQuery.from("Customers", Customer.class)
                                     .take(999);
                     return StorageClient.getCloudTableClient().execute(myQuery);
           }
          
           public static final Iterable retrieveWithName(String name) {
                     String queryString = TableQuery.generateFilterCondition(TableConstants.PARTITION_KEY, QueryComparisons.EQUAL, name);
                     TableQuery myQuery = TableQuery.from("Customers", Customer.class).where(queryString);
                     return StorageClient.getCloudTableClient().execute(myQuery);
           }
          
           public static final Iterable retrieveWithAge(int age) {
                     String queryString = TableQuery.generateFilterCondition("Age", QueryComparisons.EQUAL, age);
                     TableQuery myQuery = TableQuery.from("Customers", Customer.class).where(queryString);
                     return StorageClient.getCloudTableClient().execute(myQuery);
           }
          
           public static final Customer retrieveWithID(String id) {
                     String queryString = TableQuery.generateFilterCondition(TableConstants.ROW_KEY, QueryComparisons.EQUAL, id);
                     TableQuery myQuery = TableQuery.from("Customers", Customer.class).where(queryString);
                     for(Customer c : StorageClient.getCloudTableClient().execute(myQuery))
                                return c;
                     return null;
           }
          
           public static final Iterable retrieveWithNameAndAge(String name,int age) {
                     String queryString = TableQuery.generateFilterCondition(TableConstants.PARTITION_KEY, QueryComparisons.EQUAL, name);
                     queryString = TableQuery.combineFilters(queryString, Operators.AND,
                                           TableQuery.generateFilterCondition("Age", QueryComparisons.EQUAL, age));
                     TableQuery myQuery = TableQuery.from("Customers", Customer.class).where(queryString);
                     return StorageClient.getCloudTableClient().execute(myQuery);
           }
}
         
程式碼應該不難理解,只是新增刪動作而已。完成後即可建立.jsp頁面來測試。
<%@pagelanguage="java"contentType="text/html; charset=BIG5"
    pageEncoding="BIG5"%>
<%@pageimport="com.azure.stroage.demo.*"%>
<%@pageimport="com.microsoft.windowsazure.services.table.client.*"%> 
<%@pageimport="com.microsoft.windowsazure.services.core.storage.*"%>
<!DOCTYPEhtmlPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<metahttp-equiv="Content-Type"content="text/html; charset=BIG5">
<title>Customers</title>
</head>
<body>
<%
   if(request.getParameter("btnGenerate") != null) {
       Customers.createIfNotExists();
       Customer c1 = newCustomer("A00001","Jeffery");
       c1.setAge(18);
      
       Customer c2 = newCustomer("A00002","Tom");
       c2.setAge(28);
      
       Customers.insert(c1);
       Customers.insert(c2);
       response.sendRedirect("Customers.jsp");
   }
%>
<formname="form1"method="POST">           
   <table>
      <tr>
         <td>Customer ID</td>
         <td>Name</td>
         <td>Age</td>
      </tr>
      <%
         Customers.createIfNotExists();
         for(Customer c : Customers.retrieveAll()) {%>
         <tr>
         <td><%=c.getCustomerID()%></td>
         <td><%=c.getName()%></td>
         <td><%=c.getAge()%></td>
         </tr>
        <%} %>
        <tr>
        <tdcolspan="3">
          <inputtype="submit"name="btnGenerate"value="Generate"/>
        </td>
        </tr>
   </table>
</form>
</body>
</html>
圖19為執行畫面。
圖19
	
使用Blob Service
當瞭解Table Storage Service的大概用法後,Blob就不難了。
package com.azure.stroage.demo;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import com.microsoft.windowsazure.services.blob.client.BlobContainerPermissions;
import com.microsoft.windowsazure.services.blob.client.BlobContainerPublicAccessType;
import com.microsoft.windowsazure.services.blob.client.CloudBlobContainer;
import com.microsoft.windowsazure.services.blob.client.CloudBlockBlob;
import com.microsoft.windowsazure.services.blob.client.ListBlobItem;
import com.microsoft.windowsazure.services.core.storage.StorageException;
public class MyPictureBlob {
           private static CloudBlobContainer _container = null;
          
           public static final void insert(String name, byte[] data) throws URISyntaxException, StorageException, IOException {
                     CloudBlockBlob blob = _container.getBlockBlobReference(name);
                     ByteArrayInputStream stream = new ByteArrayInputStream(data);
                     blob.upload(stream, data.length);
           }
          
           public static final Iterable getBlobs() {
                     return _container.listBlobs();
           }
          
           public static final void delete(String name) throws URISyntaxException, StorageException {
                     CloudBlockBlob blob = _container.getBlockBlobReference(name);
                     blob.delete();
           }
          
           static {
                     try {
                                _container = StorageClient.getCloudBlobClient().getContainerReference("pics");
                                _container.createIfNotExist();                        
                                BlobContainerPermissions ps = new BlobContainerPermissions();
                                ps.setPublicAccess(BlobContainerPublicAccessType.CONTAINER);
                                _container.uploadPermissions(ps);
                     } catch (URISyntaxException | StorageException e1) {
                                // TODO Auto-generated catch block
                                e1.printStackTrace();
                     }
           }
}
 
在本文中,測試Blob的是一個供使用者瀏覽及上傳圖片的頁面。
<%@page language="java"contentType="text/html; charset=BIG5"
    pageEncoding="BIG5"%>
<%@page import="com.azure.stroage.demo.*"%>
<%@page import="com.microsoft.windowsazure.services.blob.client.*"%>
<!DOCTYPE htmlPUBLIC"-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Blobs</title>
<meta http-equiv="Content-Type" content="text/html; charset=big5"/>
</head>
<body>
<form name="upload" enctype="multipart/form-data" method="post" action="uploadHandler.jsp">
<p>上傳圖片:<input type="file" name="file" size="20 "maxlength="20"/></p>
<p><input type="submit"value="上傳"/><input type="reset"value="清除"/></p>
</form>
<table>
  <%
     for(ListBlobItem item : MyPictureBlob.getBlobs()) { %>
     <tr><td><img src='<%=item.getUri().toString() %>'/></td></tr>         
  <%} %>
</table>
</body>
</html>
下面是uploadHandler.jsp的程式碼。
<%@ page language="java" %>
<%@ page import="java.io.*"%>
<%@ page import="java.util.Iterator"%>
<%@ page import="java.util.List"%>
<%@ page import="com.azure.stroage.demo.*"%>
<%@ page import="com.microsoft.windowsazure.services.blob.client.*" %>
<%
     String contentType = request.getContentType();        
     if ((contentType != null) && (contentType.indexOf("multipart/form-data") >= 0))  {                
    	 DataInputStream in = new DataInputStream(request.getInputStream());  
    	 int formDataLength = request.getContentLength();
    	 byte dataBytes[] = new byte[formDataLength];
    	 int byteRead = 0;
    	 int totalBytesRead = 0;
    	 while (totalBytesRead < formDataLength) {
    		 byteRead = in.read(dataBytes, totalBytesRead, formDataLength);
    		 totalBytesRead += byteRead;
         }                                        
    	 String file = new String(dataBytes);                
    	 String saveFile = file.substring(file.indexOf("filename=\"") + 10);                
    	 saveFile = saveFile.substring(0, saveFile.indexOf("\n"));                
    	 saveFile = saveFile.substring(saveFile.lastIndexOf("\\") + 1,saveFile.indexOf("\""));                
    	 int lastIndex = contentType.lastIndexOf("=");                
    	 String boundary = contentType.substring(lastIndex + 1,contentType.length());                
    	 int pos;
    	 pos = file.indexOf("filename=\"");                
    	 pos = file.indexOf("\n", pos) + 1;                
    	 pos = file.indexOf("\n", pos) + 1;                
    	 pos = file.indexOf("\n", pos) + 1;                
    	 int boundaryLocation = file.indexOf(boundary, pos) - 4;                
    	 int startPos = ((file.substring(0, pos)).getBytes()).length;                
    	 int endPos = ((file.substring(0, boundaryLocation)).getBytes()).length;
    	 ByteArrayOutputStream stream = new ByteArrayOutputStream();
    	 stream.write(dataBytes, startPos, dataBytes.length - startPos - boundary.length() - 8);    	 
    	 MyPictureBlob.insert(saveFile, stream.toByteArray());
    	 stream.close();
    	 response.sendRedirect("Blobs.jsp");
     }
%>
圖20
	
使用Queue Service
Queue Service就更簡單了。
package com.azure.stroage.demo;
import java.net.URISyntaxException;
import com.microsoft.windowsazure.services.core.storage.StorageException;
import com.microsoft.windowsazure.services.queue.client.CloudQueue;
import com.microsoft.windowsazure.services.queue.client.CloudQueueMessage;
public class MyQueue {
	
	private static CloudQueue _queue =  null;
	
	public static void enqueue(String message) throws URISyntaxException, StorageException {		
		_queue.addMessage(new CloudQueueMessage(message));
	}
	
	public static String dequeue() throws URISyntaxException, StorageException {
		return _queue.peekMessage().getMessageContentAsString();
	}
	
	static {
		try {
			_queue = StorageClient.getCloudQueueClient().getQueueReference("messages");
			_queue.createIfNotExist();
		} catch (URISyntaxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (StorageException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}		
	}
}
下面是測試的頁面。
<%@ page language="java" contentType="text/html; charset=BIG5"
    pageEncoding="BIG5"%>
<%@ page import="com.azure.stroage.demo.*" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=BIG5">
<title>Insert title here</title>
</head>
<body>
   <%
      MyQueue.enqueue("Hello World");
   %>
   <%= MyQueue.dequeue() %>
</body>
</html>
佈署到真正的Windows Azure環境
第一個步驟就是修改StorageClient.java,讓他由Worker Role組態中取得Storage Services的連線參數。
Account = CloudStorageAccount.parse(RoleEnvironment.getConfigurationSettings().get("DataConnectionString"));
接著修改Azure Project中的ServiceDefinitions.csdef檔案。
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<ServiceDefinition xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceDefinition" name="WindowsAzureProject">
  <WorkerRole name="WorkerRole1" vmsize="Small">
    <Startup>
      <!-- Sample startup task calling startup.cmd from the role's approot folder -->
      <Task commandLine="util/.start.cmd startup.cmd" executionContext="elevated" taskType="simple">
        <Environment>
          <Variable name="_JAVA_OPTIONS" value="-agentlib:jdwp=transport=dt_socket,server=y,address=8090,suspend=n"/>
        </Environment>
      </Task>
    </Startup>
    <Runtime executionContext="elevated">
    	<EntryPoint>
	  		<!-- Sample entry point calling run.cmd from the role's approot folder -->
    		<ProgramEntryPoint commandLine="run.cmd" setReadyOnProcessStart="true"/>
    	</EntryPoint>
    </Runtime> 
    <Imports>
  	  <Import moduleName="RemoteAccess"/>
  	  <Import moduleName="RemoteForwarder"/>
    </Imports>
    <Endpoints>
      <InputEndpoint localPort="8080" name="http" port="80" protocol="tcp"/>
    <InputEndpoint localPort="8090" name="Debugging" port="8090" protocol="tcp"/>
    </Endpoints>
    <ConfigurationSettings>
      <Setting name="DataConnectionString"/>
    </ConfigurationSettings>    
  </WorkerRole>
</ServiceDefinition>
再修改ServiceDefinitions.cscfg檔案,加入連線參數。
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<ServiceConfiguration xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration" osFamily="2" osVersion="*" serviceName="WindowsAzureProject">
  <Role name="WorkerRole1">
    <Instances count="1"/>
    <ConfigurationSettings>
      <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.Enabled" value="true"/>
      <Setting name="Microsoft.WindowsAzure.Plugins.RemoteForwarder.Enabled" value="true"/>
      <!-- NOTE: replace the following settings with your own -->
      <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountUsername" value="test"/>
      <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountEncryptedPassword" value="......."/>
      <Setting name="Microsoft.WindowsAzure.Plugins.RemoteAccess.AccountExpiration" value="2039-12-31T23:59:59.0000000-08:00"/>      
      <!-- <Setting name="DataConnectionString" value="UseDevelopmentStorage=true"/>  -->
      <Setting name="DataConnectionString" value="DefaultEndpointsProtocol=http;AccountName=<your account name>;AccountKey=<your account key> "/>      
    </ConfigurationSettings>
    <Certificates>
      
      <Certificate name="Microsoft.WindowsAzure.Plugins.RemoteAccess.PasswordEncryption" thumbprint=".........." thumbprintAlgorithm="sha1"/>
    </Certificates>
  </Role>
</ServiceConfiguration>
最後進行Build All後,將產生的.cspkg、.cscfg上傳至Azure平台即可。