LoginSignup
4

More than 5 years have passed since last update.

JavaからGoogleSpreadsheetに書く その2(Java側)

Last updated at Posted at 2016-11-10

http://qiita.com/pilot/items/85bae4e5d5e35ffa7019 の続き

やったこと

2. Java側

以下、Windows上で

後でeclipseにインポートするためのプロジェクトディレクトリ「mytestapp1」を作成

当該ディレクトリに以下のファイルを作成

build.gradle
apply plugin: "java"
apply plugin: "application"
apply plugin: "eclipse"

repositories {
    mavenCentral()
}
dependencies {
    compile "com.google.api-client:google-api-client:1.22.0"
    compile "com.google.oauth-client:google-oauth-client-jetty:1.22.0"
    compile "com.google.apis:google-api-services-drive:v3-rev49-1.22.0"
    compile "com.google.apis:google-api-services-sheets:v4-rev34-1.22.0"
    compile "org.slf4j:slf4j-api:1.7.21"
    compile "org.slf4j:jul-to-slf4j:1.7.21" // Googleライブラリがjava.util.loggingを使っているため
    compile "ch.qos.logback:logback-classic:1.1.7"
}

compileJava {
    options.encoding = "UTF-8"
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
mainClassName = "mytestapp1.MyTestApp1"

task wrapper(type: Wrapper) {
    gradleVersion = "3.1"
}

コマンドプロンプトで当該ディレクトリに移動して以下を実行

  • mkdir src\main\java src\main\resources
  • gradle eclipse

eclipseで当該ディレクトリをインポート(「既存プロジェクトをワークスペースへ」)

「やったこと その1」の最後にダウンロードしたJSONファイルをsrc/main/resourcesに「google_client_secret.json」というファイル名で配置

eclipse上で以下の新規クラス・XMLを作成

src/main/java/mytestapp1/GoogleService.java
package mytestapp1;

import java.io.IOException;
import java.io.Reader;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.CredentialRefreshListener;
import com.google.api.client.auth.oauth2.TokenErrorResponse;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow.Builder;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.sheets.v4.Sheets;

public class GoogleService {
    private static final Logger LOGGER = LoggerFactory.getLogger(GoogleService.class);

    // Global instance of the JSON factory.
    private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();

    // Global instance of the HTTP transport.
    private static HttpTransport HTTP_TRANSPORT = null;

    // Reader for client secret.
    private Reader clientSecretReader;

    // Collection of authorization scopes
    private Collection<String> authorizationScopes;

    // Directory to store user credentials
    private String credentialStoreDirectory;

    // application name
    private String applicationName;

    /**
     * Constructor.
     *
     * @param clientSecretReader reader for client secret
     * @param authorizationScopes collection of authorization scopes
     * @param credentialStoreDirectory directory to store user credentials
     * @param applicationName application name
     */
    public GoogleService(Reader clientSecretReader, Collection<String> authorizationScopes, String credentialStoreDirectory, String applicationName) {
        this.clientSecretReader = clientSecretReader;
        this.authorizationScopes = authorizationScopes;
        this.credentialStoreDirectory = credentialStoreDirectory;
        this.applicationName = applicationName;
    }

    // Credential.
    private Credential credential = null;

    /**
     * Creates an authorized Credential object.
     *
     * @throws IOException
     * @throws GeneralSecurityException
     */
    public void authorize() throws IOException, GeneralSecurityException {
        if (credential != null) {
            return;
        }
        // Load client secrets.
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, clientSecretReader);
        // instance of the {@link FileDataStoreFactory}.
        FileDataStoreFactory dataStoreFactory = new FileDataStoreFactory(new java.io.File(credentialStoreDirectory));
        // ↑ Windowsでは「unable to change permissions~」ログが出力される
        if (HTTP_TRANSPORT == null) {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
        }
        // Build flow and trigger user authorization request.
        Builder builder = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, authorizationScopes);
        builder.setDataStoreFactory(dataStoreFactory).setAccessType("offline");
        // ↑
        // AccessType「offline」でRefreshTokenを得る(AccessTokenのexpire前60秒以後のAPI呼出時に自動refreshが行われるようになる)
        builder.addRefreshListener(new CredentialRefreshListener() {
            @Override
            public void onTokenResponse(Credential credential, TokenResponse tokenResponse) throws IOException {
                LOGGER.info("AccessTokenのrefreshが成功しました。(AccessToken=[{}], ExpiresInSeconds={}, RefreshToken=[{}])", credential.getAccessToken(), credential.getExpiresInSeconds(), credential.getRefreshToken());
            }

            @Override
            public void onTokenErrorResponse(Credential credential, TokenErrorResponse tokenErrorResponse) throws IOException {
                LOGGER.error("AccessTokenのrefreshが失敗しました。(Error=[{}], ErrorDescription=[{}], ErrorUri=[{}])", tokenErrorResponse.getError(), tokenErrorResponse.getErrorDescription(), tokenErrorResponse.getErrorUri());
            }
        });
        // ↑ AccessTokenのrefresh後のListner
        GoogleAuthorizationCodeFlow flow = builder.build();
        credential = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
        // ↑ 初回はブラウザがGoogleの許可リクエスト画面を表示する(関連ログも出力される)
        // → 「許可」押下でローカルJettyにリダイレクトされ、Credentialがファイルに保存される
        // → 以後はCredentialファイルがある限りブラウザは起動しない(自動refreshのおかげ)
        // → サーバで実行する場合はローカルPCで作成したCredentialファイルをサーバに配置しておく
        // → 何らかのエラーでサーバ上のCredentialファイルが無効になった場合は当時のファイルを再度配置する
        LOGGER.info("AccessTokenを取得しました。(AccessToken=[{}], ExpiresInSeconds={}, RefreshToken=[{}])", credential.getAccessToken(), credential.getExpiresInSeconds(), credential.getRefreshToken());
    }

    /**
     * Build and return an authorized Drive client service.
     *
     * @return an authorized Drive client service
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public Drive getDriveService() throws IOException, GeneralSecurityException {
        if (credential == null) {
            authorize();
        }
        return new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build();
    }

    /**
     * Build and return an authorized Sheets API client service.
     *
     * @return an authorized Sheets API client service
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public Sheets getSheetsService() throws IOException, GeneralSecurityException {
        if (credential == null) {
            authorize();
        }
        return new Sheets.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(applicationName).build();
    }

    SheetsWrapper getSheetsWrapperWithWorksheet(String folderName, String spreadsheetName, String worksheetName) throws IOException, GeneralSecurityException {
        DriveWrapper driveWrapper = new DriveWrapper(getDriveService());
        Sheets sheetsService = getSheetsService();

        List<File> folderList = driveWrapper.searchFolder(folderName, DriveWrapper.ROOT_FOLDER_ID);
        // ↑ AccessTokenのexpire前60秒以後、ここでAccessTokenのrefreshが実行される
        String folderId = null;
        if (folderList.size() == 0) {
            folderId = driveWrapper.createFolder(folderName, DriveWrapper.ROOT_FOLDER_ID).getId();
            LOGGER.info("フォルダ '{}' を作成しました。", folderName);
        } else if (folderList.size() == 1) {
            folderId = folderList.get(0).getId();
        } else {
            throw new IOException(String.format("フォルダ '%s' が複数存在しています。", folderName));
        }

        List<File> spreadsheetList = driveWrapper.searchSpreadsheet(spreadsheetName, folderId);
        SheetsWrapper sheetsWrapper = null;
        if (spreadsheetList.size() == 0) {
            String spreadsheetId = driveWrapper.createSpreadsheet(spreadsheetName, folderId).getId();
            sheetsWrapper = new SheetsWrapper(sheetsService, spreadsheetId);
            LOGGER.info("スプレッドシート '{}' を作成しました。", spreadsheetName);
            sheetsWrapper.renameWorksheet(0, worksheetName);
            LOGGER.info("ワークシート '{}' を作成しました。", worksheetName);
        } else if (spreadsheetList.size() == 1) {
            String spreadsheetId = spreadsheetList.get(0).getId();
            sheetsWrapper = new SheetsWrapper(sheetsService, spreadsheetId);
            if (sheetsWrapper.getWorksheetProperties(worksheetName) == null) {
                sheetsWrapper.addWorksheet(worksheetName);
                LOGGER.info("ワークシート '{}' を作成しました。", worksheetName);
            }
        } else {
            throw new IOException(String.format("スプレッドシート '%s' が複数存在しています。", spreadsheetName));
        }

        return sheetsWrapper;
    }
}
src/main/java/mytestapp1/RequestUtil.java
package mytestapp1;

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest;

public class RequestUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(RequestUtil.class);
    private static final int RETRY_COUNT = 10;
    private static final int RETRY_INTERVAL_SECONDS = 10;

    static <R extends AbstractGoogleJsonClientRequest<T>, T> T executeWithRetry(R request) throws IOException {
        for (int i = 0; i < (RETRY_COUNT - 1); i++) {
            try {
                return request.execute();
            } catch (GoogleJsonResponseException e) {
                if (e.getStatusCode() == 503) {
                    LOGGER.warn("Googleからリトライ可能なエラーが返却されました。", e);
                    LOGGER.warn("{}秒後にリトライを行います。({}/{}回目)", RETRY_INTERVAL_SECONDS * (i + 1), i + 1, RETRY_COUNT);
                    try {
                        Thread.sleep(RETRY_INTERVAL_SECONDS * (i + 1) * 1000L);
                    } catch (InterruptedException e2) {
                        // do nothing
                    }
                }
            }
        }
        return request.execute();
    }
}
src/main/java/mytestapp1/DriveWrapper.java
package mytestapp1;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;

public class DriveWrapper {
    private Drive driveService;
    private static final String QUERY_FORMAT = "name = '%s' and '%s' in parents and mimeType = '%s' and trashed = false";
    private static final String MIMETYPE_FOLDER = "application/vnd.google-apps.folder";
    private static final String MIMETYPE_SPREADSHEET = "application/vnd.google-apps.spreadsheet";
    public static final String ROOT_FOLDER_ID = "root";

    public DriveWrapper(Drive driveService) {
        this.driveService = driveService;
    }

    public List<File> searchFile(String name, String parentFolderId, String mimeType) throws IOException {
        String query = String.format(QUERY_FORMAT, name, parentFolderId, mimeType);
        return RequestUtil.executeWithRetry(driveService.files().list().setQ(query)).getFiles();
    }

    public List<File> searchFolder(String name, String parentFolderId) throws IOException {
        return searchFile(name, parentFolderId, MIMETYPE_FOLDER);
    }

    public List<File> searchSpreadsheet(String name, String parentFolderId) throws IOException {
        return searchFile(name, parentFolderId, MIMETYPE_SPREADSHEET);
    }

    public File createFile(String name, String parentFolderId, String mimeType) throws IOException {
        File metadata = new File().setName(name).setMimeType(mimeType).setParents(Arrays.asList(parentFolderId));
        return RequestUtil.executeWithRetry(driveService.files().create(metadata));
    }

    public File createFolder(String name, String parentFolderId) throws IOException {
        return createFile(name, parentFolderId, MIMETYPE_FOLDER);
    }

    public File createSpreadsheet(String name, String parentFolderId) throws IOException {
        return createFile(name, parentFolderId, MIMETYPE_SPREADSHEET);
    }
}
src/main/java/mytestapp1/SheetsWrapper.java
package mytestapp1;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.model.AddSheetRequest;
import com.google.api.services.sheets.v4.model.BatchUpdateSpreadsheetRequest;
import com.google.api.services.sheets.v4.model.GridProperties;
import com.google.api.services.sheets.v4.model.Request;
import com.google.api.services.sheets.v4.model.Sheet;
import com.google.api.services.sheets.v4.model.SheetProperties;
import com.google.api.services.sheets.v4.model.Spreadsheet;
import com.google.api.services.sheets.v4.model.UpdateSheetPropertiesRequest;
import com.google.api.services.sheets.v4.model.ValueRange;

public class SheetsWrapper {
    private Sheets sheetsService;
    private Spreadsheet spreadsheet;

    public SheetsWrapper(Sheets sheetsService, String fileId) throws IOException {
        this.sheetsService = sheetsService;
        this.spreadsheet = RequestUtil.executeWithRetry(sheetsService.spreadsheets().get(fileId).setIncludeGridData(false));
    }

    public void renameWorksheet(int index, String newName) throws IOException {
        SheetProperties sheetProperties = new SheetProperties().setIndex(index).setTitle(newName);
        UpdateSheetPropertiesRequest updateSheetPropertiesRequest = new UpdateSheetPropertiesRequest().setProperties(sheetProperties).setFields("title");
        List<Request> requests = new ArrayList<>();
        requests.add(new Request().setUpdateSheetProperties(updateSheetPropertiesRequest));
        RequestUtil.executeWithRetry(sheetsService.spreadsheets().batchUpdate(spreadsheet.getSpreadsheetId(), new BatchUpdateSpreadsheetRequest().setRequests(requests)));
        spreadsheet = RequestUtil.executeWithRetry(sheetsService.spreadsheets().get(spreadsheet.getSpreadsheetId()).setIncludeGridData(false));
        // ↑
        // rename後のgetSheets().get().getProperties().getTitle()がrename前の値を返す問題の対応
    }

    // 見つからないときはnullを返す
    public SheetProperties getWorksheetProperties(String worksheetName) {
        for (int index = 0; index < spreadsheet.getSheets().size(); index++) {
            Sheet ws = spreadsheet.getSheets().get(index);
            if (ws.getProperties().getTitle().equals(worksheetName)) {
                return ws.getProperties();
            }
        }
        return null;
    }

    public void addWorksheet(String worksheetName) throws IOException {
        SheetProperties sheetProperties = new SheetProperties().setTitle(worksheetName);
        AddSheetRequest addSheetRequest = new AddSheetRequest().setProperties(sheetProperties);
        List<Request> requests = new ArrayList<>();
        requests.add(new Request().setAddSheet(addSheetRequest));
        RequestUtil.executeWithRetry(sheetsService.spreadsheets().batchUpdate(spreadsheet.getSpreadsheetId(), new BatchUpdateSpreadsheetRequest().setRequests(requests)));
        spreadsheet = RequestUtil.executeWithRetry(sheetsService.spreadsheets().get(spreadsheet.getSpreadsheetId()).setIncludeGridData(false));
        // ↑ add後のgetSheets()がaddしたシートを返さない問題の対応
    }

    // 値取得
    public List<List<Object>> getValues(String worksheetName, int startColNum, int startRowNum, int endColNum, int endRowNum) throws IOException {
        boolean specifyColRow = startColNum > 0 || startRowNum > 0 || endColNum > 0 || endRowNum > 0;
        StringBuilder rangeBuf = new StringBuilder();
        rangeBuf.append(worksheetName);
        rangeBuf.append(specifyColRow ? '!' : "");
        rangeBuf.append((startColNum > 0) ? bijectiveBase26(startColNum) : "");
        rangeBuf.append((startRowNum > 0) ? startRowNum : "");
        rangeBuf.append(specifyColRow ? ':' : "");
        rangeBuf.append((endColNum > 0) ? bijectiveBase26(endColNum) : "");
        rangeBuf.append((endRowNum > 0) ? endRowNum : "");
        return RequestUtil.executeWithRetry(sheetsService.spreadsheets().values().get(spreadsheet.getSpreadsheetId(), rangeBuf.toString())).getValues();
    }

    // 指定列で値の入っている最後の行の行番号を取得
    // (ワークシート名のみ指定すれば「全列で」となるがメモリ節約のため列指定をできるようにしておく)
    public int getLastRowNumberWithValue(String worksheetName, int colNum) throws IOException {
        List<List<Object>> values = getValues(worksheetName, colNum, 0, colNum, 0);
        if (values != null) {
            return values.size();
        }
        return 0;
    }

    // 値書込
    // 指定列・行が現在のワークシートの大きさを超える場合にはワークシートを必要なだけ拡張する
    // 指定位置に既存の値があれば上書きする
    public void setValues(String worksheetName, int startColNum, int startRowNum, Object[][] values) throws IOException {
        List<List<Object>> valueList = new ArrayList<>();
        for (Object[] row : values) {
            valueList.add(Arrays.asList(row));
        }
        setValues(worksheetName, startColNum, startRowNum, valueList);
    }

    public void setValues(String worksheetName, int startColNum, int startRowNum, Object[] values) throws IOException {
        setValues(worksheetName, startColNum, startRowNum, new Object[][] { values });
    }

    public void setValue(String worksheetName, int colNum, int rowNum, Object value) throws IOException {
        setValues(worksheetName, colNum, rowNum, new Object[][] { { value } });
    }

    public void setValues(String worksheetName, int startColNum, int startRowNum, List<List<Object>> valueList) throws IOException {
        ValueRange valueRange = new ValueRange().setValues(valueList).setMajorDimension("ROWS");
        String range = worksheetName + "!" + bijectiveBase26(startColNum) + startRowNum;
        // 開始位置が範囲内ならupdate、範囲外ならappend
        // (updateは開始位置が範囲外だとエラー、開始位置さえ範囲内ならその後は必要に応じて列・行を追加してくれる)
        // (appendは開始位置が範囲外でも必要に応じて列・行を追加してくれるが、開始位置が空がない場合に上書きしてくれない)
        GridProperties gridProperties = getWorksheetProperties(worksheetName).getGridProperties();
        if (startColNum <= gridProperties.getColumnCount() && startRowNum <= gridProperties.getRowCount()) {
            RequestUtil.executeWithRetry(sheetsService.spreadsheets().values().update(spreadsheet.getSpreadsheetId(), range, valueRange).setValueInputOption("USER_ENTERED"));
        } else {
            RequestUtil.executeWithRetry(sheetsService.spreadsheets().values().append(spreadsheet.getSpreadsheetId(), range, valueRange).setValueInputOption("USER_ENTERED"));
        }
    }

    // 列番号を列番号文字列に変換する
    private String bijectiveBase26(int n) {
        // https://gist.github.com/theazureshadow/4a5a032944f1c9bc0f4a より
        StringBuilder buf = new StringBuilder();
        while (n != 0) {
            buf.append((char) ((n - 1) % 26 + 'A'));
            n = (n - 1) / 26;
        }
        return buf.reverse().toString();
    }
}
src/main/java/mytestapp1/CreateCredentialFile.java
package mytestapp1;

import java.io.IOException;
import java.security.GeneralSecurityException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;

/**
 * Google認証を行い、Credentialファイルを作成する。
 */
