20191213のJavaに関する記事は13件です。

C#とAndroidによるUDP通信で加速度センサの値を取得するアプリケーション

大まかな内容

ここでは、Androidの加速度センサの値をC#で取得し、その値によってAndroidの端末がどのように動いたかを感知するために作成している。

Androidのアプリケーション

このアプリケーションでは、三軸(X,Y,Z)の加速度センサの値をUDP通信で送る。端末が、どの軸方向に向いているかを分かりやすくるために、各軸が一定の値以上になった場合に音声が鳴るように作成した。

アプリケーションのGUI

ASUSTab K013の場合

ソースコード

ソケット通信を行うので、Androidmanifest.xmlに下記を追加する。

Androidmanifest.xml
<uses-permission android:name="android.permission.INTERNET"></uses-permission>

次に、画像と音声ファイルを扱うため、app内のresのdrawableとrawを作成し、その中に画像データと音声データを入れる。

app
 └res
   ├drawable
   │ ├en.jpg
   │ ├en_gleen.jpg
   │ └en_red.jpg
   └raw
     ├xjikum.mp3
     ├xjikup.mp3
     ├yjikum.mp3
     ├yjikup.mp3
     ├zjikum.mp3
     └zjikup.mp3
activity_main.xml
<EditText
        android:id="@+id/IP_Address"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="52dp"
        android:layout_marginEnd="16dp"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="0.0.0.0"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/Port"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="48dp"
        android:layout_marginEnd="16dp"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="8080"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/IP_Address" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="280dp"
        android:text="X:"
        android:textSize="32sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:text="Y:"
        android:textSize="32sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="24dp"
        android:text="Z:"
        android:textSize="32sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView2" />

    <TextView
        android:id="@+id/X_Data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="280dp"
        android:text="TextView"
        android:textSize="32sp"
        app:layout_constraintStart_toEndOf="@+id/textView"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/Y_Data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="TextView"
        android:textSize="32sp"
        app:layout_constraintStart_toEndOf="@+id/textView2"
        app:layout_constraintTop_toBottomOf="@+id/X_Data" />

    <TextView
        android:id="@+id/Z_Data"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="TextView"
        android:textSize="32sp"
        app:layout_constraintStart_toEndOf="@+id/textView3"
        app:layout_constraintTop_toBottomOf="@+id/Y_Data" />

    <TextView
        android:id="@+id/textView7"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginTop="230dp"
        android:text="加速度センサの値"
        android:textSize="32sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/Ran"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="87dp"
        android:layout_marginLeft="87dp"
        android:layout_marginBottom="70dp"
        android:text="通信開始"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginBottom="8dp"
        android:text="IPアドレス"
        android:textSize="24sp"
        app:layout_constraintBottom_toTopOf="@+id/IP_Address"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginLeft="16dp"
        android:layout_marginBottom="8dp"
        android:text="ポート番号"
        android:textSize="24sp"
        app:layout_constraintBottom_toTopOf="@+id/Port"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/End"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="87dp"
        android:layout_marginRight="87dp"
        android:layout_marginBottom="70dp"
        android:text="通信解除"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

    <ImageView
        android:id="@+id/LED1"
        android:layout_width="75dp"
        android:layout_height="75dp"
        android:layout_marginStart="100dp"
        android:layout_marginLeft="100dp"
        android:layout_marginBottom="150dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:srcCompat="@drawable/en" />

    <ImageView
        android:id="@+id/LED2"
        android:layout_width="75dp"
        android:layout_height="75dp"
        android:layout_marginEnd="100dp"
        android:layout_marginRight="100dp"
        android:layout_marginBottom="150dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:srcCompat="@drawable/en" />

    <TextView
        android:id="@+id/textView6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="352dp"
        android:layout_marginEnd="124dp"
        android:layout_marginRight="124dp"
        android:text="Delay"
        android:textSize="32sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <EditText
        android:id="@+id/Delay"
        android:layout_width="159dp"
        android:layout_height="49dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="40dp"
        android:layout_marginRight="40dp"
        android:ems="10"
        android:inputType="textPersonName"
        android:text="100"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView6" />

MainActivity.java
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.util.Timer;
import java.util.TimerTask;

public class MainActivity extends AppCompatActivity implements SensorEventListener{

    private SensorManager sensorManager;
    private TextView X_Data_TextView;   //加速度センサXの値
    private TextView Y_Data_TextView;   //加速度センサYの値
    private TextView Z_Data_TextView;   //加速度センサZの値
    private float data_X,data_Y,data_Z;
    private int xp,yp,zp,xm,ym,zm;
    private int data_x,data_y,data_z;
    private String Data;
    private Timer timer1,timer2;
    private mTimerTask1 timerTask1;
    private mTimerTask2 timerTask2;
    private Handler handler = new Handler();
    private Handler handler1 = new Handler();
    private long Delay;
    ImageView LED1,LED2;

    SoundPool soundPool;
    int mp3_xp,mp3_xm,mp3_yp,mp3_ym,mp3_zp,mp3_zm;


    @Override
    protected void onCreate(Bundle savedInstanceState)  {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        X_Data_TextView = findViewById(R.id.X_Data);
        Y_Data_TextView = findViewById(R.id.Y_Data);
        Z_Data_TextView = findViewById(R.id.Z_Data);

        LED1 = findViewById(R.id.LED1);
        LED2 = findViewById(R.id.LED2);
        LED1.setImageResource(R.drawable.en);
        LED2.setImageResource(R.drawable.en);

        //効果音付けるのに必要なやつ
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            soundPool = new SoundPool(5, AudioManager.STREAM_MUSIC, 0);
        } else {
            AudioAttributes attr = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build();
            soundPool = new SoundPool.Builder()
                    .setAudioAttributes(attr)
                    .setMaxStreams(5)
                    .build();
        }

        mp3_xp = soundPool.load(this, R.raw.xjikup, 1);
        mp3_xm = soundPool.load(this,R.raw.xjikum,1 );
        mp3_yp = soundPool.load(this, R.raw.yjikup, 1);
        mp3_ym = soundPool.load(this,R.raw.yjikum,1 );
        mp3_zp = soundPool.load(this, R.raw.zjikup, 1);
        mp3_zm = soundPool.load(this,R.raw.zjikum,1 );

        Button ran = findViewById(R.id.Ran);
        Button end = findViewById(R.id.End);

        ran.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(null != timer1){
                    timer1.cancel();
                    timer1 = null;
                }

                xp=yp=zp=xm=ym=zm=0;

                String delay = ((EditText)findViewById(R.id.Delay)).getText().toString();
                Delay = Long.parseLong(delay);

                timer1 = new Timer();
                timerTask1 = new mTimerTask1();
                timer1.schedule(timerTask1,0, Delay);

                timer2 = new Timer();
                timerTask2 = new mTimerTask2();
                timer2.schedule(timerTask2,0,Delay);
            }
        });

        end.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if(null != timer1){
                    timer1.cancel();
                    timer1 = null;
                }
                final String address = ((EditText) findViewById(R.id.IP_Address)).getText().toString();
                String port = ((EditText) findViewById(R.id.Port)).getText().toString();
                int Port = Integer.parseInt(port);
                String exit = "exit";
                byte buf[] = new byte[exit.length()];

                try {
                    buf = exit.getBytes("SHIFT_JIS");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }

                InetSocketAddress inetSocketAddress = new InetSocketAddress(address, Port);
                final DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, inetSocketAddress);

                AsyncTask<DatagramPacket, Void, Void> task = new AsyncTask<DatagramPacket, Void, Void>() {
                    @Override
                    protected Void doInBackground(DatagramPacket... datagramPackets) {
                        DatagramSocket datagramSocket = null;
                        try {
                            datagramSocket = new DatagramSocket();
                            datagramSocket.send(datagramPackets[0]);
                            datagramSocket.close();
                        } catch (SocketException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                        return null;
                    }

                };
                task.execute(datagramPacket);

            }
        });

    }



    @Override
    protected void onResume(){
        super.onResume();
        //Event Listener登録
        Sensor accel = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
        sensorManager.registerListener((SensorEventListener) this,accel,SensorManager.SENSOR_DELAY_NORMAL);
    }

    @Override
    protected void onPause(){
        super.onPause();
        //Event Listener登録解除
        sensorManager.unregisterListener((SensorEventListener) this);
    }

    @Override
    public void onSensorChanged(SensorEvent event){
        if(event.sensor.getType() == Sensor.TYPE_ACCELEROMETER){
            data_X= (500+event.values[0]*25);
            data_Y= (500+event.values[1]*25);
            data_Z= (500+event.values[2]*25);

            data_x = (int)data_X;
            data_y = (int)data_Y;
            data_z = (int)data_Z;
            Data = data_x + " " +
                    data_y + " " +
                    data_z;
            X_Data_TextView.setText(String.valueOf(data_x));
            Y_Data_TextView.setText(String.valueOf(data_y));
            Z_Data_TextView.setText(String.valueOf(data_z));
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy){

    }

    private class mTimerTask2 extends TimerTask{
        @Override
        public void run() {
            handler.post(new Runnable() {
                @Override
                public void run() {

                    if (data_X >= 700) {
                        LED1.setImageResource(R.drawable.en_gleen);
                        xp += 1;
                        if (xp == 1) {
                            yp=zp=xm=ym=zm=0;
                            soundPool.play(mp3_xp, 2, 2, 0, 0, 1f);
                        }
                    }else if (data_X <= 300) {
                        LED1.setImageResource(R.drawable.en);
                        xm += 1;
                        if (xm == 1) {
                            xp=yp=zp=ym=zm=0;
                            soundPool.play(mp3_xm, 2, 2, 0, 0, 1f);
                        }
                    }else if (data_Y >= 700) {
                        LED2.setImageResource(R.drawable.en_red);
                        yp += 1;
                        if (yp == 1) {
                            xp=zp=xm=ym=zm=0;
                            soundPool.play(mp3_yp, 2, 2, 0, 0, 1f);
                        }
                    } else if (data_Y <= 300) {
                        LED2.setImageResource(R.drawable.en);
                        ym += 1;
                        if (ym == 1) {
                            xp=yp=zp=xm=zm=0;
                            soundPool.play(mp3_ym, 2, 2, 0, 0, 1f);
                        }
                    }else if (data_Z >= 700) {
                        LED1.setImageResource(R.drawable.en_gleen);
                        LED2.setImageResource(R.drawable.en_red);
                        zp += 1;
                        if (zp == 1) {
                            xp=yp=xm=ym=zm=0;
                            soundPool.play(mp3_zp, 2, 2, 0, 0, 1f);
                        }
                    }else if (data_Z <= 300) {
                        LED1.setImageResource(R.drawable.en);
                        LED2.setImageResource(R.drawable.en);
                        zm += 1;
                        if (zm == 1) {
                            xp=yp=zp=xm=ym=0;
                            soundPool.play(mp3_zm, 2, 2, 0, 0, 1f);
                        }
                    }
                }
            });
        }

    }
    private class mTimerTask1 extends TimerTask{
        @Override
        public void run(){

            handler.post(new Runnable() {
                @Override
                public void run() {
                    final String address = ((EditText) findViewById(R.id.IP_Address)).getText().toString();
                    String port = ((EditText) findViewById(R.id.Port)).getText().toString();
                    int Port = Integer.parseInt(port);
                    byte buf[] = new byte[Data.length()];

                    try {
                        buf = Data.getBytes("SHIFT_JIS");
                    } catch (UnsupportedEncodingException e) {
                        e.printStackTrace();
                    }

                    InetSocketAddress inetSocketAddress = new InetSocketAddress(address, Port);
                    final DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length, inetSocketAddress);


                    AsyncTask<DatagramPacket, Void, Void> task = new AsyncTask<DatagramPacket, Void, Void>() {
                        @Override
                        protected Void doInBackground(DatagramPacket... datagramPackets) {
                            DatagramSocket datagramSocket = null;

                            try {
                                datagramSocket = new DatagramSocket();
                                datagramSocket.send(datagramPackets[0]);
                                datagramSocket.close();
                            } catch (SocketException e) {
                                e.printStackTrace();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }

                            return null;
                        }

                    };
                    task.execute(datagramPacket);
                }
            });

        }
    }
}

