在上面把日志文件打印到了D:\log下,考虑到Liunx服务器环境下,让最终用户修改可能不可接受,接下来完成三件事情:(1)应用程序指定输出路径(2)完善异常类的处理(3)完成页面跳转的封装处理


一、指定输出路径

由于Logback的<FILE>指定相对路径与Log4J存在差异,所以在修改日志输出路径之前,得让Eclipse能调试代码。

1、在http://www.eclipsetotale.com/tomcatPlugin.html#A3下载Tomcat插件,下载时确认自已的JDK、Eclipse、Tomcat与该插件是否配套。我曾见同事使用的插件不配套,导致Eclipse不能启动Tomcat


2、假如Eclipse的路径为%Eclipse%,在%Eclipse%下新建links\tomcatplugin\features和links\tomcatplugin\plugins文件夹,把下载的插件解压到links\tomcatplugin\plugins下,其目录结构如图:

【斗医】【4】Web应用开发20天


3、在%eclipse%\links\下创建tomcat.ini文件,填写如下内容:

path=D:\\eclipse\\links\\tomcatplugin

其中D:\\eclipse即为%eclipse%,读者视自己环境更改。


4、若Eclipse打开则请关闭,然后删除%eclipse%\configuration\org.eclipse.update目录,重启Eclipse后应该在工具栏上看到小猫咪的图标,表明加载成功:

【斗医】【4】Web应用开发20天


5、在Eclipse配置Tomcat。依次选择“Window > Preferences > Tomcat”,其中“Tomcat Version”选择“Version 7.x”,“Tomcat home”选择Tomcat放置路径。在前面说过我把Tomcat放置到了D盘下,如图:

【斗医】【4】Web应用开发20天


6、确认后点击小猫咪图标,应该能看到Tomcat在Eclisps的启动信息了


接下来我们要修改logconfig.xml中的日志输出路径了。由于要修改XML,所以我们创建一个FrameXmlUtil.java类,该类用于解析XML内容、读取结点的属性集合、保存XML内容等。

1、创建FrameXmlUtil,由于涉及代码较多这里只粘贴出代码构造,具体代码见附件。读者也可以自已写此工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class FrameXmlUtil
{
    /**
     * XML文档
     */
    private Document xmlDocument = null;
    /**
     * XML的根元素
     */
    private Element rootElement = null;                                                                                                                                  
    /**
     * 读取XML文件
     */
    public void readXmlFile(String filePath) throws FrameException
    {
        // 这里省略,具体见附件
    }                                                                                                                                                            
    /**
     * 获取结点属性集合
     */
    public Map<String, String> getAttrs(Node node)
    {
         // 这里省略,具体见附件
    }                                                                                                                                                           
    /**
     * 保存XML
     */
    public void saveXML() throws FrameException
    {
         // 这里省略,具体见附件
    }                                                                                                                                                             
    /**
     * 获取XML根元素
     */
    public Element getRootElement()
    {
        return rootElement;
    }
}


【备注】:关注解析XML在JAVA开发中有多种不同的解析方法,感兴趣的话可以在谷歌上搜索一下,有很多人写过这方面的贴子



2、 对FrameConfigUtil增加静态工厂方法modifyLogOutPath(),对D:\medical\war\etc\logconfig.xml中的<property name="LOG_HOME" value="D:\logs" />进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
 * 修改D:/medical/war/etc/logconfig.xml文件中的日志输出路径
 */
