JConsole. Компонент отображения стандартных потоков вывода.

В мире Java существует ни одна библиотека для логирования работы системы. Каждая из них обладает своими преимуществами и недостатками, писать о которых можно бесконечно долго. Но зачастую разработчики избегают использования сложных систем для ведения логов и пользуются проверенными годами (хоть и не лишенными недостатков) методами: выводом сообщений в потоки System.out и System.err.

В любом случае, читать содержимое лог-файлов, каждый раз находя их в директориях системы, не удобно. Куда приятнее видеть как изменяется их содержимое в реальном времени, как это сделано во многих IDE (пример из Eclipse):

В этой статье мы создадим свой компонент для вывода содержимого потоков System.out и System.err. Я постараюсь излагать материал как можно более подробно, чтобы он был понятен даже начинающим java программистам.

Потоки ввода-вывода в Java.

Прежде всего необходимо понять, каким образом мы будем получать содержимое потоков out и err, и как мы будем узнавать о том, что их содержимое изменилось. Для этого нам придется вспомнить (или узнать?) о потоках ввода-вывода и их иерархии в джаве.  Это достаточно большая и сложная тема, которую не раз освещали более талантливые авторы чем я :), поэтому здесь я лишь приведу ссылку на материалы по этой теме в интернет-университете intuit: Система ввода/вывода. Потоки данных (stream) и позволю себе разместить в этой статье, в качестве конспекта, следующую картинку:

Теперь обратим внимание, что интересующие нас потоки out и err мы можем  переопределить с помощью методов:

System.setErr(java.io.PrintStream newErr);
System.setOut(java.io.PrintStream newOut);

Аргументами этих методов являются классы java.io.PrintStream, чьи экземпляры с легкостью создаются на основе любой реализации абстрактного класса OutputStream. Все наследники OutputStream должны обязательно реализовывать всего один метод: 

void write(int b) throws IOException

Таким образом, для решения нашей задачи нам необходимо написать класс, наследующийся от OutputStream, реализовать в нем метод write и заменить им стандартные потоки System.out  и System.err.

Выбор компонента для вывода сообщений.

Теперь, когда мы разобрались с тем, как перехватывать вывод в стандартные потоки, нам необходимо решить куда же его перенаправлять? На самом деле ответ на этот вопрос ограничен лишь вашим воображением, но в данной статье для этих целей мы выберем визуальный компонент JTextPane из библиотеки javax.swing.
Компонент JTextPane  крайне функционален и полное его описание заняло бы слишком много времени, по этому мы опустим детали и сосредоточимся только на необходимых нам возможностях.

Разработка компонента JConsole.


>И так, давайте определимся, что конкретно мы хотим от разрабатываемого компонента:
  • При размещении компонента в системе, он должен переопределять стандартные потоки System.out  и System.err выводить их содержимое.
  • Все сообщения переопределенных потоков не должны оставаться только в компоненте, но и попадать в потоки, на которые ссылались out и err до переопределения нашим компонентом (это позволит по прежнему видеть сообщения в консоли в IDE).
  • Сообщения разных потоков должны выводиться разным цветом.
  • Цвета для вывода сообщений должны быть настраиваемыми (т.е. компонент должен обладать методами для задания цвета вывода сообщений).
Фух! Кажется с заданием и путями его решения мы определились, осталось самое интересное - реализация!

Создадим свой компонент, наследующийся от JTextPane и назовем его JConsole:


package org.myswing;

import javax.swing.JTextPane;

/**
* Компонент для вывода содержимого стандартных потоков System.out и System.err.
* @author dok
*
*/
public class JConsole extends JTextPane {

   private static final long serialVersionUID = -6128923427636817389L;

}

Напишем метод для добавления текста в наш компонент:

private void insertText(final String text, final Color color) {
       SimpleAttributeSet att= new SimpleAttributeSet();
       StyleConstants.setForeground(att, color);
       try {
           int offset  = getStyledDocument().getLength();
           getStyledDocument().insertString(offset, text, att);
       } catch (BadLocationException e) {
           e.printStackTrace();
       }
   }

