GAE/J でアクセスカウンターを作った

Google App Engine でカウンターを作ってみた(再) - すぎゃーんメモの続き。というか変更。
以前にPython版で作ったGoogle App Engine上で動くアクセスカウンターを、Javaで作り直してみた。
http://gae-counter.appspot.com/
基本的な作りは同じで、

  • Google Accounts APIでユーザー認証
  • ユーザー毎に3つまでのユニークなカウンターを作成
  • カウンター毎に画像URL(PNG or JPEG)を提供
  • アクセス毎に画像の数字が増加
  • アクセス情報(時刻、UserAgentなど)を記録

という程度。ちょっと使う画像を選択できたりサイズを少しだけ変更できるようにしてみたりした。
カウンターを分散して負荷に耐えられるように、といった工夫はしてないw


ソースはGitHubで公開しています。
http://github.com/sugyan/GAE_Counter/tree/master


画像の合成はこんなカンジで。

    private Image getImage(Key key, double rate, long count, OutputEncoding encoding) throws IOException {
        NumberImage numberImage = null;
        DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
        try {
            Entity entity = datastoreService.get(key);
            numberImage = new NumberImage(entity);
        } catch (EntityNotFoundException e) {
            LOGGER.log(Level.WARNING, "", e);
            return null;
        }
        
        LinkedList<Integer> digits = new LinkedList<Integer>();
        while (count / 10 != 0) {
            digits.addFirst(Integer.valueOf((int) (count % 10)));
            count /= 10;
        }
        digits.addFirst(Integer.valueOf((int) count));
        
        // それぞれの桁に対応する画像データを取得し、imageListにCompositeを追加
        Image[] images = new Image[10];
        LinkedList<Composite> imageList = new LinkedList<Composite>();
        int xOffset = 0;
        int maxHeight = 0;
        for (Integer digit : digits) {
            if (images[digit] == null) {
                Blob blob = numberImage.getImage(digit.toString());
                Image image = ImagesServiceFactory.makeImage(blob.getBytes());
                int resizeWidth  = (int)(image.getWidth() * rate);
                int resizeHeight = (int)(image.getHeight() * rate);
                Transform resize = ImagesServiceFactory.makeResize(resizeWidth, resizeHeight);
                image = ImagesServiceFactory.getImagesService().applyTransform(resize, image);
                int height = image.getHeight();
                if (height > maxHeight) {
                    maxHeight = height;
                }
                images[digit] = image;
            }
            Composite composite = ImagesServiceFactory.makeComposite(
                    images[digit], xOffset, 0, 1.0F, Composite.Anchor.TOP_LEFT);
            imageList.add(composite);
            xOffset += images[digit].getWidth();
        }

        // imageListのCompositeデータを用いて画像を合成、出力
        int width = xOffset;
        long color = 0xFFFFFFFFL;
        ImagesService imagesService = ImagesServiceFactory.getImagesService();
        return imagesService.composite(imageList, width, maxHeight, color, encoding);
    }


アクセス情報の記録はトランザクションを使ってカウンターと関連づけて保存。
JDO、JPAは一切使わずにすべてLowLevelAPIで処理している。

    private long record(Key key, HttpServletRequest req) {
        long count = 0;
        DatastoreService datastoreService = DatastoreServiceFactory.getDatastoreService();
        // カウントのインクリメントと、アクセスの記録を同時に行う
        try {
            // transaction開始
            Transaction transaction = datastoreService.beginTransaction();
            
            // カウンターの取得
            Counter counter = new Counter(datastoreService.get(transaction, key));
            count = counter.getCount() + 1;
            // アクセス記録
            AccessRecord record = 
                new AccessRecord(new Entity(AccessRecord.KIND, key));
            record.setCount(count);
            record.setDateTime(new Date());
            record.setReferer(new Link(req.getHeader("Referer")));
            record.setUserAgent(req.getHeader("User-Agent"));
            record.setRemoteAddr(req.getRemoteAddr());
            datastoreService.put(transaction, record.getEntity());
            // カウンターの変更
            counter.setCount(count);
            datastoreService.put(transaction, counter.getEntity());
            
            // transaction終了
            transaction.commit();
        } catch (EntityNotFoundException e) {
        } finally {
            Transaction transaction = datastoreService.getCurrentTransaction(null);
            if (transaction != null) {
                transaction.rollback();
                count -= 1;
            }
        }
        
        return count;
    }


保存されているアクセス情報は以下のQueryで検索。

    Query query = new Query(AccessRecord.KIND, key)
        .addSort(AccessRecord.DATETIME, SortDirection.DESCENDING);


主にWebアプリの作り方、というところで色々ハマってしまっていたことがあった。もう少し作り慣れたいところ。
とりあえずこのダイアリの右バーの一番下に貼っておいたので経過を観察してみることにする。