C#のアプリケーション

このアプリケーションでは、UDP通信で受け取ったAndroidの加速度センサの値をrichTextBoxに記録し、各軸の値をそれぞれTextBox表示するように作成した。

アプリケーションのGUI

ソースコード

Form1.cs
using System;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Net;

namespace UDP_Server
{
    public partial class Form1 : Form
    {
        int i;
        int[] d;
        string[] D;
        private UdpClient udpClient = null; //受信用クライアント
        public string rcvMsg = null;//受信メッセージ格納用

        public Form1()
        {
            InitializeComponent();

        }

        //ボタンをクリックしたときの処理
        private void button1_Click(object sender, EventArgs e)
        {
            //UDPの接続があるときにボタンを押した場合
            //その処理をなかったことにする(少し違う)
            if (udpClient != null)
            {
                return;
            }
            ((Button)sender).Enabled = false;

            string IPString = "127.0.0.1";
            IPAddress IPAdd = IPAddress.Parse(IPString); //IPアドレスを指定
            int Port = 8080;    //ポート番号を指定

            //UdpClientを作成し、指定したポート番号にバインドする
            IPEndPoint EP = new IPEndPoint(IPAdd, Port);
            UdpClient udp = new UdpClient(EP);

            richTextBox1.BeginInvoke(
                    new Action<string>(ShowReceivedString1), "受信を開始します");

            //非同期的な受信を開始する
            udp.BeginReceive(UdpServer, udp);
            ((Button)button1).Enabled = true;
        }

        //データを受信した時の処理
        private void UdpServer(IAsyncResult ar)
        {
            UdpClient udp = (UdpClient)ar.AsyncState;
            for (;;)
            {
                //一度非同期受信を終了する
                IPEndPoint remoteEP = null;
                byte[] rcvBytes = udp.Receive(ref remoteEP);


                //受信したデータを文字列に変換
                string rcvMsg = System.Text.Encoding.UTF8.GetString(rcvBytes);

                //"exit"を受信したら終了
                if (rcvMsg == "exit")
                {
                    break;
                }

                D = rcvMsg.Split(' ');
                for(i=0;D[i]== null; i++)
                {
                    d[i] = int.Parse(D[i]);
                }


                //受信したデータをTextBoxに表示する
                string displayMsg = string.Format("{0}", rcvMsg);
                richTextBox1.BeginInvoke(
                    new Action<string>(ShowReceivedString1), displayMsg);
                X_Data.BeginInvoke(
                    new Action<string>(ShowReceivedString_x), D[0]);
                Y_Data.BeginInvoke(
                    new Action<string>(ShowReceivedString_y), D[1]);
                Z_Data.BeginInvoke(
                    new Action<string>(ShowReceivedString_z), D[2]);
            }

            rcvMsg = "終了しました";
            //UdpClientを閉じる
            udp.Close();

            richTextBox1.BeginInvoke(
                   new Action<string>(ShowReceivedString1), rcvMsg);
        }

        private void ShowReceivedString1(string str)
        {
            if (richTextBox1.Text == "")
            {
                richTextBox1.Text = str;
                richTextBox1.SelectionStart = richTextBox1.Text.Length;
                richTextBox1.Focus();
                richTextBox1.ScrollToCaret();
            }
            else {
                richTextBox1.Text = richTextBox1.Text + "\r\n" + str;
                richTextBox1.SelectionStart = richTextBox1.Text.Length;
                richTextBox1.Focus();
                richTextBox1.ScrollToCaret();
            }

        }

        private void ShowReceivedString_x(string str)
        {
            X_Data.Text = str;

        }
        private void ShowReceivedString_y(string str)
        {
            Y_Data.Text = str;

        }
        private void ShowReceivedString_z(string str)
        {
            Z_Data.Text = str;
        }

        private void Form1_FormClosed(object sender, FormClosedEventArgs e)
        {
            //UdpClientを閉じる
            if (udpClient != null)
            {
                udpClient.Close();
            }
        }
    }
}

今後の目標

C#に加速度センサの値で制御できるLEDを模した画像を追加しようと思っているが、そのためにはマルチスレッドでpictureBoxを操作できるようにならないといけないのでそこに力を入れていく。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Thymeleafでヘッダーフッターを共通化する方法

概要

Spring BootとThymeleafを使って作るWebアプリで、各htmlのページに共通しているヘッダーやフッターを共通化する方法

環境

  • Java8
  • Spring Boot
    • version: 2.1.9.RELEASE
  • Thymeleaf
    • version: 3.0.11.RELEASE
  • Eclipse
    • version: 2018-09 (4.9.0)

サンプル

ディレクトリ構成

Java側は省略しますが、templateと静的コンテンツの配置はこんな感じ。

dir
src/main/resources
├── static
│   ├── css
│   │   ├── common.css
│   │   └── top.css
│   └── js
│       └── datepicker.js
└── templates
    ├── common.html
    └── index.html

SpringBootはtemplates配下のHTMLからCSSやJSファイルを参照する場合、
staticがルートディレクトリになるので、このような構成にしてます。

共通部品用のcommon.htmlを作成する

common.html
<html xmlns:th="http://www.thymeleaf.org">
<!-- (1)共通にしたいheadをfragment化する -->
<head th:fragment="meta_header(title,links,scripts)">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <!-- (2)共通で読み込むCSS/JS -->
    <link rel="stylesheet" href="/css/common.css" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <!-- (3)タイトルのフォーマット -->
    <title th:text="${title}+' | sample app'">各ページタイトル | sample app</title>

    <!-- (4)各View固有で読み込むもの -->
    <th:block th:replace="${links} ?: _" />
    <th:block th:replace="${scripts} ?: _" />
</head>
<body>
    <!-- (5)body内で共通化したい部品をfragment化する -->
    <header th:fragment="header">
        <h1><a th:href="@{'/'}">sample app</a></h1>
    </header>
</body>
</html>

解説

(1) fragment名は引数を取れるのでtitle,links,scriptsを引数に設定する
(2) 各ページで共通に読み込むCSS/JSを記載する
(3) 引数のtitleを使ってth:text=""にページタイトルのフォーマットを設定する
(4) 各ページで読み込むCSS/JSをth:replaceを使って差し込む
処理なしトークン(?: _)を使うことで、${links}がnullだった場合はth:block自体が無かったものになる
(5) body内でも共通化したいものがあったらth:fragmentを使って部品化する

各html側で呼び出す

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- (1)共通headerで置き換える -->
<head th:replace="common :: meta_header('top',~{::link},~{::script})">
    <!-- (2)このページ固有で読み込むCSS/JS -->
    <link rel="stylesheet" th:href="@{/css/top.css}" />
    <script type="text/javascript" th:src="@{/js/datepicker.js}"></script>
</head>
<body>
    <!-- (3)共通部品を呼び出す -->
    <div th:replace="common :: header"></div>
    <h2>top page</h2>
</body>

解説

(1) headercommon.htmlで定義したものに引数を渡して置き換える
(2) このページ(このサンプルではindex.html)固有で読み込むファイルを定義する
(3) 共通部品もth:replace="ページ名::fragment名"で置き換える