Давайте попытаемся разобраться в происходящем. В первой строке метода мы создаем простейший набор атрибутов текста и далее определяем в нем цвет, которым будет выводиться текст. В блоке try мы получаем индекс последнего символа в текущем тексте нашего документа. А затем добавляем строку с ранее созданными атрибутами.
Фундамент заложен. Теперь необходимо написать реализацию OutputStream и подменить ей стандартные потоки.

При написании своего потока вывода мы обязаны переопределить только один метод - write(int b). Но выводить строку по одному символу крайне не оптимально, поэтому для оптимизации мы так же переопределим методы write(byte[] b, int off, int len) и write(byte[] b), а так же добавим свойство, содержащее цвет, которым будем выводим содержимое этого потока.

*Вообще, для большей гибкости, мы могли бы в поле хранить не один только цвет, но экземпляр AttributeSet, но так как мы решили ограничиться в форматировании текста только цветом, его хранить и станем.

class ConsoleOutputStream extends OutputStream {

       @Override
       public void write(int b) throws IOException {
           insertText("" + (char)b, color);            
       }

       @Override  
       public void write(byte[] b, int off, int len) {
           insertText(new String(b, off, len), color);
       }

       @Override  
       public void write(byte[] b) {
           insertText(new String(b), color);
       }

       public ConsoleOutputStream(Color color) {
           this.color = color;
       }

       private Color color;
   }




Код получившегося класса достаточно прост, за одним исключением: если вы заметили, метод insertText мы вызываем так, будто это метод класса ConsoleOutputStream, но на самом деле это метод JConsole! Такой вызов нам позволяется делать только потому, что ConsoleOutputStream является вложенным классом для JConsole.

Хотя еще не вся заявленная нами функциональность готова, давайте уже полюбуемся на наше творение! Для этого создадим еще один класс Test, наследуемый от JFrame. Добавим на него наш компонент и кнопку, по нажатии которой мы будем выводить сообщение в поток System.out и генерировать ошибку, стек вызовов перед которой будет печататься в System.err:


package org.myswing.test;

import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import javax.swing.JPanel;
import javax.swing.JFrame;
import javax.swing.JButton;
import org.myswing.JConsole;

public class Test extends JFrame {

   /** Инициализируем нашу консоль */
   private JConsole getJConsole() {

       if (jTextPane == null) {
           jTextPane = new JConsole();
       }
       return jTextPane;
   }

   /** Инициализируем кнопку */
   private JButton getJButton() {

       if (jButton == null) {
           jButton = new JButton("Тест");
           jButton.addActionListener(new java.awt.event.ActionListener() {
               public void actionPerformed(java.awt.event.ActionEvent e) {

                   // Выводим сообщение в System.out 
                   System.out.println("Нажата кнопка");
                   try {
                       throw new Exception("Ошибка при выполнении приложения");
                   } catch (Exception er) {
                       // Выводим стек вызовов в System.err 
                       er.printStackTrace();
                   }    
               }
           });
       }
       return jButton;
   }

   /** Инициализируем панель, на которой размещается наша консоль и кнопка */
   private JPanel getJContentPane() {

       if (jContentPane == null) {
           jContentPane = new JPanel();
           jContentPane.setLayout(new BorderLayout());
           jContentPane.add(getJConsole(), BorderLayout.CENTER);
           jContentPane.add(getJButton(), BorderLayout.SOUTH);
       }
       return jContentPane;
   }

   /** Отправная точка любого приложения на Java */
   public static void main(String[] args) {
       SwingUtilities.invokeLater(new Runnable() {
           public void run() {
               Test thisClass = new Test();
               thisClass.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
               thisClass.setVisible(true);
           }
       });
   }

   /** Конструктор по умолчанию */
   public Test() {
       super();
       initialize();
       System.out.println("Приложение запущено...");
   }

   /** Инициализируем фрейм */
   private void initialize() {
       this.setSize(500, 500);
       this.setContentPane(getJContentPane());
       this.setTitle("JFrame");
   }

