Εδώ και καιρό,
στα πλαίσια της ανάπτυξης μια αρκετά μεγάλης web εφαρμογής, παρουσιάστηκε η ανάγκη να μπορούμε να αλλάζουμε
το configuration της εφαρμογής, ή των επιμέρους κομματιών της on the fly, με τη χρήση configuration files σε xml, text ή κάποιας άλλης μορφής.
Πολύ συχνά σε τέτοιες
περιπτώσεις, βρίσκεις υλοποιήσεις οι οποιές ανακτούν πληροφορίες απο αυτά τα configuration files χρησιμοποιώντας τα κατευθείαν απ’το δίσκο. Με άλλα λόγια, για κάθε client request, διαβάζουν τα περιεχόμενα του αρχείου απ’το δίσκο, τα επεξεργάζονται και
επιστρέφουν το response στον client. Αυτή η πρακτική
αν μή τί άλλο, σου εξασφαλίζει οτι κάθε φορά έχεις up-to-date δεδομένα, αυτά
της τελευταίας αλλαγής στο αρχείο. Το downside όμως είναι οτι για κάθε request, ανοίγεις και διαβάζεις ένα αρχείο απο το
δίσκο. Πολλά ταυτόχρονα requests σου thrash-άρουν
το δίσκο, και σύντομα αυτό θα γίνει bottleneck στην εφαρμογή σου.
Οπότε, η επόμενη
επιλογή, είναι να κρατάς αυτά τα δεδομένα στη RAM, εξασφαλίζοντας οτι ειδοποιείσαι για τις αλλαγές
στα configuration αρχεία
και ανανεώνεις την cache σου.
Για αυτό ακριβώς
το σκοπό γράφτηκε μια library, η οποία επιτρέπει το caching και την επεξεργασία των περιεχομένων configuration αρχείων. Η base class όλων είναι ο FileHandler. Ένας FileHandler αναλαμβάνει
να παρακολουθεί ένα αρχείο, και λαμβάνοντας ειδοποίηση οτι τα περιεχόμενα του
αρχείου άλλαξαν, ξαναφορτώνει τα περιεχόμενά του στη μνήμη, δίνοντας την επιπλέον
δυνατότητα ν ακαθορίσουμε custom κώδικα ο οποίος επεξεργάζεται τα περιεχόμενα του αρχείου
προτού αυτά αποθηκευθούν στην cache.
Το βασικό του interface είναι το παρακάτω:
using System;
using System.IO;
using
System.Collections;
using
System.Text;
using
System.Xml.Serialization;
namespace FileHandler
{
/// <summary>
/// FileHandler is responsible for monitoring a specific file,
/// and reload it when required
/// </summary>
[XmlRoot("file-handler")]
[ReturnTypeAttribute(typeof(byte[]))]
public class FileHandler : IDisposable
{
/// <summary>
/// Defines a function pointer - the function this points to
will
/// process the contents of the file upon initialization and
reload.
/// </summary>
public
delegate object
ProcessFile(Stream
file, FileHandler source);
/// <summary>
/// Public event definition. This is where listeners will
append
/// their own function pointers
/// </summary>
public
event ProcessFile
OnFileLoaded;
/// <summary>
/// Default, no-args constructor for Serialization purposes
/// </summary>
public
FileHandler() {}
/// <summary>
/// Initializes the Handler, for a given file path, and a key
/// </summary>
/// <param
name="sectionKey"></param>
/// <param
name="filePath"></param>
public
FileHandler(string sectionKey, string filePath) : this(sectionKey,
filePath, false) { }
/// <summary>
/// Initializes the object, specifying whether we need to
reload the file immediately
/// after it's been changed
/// </summary>
/// <param
name="sectionKey"></param>
/// <param
name="filePath"></param>
/// <param
name="initImmediate"></param>
public
FileHandler(string sectionKey, string filePath, bool
initImmediate)
{
...
}
/// <summary>
/// Performs whatever processing is required upon File load
/// </summary>
/// <param
name="filePath"></param>
protected
virtual void
Initialize(string filePath){
...
}
/// <summary>
/// Executed whenever the file we're handling is changed
/// </summary>
/// <param
name="sender"></param>
/// <param
name="e"></param>
void
FileWatcher_Changed(object sender, FileSystemEventArgs e)
{
...
}
/// <summary>
/// Retrieves the unique key name of this handler
/// </summary>
[XmlAttribute("id")]
public string Key {
...
}
/// <summary>
/// Indicates whether the FileHandler will reload the file
/// it handles immedaitely after any change, or wait until
/// its value is required
/// </summary>
[XmlAttribute("lazy-load")]
public bool EnableLazyLoading {
...
}
/// <summary>
/// Sets / Retrieves the path to the file we're watching
/// </summary>
[XmlAttribute("file-path")]
public
string FilePath {
...
}
/// <summary>
/// Overridable member that returns the processed contents
/// of the file we're handling. By default, it will return the
/// contents of the file as a byte array
/// </summary>
[XmlIgnore]
public
byte[] Value {
...
}
#region IDisposable
implementation
}
}
Το επόμενο στάδιο,
είναι να δημιουργήσουμε sub classes του FileHandler, οι οποίες θα αναλαμβάνουν να επεξεργάζονται
τα περιεχόμενα του αρχείου, αποθηκεύοντας έτσι ένα object στην cache, το οποίο θα ανανεώνεται κάθε φορά που το
αντίστοιχο configuration αρχείο στο δίσκο αλλάζει.
Το project που είναι attached στο post, περιέχει τις παρακάτω υλοποιήσεις του FileHandler:
- FileHandler. Αναλαμβάνει να διατηρεί στη μνήμη και να ανανεώνει τα περιεχόμενα ενός byte[], με τα περιεχόμενα του αρχείου
-
TextFileHandler. Subclass του FileHandler, η οποία επιστρέφει τα περιεχόμενα του
αρχείου σε μορφή String.
-
XmlFileHandler. Επιστρέφει τα περιεχόμενα του αρχείου ως XmlDocument
-
XmlObjectFactoryHandler. Χρησιμοποιεί XML Deserialization και
επιστρέφει instance ενός
τύπου ο οποίος προσδιορίζεται στον constructor.
Η υλοποίηση ενός subclass δεν είναι δύσκολη. Όλη η υποδομή
που χρειάζεται να υλοποιηθεί για να φορτώνει και να ανανώνει τα περιεχόμενα του
configuration αρχείου
υπάρχει ήδη στη base class, τον FileHandler. Το μόνο που χρειάζεται να υλοποιηθεί έιναι η
επεξεργασία των περιεχομένων του αρχείου η οποία επιστρέφει το instance που θα μπεί στην cache.
Το πιο απλό παράδειγμα,
είναι ο TextFileHandler. Τη
στιγμή που χρειάζονται τα περιεχόμενα του αρχείου, υλοποιεί την παρακάτω απλή ενέργεια:
///
<summary>
/// Returns the contents of the file as a string, using the
specified Encoding
/// </summary>
public
new string
Value {
get
{
return m_Encoding.GetString(base.Value);
}
}
Ακόμα και το απλό
αυτό task, θα μπορούσε να γίνει
καλύτερα. Αντί να γίνεται η επεξεργασία κάθε φορά που ζητείται η τιμή του Value, θα μπορούσαμε να κρατάμε την τίμή σε ένα
έτοιμο string ως private member, και να κάνουμε την επεργασία μέσω του Encoding μόνο όταν γίνεται set το Value. Αυτό υλοποιείται
καλύτερα στον XmlFileHandler.
Επειδή η επεξεργασία ενός αρχείου μέσω του XmlDocument είναι πολύ πιο επίπονη απο την απλή
μετατροπή ενός byte array σε string, ο XmlFileHandler cache-άρει ένα live instance, το οποίο και επιστρέφει ως Value:
namespace FileHandler
{
/// <summary>
/// XmlFileHandler extends FileHandler to return a
strongly-typed XmlDocument value.
/// </summary>
[XmlRoot("xml-file-handler")]
[ReturnTypeAttribute(typeof(System.Xml.XmlDocument))]
public class XmlFileHandler
: FileHandler
{
private
XmlDocument m_Document = new XmlDocument();
/// <summary>
/// Default no-args constructor
/// </summary>
public
XmlFileHandler() : base() {
base.OnFileLoaded
+= new ProcessFile(XmlFileHandler_OnFileLoaded);
}
public
XmlFileHandler(string sectionKey, string filePath) : this(sectionKey,
filePath, false) {
}
public
XmlFileHandler(string sectionKey, string filePath, bool
initImmediate) : base(sectionKey, filePath,
initImmediate)
{
base.OnFileLoaded
+= new ProcessFile(XmlFileHandler_OnFileLoaded);
}
private
object XmlFileHandler_OnFileLoaded(System.IO.Stream file, FileHandler
source) {
//
Load the stream into an XmlDocument ...
m_Document.Load(file);
return
m_Document;
}
/// <summary>
/// New property definition, to return a type-safe value
/// </summary>
[XmlIgnore]
public
new XmlDocument
Value {
get
{
return m_Document;
}
}
}
}
Δύο είναι τα ενδιαφέροντα τμήματα
του παραπάνω κώδικα:
public
XmlFileHandler() : base() {
base.OnFileLoaded
+= new ProcessFile(XmlFileHandler_OnFileLoaded);
}
Εδώ γίνεται register ο event handler που θα κληθεί κάθε φορά που τα περιεχόμενα του αρχείου αλλάξουν και
φορτωθούν στη μνήμω ώς byte[].
Ο event handler αναλαμβάνει να χρησιμοποιήσει αυτό
το byte[] και μετά απο
κάποια επεξεργασία να δημιουργήσει την τιμή / instance το οποίο και θα επιστραφεί ως value.
private object XmlFileHandler_OnFileLoaded(System.IO.Stream file, FileHandler
source) {
//
Load the stream into an XmlDocument ...
m_Document.Load(file);
return
m_Document;
}
Εδώ είναι η
επεξεργασία του XmlFileHandler. Το Stream το οποίο
δίνεται ως παράμετρος είναι ένα MemoryStream το οποίο περιέχει τα περιεχόμενα του αρχείου. Αυτή
η επιλογή τύπου, ενός Stream,
δίνει μεγαλύτερη ελευθερία απο ένα απλό byte[], όπως και μεγαλύτερο εύρος επιλογών όσον αφορά ακόμα
και την base κλάσση και
μελοντικές προσθήκες χαρακτηριστικών σε αυτήν. Το επόμενο
χαρακτηριστικό που χρειάστηκε, ήταν ένας τρόπος να «γκρουπάρουμε» αυτούς τους handlers, και να μπορούμε να ανακτούμε την τρέχουσα
τιμή τους by name. Χρειαζόμασταν ένα MultiFileHandler.
using System;
using System.IO;
using
System.Collections;
using
System.Collections.Specialized;
using
System.Configuration;
using
System.Reflection;
namespace FileHandler
{
/// <summary>
/// MultiFileHandler is a collection of FileHandlers,
retrievable by name.
/// </summary>
public class MultiFileHandler
{
//
Dictionary that will hold the FileHandler instances
private
IDictionary m_FileHandlers = new ListDictionary();
private
string m_Name = null;
private
string m_BasePath = null;
private
bool m_InitImmediate = false;
public
MultiFileHandler();
public
MultiFileHandler(string groupName;
public
MultiFileHandler(string groupName, string basePath;
public
MultiFileHandler(string groupName, string basePath, bool
initImmediate;
/// <summary>
/// Sets / Retrieves a 'group' Name for this MultiFileHandler
/// </summary>
public
string Name ;
/// <summary>
/// Sets / Retrieves the base path for all subordinate
FileHandlers
/// </summary>
public
string BasePath ;
/// <summary>
/// Indicates whether subordinate FileHandlers will
re-initialize their contents
/// immediately after a watched file has changed
/// </summary>
public
bool InitImmediate ;
/// <summary>
/// Appends a new FileHandler for the given file, with the
specified name
/// </summary>
/// <param
name="handlerKey"></param>
/// <param
name="filePath"></param>
public
void AddHandler(string
handlerKey, string filePath;
/// <summary>
/// Appends a new FileHandler for the given file, with the
specified name, indicating whether it will
/// init immediately after it's watched file has changed
/// </summary>
/// <param
name="handlerKey"></param>
/// <param
name="filePath"></param>
/// <param
name="initImmediate"></param>
public
void AddHandler(string
handlerKey, string filePath, bool initImmediate;
/// <summary>
/// Adds the specified FileHandler instance to the contained
list of Handlers
/// </summary>
/// <param
name="handler"></param>
public
void AddHandler(FileHandler
handler;
/// <summary>
/// Removes & disposes the specified FileHandler
/// </summary>
/// <param
name="handlerKey"></param>
public
void RemoveHandler(string
handlerKey;
/// <summary>
/// Retrieves the current value of the specified handler
/// </summary>
/// <param
name="handlerKey"></param>
/// <returns></returns>
public
object GetValue(string
handlerKey);
/// <summary>
/// Gets the ReturnType for this handler, from a
ReturnTypeAttribute attached to the instance
/// </summary>
/// <param
name="handler"></param>
/// <returns></returns>
private
Type GetReturnType(object
handler);
/// <summary>
/// Retrieves the specified FileHandler
/// </summary>
/// <param
name="handlerKey"></param>
/// <returns></returns>
protected
FileHandler GetHandler(string
handlerKey);
}
}
Αυτό που κάνει ο MultiFileHandler είναι απλό. Διατηρεί τα instances των FileHandlers που του προτίθενται, και επιστρέφει
τις τιμές που του δίνουν. Θεωρητικά, ακόμη και αυτός θα μπορούσε να είναι subclass του FileHandler, και να δημιουργεί όλους του FileHandlers – παιδιά του, απο ένα configuration αρχείο.
Αλλά αυτό ας το
αφήσουμε ως άσκηση στον αναγνώστη ... ( ή και στον γράφοντα :D ) ... ή για κάποια άλλη φορά :P
Το attached project είναι υλοποιημένο σαν console application, και περιέχει ένα μικρό test application. Enjoy ! :P
Angel
O:]