参考

Spring Bootでヘッダ・フッタの共通化する方法
Tutorial: Using Thymeleaf

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java(Wicket-Spring boot)で作ったシステムを学内サーバにデプロイする方法

Javaシステムのデプロイ方法を書きました

ぜひ、なにか修正すべき箇所があればご指摘お願いいたします。
自分自身、経験が浅いため、知見がほしいです。:bow_tone1:

こちらのgithubにあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Elasticsearch 7.0から、OpenJDKがバンドルされるようになっていたという話

TL;DR

  • Elasticsearchは、7.0からOpenJDKをバンドルするようになっている
  • バンドルされているOpenJDKは、AdoptOpenJDKの模様
  • デフォルトでは、バンドルされているOpenJDKで起動する
  • 変更したい場合は、JAVA_HOMEを設定する

ちなみに、LogstashにはOpenJDKがバンドルされていません

ドキュメントとリリースノート

CentOSにyumでElasticsearchをインストールした時に、インストールしたパッケージ内部で持っているJDKで起動しているのに、ふと気づきました。

ドキュメントを読んでみると、どうやらOpenJDKをバンドルしているようです。

Install Elasticsearch with RPM

Elasticsearch includes a bundled version of OpenJDK from the JDK maintainers (GPLv2+CE). To use your own version of Java, see the JVM version requirements

7.0からみたいですね。

Release Notes/ Elasticsearch version 7.0.0

Bundle java in distributions

Pull Requestを見ると、セットアップの手間、古いJDKが入っている環境の場合に引き起こされる問題を嫌ったようですね。

Bundle java in distributions

自分で用意したJDKを使用する場合は、環境変数JAVA_HOMEに指定すればよいみたいです。

Set a custom Java path to be used.

Configuring Elasticsearch

The bundled Java Development Kit used to run Elasticsearch. Can be overriden by setting the JAVA_HOME environment variable in /etc/sysconfig/elasticsearch

Directory layout of RPM

確認してみる

実際に、確認してみましょう。

今回の環境は、こちら。

$ cat /etc/redhat-release 
CentOS Linux release 7.6.1810 (Core)

Elasticsearchをインストールする

まずは、ドキュメントに沿ってElasticsearchをインストールしてみます。

Install Elasticsearch with RPM

$ sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
$ sudo vim /etc/yum.repos.d/elasticsearch.repo
$ sudo yum install --enablerepo=elasticsearch elasticsearch

作成した/etc/yum.repos.d/elasticsearch.repoの中身は、こちら。

[elasticsearch]
name=Elasticsearch repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=0
autorefresh=1
type=rpm-md

Elasticsearchを起動。

$ sudo systemctl start elasticsearch

ここで、Elasticsearchの起動に使われているJavaを見てみます。

$ ps -ef | grep java | grep -v grep
elastic+ 24025     1 39 05:49 ?        00:00:23 /usr/share/elasticsearch/jdk/bin/java -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.allocator.numDirectArenas=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.locale.providers=COMPAT -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Djava.io.tmpdir=/tmp/elasticsearch-5336255756681085720 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/elasticsearch -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log -Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m -XX:MaxDirectMemorySize=536870912 -Des.path.home=/usr/share/elasticsearch -Des.path.conf=/etc/elasticsearch -Des.distribution.flavor=default -Des.distribution.type=rpm -Des.bundled_jdk=true -cp /usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch -p /var/run/elasticsearch/elasticsearch.pid --quiet

ElasticsearchにバンドルされているJavaで動作しています。

バンドルされているのは、AdoptOpenJDKみたいですね。

$ /usr/share/elasticsearch/jdk/bin/java --version
openjdk 13.0.1 2019-10-15
OpenJDK Runtime Environment AdoptOpenJDK (build 13.0.1+9)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 13.0.1+9, mixed mode, sharing)

しかも、13…。

サポートされている範囲ではありますが、LTSなOpenJDKを使いたい人もいるでしょう。

Support Matrix / Product and JVM

Elasticsearchが使うJavaを切り替える

ここで、バンドルされているAdoptOpenJDKではなく、自前でインストールしたOpenJDKに切り替えてみます。

自分でOpenJDKをインストール。

$ sudo yum install java-11-openjdk-devel
$ java --version
openjdk 11.0.5 2019-10-15 LTS
OpenJDK Runtime Environment 18.9 (build 11.0.5+10-LTS)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.5+10-LTS, mixed mode, sharing)

このOpenJDKは、このままではElasticsearchに使ってもらえないので、設定を変更します。

こちらを見て、JAVA_HOME/usr/lib/jvm/java-11-openjdkで指定することにしましょう。

Directory layout of RPM

$ ll /usr/lib/jvm
total 0
lrwxrwxrwx. 1 root root 26 Dec 13 06:00 java -> /etc/alternatives/java_sdk
lrwxrwxrwx. 1 root root 29 Dec 13 06:00 java-11 -> /etc/alternatives/java_sdk_11
lrwxrwxrwx. 1 root root 37 Dec 13 06:00 java-11-openjdk -> /etc/alternatives/java_sdk_11_openjdk
drwxr-xr-x. 8 root root 97 Dec 13 06:00 java-11-openjdk-11.0.5.10-0.el7_7.x86_64
lrwxrwxrwx. 1 root root 34 Dec 13 06:00 java-openjdk -> /etc/alternatives/java_sdk_openjdk
lrwxrwxrwx. 1 root root 21 Dec 13 06:00 jre -> /etc/alternatives/jre
lrwxrwxrwx. 1 root root 24 Dec 13 06:00 jre-11 -> /etc/alternatives/jre_11
lrwxrwxrwx. 1 root root 32 Dec 13 06:00 jre-11-openjdk -> /etc/alternatives/jre_11_openjdk
lrwxrwxrwx. 1 root root 40 Dec 13 06:00 jre-11-openjdk-11.0.5.10-0.el7_7.x86_64 -> java-11-openjdk-11.0.5.10-0.el7_7.x86_64
lrwxrwxrwx. 1 root root 29 Dec 13 06:00 jre-openjdk -> /etc/alternatives/jre_openjdk

/etc/sysconfig/elasticsearchを設定。

# Elasticsearch Java path
#JAVA_HOME=

JAVA_HOMEに、使いたいJavaを設定します。

# Elasticsearch Java path
JAVA_HOME=/usr/lib/jvm/java-11-openjdk

Elasticsearchを再起動。

$ sudo systemctl restart elasticsearch

これで、自分でインストールしたOpenJDKが使われるようになりました。

$ ps -ef | grep java | grep -v grep
elastic+ 24695     1 83 06:08 ?        00:00:23 /usr/lib/jvm/java-11-openjdk/bin/java -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dio.netty.allocator.numDirectArenas=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Djava.locale.providers=COMPAT -Xms1g -Xmx1g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -Djava.io.tmpdir=/tmp/elasticsearch-586651067490676884 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/elasticsearch -XX:ErrorFile=/var/log/elasticsearch/hs_err_pid%p.log -Xlog:gc*,gc+age=trace,safepoint:file=/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m -XX:MaxDirectMemorySize=536870912 -Des.path.home=/usr/share/elasticsearch -Des.path.conf=/etc/elasticsearch -Des.distribution.flavor=default -Des.distribution.type=rpm -Des.bundled_jdk=true -cp /usr/share/elasticsearch/lib/* org.elasticsearch.bootstrap.Elasticsearch -p /var/run/elasticsearch/elasticsearch.pid --quiet
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Codecademy 何?

初めに

この記事は、SLP KBIT Advent Calendar 2019 の13日目の記事です。

プログラミングを自分でやってみたいときこのウェブサイトは役に立つかもしれない。

どうして Codecademy❓

良い点
○ たくさん言語を学ぶことができる
○ 説明が詳しい
○ シラバスに沿って勉強する
悪い点
○ 日本語に翻訳しなければならない
○ 無料ではない

やってみよう

1.言語を選択する
Capture.JPG
Capture2.JPG
2.説明を読む
Capture5.JPG
3.実際にやってみよう
Capture4.JPG
4.問題あったら教えてくれる
Capture6.JPG

最後に

― 色々な言語やってみたぜひやってください
― わかりやすく勉強できる
ー お金がかかるだけどとても役に立つ

参考

codecademy.com

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ジェネリクスと関数型インターフェースを活用した便利メソッドの作り方

便利メソッドの作成

多人数でチームを組んで開発を行っていると共通で行いたい処理などがでてきます。
例えば文字列操作や日付操作などは色々なシーンで使われますが、その際には便利なライブラリを利用したり、便利なメソッドが集まったクラスなどを作成したりするかと思います。

特定のクラスに対する処理であればある程度簡単に作れてしまいますが、多人数で開発する場合には様々なクラスに対して共通の処理を行いたいといった要望がでてきます。

その際に利用すると便利なものが「ジェネリクス」や「関数型インターフェース」といったものです。
この2つについてはなんとなく知っていても実際にどう使っていいかわかりにくという人もいるかと思います。

そこで今回は実際に便利メソッドを作る工程を追いながら実際にどのように使うのかを紹介してみたいと思います。
※ 作りながら雰囲気をつかんでもらうため「ジェネリクス」や「関数型インターフェース」の紹介は軽めです

ジェネリクス (総称型) とは

データ型の定義に対してパラメータのように渡すことで、同じような構造をもつプログラムを複数のデータ型に対応できるようにしたものです。

例えば JDK1.4 までは List を取り扱う時には型指定がなく Object としてなんでも入るようになっていました。

List list = new ArrayList();
list.add("string");
list.add(Integer.valueOf(100));

型が制限されておらず文字だけを入れたい場合はプログラムを作る人が気を付けて作るしかありませんでした。
ジェネリクスが出てからは型を指定できるようになり以下のように書けるようになりました。

List<String> list = new ArrayList<>();
list.add("string1");
list.add("string2");

Java のAPI仕様を見るとそれぞれ List<E>ArrayList<E> と記載されています。
この E の部分がジェネリクスといわれ、同じ構造のプログラムに対して任意の型を設定できるようになっています。

関数型インターフェースとは

関数型インターフェースを簡単に表現すると Java 8 から導入された「メソッド参照」や「ラムダ式」を代入できるインターフェースのことです。

例えば Java で良く作る getter, setter を関数型インターフェースで表すと以下のようになります。

メソッド インターフェース メソッド
getter Supplier<T> T get​()
setter Consumer<T> accept(T t)

Supplier は引数が無く任意の型を返すインターフェースです。
同じような仲間に、任意ではなくプリミティブを返す IntSupplier, BooleanSupplier なども存在します。

Consumer は引数を受け取り戻り値がないインターフェースです。
同じような仲間に、任意ではなくプリミティブを渡す IntConsumer, DoubleConsumer なども存在します。

単体テストのための便利メソッドを作ってみる

ジェネリクスや関数型インターフェースを使ったことない人にとっては上記のような説明だけでは難しいかと思います。
そこで実際に便利メソッドを作りながら簡単に使い方を解説していきます。

準備

まずは生徒クラスを作成したとします。
生徒クラスは学籍番号、生徒名、年齢を持ち setter, getter が定義されているとします。

public class Student {
    /** 学籍番号 */
    private String code;

    /** 生徒名 */
    private String name;

    /** 年齢 */
    private int age;

    //... setter, getter 略 ...
}

