日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

在spring全家桶流行的當(dāng)下,只要你做的是JAVA技術(shù)棧基本上95%以上都使用的spring或springboot框架,剩下的5%基本上是一些老項目,政府項目,銀行項目之類對安全性要求比較高的項目,比如之前前段時間的log4j2,Spring Getway遠(yuǎn)程代碼執(zhí)行漏洞;總有意想不到的BUG,所以有些公司也會自己封裝框架。那么我們也根據(jù)SpringMVC的基本實現(xiàn)原理,基于Servlet類封裝一個自己的MVC框架。

進(jìn)入正題:

聊聊技術(shù)選型:

為了是框架足夠簡單且可以實現(xiàn)mvc基本功能,我們這里只引用少量的外部類庫,項目基于JDK8,JavaWeb,并采用Maven的方式進(jìn)行構(gòu)建。

采用的設(shè)計模式:策略模式,觀察者模式

使用技術(shù):JavaEE,Maven,反射技術(shù)

項目結(jié)構(gòu)如下:

com.kexun
  annotation #mvc及orm所使用的到的注解
  controller #基于框架的示例代碼
  dao #數(shù)據(jù)庫操作類,存放sql
  db #orm框架源碼
  entity #實現(xiàn)類
  mvc #mvc框架實現(xiàn)源碼
  utils #依賴的工具類

基本實現(xiàn)邏輯:

  1. 實現(xiàn)容器監(jiān)聽器,項目啟動時掃描@ReqrestMApping標(biāo)記的方法以請求路徑為key 對應(yīng)的方法Method為value put到map,并同時初始化參數(shù)解析器
  2. 創(chuàng)建統(tǒng)一攔截Servlet,根據(jù)請求路徑去map尋找對應(yīng)的處理器方法
  3. 通過反射獲取所要調(diào)用的方法參數(shù),遍歷參數(shù)解析器尋找合適的解析器方法,對參數(shù)進(jìn)行解析注入
  4. controller處理相關(guān)業(yè)務(wù)邏輯,并標(biāo)記相應(yīng)注解
  5. 根據(jù)注解標(biāo)記判斷是轉(zhuǎn)發(fā)到頁面還是返回文本數(shù)據(jù)

代碼實現(xiàn):

  1. 創(chuàng)建容器類,初始化時的數(shù)據(jù)
public class Containers {
    //用于存儲controller對應(yīng)的路徑及方法對象
    public static HashMap<String, Method> mappingMethods = new HashMap<>();
    //存儲參數(shù)解析器
    public static List<BaseResolver> resolvers = new ArrayList<>();
}

2.初始化mapping 和 resolvers

public class InitMapping implements ServletContextListener {   
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //掃描controller包到容器
        Set<Class<?>> classes = ClassUtils.getClasses("com.kexun.controller");
        //遍歷controller包下所有的類
        for (Class<?> aClass : classes) {
            //獲取類注解RequestMapping
            RequestMapping classMapping = aClass.getAnnotation(RequestMapping.class);
            String classMappingVal = null;
            if (classMapping != null) {
                //對類上的RequestMapping值進(jìn)行拼接 并對多余的 / 進(jìn)行處理
                classMappingVal = classMapping.value().replace("/", "");
            }
             //反射獲取類所有方法
            Method[] methods = aClass.getMethods();
            for (Method method : methods) {
                RequestMapping mapping = method.getAnnotation(RequestMapping.class);
                //獲取并且判斷是否存在RequestMapping 注解 如不存在則不做操作
                if (mapping != null) {
                    String methodMappingVal = mapping.value().replace("/", "");
                    //String key = "/" + classMappingVal + "/" + methodMappingVal;
                    String key = "";
                    //組裝方法請求路徑 
                    if (classMappingVal != null) {
                        key += ("/" + classMappingVal);
                    }
                    key += ("/" + methodMappingVal);
                    boolean b = Containers.mappingMethods.containsKey(key);
                    if (b) {
                        throw new RuntimeException("存在相同方法路徑" + key);
                    } else {
                        System.out.println("初始化Controller:" + key);
                        //put到map容器
                        Containers.mappingMethods.put(key, method);
                    }
                }
            }
        }
        //初始化解析器到容器
        Containers.resolvers.add(new IntResolver());
        Containers.resolvers.add(new BaseEntityResolver());
        Containers.resolvers.add(new HttpServletRequestResolver());
        Containers.resolvers.add(new HttpServletResponseResolver());
        Containers.resolvers.add(new HttpSessionResolver());
        Containers.resolvers.add(new StringResolver());
        Containers.resolvers.add(new RequestBodyResolver());
    }
}