public static void modifyLogOutPath(ServletContext context) throws FrameException
{
    String webPath = context.getRealPath("/");
    StringBuilder filePath = new StringBuilder(webPath);
    filePath.append(File.separator).append("etc");
    filePath.append(File.separator).append("logconfig.xml");
                                                                              
    // 载入D:\medical\war\etc\logconfig.xml文件
    FrameXmlUtil xmlUtil = new FrameXmlUtil();
    xmlUtil.readXmlFile(filePath.toString());                                                                             
    // 初步判断文件的合法性
    Element rootElement = xmlUtil.getRootElement();
    if (rootElement == null)
    {
        return;
    }                                                                                                                                                                   
    // 由logconfig.xml知日志输出路径宏定义就在根结点下,所以此处遍历根结点的孩子
    NodeList childNodeList = rootElement.getChildNodes();
    for (int index = 0; index < childNodeList.getLength(); index++)
    {
        Node childNode = childNodeList.item(index);
        if (Node.ELEMENT_NODE != childNode.getNodeType())
        {
            continue;
        }
                                                                                     
        // 判断孩子结点是否为<property name="LOG_HOME" value="D:\logs" />
        Element childElement = (Element) childNode;
        String elementName = childElement.getNodeName();
        String logHome = childElement.getAttribute("name");
        if (FrameConstant.LOG_PROPERTY.equals(elementName) && FrameConstant.LOG_HOME.equals(logHome))
        {
            StringBuilder path = new StringBuilder(webPath);                      path.append(File.separator).append("var").append(File.separator).append("logs");                          childElement.setAttribute(FrameConstant.LOG_VALUE, path.toString());
            break;
         }
    }                                                                                                                                                             
    // 保存修改后的XML文件
    xmlUtil.saveXML();
}


3、若使用logback则在加载logconfig.xml之后调用FrameConfigUtil.modifyLogOutPath()方法,所以修改FrameLauncher的init()方法,修改后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void init() throws ServletException
{
    ServletContext context = getServletContext();
    try
    {
        FrameConfigUtil.modifyLogOutPath(context);
        FrameConfigUtil.initLogConfig(context);
    }
    catch (FrameException e)
    {
        throw new ServletException("[FrameLauncher] init error.", e);
    }
}

好了,在Eclipse中点击小猫咪图标启动Tomcat服务,在浏览器中输入http://localhost:8080/medical,理论上应该在D:\medical\war\var\logs下生成日志文件,但理想与现实之间往往存在差距。这是什么原因呢?细心观察在Tomcat目录下多出一个medicalwarvarlogs文件夹,里面的日志文件正是我们想要的!!

这说明什么?

说明我们修改logconfig.xml之后的<property name="LOG_HOME" value="D:\medical\war\var\logs"/>存在问题。

那是什么问题呢?想想什么是转义字符?\n是什么?\b是什么?明白了吧:value="D:\medical\war\var\logs"是错误的,应该对\再进行转义。修改FrameConfigUtil.modifyLogOutPath()方法中输出路径代码处增加如下处理:

1
String realPath = path.toString().replaceAll("\\\\""/");              childElement.setAttribute(FrameConstant.LOG_VALUE, realPath);


再重启Tomcat服务,在浏览器中输入http://localhost:8080/medical,点击下图所示的超链接,此时会发现日志文件能按您的预期输出了。

【斗医】【4】Web应用开发20天


二、完善异常类

在上面日志文件输出搞定之后,让我们回到第二天的异常类封装问题上,当时说异常的描述应该从中英文资源中读取,随着我们的想法自然地前行吧。

1、在D:\medical\war\etc\下新建local文件夹,然后在local下分别创建en、zh文件夹,再在en、zh下分别创建resource.properties文件,其目录结构如下:

【斗医】【4】Web应用开发20天


2、向resource.properties中填写信息,由于我们这个斗医系统是给中国人看的,外国人不懂中医是什么,所以只填写zh下的resource.properties内容即可。这里暂时把前面所涉及的错误码填写上:

#UTF-8

1=创建XML生成器时异常

5=SAX解析XML文件时异常

10=解析XML时出现IO异常

15=解析XML时参数设置异常

20=生成XML翻译器失败

25=翻译XML失败

100=HTTP请求动作跳转异常


3、考虑到异常信息只要从一个地方读取,这里新建一个FrameCache.java文件,专门用于缓存全局数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class FrameCache
{
    private static FrameCache instance = new FrameCache();                                                                                                    
    /**
     * 全局资源文件缓存
     */
    private Properties resourceProp = new Properties();
                     
    private FrameCache()
    {
    }
                                                            
    public static FrameCache getInstance()
    {
        return instance;
    }                                                                                                                                                     
    public void setResourceProp(Properties resourceProp)
    {
        this.resourceProp = resourceProp;
    }                                                                                                                                                    
    public String getResourceValue(String resourceKey)
    {
        return resourceProp.getProperty(resourceKey, resourceKey);
    }
}