単体テストのデータ準備のため、テストデータとして3人の生徒を作りListに詰めます。

final Student student1 = new Student();
student1.setCode("S01001");
student1.setName("山田太郎");
student1.setAge(20);

final Student student2 = new Student();
student1.setCode("S02001");
student1.setName("山田次郎");
student1.setAge(19);

final Student student3 = new Student();
student1.setCode("S03001");
student1.setName("山田三郎");
student1.setAge(18);

final List<Student> students = new ArrayList<>();
students.add(student1);
students.add(student2);
students.add(student3);

3人分であればまだ見れなくないですが、これが10人、20人と増えてくると大変なことになります。

List の作成を簡易化するメソッド作成

まずは List の作成を簡易化するメソッドを作成してみました。

public <T> List<T> createInstanceList(Supplier<T> supplier, int size) {
    return IntStream.range(0, size)
            .mapToObj(i -> supplier.get())
            .collect(toList());
}

このプログラムを少しずつ解説していきます。

public <T> List<T> createInstanceList(Supplier<T> supplier, int size) 

まずはメソッドの宣言についですが、 <T> とジェネリクスが定義されています。
ジェネリクスを使っているため任意のクラスに対して使えるメソッドとなりました。

戻り型は List<T> となっているため任意の型の List が返却されます。

第一引数は Supplier<T> が宣言されているため、ジェネリクスの紹介にもあった通り任意の型を返すためだけのインターフェースを受け取れます。

第荷引数は int で List のサイズを指定できるようにしています。

IntStream.range(0, size)

次に返却する値ですが、まず IntStream.range(0, size) でストリームを作成しています。
range を使用しているため 0 から size - 1 まで繰り返します。
size が 3 の場合 0, 1, 2 という数値が繰り返されます。

.mapToObj(i -> supplier.get())

先ほど IntStream から 0, 1, 2 が渡されてきますがそれを無視して supplier.get() を使用しています。
これは引数で渡されてきた関数インターフェースの取得メソッドを呼び結果を返しています。

.collect(toList());

最後に数値の繰り返し回数 (例では3回) 関数型インタフェースから受け取った値を List に詰めて返します。

List 簡易化メソッドを適用

説明が難しい部分があるため実際にこのメソッドを使うとどう変わるか最初のコードを変更してみます。

final List<Student> students = createInstanceList(Student::new, 3);
students.get(0).setCode("S01001");
students.get(0).setName("山田太郎");
students.get(0).setAge(20);
students.get(1).setCode("S02001");
students.get(1).setName("山田次郎");
students.get(1).setAge(19);
students.get(2).setCode("S03001");
students.get(2).setName("山田三郎");
students.get(2).setAge(18);

生徒のインスタンス作成とListへのセットが無くなりました。

final List<Student> students = createInstanceList(Student::new, 3);

最初のこの行で List の作成と内部のインスタンス作成を同時にやっています。
Student::newコンストラクター参照 といわれ、new Student() の結果を返す関数型インターフェース を返します。

コンストラクタ参照が Supplier として扱われ、先程作成したメソッドの第一引数としてわたせるようになります。
先程のメソッドにあった supplier.get() で new Student() の結果が返ることになります。

List 内のオブジェクトに値をセットするメソッド作成

先程のメソッドを作成しコードは短くなりましたが値をセットするところがまだ煩雑です。
そのため以下のようなコードを書いてみます。

public <T, U> void setValues(List<T> obj, BiConsumer<T, U> biConsumer, U... values) {
    for (int i = 0; i < obj.size(); i++) {
        biConsumer.accept(obj.get(i), values[i]);
    }
}

※ 解説用のため例外処理は記載していません

このプログラムも少しずつ解説していきます。

public <T, U> void setValues(List<T> obj, BiConsumer<T, U> biConsumer, U... values)

まずはメソッドの宣言についですが、<T, U> とジェネリクスが定義されています。
最初のメソッドとは違い2つの任意の型を取り扱います。

戻り型は void のため値を返却しません。

第一引数は List obj となっているため値をセットしたい任意の型のリストを渡します。

第二引数は BiConsumer biConsumer となっています。
こちらは後に解説します。

第三引数は U... values となっており可変引数を渡せるようになっています。
この引数で List 内のオブジェクトに対してセットしたい値を可変でしていできます。

値セット用メソッドを適用

こちらも実際にこのメソッドを使うとどう変わるかコードを変更してみます。

final List<Student> students = createInstanceList(Student::new, 3);
setValues(students, Student::setCode, "S01001", "S02001", "S03001");
setValues(students, Student::setName, "山田太郎", "山田次郎", "山田三郎");
setValues(students, Student::setAge, 20, 19, 18);

かなりコードが短くなり見通しが良くなりました。

第一引数に 値をセットしたい 生徒のリスト をセットしています。

第二引数に setter のメソッド参照 を渡しています。
このメソッド参照が先程作成したメソッドの BiConsumer<T, U> として渡されて、 biConsumer.accept(...) で呼び出されます。

第三引数以降は 可変引数 のため、セットしたい値を並べます。

クラスメソッド参照とインスタンスメソッド参照

ここで一つ疑問があります。
setter メソッドは引数が1つで戻り値がないため通常は Consumer<T> を使うはずです。
ではなぜ今回 BiConsumer という2つの引数をとる関数型インターフェースを使うかを簡単に解説します。

例えば引数を Conumer<T> を使用した場合以下のようなプログラムを作成することができます。

public <T> void setValue(Consumer<T> consumer, T value) {
    consumer.accept(value);
}

使い方はこうです。

Student student = new Student();
setValue(student::setCode, "S01001");

BiConsumer を使った時との違いは以下です。

インターフェース メソッド参照
BiConsumer<T, U> Student::setCode
Consumer<T> student::setCode

BiConsumer を使った時にはクラスのメソッド参照、Consumerを使った時には インスタンスのメソッド参照 を使っています。

今回 List 内のインスタンスに対して値をセットしたいため、List 内部のインスタンスごとにメソッド参照を渡すのは意味がありません。
そこで クラスのメソッド参照 を渡して List の中にあるインスタンス毎に setter を呼ぶようにしています。

まとめ

今回作成したメソッドは以下2つです。

public <T> List<T> createInstanceList(Supplier<T> supplier, int size) {
    return IntStream.range(0, size)
            .mapToObj(i -> supplier.get())
            .collect(toList());
}

public <T, U> void setValues(List<T> obj, BiConsumer<T, U> biConsumer, U... values) {
    for (int i = 0; i < obj.size(); i++) {
        biConsumer.accept(obj.get(i), values[i]);
    }
}

私は普段たくさんの学生と一緒に開発の仕事を行っています。
スキルの違いも大きいためできる限り同じようなプログラムを書けるようになったり、開発効率をあげるためにも便利なメソッドの作成は必要不可欠です。

簡単な便利メソッドはすぐに作れますが、できるだけ汎用的なものを作ろうとすればするほど、今回紹介したジェネリクスや関数型インターフェース等を知っているとより便利なものが作れるようになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Tomcatサーバーの構成を公開できませんでした。

自己紹介