3.參數(shù)解析器

根據(jù)目標(biāo)方法的參數(shù)類型進(jìn)行解析,我這里實現(xiàn)了幾個常用的參數(shù)類型解析器,如需擴(kuò)展可以實現(xiàn) BaseResolver接口

//解析策略接口
public interface BaseResolver {
    boolean supportsParameter(Parameter parameter);
    Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp);
}

boolean supportsParameter方法:判斷參數(shù)是否符合預(yù)期類型

Object resolveArgument方法:完成參數(shù)的解析邏輯

各解析器實現(xiàn)如下:



//String類型參數(shù)解析器
public class StringResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        return parameter.getType().isAssignableFrom(String.class);
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        return req.getParameter(parameter.getName());
    }
}
//int類型的參數(shù)解析器
public class IntResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        return parameter.getType().isAssignableFrom(int.class) || parameter.getType().isAssignableFrom(Integer.class);
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        System.out.println("設(shè)置參數(shù):" + parameter.getName());
        return Integer.parseInt(req.getParameter(parameter.getName()));
    }
}




//有標(biāo)記 RequestBody 參數(shù)的解析
public class RequestBodyResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        return parameter.getAnnotation(RequestBody.class) != null;
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        String bodyData = getBodyData(req);
        System.out.println("body:"+bodyData);
        try {
          //將JSON類型的參數(shù)映射到Jav實體類
            return JSON.toJavaObject(JSON.parseobject(bodyData), parameter.getType().newInstance().getClass());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    //獲取請求body數(shù)據(jù)
    private String getBodyData(HttpServletRequest request) {
        StringBuffer data = new StringBuffer();
        String line = null;
        BufferedReader reader = null;
        try {
            reader = request.getReader();
            while (null != (line = reader.readLine())) {
                data.append(line);
            }


        } catch (IOException e) {
            e.printStackTrace();
        }
        return data.toString();
    }
}


//HttpSession類型參數(shù)解析器
public class HttpSessionResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        return parameter.getType().isAssignableFrom(HttpSession.class);
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        return req.getSession();
    }
}
//HttpServletResponse 類型的參數(shù)解析器
public class HttpServletResponseResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        return parameter.getType().isAssignableFrom(HttpServletResponse.class);
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        return resp;
    }
}