4、在Servlet加载时需要把这些资源信息读入内存,这样才能根据错误码读取到异常描述信息,所以需要

(1)在FrameConfigUtil中定义loadResource(),用于加载Properties文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void loadResource(ServletContext context) throws FrameException
{
    // 获取中文资源文件路径
    StringBuilder resourcePath = new StringBuilder(context.getRealPath("/"));
        resourcePath.append(File.separator).append("etc").append(File.separator).append("local");
        resourcePath.append(File.separator).append("zh").append(File.separator).append("resource.properties");
                                                                                                            
    // 具体加载动作
    Properties resourceProp = new Properties();
    try
    {
        InputStream in new BufferedInputStream(new FileInputStream(resourcePath.toString()));
        resourceProp.load(in); 
    }
    catch (IOException e)
    {
        throw new FrameException(FrameErrorCode.Prop_ERROR_LOAD, e);
    }                                                                                                
    // 设置到全局缓存中
    FrameCache.getInstance().setResourceProp(resourceProp);
}

(2)在FrameLauncher的init()方法中调用loadResource()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
ServletContext context = getServletContext();
try
{
    // 加载logback配置
    FrameConfigUtil.modifyLogOutPath(context);
    FrameConfigUtil.initLogConfig(context);                                                                   
    // 加载中文资源配置
    FrameConfigUtil.loadResource(context);
}
catch (FrameException e)
{
    throw new ServletException("[FrameLauncher] init error.", e);
}


5、为了测试,我们在FrameLauncher.doGet()方法中打印一句话

1
System.out.println(FrameCache.getInstance().getResourceValue("1"));


6、在Eclipse中启动Tomcat服务,在浏览器中输入http://localhost:8080/medical,点击“Test Logback”超链接,可以看到有类似如下输出:

【斗医】【4】Web应用开发20天


【备注】:这里的乱码是由于字符的编码问题,在此处我们只要能加载即可,具体的可读性遗留到后面界面展示时处理。



7、完善FrameException读取异常描述的遗留,把原来标注“// errorDesc应该从中英文资源文件中根据errorCode读取”的地方,使用errorDesc = FrameCache.getInstance().getResourceValue(String.valueOf(errorCode));代替。


三、页面跳转封装

接下来我们做一件系统较为重要的事情:页面跳转封装。比如在系统导航菜单上点击了“话题”菜单项,那么希望系统能进入“话题”页面,站在纯HTML角度来看只需要<a>标签即可,但有时候还需要处理一些逻辑,所以此封装还是较有意义的。封装之后的XML类似如下:

1
2
3
4
5
6
<business name="topic" mustlogin="false" business-class="com.medical.FrameTopic">
    <forward>
        <path name="success"  path="/module/topic.html" />
        <path name="failure"  path="/module/topic.html" />
    </forward>
</business>

从这个XML上很容易看出,这个业务是进入topic页面,在进入之前用户可以不登录,同时进入页面之前的逻辑部分由FrameTopic处理,无论处理结果如何都最终进入topic.html。下面封装FrameBusiness类来对应这个业务实体:

1、创建com.medical.frame.config.FrameForward类,它包括successPath(成功时跳转路径)、failurePath(失败时跳转路径),然后再对外提供get和set方法


1
2
3
4
5
6
7
8
9
10
11
12
public class FrameForward
{
    /**
     * 成功跳转路径
     */
    private String successPath = null;                                                                    
    /**
     * 失败跳转路径
     */
    private String failurePath = null;
                                                                                             // 省略相关的get&set方法 
}