みなさん初めまして。Falchanと申します!
今年の4月から未経験からJavaの学習を始めてるバチバチの初心者です(^^♪
4月からやってるというもの、壁に当たりすぎて実質1,2ヶ月分の学習しか出来ていない気がしますが(~_~;)
そんなことはさておき、今更ながらアウトプットもかねて自分と同じ未経験の方の力になれたらと思い記事を書かせていただきます。
どこか間違いがあったり、ご意見、アドバイスなどがありましたらコメントしていただけると幸いです。
いいねも頂けると嬉しいです(小声)
前置きが長くなってしまいましたが本題へ...

Javaを使用してWebアプリケーションを作成し、実行の際に発生したエラーです。

実行環境

  • 【Spring Tool Suite 4】 Version: 4.4.2.RELEASE
  • Apache-Tomcat Version: 9.0.27
  • JRE Java8

実行環境を書く理由はそれぞれのソフトの互換性があるため、バージョンが違うだけでうまく実行できないという状況が発生します。
なので、「上記に記載しているソフト、バージョンだと動いたよ」という証拠にもなるため、記載しています。

サーバーで実行した際に表示されたエラー

エラーメッセージ.png

今回のエラーが発生する原因としては、server.xmlに記述されている内容で/BBSという文字列が2つ以上存在しているということです。

簡単に言うと、名前が被ってるから別々の名前にして~!!!というtomcatさんからのお願いだと思ってください。
tomcatさんからすると、同じ名前が2つ以上あると「え。。。どっちを実行したらいいの??」と混乱している状態になりますので(^^;)
別々の名前にしてあげましょう!

※"/BBS"という部分は読者の方の環境によって変わります。(作成したプロジェクトの名前がこの部分にあたります。)

解決方法

というわけで、別々の名前にする方法を解説していきます!

1.プロジェクトエクスプローラー内のServersという名前のプロジェクトを開いてください。
2.中に入っているserver.xmlというファイルを右クリック > 次で開く > テキストエディター の順番で操作してください。

server.xml設定.png

すると、意味の分からない文字列がたくさん記述されたソースが表示されると思いますので、一番下までスクロールしちゃってください!
すると、文字の頭に<Contextという文字が記述されている一文が見つかります。
おっと?赤線で囲われているpath="/BBS"が2つありますね...

そうです。これが今回のエラーが発生している原因!
名前が被っちゃってるわけですよ!
※ちなみに、私は見えやすくするために改行していますが、デフォルトでは全て1行に記述されています。
こんな感じで記述されています⇒ <Context docBase..省略../><Context docBase..省略../>

server.xml記述内容.png

なのでどちらかの名前を変えちゃえば解決です(^▽^)/

ってことで変更すると...
server.xml変更後.png

こんな感じです!
下の方にある"/sample"のsampleの部分はプロジェクト名に合わせてください。
合わせることで、この記述がどのプロジェクトに関する記述だな!って一目で分かります。
自分が分かりやすくするためでもありますし、他の人がすぐ分かるようにという気遣いでもあります!
※""内の / は必ずつけて文字だけ変更するようにしてください。

path="/sample"
path="/hoge"
path="/hello"

変更が完了し、保存をかけたらエラーは発生しない状態になっていませんか?
確認してみてください。

ちなみに、このpath=""の中を変更することでアクセスするURLの中身が変わっています。

URL結果.png
少し見づらいかもしれませんが...
URLの後ろの方が先ほど私が変更したpath="/sample"になっているということが分かるかと思います。
試しに自分で他の文字に変更して確かめてみてください!

最後まで読んでいただきありがとうございます!
読みづらいなど思った方申し訳ございません。
練習して改善していこうと思いますので、ご意見を頂けると幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Java FragmentでCameraXライブラリ使ってみた

Google I/O 2019で紹介されたCameraXをJavaでFragment上で試してみたので簡単にご紹介。

準備

公式のチュートリアルをみながら進めていくと楽チンなのでこちらを見ながら進めていきます。

プロジェクト作成や、dependenciesの追加などはチュートリアル通りに進めるだけなので本記事では割愛。

プレビュー用のビューを追加

Fragmentで表示するlayout.xmlにTextureViewを追加します。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF">

    <TextureView
        android:id="@+id/view_finder"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <RelativeLayout
        android:id="@+id/camera_header"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#456700"
        android:padding="15dp">

        <ImageButton
            android:id="@+id/camera_back"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/common_backbtn"
            android:layout_alignParentLeft="true"/>

    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/camera_bottom_control"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:background="#456700"
        android:padding="20dp">

        <Button
            android:id="@+id/capture_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="撮影する"
            />

    </RelativeLayout>

</RelativeLayout>

見た目はこんな感じ↓
スクリーンショット 2019-12-12 15.20.00.png

TextureViewは全面に配置してその上に画面要素を重ねて表示しています。

カメラを起動

プレビューの設定を行ってフラグメントのライフサイクルとバインドするだけです。

CameraX.bindToLifecycleで作成したPreviewConfigをバインドするだけなので簡単ですね。

public void startCamera() {
    PreviewConfig.Builder builder = new PreviewConfig.Builder();

    builder.setTargetResolution(new Size(App.width, App.height));
    PreviewConfig previewConfig = builder.build();

    preview = new Preview(previewConfig);
    preview.setOnPreviewOutputUpdateListener(new Preview.OnPreviewOutputUpdateListener() {
        @Override
        public void onUpdated(@NonNull Preview.PreviewOutput output) {
            ViewGroup viewGroup = (ViewGroup) viewFinder.getParent();
            viewGroup.removeView(viewFinder);
            viewGroup.addView(viewFinder, 0);

            viewFinder.setSurfaceTexture(output.getSurfaceTexture());
            updateTransform();
        }
    });
    CameraX.bindToLifecycle(this, preview);
}

public void updateTransform() {
    Matrix matrix = new Matrix();

    float centerX = viewFinder.getWidth() / 2f;
    float centerY = viewFinder.getHeight() / 2f;

    float rotationDegrees;
    switch (viewFinder.getDisplay().getRotation()) {
        case Surface.ROTATION_0:
            rotationDegrees = 0f;
            break;
        case Surface.ROTATION_90:
            rotationDegrees = 90f;
            break;
        case Surface.ROTATION_180:
            rotationDegrees = 180f;
            break;
        case Surface.ROTATION_270:
            rotationDegrees = 270f;
            break;
        default:
            return;
    }

    matrix.postRotate(-rotationDegrees, centerX, centerY);
    viewFinder.setTransform(matrix);
}

基本的にはこれだけ書いて、カメラのパーミッション取得後にstartCameraを呼び出せばプレビューが表示されます。

カメラのキャプチャ

カメラのプレビューをするだけじゃ何にも使えないんでcapture_buttonが押されたらその瞬間の画像をキャプチャしてみましょう。

上記startCameraメソッドのバインド前に下記コードを追加します。

ImageCaptureConfig.Builder imageBuilder = new ImageCaptureConfig.Builder();
imageBuilder.setTargetResolution(new Size(App.width, App.height));
imageBuilder.setCaptureMode(ImageCapture.CaptureMode.MAX_QUALITY);
imageBuilder.setFlashMode(FlashMode.ON);
ImageCaptureConfig imageCaptureConfig = imageBuilder.build();
imageCapture = new ImageCapture(imageCaptureConfig);

画像キャプチャの設定をしています。
ここではFlashをONにしてクオリティも最大にしています。
ImageCaptureConfigを作成したら、バインドメソッドにImageCaptureConfigも追加しましょう。

CameraX.bindToLifecycle(this, preview, imageCapture);

これで準備は完了です。

imageCapture.takePicture()を呼べばキャプチャができるのでボタンにイベントを設定しましょう。

btnCamera.setOnClickListener((View v) -> {

    imageCapture.takePicture(CameraFragment.this, new ImageCapture.OnImageCapturedListener() {
            @Override
            public void onCaptureSuccess(ImageProxy imageProxy, int rotationDegrees) {

            }

            @Override
            public void onError(
                @NonNull ImageCapture.ImageCaptureError imageCaptureError, @NonNull String message,
                                @Nullable Throwable cause) {

            }
        });
    }
});

takePictureの引数にはExecutorを求められるのでここではFragmentにExecutorをimplementsして写真撮影後の処理をUiThreadで処理を実行できるようにしておきます。

public class CameraFragment implements Executor {

    @Override
    public void execute(Runnable command) {
        if(getActivity() != null){
            getActivity().runOnUiThread(command);
        }
    }
}

撮影ボタンを押して撮影に成功するとonCaptureSuccess(ImageProxy imageProxy, int rotationDegrees)が呼ばれてimageProxyから画像が取得できました!

感想

いやあ簡単ですね...

Camera2でゴリゴリ書いてたのはなんだったのか。

カメラの実装地味に大変なのでこういうの待ってたの一言です。

ハマりポイント

さて、非常に簡単そうに記載してきましたが、いくつかハマりポイントがあったのでご紹介していきます。

①シャッターオンが鳴らない!

シャッター音が鳴らないんですよ。はい。

キャプチャ設定でなんか設定できるかなと探したんですが、どこにもないんですよね...

何としても鳴らしたい人は無理やりこう。

MediaActionSound sound = new MediaActionSound();
sound.play(MediaActionSound.SHUTTER_CLICK);

②ImageProxyどうやってBitmapに変換すんの?

はい、これです。

public Bitmap imageProxyToBitmap(ImageProxy image, int rotationDegrees) {
    ImageProxy.PlaneProxy planeProxy = image.getPlanes()[0];
    ByteBuffer buffer = planeProxy.getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);

    Matrix mat = new Matrix();
    mat.postRotate(rotationDegrees);

    if (rotationDegrees != 0) {
        return rotate(BitmapFactory.decodeByteArray(bytes, 0, bytes.length), rotationDegrees);
    } else {
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    }
}

public Bitmap rotate(Bitmap in, int angle) {
        Matrix mat = new Matrix();
        mat.postRotate(angle);
        return Bitmap.createBitmap(in, 0, 0, in.getWidth(), in.getHeight(), mat, true);
}

③TextureViewの配置の仕方

Q.レイアウトファイルでTextureViewを全面においてるのなんで?プレビュー領域に合わせておいちゃダメなの?

A.あえて全面においてます。