public class CreateCredentialFile {
    private static final Logger LOGGER = LoggerFactory.getLogger(CreateCredentialFile.class);

    /**
     * Google認証を行い、Credentialファイルを作成する。<br>
     * ブラウザが起動し、Googleの許可リクエスト画面が表示される。
     *
     * @param args
     * @throws IOException
     * @throws GeneralSecurityException
     */
    public static void main(String[] args) throws IOException, GeneralSecurityException {
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();

        MyTestApp1.getGoogleService().authorize();
        LOGGER.info("Credentialファイルを保存しました。");
    }
}
src/main/java/mytestapp1/MyTestApp1.java
package mytestapp1;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;

import com.google.api.services.drive.DriveScopes;
import com.google.api.services.drive.model.File;
import com.google.api.services.sheets.v4.Sheets;
import com.google.api.services.sheets.v4.SheetsScopes;

/**
 * Googleスプレッドシートへの書き込みを行う。
 */
public class MyTestApp1 {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyTestApp1.class);

    // Application name.
    private static final String APPLICATION_NAME = "MyTestApp1";

    // Return reader for Google client secret.
    private static Reader getClientSecretReader() {
        return new InputStreamReader(MyTestApp1.class.getResourceAsStream("/google_client_secret.json"));
    }

    // Google authorization scopes required by this application.
    // If modifying these scopes, delete your previously saved credentials.
    private static final List<String> AUTHORIZATION_SCOPE_LIST = Arrays.asList(DriveScopes.DRIVE, SheetsScopes.SPREADSHEETS);

    // Directory to store Google user credentials for this application.
    private static final String CREDENTIAL_STORE_DIRECTORY = System.getProperty("user.home") + "/.google_credentials/" + APPLICATION_NAME;

    public static void main(String[] args) throws IOException, GeneralSecurityException {
        SLF4JBridgeHandler.removeHandlersForRootLogger();
        SLF4JBridgeHandler.install();

        final String folderName = "フォルダ1";
        final String spreadsheetName = "スプレッドシート1";
        final String worksheetName = "ワークシート1";

        // Build a new authorized API client service.
        GoogleService googleService = getGoogleService();
        SheetsWrapper sheetsWrapper = googleService.getSheetsWrapperWithWorksheet(folderName, spreadsheetName, worksheetName);
        int lastRowNumberWithValue = sheetsWrapper.getLastRowNumberWithValue(worksheetName, 1);

        // 値の入っている最後の行の次の行から書き込み
        Object[][] values = { { 1, "A" }, { 2.1D, "B" }, { 2.50E-3, "C" } };
        sheetsWrapper.setValues(worksheetName, 1, lastRowNumWithValue + 1, values);
        LOGGER.info("書き込みました。");
    }

    static GoogleService getGoogleService() {
        return new GoogleService(getClientSecretReader(), AUTHORIZATION_SCOPE_LIST, CREDENTIAL_STORE_DIRECTORY, APPLICATION_NAME);
    }

    private static Reader getClientSecretReader() {
        return new InputStreamReader(MyTestApp1.class.getResourceAsStream("/google_client_secret.json"));
    }
}
src/main/resources/logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE logback>
<configuration>
    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
        <resetJUL>true</resetJUL>
    </contextListener>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

3. 実行

まずCreateCredentialFileクラスを実行すると、ブラウザが起動して以下の画面が表示される
(Googleログイン済のブラウザを用意しておくと良い)

18.jpg

「許可」押下で以下の画面が表示される

19.jpg

次にMyTestApp1クラスを実行すると、書き込みが行われる

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4