2、创建com.medical.frame.config.FrameBusiness类,里面有name、mustLogin、businessClass和FrameForward对象,然后再对外提供get和set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FrameBusiness
{
    /**
     * 业务名称
     */
    private String name = null;                                                               
    /**
     * 是否必须登录
     */
    private boolean mustLogin = false;                                                      
    /**
     * 业务逻辑处理类
     */
    private String businessClass = null;                                                      
    /**
     * 业务跳转路径
     */
    private FrameForward forward = null;
                                                                                             // 省略相关的get&set方法
}


3、假如系统有多个业务,我们不希望所有业务的配置都放到一个XML,同时不同业务的XML又希望按业务文件夹放置,如下:

(1)在运行环境D:\medical\war\WEB-INF下创建config文件夹

(2)在config下创建sm和test两个文件夹

(3)在sm和test下分别创建system-action.xml和test-action.xml

(4)在system-action.xml中填充如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<business-config>
    <business name="index" mustlogin="false">
        <forward>
            <path name="success"  path="/main.html"/>     
            <path name="failure"  path="/main.html"/>
        </forward>
    </business>
    <business name="login" mustlogin="">
        <forward>
            <path name="success"  path="/login.html"/>        
            <path name="failure"  path="/login.html"/>
        </forward>
    </business>
</business-config>



【备注】:后面若无特殊说明,文件均以UTF-8编码


(5)的test-action.xml中填充如下内容:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<business-config>
    <business name="test" mustlogin="true" business-class="com.medical.frame.Demo">
        <forward>
            <path name="success"  path="/main.html"/>      
            <path name="failure"  path="/main.html"/>
        </forward>
    </business>
</business-config>

4、前面说过action这个Servlet,现在我们希望该Servlet在启动时把上面的业务配置文件(xxx-action.xml)里的业务配置加载到内存

(1)在FrameLauncher.init()方法中调用FrameConfigUtil.loadBusiness(),加载业务配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void init() throws ServletException
{
    ServletContext context = getServletContext();
    try
    {
        // 加载logback配置
        FrameConfigUtil.modifyLogOutPath(context);
        FrameConfigUtil.initLogConfig(context);                                           
        // 加载中文资源配置
        FrameConfigUtil.loadResource(context);                                              
        // 加载业务配置文件
        FrameConfigUtil.loadBusiness(context);
    }
    catch (FrameException e)
    {
        throw new ServletException("[FrameLauncher] init error.", e);
    }
}


(2)在FrameConfigUtil中定义一个公共静态方法loadBusiness(),用于加载D:\medical\war\WEB-INF\config下的所有业务配置

1
2
3
4
5
public static void loadBusiness(ServletContext context) throws FrameException
{
    findActFile(context, "/WEB-INF/config");
    parseBusiness(context);
}


(3)上面的loadBusiness()方法中的findActFile()作用是查找出"/WEB-INF/config"下的所有以-action.xml结尾的文件,并把文件名保存到全局缓存中

I、在FrameCache中定义文件集合,并提供get()/add()方法

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 系统业务配置文件路径集合
 */
private Set<String> busActFileSet = new HashSet<String>();
public Set<String> getBusinessActFile()
{
    return busActFileSet;
}                                                                                                                              
public void addBusinessActFile(String businessActFile)
{
    this.busActFileSet.add(businessActFile);
}


II、在FrameConfigUtil.findActFile()中查找-action.xml文件,并保存在全局缓存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void findActFile(ServletContext context, String path)
{
    Set<String> businessSet = context.getResourcePaths(path);
    if (businessSet == null)
    {
        return;
    }
    for (String item : businessSet)
    {
        if (item == null)
        {
            continue;
        }
        if (item.endsWith(FrameConstant.BUS_CONF_POSTFIX))
        {
            FrameCache.getInstance().addBusinessActFile(item);
        }
        else
        {
            findActFile(context, item);
        }
    }
}


(4)FrameConfigUtil.parseBusiness()是真正的解析过程,也是本文的重点,所以请读者重点关注。下面把其逻辑分别列举出:

I、从全局缓存中读取XXX-action.xml文件集合,判断是否为空,若为空则返回

1
2
3
4
5
Set<String> actionFileSet = FrameCache.getInstance().getBusinessActFile();
if (actionFileSet == null || actionFileSet.isEmpty())
{
    return;
}