PreviewサイズとTextureViewのサイズ縦横比が一致しないとプレビューが歪んで表示される問題や、画面サイズに対して小さいプレビュー領域の画面を作成した場合、TextureViewをプレビュー領域に合わせて作っていると撮影したものがそのTextureView縮小されて表示される為プレビューが非常に見えづらくなってしまいます。

TextureViewを全面においておけば上記問題は全て解決され、普段使っている全面のカメラと同じ感覚で撮影できるので違和感なく撮影できちゃいます。

ユーザから見えてる領域だけの画像が欲しければ、撮影後に切り抜けばOKです。

終わりに

CameraXはまだアルファ版なのでリリース版ではインターフェースが変わる可能性があるので注意が必要です。

またここでは紹介していませんが、デバイスに固有のベンダー効果(ピンぼけ、HDR、夜景)なんかも簡単に使用できるAPIが用意されているようなのでそちらも今後試してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelとSpringBootでDIコンテナを利用してみる

はじめに

これはユアマイスターAdventCalendar2019の13日目の記事です。
(社会人になってから学んだことをアウトプットする記事になります。)

今回の経緯

社会人になってからDI(依存性の注入)という概念を知りました。
WEBサービスの開発を行う際に、フレームワークを利用する場面は多々ありましたが、主に利用していたCake PHPではDIという概念は出ていなかったと記憶しています。
(記憶違いだったらすみません。)

DIは、インスタンスをnewで作成して利用するのではなく、DIコンテナやサービスコンテナ呼ばれるもの(SpringBootではDIコンテナ、Laravelではサービスコンテナと呼ばれます)を利用して、あらかじめ登録されたインスタンスを利用します。

今回は、業務で利用しているSpringBootでのDIコンテナの利用、最近独学で学んでいるLaravelでのサービスコンテナを利用してみるというテーマで記事を書いてみたいと思います。

SpringBootの場合

コンストラクターインジェクションを利用する

ユアマイスターアドベントカレンダー2019 の7日目の記事で書いたコードを用いて書いていきます。
SpringBootで動的にDBを切り替えてみる
https://github.com/Masaki-Ogawa/datasourceDemo

  1. PersonRepository.java
PersonRepository.java
package com.example.dataSourceDemo.domain.repositories;

import com.example.dataSourceDemo.domain.models.Person;
import org.springframework.data.jpa.repository.JpaRepository;

@Repository
public interface PersonRepository extends JpaRepository<Person, Integer> {

}

JpaRepositoryを継承したRepositoryクラス

  1. PersonServiceImpl.java
PersonServiceImpl.java
package com.example.dataSourceDemo.domain.services;

import com.example.dataSourceDemo.annotations.DataSource;
import com.example.dataSourceDemo.annotations.DataSource.DataSourceType;
import com.example.dataSourceDemo.domain.models.Person;
import com.example.dataSourceDemo.domain.repositories.PersonRepository;
import java.util.List;
import org.springframework.stereotype.Service;

@Service
public class PersonServiceImpl implements PersonService {

  private final PersonRepository personRepository;

  public PersonServiceImpl(
      PersonRepository personRepository) {
    this.personRepository = personRepository;
  }

  /**
   * stgのDBからPersonテーブルのレコードを取得するメソッド
   * @return Personテーブルのレコード
   */
  @Override
  public List<Person> findAllPersonInStg() {
    return personRepository.findAll();
  }

  /**
   * stgのDBからPersonテーブルのレコードを取得するメソッド
   * @return Personテーブルのレコード
   */
  @DataSource(value = DataSourceType.PROD)
  @Override
  public List<Person> findAllPersonInProd() {
    return personRepository.findAll();
  }
}

ここでDIを行っています。

具体的には、

private final PersonRepository personRepository;

  public PersonServiceImpl(
      PersonRepository personRepository) {
    this.personRepository = personRepository;
  }

の部分でコンストラクターインジェクションによるDIを行っています。
@Service@Repositoryというアノテーションを利用することにより、DIコンテナに登録されます。
利用するには上記のように、コンストラクターインジェクション等を利用して、インスタンスを作成します。

参考
Spring Framework 要点まとめ ~ DIについて

LaravelでのDI

サービスプロバイダーを利用する

作成したサービスをサービスコンテナ登録するためのサービスプロバイダーを作成します。
今回はDemoServiceというサービスクラスを作成しました。

AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->bind('App\Services\DemoService');
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

上記のようにサービスを登録します。

DemoService.php
class DemoService {
    public function show() {
        echo "何かを表示します";
    }
}

サービスクラスは今回は適当ですが、何か文字を出力するメソッドのみ実装します。

DemoController.php
class MessageController extends Controller
{
    protected $demoService;

    public function __construct(DemoService $demoService)
    {
        $this->demoService = $demoService;
    }

    public function index(Request $request) {

        return $this->demoService->show();
    }
}

このようにこちらもコンストラクターインジェクションを利用して、こちらもDIを行います。

終わりに

個人的には、@Service@Controller等のアノテーションで、DIコンテナに登録できるSpringBootの方が利用しやすいと感じています。
Java、SpringBootを利用してやはり、アノテーションの強力さに気づかされる場面が多々あります。

また、7日目のアドベントカレンダーで書きましたが、それぞれのフレームワークに良さや悪さがあり、多数のフレームワークに触るという経験は、今後何かものを作るときに、「どんなものを採用すれば、そのプロダクトにとって一番良いのか?」という判断材料になると思います。

今後も、業務、業務外を含めて触れていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

日付と時刻

個人的な勉強のためにまとめたものです

日付と時刻

Java SE 8から日付や時刻を扱うための新しいAPIが追加された

クラス

LocalDate

日付を管理するクラス

LocalTime

タイムゾーンなし時刻を管理するクラス
時間は、ナノ秒の精度まで表すことができる

DateTimeFormatter

指定した日付パターンに変更するクラス
「事前定義されている標準フォーマッター」、「ローカル固有のフォーマッター」、「カスタムパターンを使ったフォーマッター」の3種類がある(より複雑なフォーマッターはDateTimeFormatterBuilderによって提供)

Duration

時刻の差を管理するクラス

Period

日付の差を管理するクラス

ZonedDateTime

タイムゾーンあり時刻を管理するクラス

メソッド

ofメソッド

日付けを指定してインスタンス生成するときに使うメソッド

nowメソッド

現在の時刻でインスタンス生成したいときに使うメソッド

parseメソッド

文字列の日付を日時の情報に変換するメソッド。

formatメソッド

日付の情報を文字列に変換するメソッド
事前定義されている標準フォーマッターは、java.time.format.DateTimeFormatterクラスに定義されている

フォーマッター 例  
BASIC_ISO_DATE 20191213
ISO_ZONED_DATE_TIME 2019-12-13T00:00:00+09:00[Asia/Tokyo]
ISO_INSTANT 2019-12-12T15:00:00Z
ISO_DATE_TIME 2019-12-13T00:00:00

ISO_ZONED_DATE_TIMEISO_INSTANTは、ゾーン付き時刻を表すZonedDateTimeのフォーマッター

untilメソッド

LocalDateのは日付の差を、LocalTimeのは時刻の差を計算するメソッド

betweenメソッド

Periodのは日付の差を、Durtionのは時刻の差を計算するメソッド

toDays、toHours、toMinutes、toMillis、toNanosメソッド

Durtionには、日、時間、分、秒、ナノ秒というように、時間の差を取得したい単位ごとにメソッドが用意されている

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Alibaba Java Coding Guidelinesについて

はじめに

初めてQuiitaにブログを書く機会が現れたので、ここに来ました!
現在、自分の技術武器は二つがあります、JavaとAlibaba Cloud。そしてこの二つを組み合わせ、何か書きたかったです。
色々がありますが、今回Alibaba Java Coding Guidelinesについてちょっと紹介したいと思います。
何で?自分のコードもっとキレイにしたいですから!

Alibaba Java Coding Guidelinesとは?

長年にわたるAlibabaの技術チーム内部のベストプログラミングプラクティス。
様々なJavaプログラミングチームが同じルールに則ってプログラミングすることにより、コードクオリティーを高め、お互いのプログラムの再利用と理解を促進しているものです。

このGuidelineは、他のプログラマーも利用できるようGitHubで公開されており、現在2万弱の★がついています。また、アップデートされ続けています。
しかしこのドキュメントは今のところ英語中国語しか存在していません。

下記に記事の構成をメモします。

このCoding Guidelineは下記の5つに分けられます。
1. プログラミング仕様
2. 例外とログ
3. MySQLの仕様
4. プロジェクト仕様
5. セキュリティ仕様
6.
各仕様は3つのレベルに分類されます。
1. 必須
2. お勧め
3. 参照

詳細は次の通りです。
[説明]:内容説明
[正例]:コーディングと実装が近づい推奨について説明【?】
[反例]:注意事項と実際のエラー事例


具体例


プログラミング仕様

命名規則

[必須] 定数変数名は全て大文字記載、区切りにアンダースコアを用いなければなりません。
各定数変数名は、意味的に完全で明確でなければなりません。

正例:
MAX_STOCK_COUNT

反例:
MAX_COUNT


ドキュメントを読み、自身のコードと比較するのも手ですが、下記プラグインでコードをチェックすることも可能です。(自分のコードに使ったら、結構怒られました)
● PMD implementations
● IntelliJ IDEA plugin
● Eclipse plugin

試験

Alibaba Cloud AcademyにAlibaba Java Coding Guidelinesの試験があります。ドキュメントを読み、実際の知識を試すことができます。試験は無料で、合格したらバッジがもらえます。

試験のリンクはこちらです

おわりに

このベストプログラミングプラクティスは、自分のコードのクオリティーを高めるのにとても役立つものです。潜在的なコードエラーを避けて、最高のコード作れるようになりましょう!