   private JPanel jContentPane = null;
   private JConsole jTextPane = null;
   private JButton jButton = null;

   private static final long serialVersionUID = -4303255380322232017L;
}


Запустив приложение и нажав на кнопку, увидим примерно следующую картину:

Выглядит все именно так, как нам и хотелось. Но к сожалению сообщения, переопределенных потоков, остаются только в компоненте и не доходят до консоли в IDE. А цвета, которыми выводятся сообщения, наглухо вшиты в код нашего компонента. Давайте исправим это.

Для этого, во первых, создадим у нашего потока поле с ссылкой на заменяемый поток. Во вторых напишем методы для изменения цвета нашего потока. И в третьих, перед переопределением потоков, будем запоминать их и прокидывать до них вызовы наших реализаций методов write.

Исходный код получившегося компонента:

package org.myswing;

import java.awt.Color;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;

import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

/**
 * Компонент для вывода содержимого стандартных потоков System.out и System.err.
 * 
 * @author dok
 * 
 */
public class JConsole extends JTextPane {

 class ConsoleOutputStream extends OutputStream {

  /** Устанавливает цвет вывода содержимого потока */
  public void setColor(Color color) {
   this.color = color;
  }

  /** Возвращает цвет вывода содержимого потока */
  public Color getColor() {
   return color;
  }

  @Override
  public void write(int b) throws IOException {
   old.write(b);
   insertText("" + (char) b, color);
  }

  @Override
  public void write(byte[] b, int off, int len) throws IOException {
   old.write(b, off, len);
   insertText(new String(b, off, len), color);
  }

  @Override
  public void write(byte[] b) throws IOException {
   old.write(b);
   insertText(new String(b), color);
  }

  /**
   * Конструктор класса
   * 
   * @param old -
   *            заменяемый поток.
   * @param color -
   *            цвет, которым будет выводиться содержимое потока.
   */
  public ConsoleOutputStream(PrintStream old, Color color) {
   if (old == null) {
    throw new NullPointerException(
      "Ссылка на заменяемый поток не указана");
   }
   if (color == null) {
    throw new NullPointerException("Ссылка на цвет не указана");
   }
   this.color = color;
   this.old = old;
  }

  private Color color;

  private PrintStream old;
 }

 /** Устанавливает цвет вывода содержимого потока out */
 public void setColorOut(Color colorOut) {
  this.out.setColor(colorOut);
 }

 /** Возвращает цвет вывода содержимого потока out */
 public Color getColorOut() {
  return out.getColor();
 }

 /** Устанавливает цвет вывода содержимого потока err */
 public void setColorErr(Color colorErr) {
  this.err.setColor(colorErr);
 }

 /** Возвращает цвет вывода содержимого потока err */
 public Color getColorErr() {
  return err.getColor();
 }

 public JConsole() {
  setEditable(false);
  err = new ConsoleOutputStream(System.err, Color.RED);
  out = new ConsoleOutputStream(System.out, Color.BLACK);
  System.setErr(new PrintStream(err));
  System.setOut(new PrintStream(out));
 }

 private void insertText(final String text, final Color color) {
  SimpleAttributeSet att = new SimpleAttributeSet();
  StyleConstants.setForeground(att, color);
  try {
   int offset = getStyledDocument().getLength();
   getStyledDocument().insertString(offset, text, att);
  } catch (BadLocationException e) {
   e.printStackTrace();
  }
 }

 private ConsoleOutputStream err;

 private ConsoleOutputStream out;

 private static final long serialVersionUID = -6128923427636817389L;
}


3 комментария:

Антон Павлов комментирует...

Спасибо!!!))) Прикрутил вашу консоль к себе в проект)

Sander Bykov комментирует...

Спасибо за подробное изложение материала!

Анонимный комментирует...

А как можно организовать консольный вывод в массив?
Пытаюсь написать "автоответчик" на вопросы сервера

PrintStream st = new PrintStream(new FileOutputStream("output_answer.txt"));
Через файл получается, но уж очень некрасиво и громоздко