II、若集合不为空,则把XXX-action.xml文件用FrameXMlUtil工具解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 遍历XXX-action.xml文件
for (String fileName : actionFileSet)
{
    InputSource in = null;
    try
    {
        URL url = context.getResource(fileName);
        in = new InputSource(url.toExternalForm());
    }
    catch (MalformedURLException e)
    {
        throw new FrameException(FrameErrorCode.XML_ERROR_GET_PATH);
    }
    try
    {
        FrameXmlUtil xmlUtil = new FrameXmlUtil();
        xmlUtil.readXmlFile(in);
        //.......
    }
    catch (FrameException e)
    {
        throw e;
    }
}

III、真正解析结点形成FrameBusiness对象,并缓存到全局变量中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
FrameCache.java部分代码:
/**
 * 系统配置业务
 */
private Map<String, FrameBusiness> businessMap = new HashMap<String, FrameBusiness>();
public FrameBusiness getBusiness(String businessName)
{
    return businessMap.get(businessName);
}
                                                                                         
public void addBusiness(FrameBusiness business)
{
    businessMap.put(business.getName(), business);
}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
FrameConfigUtil.parseBusiness(ServletContext)部分代码:
FrameXmlUtil xmlUtil = new FrameXmlUtil();
xmlUtil.readXmlFile(in);
Element rootElement = xmlUtil.getRootElement();
NodeList busNodeList = rootElement.getElementsByTagName(FrameConstant.BUS_KEY);
if (busNodeList == null || busNodeList.getLength() == 0)
{
    continue;
}
// 开始解析
for (int i = 0; i < busNodeList.getLength(); i++)
{
    Node busNode = busNodeList.item(i);
    Map<String, String> busNodeAttributs = xmlUtil.getAttrs(busNode);
    FrameBusiness business = new FrameBusiness();
    business.setName(busNodeAttributs.get(FrameConstant.BUS_NAME));
    business.setBusinessClass(busNodeAttributs.get(FrameConstant.BUS_CLASS));
    String mustLogin = busNodeAttributs.get(FrameConstant.BUS_MUST_LOGIN);
    mustLogin = mustLogin == null "false" : mustLogin;
    business.setMustLogin("TRUE".equalsIgnoreCase(mustLogin));
    // 解析<forward>属性
    NodeList forwardNodeList = ((Element) busNode).getElementsByTagName(FrameConstant.BUS_FORWARD);
    if (forwardNodeList == null || forwardNodeList.getLength() != 1)
    {
        continue;
    }
    Element forwardElement = (Element) forwardNodeList.item(0);
    NodeList pathNodeList = forwardElement.getElementsByTagName(FrameConstant.BUS_PATH);
    if (pathNodeList == null || pathNodeList.getLength() == 0)
    {
        continue;
    }
    FrameForward forward = new FrameForward();
    for (int j = 0; j < pathNodeList.getLength(); j++)
    {
        Node fowardNode = pathNodeList.item(j);
        Map<String, String> forwardAttrMap = xmlUtil.getAttrs(fowardNode);
        String forwardName = forwardAttrMap.get(FrameConstant.BUS_FORWARD_NAME);
        if (FrameConstant.BUS_FORWARD_SUCCESS.equals(forwardName))
        {
            forward.setSuccessPath(forwardAttrMap.get(FrameConstant.BUS_PATH));
        }
        else if (FrameConstant.BUS_FORWARD_FAILURE.equals(forwardName))
        {
            forward.setFailurePath(forwardAttrMap.get(FrameConstant.BUS_PATH));
        }
    }
    business.setForward(forward);
    // 添加到全局缓存中
    FrameCache.getInstance().addBusiness(business);
}


【备注】:上面的代码是该封装的关键代码,限于篇幅问题,这里只把较为关键的代码粘贴出,其中涉及FrameXmlUtil.readXmlFile(InputSource in)、FrameConstant接口常量值定义等都没有列出,具体可参见附件。

若读者有兴趣运行的话,强烈建议读者亲身写一下。