特に私!

最後まで読んで頂き、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】OS依存な標準ライブラリを挙げてみる

はじめに

この記事はJava Advent Calendar 2019の13日目の記事です。13日の金曜日です。不吉ですね。

不吉といえば、OS依存ってなんか不吉な感じがしませんか?
たとえばWindows環境で動作確認を行った時は問題なかったけど、Linux環境で動かしたらエラーになった…なんてことがあったりします。Write once, run anywhereなJavaですが、OSごとの違いを考慮しなくていいわけではありません。
そこでこの記事では、Javaの標準ライブラリでOS依存が隠れている例をいくつか紹介してみます。

この記事における「OS依存」

一口にOS依存といっても色々なことが考えられますが、この記事ではプログラムの実行結果がOSによって変わってしまう場合のことを「OS依存」と表現することにします。
内部的にOSネイティブなAPIを呼ぶメソッドはたくさんありそうですが、どのOSでも同じ実行結果になるのであれば、この記事においてはOS依存ではないと考えます。

OS依存ってそんなに悪いの?

良いか悪いかは場合によりけりです。OS依存が原因で特定のOSでしか正常に動作しないコードになってしまう場合がある一方で、OSごとに挙動が違うほうが都合がいい場合もあります。ただ、使おうとしている標準ライブラリの仕様(OSに依存するかどうか)を把握しないと、OSに依存していて良いか悪いかの判断ができません。
そういうわけで、良いか悪いかはさておき、OS依存な標準ライブラリを私の把握している範囲内で紹介してみるというのがこの記事の趣旨です。
とはいえ、私ごときが把握している範囲なんてたかが知れていますし、そもそも全部紹介するのはボリューム的にも現実的ではないので、いくつかピックアップして紹介するような感じです。(予防線)

サンプルコードの動作環境

  • Java 13
  • macOS Mojave
  • Windows 10 Home

Linuxではめんどくさいので確認していませんが、だいたいMacと同じ結果になると思います。

本題

エンコーディング、改行コード関連

InputStreamReaderを使う場合

下記はテキストファイルから文字列を読み取って出力するサンプルコードですが、OS依存なコードが含まれています。さて、どこでしょう?

import java.io.*;