//HttpServletRequest類型的參數(shù)解析器
public class HttpServletRequestResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        return parameter.getType().isAssignableFrom(HttpServletRequest.class);
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        return req;
    }
}
//BaseEntity 實現(xiàn)了BaseEntity的類的解析器 解析參數(shù)到實體類
public class BaseEntityResolver implements BaseResolver {
    @Override
    public boolean supportsParameter(Parameter parameter) {
        try {
            return parameter.getType().newInstance() instanceof BaseEntity;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return false;
    }


    @Override
    public Object resolveArgument(Parameter parameter, HttpServletRequest req, HttpServletResponse resp) {
        Enumeration<String> parameterNames = req.getParameterNames();
        Object o = null;
        try {
            o = parameter.getType().newInstance();
            while (parameterNames.hasMoreElements()) {
                String s = parameterNames.nextElement();
                String parameter1 = req.getParameter(s);
                BeanUtils.setProperty(o, s, parameter1);
            }


        } catch (Exception e) {
            e.printStackTrace();
        }
        return o;
    }
}

4.注冊Servlet攔截所有請求,并根據(jù)請求路徑分發(fā)到對應(yīng)處理器方法 代碼實現(xiàn)如下:

public class WebServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        Enumeration<String> parameterNames = req.getParameterNames();
        String contextPath = req.getServletPath();
        System.out.println(contextPath);
        Method method = Containers.mappingMethods.get(contextPath);
        if (method == null) {
            //請求路徑不存在時 返回404 也可以重定向或轉(zhuǎn)發(fā)到404頁面
            resp.sendError(404, "頁面不存在");
        } else {
            //獲取請求類型
            String contentType = req.getContentType();
            String method1 = req.getMethod();
            System.out.println("method:" + method1 + " contentType:" + contentType);
            try {
                //獲取方法所在的類并實例化 
                Object o = method.getDeclaringClass().newInstance();
                //獲取方法所有的對象
                Parameter[] parameters = method.getParameters();
                //存放方法參數(shù)解析注入后的值 參數(shù)列表
                ArrayList<Object> params = new ArrayList<>();
              //獲取對應(yīng)參數(shù)解析器,注入?yún)?shù)
                for (Parameter parameter : parameters) {
                    Object o2 = null;
                    //獲取解析器
                    List<BaseResolver> resolvers = Containers.resolvers;
                    for (BaseResolver resolver : resolvers) {
                        //判斷是否符合預(yù)期類型
                        if (resolver.supportsParameter(parameter)) {
                        //符合 進(jìn)行解析
                            o2 = resolver.resolveArgument(parameter, req, resp);
                            break;
                        }
                    }
                    //保存解析后的值
                    params.add(o2);


                }
                //根據(jù)方法所在類及所需參數(shù)通過反射調(diào)用方法
                Object invoke = method.invoke(o, params.toArray());
                if (invoke != null) {
                    //判斷方法是否標(biāo)記了ResponseBody注解
                    ResponseBody annotation = method.getAnnotation(ResponseBody.class);
                    if (annotation != null) {
                        //有 ResponseBody 注解并且是String類型 直接響應(yīng)請求
                        if (invoke instanceof String) {
                            resp.setContentType("text/html");
                            resp.setCharacterEncoding("UTF-8");
                            // 獲取PrintWriter對象
                            PrintWriter out = resp.getWriter();
                            out.print(invoke);
                            // 釋放PrintWriter對象
                            out.flush();
                            out.close();
                        } else {
                            //如果不是String類型則裝換成JSON格式字符串在進(jìn)行響應(yīng)
                            resp.setContentType("application/json; charset=utf-8");
                            resp.setCharacterEncoding("UTF-8");
                            // 獲取PrintWriter對象
                            PrintWriter out = resp.getWriter();
                            out.print(JSON.toJSONString(invoke));
                            // 釋放PrintWriter對象
                            out.flush();
                            out.close();
                        }


                    } else {
                        //沒有標(biāo)記注解 返回String類型 則轉(zhuǎn)發(fā)到返回字符串所對應(yīng)的jsp頁面
                        if (invoke instanceof String) {
                            RequestDispatcher dispatcher = req.getRequestDispatcher("/WEB-INF/views/" + invoke + ".jsp");
                            dispatcher.forward(req, resp);
                        } else {
                            new RuntimeException("返回值不是String類型 且沒有@ResponseBody注解");
                        }
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }

        }

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }

5.所用到的注解標(biāo)記

RequestBody

@Target({ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
}

RequestMapping

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
    String value() default "";
}

ResponseBody

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}

6.所用到工具類

ClassUtils



public class ClassUtils {
    public static Set<Class<?>> getClasses(String pack) {


        // 第一個class類的集合
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
        // 是否循環(huán)迭代
        boolean recursive = true;
        // 獲取包的名字 并進(jìn)行替換
        String packageName = pack;
        String packageDirName = packageName.replace('.', '/');
        // 定義一個枚舉的集合 并進(jìn)行循環(huán)來處理這個目錄下的things
        Enumeration<URL> dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(
                    packageDirName);
            // 循環(huán)迭代下去
            while (dirs.hasMoreElements()) {
                // 獲取下一個元素
                URL url = dirs.nextElement();
                // 得到協(xié)議的名稱
                String protocol = url.getProtocol();
                // 如果是以文件的形式保存在服務(wù)器上
                if ("file".equals(protocol)) {
                    System.err.println("file類型的掃描");
                    // 獲取包的物理路徑
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    // 以文件的方式掃描整個包下的文件 并添加到集合中
                    findAndAddClassesInPackageByFile(packageName, filePath,
                            recursive, classes);
                } else if ("jar".equals(protocol)) {
                    // 如果是jar包文件
                    // 定義一個JarFile
                    System.err.println("jar類型的掃描");
                    JarFile jar;
                    try {
                        // 獲取jar
                        jar = ((JarURLConnection) url.openConnection())
                                .getJarFile();
                        // 從此jar包 得到一個枚舉類
                        Enumeration<JarEntry> entries = jar.entries();
                        // 同樣的進(jìn)行循環(huán)迭代
                        while (entries.hasMoreElements()) {
                            // 獲取jar里的一個實體 可以是目錄 和一些jar包里的其他文件 如META-INF等文件
                            JarEntry entry = entries.nextElement();
                            String name = entry.getName();
                            // 如果是以/開頭的
                            if (name.charAt(0) == '/') {
                                // 獲取后面的字符串
                                name = name.substring(1);
                            }
                            // 如果前半部分和定義的包名相同
                            if (name.startsWith(packageDirName)) {
                                int idx = name.lastIndexOf('/');
                                // 如果以"/"結(jié)尾 是一個包
                                if (idx != -1) {
                                    // 獲取包名 把"/"替換成"."
                                    packageName = name.substring(0, idx)
                                            .replace('/', '.');
                                }
                                // 如果可以迭代下去 并且是一個包
                                if ((idx != -1) || recursive) {
                                    // 如果是一個.class文件 而且不是目錄
                                    if (name.endsWith(".class")
                                            && !entry.isDirectory()) {
                                        // 去掉后面的".class" 獲取真正的類名
                                        String className = name.substring(
                                                packageName.length() + 1, name
                                                        .length() - 6);
                                        try {
                                            // 添加到classes
                                            classes.add(Class
                                                    .forName(packageName + '.'
                                                            + className));
                                        } catch (ClassNotFoundException e) {
                                            // log
                                            // .error("添加用戶自定義視圖類錯誤 找不到此類的.class文件");
                                            e.printStackTrace();
                                        }
                                    }
                                }
                            }
                        }
                    } catch (IOException e) {
                        // log.error("在掃描用戶定義視圖時從jar包獲取文件出錯");
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }


        return classes;
    }


    public static void findAndAddClassesInPackageByFile(String packageName,
                                                        String packagePath, final boolean recursive, Set<Class<?>> classes) {
        // 獲取此包的目錄 建立一個File
        File dir = new File(packagePath);
        // 如果不存在或者 也不是目錄就直接返回
        if (!dir.exists() || !dir.isDirectory()) {
            // log.warn("用戶定義包名 " + packageName + " 下沒有任何文件");
            return;
        }
        // 如果存在 就獲取包下的所有文件 包括目錄
        File[] dirfiles = dir.listFiles(new FileFilter() {
            // 自定義過濾規(guī)則 如果可以循環(huán)(包含子目錄) 或則是以.class結(jié)尾的文件(編譯好的java類文件)
            @Override
            public boolean accept(File file) {
                return (recursive && file.isDirectory())
                        || (file.getName().endsWith(".class"));
            }
        });
        // 循環(huán)所有文件
        for (File file : dirfiles) {
            // 如果是目錄 則繼續(xù)掃描
            if (file.isDirectory()) {
                findAndAddClassesInPackageByFile(packageName + "."
                                + file.getName(), file.getAbsolutePath(), recursive,
                        classes);
            } else {
                // 如果是java類文件 去掉后面的.class 只留下類名
                String className = file.getName().substring(0,
                        file.getName().length() - 6);
                try {
                    // 添加到集合中去
                    //classes.add(Class.forName(packageName + '.' + className));
                    //經(jīng)過回復(fù)同學(xué)的提醒,這里用forName有一些不好,會觸發(fā)static方法,沒有使用classLoader的load干凈
                    classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
                } catch (ClassNotFoundException e) {
                    // log.error("添加用戶自定義視圖類錯誤 找不到此類的.class文件");
                    e.printStackTrace();
                }
            }
        }
    }


}

7.web.xml 配置:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">


    <display-name>Archetype Created Web Application</display-name>
    <!--啟動監(jiān)聽器配置-->
    <listener>
        <listener-class>com.kexun.mvc.listen.InitMapping</listener-class>
    </listener>
     <!--統(tǒng)一攔截Servlet配置-->
    <servlet>
        <servlet-name>webServlet</servlet-name>
        <servlet-class>com.kexun.mvc.servlet.WebServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <!--靜態(tài)資源默認(rèn)處理Servlet-->
    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.Apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
     <!--靜態(tài)資源默認(rèn)處理Servlet-->
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/static/*</url-pattern>
    </servlet-mapping>
     <!--統(tǒng)一攔截servlet 攔截所有請求-->
    <servlet-mapping>
        <servlet-name>webServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>




</web-app>

8.項目所用到的類庫

 

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>


        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
        </dependency>


        <dependency>
            <groupId>MySQL</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.73</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>


        <dependency>
            <groupId>org.apache.Tomcat</groupId>
            <artifactId>tomcat-catalina</artifactId>
            <version>8.5.61</version>
        </dependency>


    </dependencies>

 

9.基本使用

使用方法基本與SpringMvc一致,注解大同小異 。

示例代碼如下:



public class IndexController {


    //獲取字符串,int類型參數(shù) 返回頁面
    @RequestMapping("index")
    public String index(HttpServletRequest request, String username, Integer age) throws Exception {
        request.setAttribute("username", username);
        request.setAttribute("age", age);
        return "index";
    }
     
     //獲取對象參數(shù) 返回對象
    @ResponseBody
    @RequestMapping("addManage")
    public Map<String, Object> addManage(Manage manage) throws Exception {
        System.out.println("manage:" + manage);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("message", "添加成功");
        return result;
    }
    //獲取JSON請求 解析為對象 返回對象類型
    @ResponseBody
    @RequestMapping("addManageJSON")
    public Map<String, Object> addManageJSON(@RequestBody Manage manage) throws Exception {
        System.out.println("manage:" + manage);
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("message", "添加成功");
        return result;
    }




}

 

此框架實用與一些小項目,案例,需要快速搭建的,開箱即用的場景;沒有復(fù)雜的配置,同時也適用于學(xué)習(xí),不允許適用開源框架的場景下,這便是個很好的選擇

項目源碼已上傳至碼云:

https://gitee.com/gdianqimeng/kexun-mvc-orm

個人博客地址:https://muzidong.com

分享到:
標(biāo)簽:框架 SpringMVC
用戶無頭像

網(wǎng)友整理

注冊時間:

網(wǎng)站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨大挑戰(zhàn)2018-06-03

數(shù)獨一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運動步數(shù)有氧達(dá)人2018-06-03

記錄運動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定