public class InputStreamReaderSample {
    public static void main(String[] args) throws Exception {

        try(var fis = new FileInputStream("hoge.txt");
            var isr = new InputStreamReader(fis);
            var br = new BufferedReader(isr)) {
            String line;
            while((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }
    } 
}

まあ、上に書いてあるじゃんという話なのですが…

お察しの通り、InputStreamReaderのコンストラクタです。ここで入力ファイルのエンコーディングを指定していません。
そうすると、どうなるのでしょうか?JavaDocを見てみると、下記のような記述があります。

デフォルトの文字セットを使うInputStreamReaderを作成します。

これだけではよくわかりませんね。「デフォルトの文字セット」とは何でしょうか。
軽くソースを追ってみましたが、現時点での実装では java.nio.charset.Charset.defaultCharset() の戻り値が「デフォルトの文字セット」のようです。手元で確認したところ、Macだと「UTF-8」、Windowsだと「windows-31j」になりました。OSによって異なるので、OS依存ということになります。

OSに依存しないコードにするにはどうすればいいでしょうか。幸い、InputStreamReaderにはエンコーディングを指定するコンストラクタもあるため、そちらを使えばよいです。
下記はUTF-8を指定する例です。

import java.io.*;
import java.nio.charset.StandardCharsets;

public class InputStreamReaderSample {
    public static void main(String[] args) throws Exception {

        try(var fis = new FileInputStream("hoge.txt");
            var isr = new InputStreamReader(fis, StandardCharsets.UTF_8); // ←ここ
            var br = new BufferedReader(isr)) {
            String line;
            while((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }
    } 
}

ちなみに、改行コードの違いで BufferedReader#readLine() はOS依存になるのではと思った方もいらっしゃるかもしれませんが、JavaDocに下記の記述があるのでOS依存ではなさそうです。(LF、CR、CR+LFのいずれも改行と見なされる)

ラインは、ライン・フィード('')、キャリッジ・リターン('\r')、キャリッジ・リターンの直後に改行、またはファイルの終わり(EOF)に到達することによって終了されるとみなされます。

BufferedReader#lines() も、 BufferedReader#readLine() をラップしたメソッドなので同様です。

OutputStreamWriterを使う場合

上記はファイル入力の例ですが、今度はファイル出力の例です。下記の例にもOS依存のコードがあります。どこがOS依存なのかは、なんとなく想像がつくでしょうか。

import java.io.*;

public class OutputStreamWriterSample {
    public static void main(String[] args) throws Exception {
        try(var fos = new FileOutputStream("hogehoge.txt");
            var osw = new OutputStreamWriter(fos);
            var bw = new BufferedWriter(osw)) {
            bw.write("にほんご");
        }
    }
}

はい、OutputStreamWriterのコンストラクタでもエンコーディングを指定していませんので、ここがOS依存のコードです。JavaDocに下記の記述があります。

デフォルトの文字セットを使うOutputStreamWriterを作成します。

InputStreamReaderの例と同じですね。これも、コンストラクタでエンコーディングを指定すればOS非依存になります。

import java.io.*;
import java.nio.charset.StandardCharsets;

public class OutputStreamWriterSample {
    public static void main(String[] args) throws Exception {
        try(var fos = new FileOutputStream("hogehoge.txt");
            var osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); // ←ここ
            var bw = new BufferedWriter(osw)) {
            bw.write("にほんご");
        }
    }
}

BufferedWriterを使う場合

上の例では改行コードを出力していませんでしたね。追加してみます。

import java.io.*;
import java.nio.charset.StandardCharsets;

public class OutputStreamWriterSample {
    public static void main(String[] args) throws Exception {
        try(var fos = new FileOutputStream("hogehoge.txt");
            var osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
            var bw = new BufferedWriter(osw)) {
            bw.write("にほんご");
            bw.newLine(); // ←ここ
        }
    }
}

しかし、この BufferedWriter#newLine() もOS依存なのです。JavaDocの記述は下記の通りです。

改行文字は、システムのline.separatorプロパティにより定義され、必ずしも単一の改行文字('\n')であるとは限りません。

line.separatorはわりとよく見かけるのでご存知の方も多いと思いますが、これは実行環境のOSで一般的に使われる改行コードを値として持つシステムプロパティです。Macだと \n 、Windowsだと \r\n になります。これに関してはむしろOS依存のほうが望ましい場合が多いような気もしますが、一応取り上げてみました。1
これをOS非依存にしたいのであれば、いくつか方法がありそうですが、 BufferedWriter#newLine() を使わずに固定値を書き込むのが素直なやり方かなと思います。

import java.io.*;
import java.nio.charset.StandardCharsets;

public class OutputStreamWriterSample {
    public static void main(String[] args) throws Exception {
        var fos = new FileOutputStream("hogehoge.txt");
        var osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
        try(var bw = new BufferedWriter(osw)) {
            bw.write("にほんご");
            bw.write("\r\n"); // ←ここ
        }
    }
}

Filesを使う場合

ここまで書いておいてアレですが、昨今ではファイル入出力はFilesクラスを使うのが一般的ではないかと思います。Filesクラスを使う場合はどうなるのでしょうか。

FilesクラスはOSごとのファイルシステムに依存するので、OS依存なメソッドがたくさんありそうですね。ただ、私はあまり把握できてないのでそこには触れないことにします。

ここで言いたいのは、少なくともファイル入出力の際のエンコーディングについては、省略時はOSに関わらずUTF-8を使用することになっているので、OSに依存しないということです。2
FilesクラスのJavaDocには、下記の記述がそこかしこにあります。

ファイルから取得したバイトは、UTF-8文字セットを使用して文字にデコードされます。

たとえばファイルから入力した文字列を出力するコードはこんな感じですね。エンコーディングをどこにも指定していませんが、どのOSでもUTF-8が使われるのでOS非依存です。

import java.nio.file.Files;
import java.nio.file.Path;

public class FilesSample {
    public static void main(String[] args) throws Exception {
        try(var lines = Files.lines(Path.of("hoge.txt"))) {
            lines.forEach(System.out::println);
        }
    }
}

OSに関わらずUTF-8を使うようにしたのは良いと思いますが、旧APIからの類推で、OS毎の「デフォルトの文字セット」が使われると勘違いしてしまう場合もあるかもしれません。これはこれで注意が必要です。

この通りエンコーディングについてはOS非依存なのですが、改行コードについてはOS依存です。たとえば Files#write(Path, Iterable<? extends CharSequence>, Charset, OpenOption...) を使うと、Listの各要素を一行としたテキストをファイルに書き込むということができます。その際に改行コードは自動的に付与されるわけですが、その改行コードはline.separatorの値になります。
JavaDocの記述は下記の通りです。

システム・プロパティ line.separatorで定義されているように、各行の終端がプラットフォームの行区切り文字で表されるファイルに順々に書き込まれます。

サンプルコードはこんな感じです。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class FilesLineSeparatorSample {
    public static void main(String[] args) throws Exception {
        var words = List.of("アリス", "ボブ", "キャロル");
        Files.write(Path.of("foobar.txt"), words, StandardCharsets.UTF_8);
    }
}

Macなら アリス\nボブ\nキャロル\n になり、Windowsだと アリス\r\nボブ\r\nキャロル\r\n になるはずです。

これをOS非依存にしたいなら、ちょっと面倒ですね。 Files#newBufferedWriter() で取得したBufferedWriterを使って改行コードを固定値として書き込むか、メモリに余裕があれば指定したい改行コードで連結した文字列を作って Files#writeString() を使うか、いっそline.separatorを書き換えてしまうか。
2番目の方法ならこんな感じでしょうか。

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.util.List;

public class FilesLineSeparatorSample {
    public static void main(String[] args) throws Exception {
        var words = List.of("アリス", "ボブ", "キャロル");
        var lineSeparator = "\r\n";
        var str = String.join(lineSeparator, words) + lineSeparator;
        Files.writeString(Path.of("foobar.txt"), str, StandardCharsets.UTF_8);
    }
}

String#getBytes()を使う場合

ファイル入出力からちょっと離れますが、エンコーディングがOSに依存する例がまだあります。

たとえば、入力値が全て全角文字かどうかを判定する下記のようなコードがあったとします。

public class StringSample {
    public static void main(String[] args) {
        var str = args[0];
        if(str.getBytes().length == str.length() * 2) {
            System.out.println("入力文字は全て全角文字です。");
        } else {
            System.out.println("入力文字に半角文字が含まれます。");
        }
    }
}

「全角文字は2バイトだから、バイト数が文字列の長さの2倍と等しければ全て全角文字のはず」という発想です。3
これはWindowsでは一応うまく動きますが、Macでは想定通りに動きません。理由はもうお察しかもしれませんが、 String#getBytes() でエンコーディングを指定しておらず、「デフォルトの文字セット」が使われるためです。
String#getBytes() はStringが内部的に持っているバイト配列を取得するメソッドではなく、「デフォルトの文字セット」を使ってエンコードした際のバイト配列を取得するメソッドなので、OSに依存します。
JavaDocでは下記のような記述になっています。

プラットフォームのデフォルトの文字セットを使用してこのStringをバイト・シーケンスにエンコード化し、結果を新規バイト配列に格納します。

ソースを軽く追ってみましたが、「プラットフォームのデフォルトの文字セット」はファイル入出力の場合と同じで、 java.nio.charset.Charset.defaultCharset() の戻り値のようです。なので、Macの場合は「UTF-8」、Windowsの場合は「windows-31j」とOS依存になります。
上のほうで「全角文字は2バイト」とかほざいてますが、UTF-8の場合は2バイトではなく3バイトです。なので、その場合は文字列の長さの3倍と比較しないと想定通りに動作しません。4
この問題を解決する方法ですが、やはりお察しのことと思いますがエンコーディングを明示すればいいです。

public class StringSample {
    public static void main(String[] args) throws Exception {
        var str = args[0];
        if(str.getBytes("windows-31j").length == str.length() * 2) {
            System.out.println("入力文字は全て全角文字です。");
        } else {
            System.out.println("入力文字に半角文字が含まれます。");
        }
    }
}

あと、書いていて気づきましたがバイト配列を渡すコンストラクタでも同様の配慮が必要ですね。あまり使わないような気はしますが。

public class StringConstructorSample {
    public static void main(String[] args) throws Exception {
        byte[] bytes = { -109, -6, -106, 123, -116, -22 };
        System.out.println(new String(bytes, "windows-31j")); // -> 日本語
    }
}

ファイルシステム関連

File#listFiles()を使う場合

指定したディレクトリの中のファイル一覧を取得するというのはよくある処理ですが、実はこれもOS依存なのをご存知でしょうか?
何かというと、ファイル一覧の並び順のことです。 File#listFiles() のJavaDocには下記の記述があります。

結果の配列の名前文字列は特定の順序にはなりません。アルファベット順になるわけではありません。

実際にこんな感じのサンプルコードでやってみました。

import java.io.File;
import java.util.Arrays;

public class FileListSample {
    public static void main(String[] args) {
        Arrays.stream(new File("hoge").listFiles())
            .forEach(System.out::println);
    }
}

結果としては、Macの場合はたしかに(人間から見ると)規則性のない並び順になるのですが、Windowsの場合はファイル名で昇順ソートしたかのような並び順になりました。理由はちゃんとは調べてないのですが、おそらく内部的にOSネイティブなAPIが呼ばれて、その取得順がそのまま最終的な出力結果になっているのだと思います。 File#listFiles() の仕様としては、内部的に取得された並び順を変えるようなことは何もしないということでしょうね。それで、WindowsのネイティブなAPIはたまたま昇順ソートしてから返すようになっていると。まあ理由はどうあれ、OS毎に結果が違うのでこれもOS依存ですね。

これをOS非依存にしたいなら、出力結果をソートすればいいでしょうか。

import java.io.File;
import java.util.Arrays;

public class FileListSample {
    public static void main(String[] args) {
        Arrays.stream(new File("hoge").listFiles())
            .sorted()
            .forEach(System.out::println);
    }
}

しかし、これはこれでOS依存のようなんですよね…
File#compareTo() のJavaDocには下記の記述があります。

このメソッドが定義する順序はベースとなるシステムに依存します。 UNIXシステムの場合、アルファベットの大文字と小文字がパス名の比較で意味を持ちます。Microsoft Windowsシステムでは意味を持ちません。

じゃあ文字列としてソートすればOS非依存になるでしょうか。

import java.io.File;
import java.util.*;

public class FileListSample {
    public static void main(String[] args) {
        Arrays.stream(new File("hoge").listFiles())
            .sorted(Comparator.comparing(File::getName))
            .forEach(System.out::println);
    }
}

まあ、そこまでする必要があるケースは稀ではないかとは思いますが。

ちなみに、 File#list() を使う場合も同様の結果になります。

Files#list()を使う場合

Filesクラスにも、もちろん同様なメソッドが用意されています。そして、Macでは不規則に見える並び順になり、Windowsだとファイル名で昇順ソートされた順番で返ってくるのも同じです。
ただ、これに関してはJavaDocにそれらしい記述がないんですよね。謎です。

import java.nio.file.*;

public class FilesListSample {
    public static void main(String[] args) throws Exception {
        try(var files = Files.list(Path.of("hoge"))) {
            files.forEach(System.out::println);
        }
    }
}

OS非依存に近づけたいなら、やはりソートすることになると思います。

import java.nio.file.*;

public class FilesListSample {
    public static void main(String[] args) throws Exception {
        try(var files = Files.list(Path.of("hoge"))) {
            files.sorted()
                .forEach(System.out::println);
        }
    }
}

終わりに

探せばまだまだあるような気がしますが、思いのほか長くなったのでこの辺で…

まとめ

基本的には、JavaDocを読めば大丈夫かなと思います。ただ、 Files#list() のようにJavaDocに書かれていない場合もあります。そういう場合はどうすればいいのか難しいところですが、この例は File#listFiles() からの類推で判明したので、こうやってOS依存な例を色々見て、知識と勘を研いていくしかないのかなと思いました。勉強あるのみですね。

参考


  1. ちなみに、ここまで触れずに何気なく使ってきた System.out.println() もline.separatorの値を改行コードとして使うのでOS依存です。システムプロパティに依存するライブラリはほとんどOS依存かもしれません。 

  2. UTF-8以外の任意のエンコーディングを指定することももちろん可能です。 

  3. エンコーディング以前に、サロゲートペアの場合はダメじゃんとかツッコミどころは色々ありそうですが… 

  4. こういうバグは実際に見たことがあります… 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Selenideを用いてブラウザ自動操作環境を5分で作成してみる

はじめに

この記事はエムスリーキャリア Advent Calendar 2019 13日目の記事です。
この記事ではSelenium WebDriverのラッパーであるSelenideを使ってブラウザ操作をサクッと試してみます。

対象読者

  • とりあえずブラウザの自動操作を試してみたい人
  • JavaでUIテストを書いてみたい人
  • ブラウザ操作を行うツールを作成したい人

開発環境の準備

それではまず開発環境の準備から始めていきます。
IDEにはeclispeを使用します。
https://mergedoc.osdn.jp/よりJava Full Editionをダウンロードし、任意の場所に解凍します。
eclipse.png

解凍が完了したら、eclipse.exeを起動します。

selenideの準備

次にGradleプロジェクトを作成します。
image.png

プロジェクトが作成されたらbuild.gradleに以下の一文を追加します。

testCompile 'com.codeborne:selenide:5.5.1'

image.png

以上で環境の準備は完了となります。

実行

最後にGradleプロジェクトを作成した際に/[プロジェクト名]/src/test/java/[プロジェクト名]/LibraryTest.javaが作成されているので、元々の処理をコメントアウトし、以下のように書き直します。

/*
 * This Java source file was generated by the Gradle 'init' task.
 */
package selenideSample;

import static com.codeborne.selenide.Selenide.*;

import org.junit.Test;

public class LibraryTest {
    @Test public void testSomeLibraryMethod() {
//        Library classUnderTest = new Library();
//        assertTrue("someLibraryMethod should return 'true'", classUnderTest.someLibraryMethod());
          open("https://www.yahoo.co.jp/");
    }
}

さて、いよいよ実行します。
LibraryTest.javaのファイルを右クリックし、JUnitテストを実行します。
image.png

実行が成功すると新しくchromeが起動され、Yahooの画面が表示されることを確認できます。(一定時間が経過するとブラウザは自動的に閉じられます)
image.png

後書き

コード上でブラウザ操作実現する環境が簡単にできました。
今回は指定したURLを開いただけでしたが、他にもテキストボックスへの入力やプルダウンの選択、チェックボックスのチェックなど一般的なブラウザ操作全般をコードで記述できるのでとても便利です。
私は業務では大量のテストデータをブラウザ上から作成するときなどに活用しています。
selenideは公式リファレンスも充実しているので是非ご活用いただければ幸いです。

参考

https://selenide.org/quick-start.html
https://qiita.com/tatesuke/items/589e30ab9b3dc7037e26

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む