IntelliJ IDEA使用技巧

IntelliJ IDEA使用技巧

一、高效定位代码

  • 打开搜索框: Ctrl+Shift+A
  • 查看所有快捷键: Ctrl+Shift+A打开搜索框,输入keymap
  • 打开或关闭某个窗口,可以从View->Tool Window看到对应的命令: Alt+数字

1. 代码跳转

项目之间跳转
  • 切换到前一个项目/后一个项目:Ctrl+Alt+左括号/右括号
文件之间跳转
  • 最近打开的文件:Ctrl+E
  • 最近修改的文件:Alt+Shift+C
利用书签跳转

可以在搜索命令框搜索bookmark

  • 显示书签: Shift+F11
  • 设置无序书签: F11
  • 设置有序书签: Ctrl+F11,然后设置序号
  • 跳转到各个书签位置: Ctrl+序号,例如 Ctrl+1,Ctrl+3
添加到收藏

使用 Alt+2 可以打开收藏视图

  • 收藏类或类中的某个方法:将光标放在类名或方法名上,使用 Alt+Shift+F 收藏类或方法的代码
代码编辑区和文件区跳转
  • 跳转到文件区: Alt+1
  • 跳转到代码编辑区域: Esc

2. 精准搜索

  • 搜索类:Ctrl+N
  • 搜索文件:Ctrl+Shift+N
  • 搜索符号:Ctrl+Shift+Ctrl+N
  • 搜索字符串:Ctrl+Shift+F

二、批处理操作

1. 列操作

快捷键:

  • 选中某个单词: Ctrl+Shift+左右箭头
  • 跳转到前/后一个单词: Ctrl+左右箭头
  • 将选中字符串全部转换为大写或小写: Ctrl+Shift+U
  • 选中所有匹配的目标: Ctrl+Shift+Alt+J

例1:如何将下面左边内容快速地转换为右边内容

# line1               # line1
100: Aaaa             AAAA(100)
200: BbB              BBB(200)
300: cC               CC(300)

# line2               # line2
400: DDeeD            DDDDD(400)
500: eE               EE(500)

# line3               # line3
600: FfFF             FFFF(600)
700: Ggg              GGG(700)

具体做法:

  • (1) 使用 Ctrl+Shift+左右箭头 选中某个统一的符号,例如 ,使用Ctrl+Shift+Alt+J选中所有行中统一的符号
  • (2) 使用Ctrl+Shift+左右箭头选中所有数字并剪切至行尾,添加左右括号,统一删除:和空格
  • (3) 使用Ctrl+Shift+左右箭头选中每一行中的单词,使用Ctrl+Shift+U转换大小写,剪切至数字前面

2. 代码模板

使用Ctrl+Shift+A 搜索到 Live Template,在其中可以添加 Live TemplateTemplate Group

例1:可以定义template为psvm,描述为public static void main,具体内容为:

public static void main(String[] args) {
    $END$
}

设置好应用场景。以后在输代码时,当输入psvm并按回车键,会自动出现上面代码块,并且光标停留在 $END$ 区域。

例2:可以定义template为psc,描述为public String,具体内容为:

private String $VAR1$; //$VAR2$
$END$

设置好应用场景。以后在输代码时,当输入psc并按回车键,会自动出现上面代码块,并且光标会依次停留在 $VAR1$$END$ 区域。

3. postfix

使用Ctrl+Shift+A 搜索到 Postfix Completion,在其中可以查看预设的postfix。

下面是常见的几种postfix:

  • for: 输入数字.for后按回车键,会出现可供选择的postfix,选择其中一个后会自动填充代码

例如输入 100.for ,选择 fori 并按回车键后,会自动填充如下代码:

for (int i = 0; i < 100; i++) {

}
  • sout: 输入 XXX.sout后按回车键会自动填充代码

例如输入 new Date().sout并按回车键后,会自动填充如下代码:

System.out.println(new Date());
  • return: 输入XXX.r,选择return并按回车,会自动填充代码

例如输入 user.r,选择return并按回车键后,会自动填充如下代码:

return user;
  • nn: 输入XXX.nn按回车键会自动填充代码

例如输入 user.nn并按回车键后,会自动填充如下代码:

if (user != null) {

}

4. alter+enter,自动提示

  • 自动创建函数
  • list replace
  • 字符串format或build
  • 实现接口
  • 单词拼写
  • 导入包

三、编写高质量的代码

1. 重构

  • 重构变量/变量重命名: Shift+F6

如果在编写代码的过程中,想要修改某个变量的变量名以及该变量所有出现位置的名称,可以使用Shift+F6实现全部修改。

  • 重构方法/修改方法签名: Ctrl+F6

如果在修改代码的过程中,想要修改某个被调用方法的方法签名,可以使用Ctrl+F6实现参数的删除、增加或修改。

2. 抽取

  • 抽取局部变量: Ctrl+Alt+V

例1:有如下一段代码,我们想要将字符串”123”抽取出来定义成一个变量,我们可以在某个字符串”123”上使用Ctrl+Alt+V将其定义为变量。

public void fun() {
    System.out.println("123");
    System.out.println("123");
    System.out.println("123");
    System.out.println("123");
    System.out.println("123");
}

最终抽取结果如下:

public void fun() {
    String var = "123";
    System.out.println(var);
    System.out.println(var);
    System.out.println(var);
    System.out.println(var);
    System.out.println(var);
}
  • 抽取静态变量: Ctrl+Alt+C
  • 抽取成员变量: Ctrl+Alt+F
  • 抽取方法参数: Ctrl+Alt+P

例2:假设有如下一段代码,我们想要将局部变量x抽取为方法参数,我们可以在x上使用Ctrl+Alt+P将其抽取为方法参数

public void fun() {
    String x = "abc";
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
}

最终抽取结果如下:

public void fun(String x) {
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
}
  • 抽取方法: Ctrl+Alt+M

例3:假设有如下一段代码,我们想将其抽取为三个函数调用,我们可以在一个代码块上使用Ctrl+Alt+M将其抽取为一个方法

public void fun(String x) {
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
    System.out.println(x);
}

最终抽取结果如下:

public void fun(String x) {
    fun1(x);
    fun2(x);
    fun3(x);
}

private void fun3(String x) {
    System.out.println(x);
    System.out.println(x);
}

private void fun2(String x) {
    System.out.println(x);
}

private void fun1(String x) {
    System.out.println(x);
    System.out.println(x);
}

四、寻找修改轨迹

1. git集成

  • 查看代码记录:annotate
  • 查看所有修改:Ctrl+Alt+Shift+上下箭头
  • 撤销(某一块、某一个文件、某一个项目的)修改:Ctrl+Alt+Z

2. local history

当项目不受版本控制时,我们可以通过local history查看代码的修改记录

五、关联一切

1. 关联Spring

File -> Project Structure -> Facets -> Add -> Spring -> 将配置文件选中后,将可以让IDEA帮助管理Spring代码

2. 关联数据库

View -> Tool Window -> Database -> Add -> Data Source -> 选择具体的数据库,填入相关信息,便可以在IDEA中方便的使用数据库的信息。

六、调试程序

七、其他操作

  • 查看类中所有方法和属性: Ctrl+F12
  • 查看Maven依赖结构: Ctrl+Alt+Shift+U
  • 产看类图及继承关系: Ctrl+Alt+Shift+U
  • 查看类继承层次结构:Ctrl+H
  • 查看方法调度层次结构: Ctrl+Alt+H
分享到 评论

新一代构建工具Gradle

分享到 评论

全面解析Java注解

全面解析Java注解

一、 注解概述

1. JDK自带注解

  • @Override 告诉编译器该方法覆盖了父类的方法
  • @Deprecated 表示该方法已经过时
  • @Suppvisewarnings 表示忽略了”deprecation”警告

2. 常见第三方注解

Spring中常见注解:@Autowired, @Service,@Repository

Mybatis中常见注解:@InsertProvider,@UpdataProvider,@Options

3. Java注解的分类

  • 按运行机制分类
    • 源码注解:注解只在源码中存在,编译成.class文件就不存在了。
    • 编译时注解:注解在源码和.class文件中都存在,如JDK自带注解。
    • 运行时注解:在运行阶段依然起作用,甚至会影响程序的运行逻辑。
  • 按注解来源分类:
    • JDK自带注解
    • 第三方注解
    • 自定义注解

二、自定义注解

1. 定义注解

自定义注解的语法要求
  • 使用@interface关键字定义注解,其中成员必须以无参无异常方式声明,同时可以用default为成员指定一个默认值;
  • 自定义注解的成员类型是受限的,合法的类型包括原始类型、String、Class、Annotation和Enumeration;
  • 如果注解只有一个成员,那么成员名必须取名为value(),并且在使用时可以忽略成员名和赋值号(=);
  • 注解类可以没有成员,没有成员的注解称为标识注解。
元注解说明
  • @Target: 指定注解的作用域:
    • CONSTRUCTOR,构造方法
    • FIELD,属性
    • LOCAL_VARIABLE,局部变量
    • METHOD,方法
    • PACKAGE,包
    • PARAMETER,参数
    • TYPE 类或接口
  • @Retention: 指定注解的生命周期
    • SOURCE,源码级别,编译时丢失
    • CLASS,编译级别,运行时丢失
    • RUNTIME,运行级别,可以通过反射获取
  • @Inherited: 标识注解允许子类继承
  • @Documented: 指定生成javadoc时会包含注解
例1: 自定义注解的实例:
@Target({ElementType.METHOD, ElementType.Type})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Description {
    String desc();
    String author();
    int age() default 18;
}

2. 使用自定义注解

使用注解的语法: @<注解名>(<成员名1>=<成员值1>, <成员名2>=<成员值2>, …)

例2:使用自定义注解的实例
@Description(desc="I am eyeColor", author="Mooc Boy", age=18)
public String eyeColor() {
    return "red";
}

三、解析注解

解析注解:通过反射获取类、函数或成员上的运行时注解信息,从而实现动态控制程序的运行逻辑。

例1:下面通过实例演示解析注解的过程

(1) 定义使用自定义注解的父类

@Description(desc = "I am annotation in parent class", author = "Parent")
public class Parent {
    @Description(desc = "I am annotation in parent method", author = "Parent")
    public String name() {
        return "name:parent";
    }
}

(2) 定义使用自定义注解的子类

@Description(desc = "I am annotation in child class", author = "Child")
public class Child extends Parent {
    @Description(desc = "I am annotation in child method", author = "Child")
    @Override
    public String name() {
        return "name:child";
    }
}

(3) 解析类和方法上的注解

public class Analyzer {
    public static void main(String[] args) {
        try {
            // 类注解解析方式
            Class clazz = Class.forName("Child");
            boolean exists = clazz.isAnnotationPresent(Description.class);
            if(exists) {
                Description description = (Description) clazz.getAnnotation(Description.class);
                System.out.println(description.desc());
            }

            // 第一种方法注解解析方式
            Method[] methods = clazz.getMethods();
            for(Method method : methods) {
                exists = method.isAnnotationPresent(Description.class);
                if(exists) {
                    Description description = method.getAnnotation(Description.class);
                    System.out.println(description.desc());
                }
            }

            // 第二种方法注解解析方法
            for(Method method:methods) {
                Annotation[] annotations = method.getAnnotations();
                for(Annotation annotation:annotations) {
                    if(annotation instanceof Description) {
                        Description description = (Description) annotation;
                        System.out.println(description.desc());
                    }
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

解析结果如下:

I am annotation in child class
I am annotation in child method
I am annotation in child method

注意:

  • @Inherited只能实现类上注解的继承,而无法实现接口上注解的继承,即接口的注解无法影响到实现接口的类上面。
  • 父类方法的注解也无法被子类继承。
例2:修改注解如下:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
@Inherited
@Documented
public @interface Description {
    String desc();
    String author();
    int age() default 18;
}

此时解析结果没有输出,说明该注解只作用于编译阶段,运行时注解丢失

例3:修改子类如下
public class Child extends Parent {
    @Override
    public String name() {
        return "name:child";
    }
}

此时解析结果如下,说明父类方法的注解无法被子类方法继承:

I am annotation in parent class
例4:修改父类如下
@Description(desc = "I am annotation in parent interface", author = "Parent")
public interface Parent {
    String name();
}

修改子类如下:

public class Child implements Parent {
    @Override
    public String name() {
        return "name:child";
    }
}

此时解析结果没有输出,说明接口的注解无法影响到实现接口的类。

四、项目实战

定义类:

public class User {
    private int id;
    private String username;
    private String nickname;
    private int age;
    private String city;
    private String email;
    private String mobile;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getMobile() {
        return mobile;
    }
    public void setMobile(String mobile) {
        this.mobile = mobile;
    }
}

需求:给定上面展示的实体类及设置了相关属性的对象,实现实体对象对应的SQL查询语句

public static void main(String[] args) {
    // 查询id为10的用户信息
    User user1 = new User();
    user1.setId(10);

    // 查询username为"lucy"的用户信息
    User user2 = new User();
    user2.setUsername("lucy");

    // 查询email在"liu@sina.com,zh@163.com,777@qq.com"中的用户信息
    User user3 = new User();
    user3.setEmail("liu@sina.com,zh@163.com,777@qq.com");

    String sql1 = query(user1);
    String sql2 = query(user2);
    String sql3 = query(user3);

    System.out.println(sql1);
    System.out.println(sql2);
    System.out.println(sql3);
}

实现:通过自定义注解和注解解析实现SQL转化过程

(1) 定义注解Table

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
    String value();
}

(2) 定义注解Column

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value();
}

(3) 给实体类添加注解

@Table("user")
public class User {
    @Column("id")
    private int id;
    @Column("username")
    private String username;
    @Column("nickname")
    private String nickname;
    @Column("age")
    private int age;
    @Column("city")
    private String city;
    @Column("email")
    private String email;
    @Column("mobile")
    private String mobile;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getNickname() {
        return nickname;
    }
    public void setNickname(String nickname) {
        this.nickname = nickname;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getCity() {
        return city;
    }
    public void setCity(String city) {
        this.city = city;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public String getMobile() {
        return mobile;
    }
    public void setMobile(String mobile) {
        this.mobile = mobile;
    }
}

(4) 通过注解解析实现转化过程

private static String query(User user) {
    StringBuilder sb = new StringBuilder();

    Class clazz = user.getClass();
    boolean exists = clazz.isAnnotationPresent(Table.class);
    if(!exists)
        return null;
    Table table = (Table) clazz.getAnnotation(Table.class);
    String tableName = table.value();
    sb.append("select * from ").append(tableName).append(" where 1=1");

    Field[] fields = clazz.getDeclaredFields();
    for(Field field:fields) {
        exists = field.isAnnotationPresent(Column.class);
        if(!exists)
            continue;
        Column column = field.getAnnotation(Column.class);
        String fieldName = field.getName();
        String methodName = "get"+fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        Object fieldValue=null;
        try {
            Method method = clazz.getMethod(methodName);
            fieldValue = method.invoke(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if(fieldValue==null || (fieldValue instanceof Integer && (Integer)fieldValue==0)){
            continue;
        }
        sb.append(" and ").append(fieldName);
        if(fieldValue instanceof String) {
            if(((String)fieldValue).contains(",")) {
                String[] values = ((String)fieldValue).split(",");
                sb.append(" in (");
                for(String value: values) {
                    sb.append("'").append(value).append("'").append(",");
                }
                sb.deleteCharAt(sb.length()-1);
                sb.append(")");
            }else {
                sb.append("=").append("'").append(fieldValue).append("'");
            }
        }else {
            sb.append("=").append(fieldValue);
        }
    }
    return sb.toString();
}

(4) 测试输出结果:

select * from user where 1=1 and id=10
select * from user where 1=1 and username='lucy'
select * from user where 1=1 and email in ('liu@sina.com','zh@163.com','777@qq.com')
分享到 评论

07-文件上传

文件上传

在Spring MVC中有两种实现上传文件的办法,第一种是Servlet3.0以下的版本通过commons-fileupload与commons-io完成的通用上传,第二种是Servlet3.0以上的版本通过Spring内置标准上传,不需借助第3方组件。通用上传也兼容Servlet3.0以上的版本。

一、Servlet3.0以下版本

1. 添加上传依赖包

因为需要借助第三方上传组件commons-fileupload与commons-io,所以要修改pom.xml文件添加依赖。

<!--文件上传 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

2. 新增上传页面upload1.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>上传文件</title>
    </head>
    <body>
        <h2>上传文件</h2>
        <form action="fileSave" method="post" enctype="multipart/form-data">
            <p>
                <label for="files">文件:</label> <input type="file" name="files" id="files" multiple="multiple" />
            </p>
            <p>
                <button>提交</button>
            </p>
            <p>${message}</p>
        </form>
    </body>
</html>

需要注意的关键点:

  • method的值必为Post;
  • enctype必须为multipart/form-data,该类型的编码格式专门用于二进制数据类型;
  • 上传表单元素必须拥有name属性;

3. 修改配置文件,增加上传配置

默认情总下Spring MVC对文件上传的视图内容是不能解析的,需要修改springmvc-servlet.xml配置文件配置一个CommonsMultipartResolver类型的解析器解析上传的内容。

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8" />
    <property name="maxUploadSize" value="10485760000" />
    <property name="maxInMemorySize" value="40960" />
</bean>

其具有的各属性的意义如下:

  • defaultEncoding:默认编码格式
  • maxUploadSize:上传文件最大限制(字节byte)
  • maxInMemorySize:缓冲区大小

当Spring的前置中心控制器接收到客户端发送的一个多分部请求,定义在上下文中的解析器将被激活并开始处理。解析器将当前的HttpServletRequest包装成一个支持多部分文件上传的MultipartHttpServletRequest对象,在控制器中可以获得上传的文件信息。

  • CommonsMultipartResolver用于通用的文件上传,支持各种版本的Servlet。
  • StandardServletMultipartResolver用于Servlet3.0以上的版本上传文件。

4. 增加控制器与Action

package org.spring.mvc.controller;

import java.io.File;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;

@Controller
@RequestMapping("/upload")
public class FileUploadController {
    @RequestMapping("/file1")
    public String file1(Model model) {
        return "upload/upload1";
    }

    @RequestMapping(value = "/fileSave", method = RequestMethod.POST)
    public String fileSave(Model model, MultipartFile[] files, HttpServletRequest request) throws Exception {
        for (MultipartFile file : files) {
            System.out.println(file.getOriginalFilename());
            System.out.println(file.getSize());
            System.out.println("--------------------------");
            File tempFile = new File("D:/", file.getOriginalFilename());
            file.transferTo(tempFile);
        }
        return "upload/upload1";
    }
}

注意这里定义的是一个MultipartFile数组,可以接受多个文件上传,如果单文件上传可以修改为MultipartFile类型;另外上传文件的细节在这里并没有花时间处理,比如文件重名的问题,路径问题,关于重名最简单的办法是重新命名为GUID文件名。

5. 测试运行

访问路径:http://localhost:8080/spring-mvc/upload/file1,运行结果如图1所示:


图 1

选择三个文件file1.txt,file2.txt,file3.txt,点击提交。


图 2

控制台输出:

file1.txt
15
--------------------------
file2.txt
15
--------------------------
file3.txt
15
--------------------------

图 3

二、Servlet3.0以上版本

Servlet3.0以上的版本不再需要第三方组件Commons.io和commons-fileupload,上传的方式与4.1提到基本一样,但配置稍有区别,可以使用@MultipartConfig注解在Servlet上进行配置上传,也可以在web.xml上进行配置。

1. 修改web.xml配置上传参数

<!--Servlet3.0以上文件上传配置 -->
<multipart-config>
    <max-file-size>5242880</max-file-size><!--上传单个文件的最大限制5MB -->
    <max-request-size>20971520</max-request-size><!--请求的最大限制20MB,一次上传多个文件时一共的大小 -->
    <file-size-threshold>0</file-size-threshold><!--当文件的大小超过临界值时将写入磁盘 -->
</multipart-config>

multipart-config的各参数含义如下:

  • file-size-threshold:数字类型,当文件大小超过指定的大小后将写入到硬盘上。默认是0,表示所有大小的文件上传后都会作为一个临时文件写入到硬盘上。
  • location:指定上传文件存放的目录。当我们指定了location后,我们在调用Part的write(String fileName)方法把文件写入到硬盘的时候可以,文件名称可以不用带路径,但是如果fileName带了绝对路径,那将以fileName所带路径为准把文件写入磁盘,不建议指定。
  • max-file-size:数值类型,表示单个文件的最大大小。默认为-1,表示不限制。当有单个文件的大小超过了max-file-size指定的值时将抛出IllegalStateException异常。
  • max-request-size:数值类型,表示一次上传文件的最大大小。默认为-1,表示不限制。当上传时所有文件的大小超过了max-request-size时也将抛出IllegalStateException异常。

2. 修改pom.xml依赖信息

删除pom.xml中对文件上传第三方的依赖。

3. 修改springmvc-servlet.xml配置信息

将原有的文件上传通用解析器更换为标准解析器:

<!--文件上传解析器 -->
<bean id="multipartResolver"
    class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>

4. 定义视图

在view/up/下定义名称为upload2.jsp文件:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>上传文件 - Servlet3.0</title>
    </head>
    <body>
        <h2>上传文件 - Servlet3.0</h2>
        <form action="file3Save" method="post" enctype="multipart/form-data">
            <p>
                <label for="files">文件:</label> <input type="file" name="files" id="files" multiple="multiple" />
            </p>
            <p>
                <button>提交</button>
            </p>
            <p>${message}</p>
        </form>
    </body>
</html>

5. 定义Action

@RequestMapping("/file2")
public String file2(Model model){
    return "upload/upload2";
}

@RequestMapping(value="/file3Save", method=RequestMethod.POST)
public String file3Save(Model model,MultipartFile[] files, HttpServletRequest request) throws Exception{
    for (MultipartFile file : files) {
        System.out.println(file.getOriginalFilename());
        System.out.println(file.getSize());
        System.out.println("--------------------------");
        File tempFile=new File(file.getOriginalFilename());
        file.transferTo(tempFile);
    }
    return "upload/upload2";
}

6. 测试运行

访问路径:http://localhost:8080/spring-mvc/upload/file2

分享到 评论

06-Spring MVC验证器

Spring MVC验证器

在展示Spring MVC验证器之前,先在前面项目的基础上,通过一个相对综合的示例串联前面学习过的一些知识点,主要实现产品管理管理功能,包含产品的添加,删除,修改,查询,多删除功能。

一、综合示例

1. 新建POJO实体(entity)

在包org.spring.mvc.model包中新增产品类型类ProductType:

package org.spring.mvc.model;

import java.io.Serializable;

/**
 * 产品类型
 */
public class ProductType implements Serializable {
    private static final long serialVersionUID = 2L;
    // 编号
    private int id;
    // 名称
    private String name;

    public ProductType() {
    }

    public ProductType(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "编号:" + this.getId() + ",名称:" + this.getName();
    }
}

修改产品POJO类Product:

package org.spring.mvc.model;

import java.io.Serializable;

/**
 * 产品
 */
public class Product implements Serializable {
    private static final long serialVersionUID = 1L;
    // 编号
    private int id;
    // 名称
    private String name;
    // 价格
    private double price;
    // 产品类型
    private ProductType productType;

    public Product() {
        productType = new ProductType();
    }

    public Product(String name, double price) {
        super();
        this.name = name;
        this.price = price;
    }

    public Product(int id, String name, double price) {
        super();
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public Product(int id, String name, double price, ProductType type) {
        super();
        this.id = id;
        this.name = name;
        this.price = price;
        this.productType = type;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public ProductType getProductType() {
        return productType;
    }

    public void setProductType(ProductType productType) {
        this.productType = productType;
    }

    @Override
    public String toString() {
        return "编号(id):" + this.getId() + ",名称(name):" + this.getName() + ",价格(price):" + this.getPrice() + ",类型(productType.Name):" + this.getProductType().getName();
    }
}

2. 新建业务层(Service)

在包org.spring.mvc.service中创建产品类型服务接口ProductTypeService:

package org.spring.mvc.service;

import java.util.List;
import org.spring.mvc.model.ProductType;

/**
 * 产品类型服务
 *
 */
public interface ProductTypeService {
    /**
     * 根据产品类型编号获得产品类型对象
     */
    public ProductType getProductTypeById(int id);

    /**
     * 获得所有的产品类型
     */
    public List<ProductType> getAllProductTypes();
}

创建产品类型服务接口ProductTypeService的实现类ProductTypeServiceImpl:

package org.spring.mvc.service;

import java.util.ArrayList;
import java.util.List;

import org.spring.mvc.model.ProductType;
import org.springframework.stereotype.Service;

@Service
public class ProductTypeServiceImpl implements ProductTypeService {
    private static List<ProductType> productTypes;

    static {
        productTypes = new ArrayList<ProductType>();
        productTypes.add(new ProductType(11, "数码电子"));
        productTypes.add(new ProductType(21, "鞋帽服饰"));
        productTypes.add(new ProductType(31, "图书音像"));
        productTypes.add(new ProductType(41, "五金家电"));
        productTypes.add(new ProductType(51, "生鲜水果"));
    }

    @Override
    public ProductType getProductTypeById(int id) {
        for (ProductType productType : productTypes) {
            if (productType.getId() == id) {
                return productType;
            }
        }
        return null;
    }

    @Override
    public List<ProductType> getAllProductTypes() {
        return productTypes;
    }
}

在包org.spring.mvc.service中创建产品服务接口ProductService:

package org.spring.mvc.service;

import java.util.List;

import org.spring.mvc.model.Product;

public interface ProductService {
    /**
     * 获得所有的产品
     */
    List<Product> getAllProducts();

    /**
     * 通过编号获得产品
     */
    Product getProductById(int id);

    /**
     * 通过名称获得产品
     */
    List<Product> getProductsByName(String productName);

    /**
     * 新增产品对象
     */
    void addProduct(Product enttiy) throws Exception;

    /**
     * 更新产品对象
     */
    public void updateProduct(Product entity) throws Exception;

    /**
     * 删除产品对象
     */
    void deleteProduct(int id);

    /**
     * 多删除产品对象
     */
    void deletesProduct(int[] ids);
}

创建产品服务接口ProductService的实现类ProductServiceImpl:

package org.spring.mvc.service;

import java.util.ArrayList;
import java.util.List;

import org.spring.mvc.model.Product;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {
    private static List<Product> products;

    static {
        ProductTypeService productTypeService = new ProductTypeServiceImpl();
        products = new ArrayList<Product>();
        products.add(new Product(198, "Huwei P8", 4985.6, productTypeService.getProductTypeById(11)));
        products.add(new Product(298, "李宁运动鞋", 498.56, productTypeService.getProductTypeById(21)));
        products.add(new Product(398, "Spring MVC权威指南", 49.856,productTypeService.getProductTypeById(31)));
        products.add(new Product(498, "山东国光苹果", 4.9856, productTypeService.getProductTypeById(51)));
        products.add(new Product(598, "8开门超级大冰箱", 49856.1, productTypeService.getProductTypeById(41)));
    }

    /**
     * 获得所有的产品
     */
    @Override
    public List<Product> getAllProducts() {
        return products;
    }

    /**
     * 通过编号获得产品
     */
    @Override
    public Product getProductById(int id) {
        for (Product product : products) {
            if (product.getId() == id) {
                return product;
            }
        }
        return null;
    }

    /**
     * 通过名称获得产品名称
     */
    @Override
    public List<Product> getProductsByName(String productName) {
        if (productName == null || productName.equals("")) {
            return getAllProducts();
        }
        List<Product> result = new ArrayList<Product>();
        for (Product product : products) {
            if (product.getName().contains(productName)) {
                result.add(product);
            }
        }
        return result;
    }

    /**
     * 新增
     * 
     * @throws Exception
     */
    @Override
    public void addProduct(Product entity) throws Exception {
        if (entity.getName() == null || entity.getName().equals("")) {
            throw new Exception("产品名称必须填写");
        }
        if (products.size() > 0) {
            entity.setId(products.get(products.size() - 1).getId() + 1);
        } else {
            entity.setId(1);
        }
        products.add(entity);
    }

    /**
     * 更新
     */
    public void updateProduct(Product entity) throws Exception {
        if (entity.getPrice() < 0) {
            throw new Exception("价格必须大于0");
        }
        Product source = getProductById(entity.getId());
        source.setName(entity.getName());
        source.setPrice(entity.getPrice());
        source.setProductType(entity.getProductType());
    }

    /**
     * 删除
     */
    @Override
    public void deleteProduct(int id) {
        products.remove(getProductById(id));
    }

    /**
     * 多删除
     */
    @Override
    public void deletesProduct(int[] ids) {
        for (int id : ids) {
            deleteProduct(id);
        }
    }
}

3. 实现展示、查询、删除与多删除功能

在org.spring.mvc.controller包中定义一个名为ProductController的控制器

index请求处理方法在路径映射注解@RequestMapping中也并未指定value值是让该action为默认action,所有当我们访问系统时这个index就成了欢迎页。

package org.spring.mvc.controller;

import org.spring.mvc.model.Product;
import org.spring.mvc.service.ProductService;
import org.spring.mvc.service.ProductTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/product")
public class ProductController {
    @Autowired
    ProductService productService;
    @Autowired
    ProductTypeService productTypeService;

    /**
     * 展示与搜索
     */
    @RequestMapping
    public String index(Model model, String searchKey) {
        model.addAttribute("products", productService.getProductsByName(searchKey));
        model.addAttribute("searchKey", searchKey);
        return "product/index";
    }

    /**
     * 删除,id为路径变量
     */
    @RequestMapping("/delete/{id}")
    public String delete(@PathVariable int id) {
        productService.deleteProduct(id);
        return "redirect:/product";
    }

    /**
     * 多删除,ids的值为多个id参数组成
     */
    @RequestMapping("/deletes")
    public String deletes(@RequestParam("id") int[] ids) {
        productService.deletesProduct(ids);
        return "redirect:/product";
    }
}

定义所有页面风格用的main.css样式:

@CHARSET "UTF-8";

* {
    margin: 0;
    padding: 0;
    font-family: microsoft yahei;
    font-size: 14px;
}

body {
    padding-top: 20px;
}

.main {
    width: 90%;
    margin: 0 auto;
    border: 1px solid #777;
    padding: 20px;
    border-radius: 5px;
}

.main .title {
    font-size: 20px;
    font-weight: normal;
    border-bottom: 1px solid #ccc;
    margin-bottom: 15px;
    padding-bottom: 5px;
    color: #006ac1;
}

.main .title span {
    display: inline-block;
    font-size: 20px;
    color: #fff;
    padding: 0 8px;
    background: orangered;
    border-radius: 5px;
}

a {
    color: #006ac1;
    text-decoration: none;
}

a:hover {
    color: orangered;
}

.tab td, .tab, .tab th {
    border: 1px solid #777;
    border-collapse: collapse;
}

.tab td, .tab th {
    line-height: 26px;
    height: 26px;
    padding-left: 5px;
}

.abtn {
    display: inline-block;
    height: 18px;
    line-height: 18px;
    background: #006ac1;
    color: #fff;
    padding: 0 5px;
    border-radius: 5px;
}

.btn {
    height: 18px;
    line-height: 18px;
    background: #006ac1;
    color: #fff;
    padding: 0 8px;
    border: 0;
    border-radius: 5px;
}

.abtn:hover, .btn:hover {
    background: orangered;
    color: #fff;
}

p {
    padding: 5px 0;
}

fieldset {
    border: 1px solid #ccc;
    padding: 5px 10px;
}

fieldset legend {
    margin-left: 10px;
    font-size: 16px;
}

a.out, input.out {
    height: 23px;
    line-height: 23px;
}

form {
    margin: 10px 0;
}

在view目录下新建目录product,在product目录下新建一个视图index.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <link href="<c:url value="/styles/main.css"/>" type="text/css" rel="stylesheet" />
        <title>产品管理</title>
    </head>
    <body>
        <div class="main">
            <h2 class="title">
                <span>产品管理</span>
            </h2>
            <form method="get">
                名称:<input type="text" name="searchKey" value="${searchKey}" /> <input type="submit" value="搜索" class="btn out" />
            </form>
            <form action="product/deletes" method="post">
                <table border="1" width="100%" class="tab">
                    <tr>
                        <th><input type="checkbox" id="chbAll"></th>
                        <th>编号</th>
                        <th>产品名</th>
                        <th>价格</th>
                        <th>类型</th>
                        <th>操作</th>
                    </tr>
                    <c:forEach var="product" items="${products}">
                        <tr>
                            <th><input type="checkbox" name="id" value="${product.id}"></th>
                            <td>${product.id}</td>
                            <td>${product.name}</td>
                            <td>${product.price}</td>
                            <td>${product.productType.name}</td>
                            <td><a href="product/delete/${product.id}" class="abtn">删除</a> <a href="product/edit/${product.id}" class="abtn">编辑</a></td>
                        </tr>
                    </c:forEach>
                </table>
                <p style="color: red">${message}</p>
                <p>
                    <a href="product/add" class="abtn out">添加</a> <input type="submit" value="删除选择项" class="btn out" />
                </p>
                <script type="text/javascript" src="<c:url value="/scripts/jQuery1.11.3/jquery-1.11.3.min.js"/>"></script>
            </form>
        </div>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/product,运行结果如图1所示:


图 1

4. 新增产品

在ProductController控制器中添加两个Action,一个用于渲染添加页面,另一个用于响应保存功能:

/**
 *  新增,渲染出新增界面
 */
@RequestMapping("/add")
public String add(Model model) {
    // 与form绑定的模型
    model.addAttribute("product", new Product());
    // 用于生成下拉列表
    model.addAttribute("productTypes", productTypeService.getAllProductTypes());
    return "product/add";
}

/**
 * 新增保存,如果新增成功转回列表页,如果失败回新增页,保持页面数据
 */
@RequestMapping("/addSave")
public String addSave(Model model, Product product) {
    try {
        // 根据类型的编号获得类型对象
        product.setProductType(productTypeService.getProductTypeById(product.getProductType().getId()));
        productService.addProduct(product);
        return "redirect:/product";
    } catch (Exception exp) {
        // 与form绑定的模型
        model.addAttribute("product", product);
        // 用于生成下拉列表
        model.addAttribute("productTypes", productTypeService.getAllProductTypes());
        // 错误消息
        model.addAttribute("message", exp.getMessage());
        return "product/add";
    }
}

在view/product目录下新增视图add.jsp页面:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <link href="<c:url value="/styles/main.css"/>" type="text/css" rel="stylesheet" />
        <title>新增产品</title>
    </head>
    <body>
        <div class="main">
            <h2 class="title">
                <span>新增产品</span>
            </h2>
            <form:form action="addSave" modelAttribute="product">
                <fieldset>
                    <legend>产品</legend>
                    <p>
                        <label for="name">产品名称:</label>
                        <form:input path="name" />
                    </p>
                    <p>
                        <label for="title">产品类型:</label>
                        <form:select path="productType.id" items="${productTypes}" itemLabel="name" itemValue="id">
                        </form:select>
                    </p>
                    <p>
                        <label for="price">产品价格:</label>
                        <form:input path="price" />
                    </p>
                    <p>
                        <input type="submit" value="保存" class="btn out">
                    </p>
                </fieldset>
            </form:form>
            <p style="color: red">${message}</p>
            <p>
                <a href="<c:url value="/product" />" class="abtn out">返回列表</a>
            </p>
        </div>
    </body>
</html>

点击添加按钮,运行结果如图2所示:


图 2

5. 编辑产品

在ProductController控制器中添加两个Action,一个用于渲染编辑页面,根据要编辑的产品编号获得产品对象,另一个用于响应保存功能。

/**
 * 编辑,渲染出编辑界面,路径变量id是用户要编辑的产品编号
 */
@RequestMapping("/edit/{id}")
public String edit(Model model, @PathVariable int id) {
    // 与form绑定的模型
    model.addAttribute("product", productService.getProductById(id));
    // 用于生成下拉列表
    model.addAttribute("productTypes", productTypeService.getAllProductTypes());
    return "product/edit";
}

/**
 *  编辑后保存,如果更新成功转回列表页,如果失败回编辑页,保持页面数据
 */
@RequestMapping("/editSave")
public String editSave(Model model, Product product) {
    try {
        // 根据类型的编号获得类型对象
        product.setProductType(productTypeService.getProductTypeById(product.getProductType().getId()));
        productService.updateProduct(product);
        return "redirect:/product";
    } catch (Exception exp) {
        // 与form绑定的模型
        model.addAttribute("product", product);
        // 用于生成下拉列表
        model.addAttribute("productTypes", productTypeService.getAllProductTypes());
        // 错误消息
        model.addAttribute("message", exp.getMessage());
        return "product/edit";
    }
}

在view/product目录下新增视图edit.jsp页面:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <link href="<c:url value="/styles/main.css" />" type="text/css" rel="stylesheet" />
        <title>编辑产品</title>
    </head>
    <body>
        <div class="main">
            <h2 class="title"><span>编辑产品</span></h2>
            <form:form action="../editSave" modelAttribute="product">
            <fieldset>
                <legend>产品</legend>
                <p>
                    <label for="name">产品名称:</label>
                    <form:input path="name"/>
                </p>
                <p>
                    <label for="title">产品类型:</label>
                    <form:select path="productType.id" items="${productTypes}"  itemLabel="name" itemValue="id">
                    </form:select>
                </p>
                <p>
                    <label for="price">产品价格:</label>
                    <form:input path="price"/>
                </p>
                <p>
                  <form:hidden path="id"/>
                  <input type="submit" value="保存" class="btn out">
                </p>
            </fieldset>
            </form:form>
            <p style="color: red">${message}</p>
            <p>
                <a href="<c:url value="/product" />"  class="abtn out">返回列表</a>
            </p>
        </div>
    </body>
</html>

点击编辑按钮,运行结果如图3所示:


图 3

二、Spring MVC验证器

Spring MVC不仅是在架构上改变了项目,使代码变得可复用、可维护与可扩展,其实在功能上也加强了不少。验证与文件上传是许多项目中不可缺少的一部分。在项目中验证非常重要,首先是安全性考虑,如防止注入攻击,XSS等;其次还可以确保数据的完整性,如输入的格式,内容,长度,大小等。Spring MVC可以使用验证器Validator与JSR303完成后台验证功能。这里也会介绍方便的前端验证方法。

1. Validator验证器

Spring MVC Validator验证器是一个接口,通过实现该接口可以定义对实体对象的验证。该接口如下所示:

package org.springframework.validation;

/**
 * Spring MVC内置的验证器接口
 */
public interface Validator {
    /**
     * 是否可以验证该类型
     */
    boolean supports(Class<?> clazz);

    /**
     * 执行验证 target表示要验证的对象 error表示错误信息
     */
    void validate(Object target, Errors errors);
}

(1) 定义验证器

package org.spring.mvc.model;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

/**
 * 产品验证器
 *
 */
public class ProductValidator implements Validator {
    /**
     *  当前验证器可以验证的类型
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return Product.class.isAssignableFrom(clazz);
    }

    /**
     *  执行校验
     */
    @Override
    public void validate(Object target, Errors errors) {
        // 将要验证的对象转换成Product类型
        Product entity = (Product) target;
        // 如果产品名称为空或为空格,使用工具类
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required", "产品名称必须填写");
        // 价格,手动判断
        if (entity.getPrice() < 0) {
            errors.rejectValue("price", "product.price.gtZero", "产品价格必须大于等于0");
        }
        // 产品类型必须选择
        if (entity.getProductType().getId() == 0) {
            errors.rejectValue("productType.id", "product.productType.id.required", "请选择产品类型");
        }
    }
}

ValidationUtils是一个工具类,中间有一些方可以用于判断内容是否有误。

(2) 执行校验

/**
 * 新增保存,如果新增成功转回列表页,如果失败回新增页,保持页面数据
 *  
 * @param model
 * @param product
 * @return
 */
@RequestMapping("/addSave")
public String addSave(Model model, Product product, BindingResult bindingResult) {
    // 创建一个产品验证器
    ProductValidator validator = new ProductValidator();
    // 执行验证,将验证的结果给bindingResult,该类型继承Errors
    validator.validate(product, bindingResult);

    // 获得所有的字段错误信息,非必要
    for (FieldError fielderror : bindingResult.getFieldErrors()) {
        System.out.println(fielderror.getField() + "," + fielderror.getCode() + "," + fielderror.getDefaultMessage());
    }

    try {
        // 是否存在错误,如果没有,执行添加
        if (!bindingResult.hasErrors()) {
            // 根据类型的编号获得类型对象
            product.setProductType(productTypeService.getProductTypeById(product.getProductType().getId()));
            productService.addProduct(product);
            return "redirect:/product2";
        } else {
            // 与form绑定的模型
            model.addAttribute("product", product);
            // 用于生成下拉列表
            model.addAttribute("productTypes", productTypeService.getAllProductTypes());
            return "product2/add";
        }
    } catch (Exception exp) {
        // 与form绑定的模型
        model.addAttribute("product", product);
        // 用于生成下拉列表
        model.addAttribute("productTypes", productTypeService.getAllProductTypes());
        // 错误消息
        model.addAttribute("message", exp.getMessage());
        return "product2/add";
    }
}

注意在参数中增加了一个BindingResult类型的对象,该类型继承自Errors,获得绑定结果,承载错误信息,该对象中有一些方法可以获得完整的错误信息,可以使用hasErrors方法判断是否产生了错误。

(4) 在UI中添加错误标签

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <link href="<c:url value="/styles/main.css"/>" type="text/css" rel="stylesheet" />
        <title>新增产品</title>
    </head>
    <body>
        <div class="main">
            <h2 class="title"><span>新增产品</span></h2>
            <form:form action="addSave" modelAttribute="product">
            <fieldset>
                <legend>产品</legend>
                <p>
                    <label for="name">产品名称:</label>
                    <form:input path="name"/>
                    <form:errors path="name" cssClass="error"></form:errors>
                </p>
                <p>
                    <label for="title">产品类型:</label>
                    <form:select path="productType.id">
                         <form:option value="0">--请选择--</form:option>
                         <form:options items="${productTypes}"  itemLabel="name" itemValue="id"/>
                    </form:select>
                    <form:errors path="productType.id" cssClass="error"></form:errors>
                </p>
                <p>
                    <label for="price">产品价格:</label>
                    <form:input path="price"/>
                    <form:errors path="price" cssClass="error"></form:errors>
                </p>
                <p>
                  <input type="submit" value="保存" class="btn out">
                </p>
            </fieldset>
            </form:form>
            <p style="color: red">${message}</p>
            <p>
                <a href="<c:url value="/product2" />"  class="abtn out">返回列表</a>
            </p>
        </div>
    </body>
</html>

(5) 测试运行

访问路径:http://localhost:8080/spring-mvc/product2,运行结果如图4所示:


图 4

控制台输出:

name,required,产品名称必须填写
price,product.price.gtZero,产品价格必须大于等于0
productType.id,product.productType.id.required,请选择产品类型

2. JSR303验证器

JSR是Java Specification Requests的缩写,意思是Java规范提案,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。JSR303只是一个标准,是一个数据验证规范,对这个标准的实现有:hibernate-validator,Apache BVal等。这里我们使用hibernate-validator实现校验。

(1) 添加hibernate-validator依赖

修改配置pom.xml配置文件,添加依赖。

<!--JSR303 Bean校验-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.2.2.Final</version>
</dependency>

(2) 注解实体类

为Bean的name和price属性设置验证规则:

// 名称
@Size(min=1,max=50,message="名称长度必须介于{2}-{1}之间")
@Pattern(regexp="^[\\w\\u4e00-\\u9fa5]{0,10}$",message="格式错误,必须是字母数字与中文")
private String name;
// 价格
@Range(min=0,max=1000000,message="价格只允许在{2}-{1}之间")
private double price;

常用验证的注解如下所示:

空值检查
  • @Null 验证对象是否为null
  • @NotNull 验证对象是否不为null, 无法查检长度为0的字符串
  • @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
  • @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
  • @AssertTrue 验证 Boolean 对象是否为 true
  • @AssertFalse 验证 Boolean 对象是否为 false
长度检查
  • @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
  • @Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
  • @Past 验证 Date 和 Calendar 对象是否在当前时间之前
  • @Future 验证 Date 和 Calendar 对象是否在当前时间之后
正则
  • @Pattern 验证 String 对象是否符合正则表达式的规则
数值检查

建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为String为””,Integer为null

  • @Min 验证 Number 和 String 对象是否大等于指定的值
  • @Max 验证 Number 和 String 对象是否小等于指定的值
  • @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度
  • @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度
  • @Digits 验证 Number 和 String 的构成是否合法
  • @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。
范围
  • @Range(min=, max=) 检查被注解对象的值是否处于min与max之间,闭区间,包含min与max值
  • @Range(min=10000,max=50000,message=”必须介于{2}-{1}之间”)
其它注解
  • @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证),该注解使用在Action的参数上。
  • @CreditCardNumber信用卡验证
  • @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。
  • @ScriptAssert(lang= ,script=, alias=)
  • @URL(protocol=,host=, port=,regexp=, flags=)

(3) 注解控制器参数

在需要使用Bean验证的参数对象上注解@Valid,触发验证

@RequestMapping("/addGoodsSave")
public String addSave(Model model, @Valid Product product, BindingResult bindingResult) {
    try {
        // 是否存在错误,如果没有,执行添加
        if (!bindingResult.hasErrors()) {
            // 根据类型的编号获得类型对象
            product.setProductType(productTypeService.getProductTypeById(product.getProductType().getId()));
            productService.addProduct(product);
            return "redirect:/product3";
        } else {
            // 与form绑定的模型
            model.addAttribute("product", product);
            // 用于生成下拉列表
            model.addAttribute("productTypes", productTypeService.getAllProductTypes());
            return "product3/addGoods";
        }
    } catch (Exception exp) {
        // 与form绑定的模型
        model.addAttribute("product", product);
        // 用于生成下拉列表
        model.addAttribute("productTypes", productTypeService.getAllProductTypes());
        return "product3/addGoods";
    }
}

(4) 在UI中添加错误标签

这里与Spring MVC Validator基本一致,在product3目录下新增一个名为addGoods.jsp的页面

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
        <link href="<c:url value="/styles/main.css" />" type="text/css" rel="stylesheet" />
        <title>新增产品</title>
    </head>
    <body>
        <div class="main">
            <h2 class="title">
                <span>新增产品</span>
            </h2>
            <form:form action="addGoodsSave" modelAttribute="product">
                <fieldset>
                    <legend>产品</legend>
                    <p>
                        <label for="name">产品名称:</label>
                        <form:input path="name" />
                        <form:errors path="name" cssClass="error"></form:errors>
                    </p>
                    <p>
                        <label for="title">产品类型:</label>
                        <form:select path="productType.id">
                            <form:option value="0">--请选择--</form:option>
                            <form:options items="${productTypes}" itemLabel="name" itemValue="id" />
                        </form:select>
                        <form:errors path="productType.id" cssClass="error"></form:errors>
                    </p>
                    <p>
                        <label for="price">产品价格:</label>
                        <form:input path="price" />
                        <form:errors path="price" cssClass="error"></form:errors>
                    </p>
                    <p>
                        <input type="submit" value="保存" class="btn out">
                    </p>
                </fieldset>
            </form:form>
            <p style="color: red">${message}</p>
            <p>
                <a href="<c:url value="/product3" />" class="abtn out">返回列表</a>
            </p>
        </div>
    </body>
</html>

(5) 测试运行

访问路径:http://localhost:8080/spring-mvc/product3/addGoodsSave,运行结果如图5所示:


图 5

(6) 小结

从上面的示例可以看出这种验证更加方便直观,一次定义反复使用,验证在编辑、更新时同样可以使用;另外验证的具体信息可以存放在配置文件中,如message.properties,这样便于国际化与修改。

3. 使用jQuery扩展插件Validate实现前端校验

暂时没看

分享到 评论

05-SpringMVC视图解析器

视图解析器

多数MVC框架都为Web应用程序提供一种它自己处理视图的办法,Spring MVC 提供视图解析器,它使用ViewResolver进行视图解析,让用户在浏览器中渲染模型。ViewResolver是一种开箱即用的技术,能够解析JSP、Velocity模板、FreeMarker模板和XSLT等多种视图。

Spring处理视图最重要的两个接口是ViewResolver和View。ViewResolver接口在视图名称和真正的视图之间提供映射关系; 而View接口则处理请求将真正的视图呈现给用户。


图 1

一、ViewResolver视图解析器

在Spring MVC控制器中,所有的请求处理方法(Action)必须解析出一个逻辑视图名称,无论是显式的(返回String,View或ModelAndView)还是隐式的(基于约定的,如视图名就是方法名)。Spring中由视图解析器处理这个逻辑视图名称,Spring常用的视图解析器有如下几种:


图 2

1. AbstractCachingViewResolver

用来缓存视图的抽象视图解析器。通常情况下,视图在使用前就准备好了。继承该解析器就能够使用视图缓存。这是一个抽象类,这种视图解析器会把它曾经解析过的视图缓存起来,然后每次要解析视图的时候先从缓存里面找,如果找到了对应的视图就直接返回,如果没有就创建一个新的视图对象,然后把它放到一个用于缓存的map中,接着再把新建的视图返回。使用这种视图缓存的方式可以把解析视图的性能问题降到最低。

2. XmlViewResolver

XML视图解析器。它实现了ViewResolver接口,接受相同DTD定义的XML配置文件作为Spring的XML bean工厂。它继承自AbstractCachingViewResolver抽象类,所以它也是支持视图缓存的。通俗来说就是通过xml指定逻辑名称与真实视图间的关系,示例如下:

<bean class="org.springframework.web.servlet.view.XmlViewResolver">
   <property name="location" value="/WEB-INF/views.xml"/>
   <property name="order" value="2"/>
</bean>

views.xml是逻辑名与真实视图名的映射文件,order是定义多个视图时的优先级,可以这样定义:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                         http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
    <bean id="index" class="org.springframework.web.servlet.view.InternalResourceView">
        <property name="url" value="/index.jsp" />
    </bean>
</beans>

id就是逻辑名称,在使用时可以在请求处理方法中这样指定:

@RequestMapping("/index")
public String index() {
   return "index";
}

从配置可以看出最终还是使用InternalResourceView完成了视图解析。

3. ResourceBundleViewResolver

它使用了ResourceBundle定义下的bean,实现了ViewResolver接口,指定了绑定包的名称。通常情况下,配置文件会定义在classpath下的properties文件中,默认的文件名字是views.properties。

4. UrlBasedViewResolver

它简单实现了ViewResolver接口,不用显式定义,直接影响逻辑视图到URL的映射。它让你不用任何映射就能通过逻辑视图名称访问资源。它是对ViewResolver的一种简单实现,而且继承了AbstractCachingViewResolver,主要就是提供的一种拼接URL的方式来解析视图,它可以让我们通过prefix属性指定一个指定的前缀,通过suffix属性指定一个指定的后缀,然后把返回的逻辑视图名称加上指定的前缀和后缀就是指定的视图URL了。如prefix=/WEB-INF/views/,suffix=.jsp,返回的视图名称viewName=bar/index,则UrlBasedViewResolver解析出来的视图URL就是/WEB-INF/views/bar/index.jsp。redirect:前缀表示重定向,forword:前缀表示转发。使用UrlBasedViewResolver的时候必须指定属性viewClass,表示解析成哪种视图,一般使用较多的就是InternalResourceView,利用它来展现jsp,但是当我们使用JSTL的时候我们必须使用org.springframework.web.servlet.view.JstlView。

5. InternalResourceViewResolver

内部视图解析器。它是URLBasedViewResolver的子类,所以URLBasedViewResolver支持的特性它都支持。它在实际应用中使用的最广泛的一个视图解析器。

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
    <!-- 前缀 -->
    <property name="prefix" value="/WEB-INF/views/" />
    <!-- 后缀 -->
    <property name="suffix" value=".jsp" />
</bean>

在JSP视图技术中,Spring MVC经常会使用UrlBasedViewResolver视图解析器,该解析器会将视图名称翻译成URL并通过RequestDispatcher处理请求后渲染视图。修改springmvc-servlet.xml配置文件,增加如下视图解析器:

<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

6. VelocityViewResolver

Velocity视图解析器,UrlBasedViewResolver的子类,VelocityViewResolver会把返回的逻辑视图解析为VelocityView。

7. FreeMarkerViewResolver

FreeMarker视图解析器,UrlBasedViewResolver的子类,FreeMarkerViewResolver会把Controller处理方法返回的逻辑视图解析为FreeMarkerView,使用FreeMarkerViewResolver的时候不需要我们指定其viewClass,因为FreeMarkerViewResolver中已经把viewClass为FreeMarkerView了。Spring本身支持了对Freemarker的集成。只需要配置一个针对Freemarker的视图解析器即可。

8. ContentNegotiatingViewResolver

内容协商视图解析器,这个视图解析器允许你用同样的内容数据来呈现不同的view,在RESTful服务中可用。

二、链式视图解析器

Spring支持同时配置多个视图解析器,也就是链式视图解析器。这样,在某些情况下,就能够重写某些视图。如果我们配置了多个视图解析器,并想要给视图解析器排序的话,设定order属性就可以指定解析器执行的顺序。order的值越高,解析器执行的顺序越晚,当一个ViewResolver在进行视图解析后返回的View对象是null的话就表示该ViewResolver不能解析该视图,这个时候如果还存在其他order值比它大的ViewResolver就会调用剩余的ViewResolver中的order值最小的那个来解析该视图,依此类推。InternalResourceViewResolver这种能解析所有的视图,即永远能返回一个非空View对象的ViewResolver,一定要把它放在ViewResolver链的最后面。

<bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver"
    id="internalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/view/" />
    <property name="suffix" value=".jsp" />
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    <property name="contentType" value="text/html;charset=UTF-8" />
    <property name="order" value="2" />
</bean>
<bean id="viewResolver"
    class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
    <property name="cache" value="true" />
    <property name="prefix" value="" />
    <property name="suffix" value=".html" />
    <property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" />
    <property name="exposeSpringMacroHelpers" value="true" />
    <property name="exposeRequestAttributes" value="true" />
    <property name="exposeSessionAttributes" value="true" />
    <property name="requestContextAttribute" value="rc" />
    <property name="contentType" value="text/html;charset=UTF-8" />
    <property name="order" value="1" />
</bean>

viewClass指定了视图渲染类,viewNames指定视图名称匹配规则如名称以html开头或结束,contentType支持了页面头部信息匹配规则。

三、FreeMarker与多视图解析示例

1. 新增两个视图解析器

修改Spring MVC配置文件springmvc-servlet.xml,在beans结点中增加两个视图解析器,一个为内部解析器用于解析jsp与JSTL,另一个为解析FreeMaker格式。修改后的配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-4.3.xsd
        http://www.springframework.org/schema/mvc 
        http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">

    <!-- 自动扫描包,实现支持注解的IOC -->
    <context:component-scan base-package="org.spring.mvc" />

    <!-- Spring MVC不处理静态资源 -->
    <mvc:default-servlet-handler />

    <!-- 支持mvc注解驱动 -->
    <mvc:annotation-driven enable-matrix-variables="true" />

    <!-- 视图解析器 -->
    <bean
        class="org.springframework.web.servlet.view.InternalResourceViewResolver"
        id="internalResourceViewResolver">
        <!-- 前缀 -->
        <property name="prefix" value="/WEB-INF/view/" />
        <!-- 后缀 -->
        <property name="suffix" value=".jsp" />
        <!--指定视图渲染类 -->
        <property name="viewClass"
            value="org.springframework.web.servlet.view.JstlView" />
        <!--设置所有视图的内容类型,如果视图本身设置内容类型视图类可以忽略 -->
        <property name="contentType" value="text/html;charset=UTF-8" />
        <!-- 优先级,越小越前 -->
        <property name="order" value="2" />
    </bean>

    <!-- FreeMarker视图解析器与属性配置 -->
    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
        <!--是否启用缓存 -->
        <property name="cache" value="true" />
        <!--自动添加到路径中的前缀 -->
        <property name="prefix" value="" />
        <!--自动添加到路径中的后缀 -->
        <property name="suffix" value=".html" />
        <!--指定视图渲染类 -->
        <property name="viewClass"
            value="org.springframework.web.servlet.view.freemarker.FreeMarkerView" />
        <!-- 设置是否暴露Spring的macro辅助类库,默认为true -->
        <property name="exposeSpringMacroHelpers" value="true" />
        <!-- 是否应将所有request属性添加到与模板合并之前的模型。默认为false。 -->
        <property name="exposeRequestAttributes" value="true" />
        <!-- 是否应将所有session属性添加到与模板合并之前的模型。默认为false。 -->
        <property name="exposeSessionAttributes" value="true" />
        <!-- 在页面中使用${rc.contextPath}就可获得contextPath -->
        <property name="requestContextAttribute" value="rc" />
        <!--设置所有视图的内容类型,如果视图本身设置内容类型视图类可以忽略 -->
        <property name="contentType" value="text/html;charset=UTF-8" />
        <!-- 优先级,越小越前 -->
        <property name="order" value="1" />
    </bean>
    <!-- 配置FreeMarker细节 -->
    <bean id="freemarkerConfig"
        class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <!-- 模板路径 -->
        <property name="templateLoaderPath" value="/WEB-INF/htmlviews" />
        <property name="freemarkerSettings">
            <props>
                <!-- 刷新模板的周期,单位为秒 -->
                <prop key="template_update_delay">5</prop>
                <!--模板的编码格式 -->
                <prop key="defaultEncoding">UTF-8</prop>
                <!--url编码格式 -->
                <prop key="url_escaping_charset">UTF-8</prop>
                <!--此属性可以防止模板解析空值时的错误 -->
                <prop key="classic_compatible">true</prop>
                <!--该模板所使用的国际化语言环境选项 -->
                <prop key="locale">zh_CN</prop>
                <!--布尔值格式 -->
                <prop key="boolean_format">true,false</prop>
                <!--日期时间格式 -->
                <prop key="datetime_format">yyyy-MM-dd HH:mm:ss</prop>
                <!--时间格式 -->
                <prop key="time_format">HH:mm:ss</prop>
                <!--数字格式 -->
                <prop key="number_format">0.######</prop>
                <!--自动开启/关闭空白移除,默认为true -->
                <prop key="whitespace_stripping">true</prop>
            </props>
        </property>
    </bean>

    <!-- 配置映射媒体类型的策略 -->
    <bean
        class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <property name="removeSemicolonContent" value="false" />
    </bean>

    <bean name="/controller01" class="org.spring.mvc.controller.Controller01"></bean>
</beans>

需要注意的是视图解析器的order越小,解析优先级越高。在视图解析的过程中,如果order为1的视图解析器不能正确解析视图的话,会将结果交给order为2的视图解析器,这里为2的视图解析器是InternalResourceViewResolver,它总是会生成一个视图的,所以一般InternalResourceViewResolver在放在视图解析链的末尾,万一没有找到对应的视图,它还会生成一个404的view并返回。

2. 修改pom.xml,添加依赖

为了使用FreeMarker,需要引用spring-context-support与FreeMarker的jar包。修改后的pom.xml配置文件如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.spring.mvc</groupId>
    <artifactId>spring-mvc</artifactId>
    <version>0.0.1</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>4.3.0.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!--Spring框架核心库 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Spring MVC -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- JSTL -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>
        <!-- Servlet核心包 -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
         <!--JSP应用程序接口 -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <!-- jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.5.2</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.2</version>
        </dependency>
        <!-- FreeMarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.23</version>
        </dependency>
    </dependencies>
</project>

添加依赖成功后的jar包如图3所示:


图 3

3. 定义Controller与Action

定义控制器,增加两个请求处理方法jstl与ftl,ftl让第1个解析器解析,jstl让第2个视图解析器解析,第1个视图解析器也是默认的视图解析器。

package org.spring.mvc.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/controller11")
public class Controller11 {
    @RequestMapping("/jstl")
    public String jstl(Model model) {
        model.addAttribute("message", "Hello JSTL View!");
        return "controller11/jstl";
    }

    @RequestMapping("/ftl")
    public String ftl(Model model) {
        model.addAttribute("users", new String[]{"tom","mark","jack"});
        model.addAttribute("message", "Hello FreeMarker View!");
        return "controller11/ftl";
    }
}

4. 新增目录与视图

在WEB-INF/view/controller11目录下新增jsp页面jstl.jsp页面,在WEB-INF/html/controller11目录下新增ftl.html页面。目录结构如下:


图 4

5. 运行结果

访问路径:http://localhost:8080/spring-mvc/controller11/jstl,运行结果如图3所示:


图 5

访问路径:http://localhost:8080/spring-mvc/controller11/ftl,运行结果如图6所示:


图 6

6. 小结

当访问/controller11/ftl时会找到action ftl方法,该方法返回controller11/ftl字符串,视图解析器中order为1的解析器去controller11目录下找名称为ftl的视图,视图存在,将视图与模型渲染后输出。当访问/controller11/jstl时会找到action jstl访问,该方法返回controller11/jstl字符串,视图解析器中order为1的解析器去controller11目录下找名称为jstl的视图,未能找到,解析失败,转到order为2的视图解析器解析,在目录controller11下找到jstl的文件成功,将视图与模板渲染后输出。

如果想视图解析器更加直接的选择可以使用属性viewNames,如viewNames=”html*”,则会只解析视图名以html开头的视图。

分享到 评论

04-SpringMVC表单标签库

Spring MVC 表单标签库

一、简介

从Spring2.0起就提供了一组全面的自动数据绑定标签来处理表单元素。生成的标签兼容HTML 4.01与XHTML 1.0。表单标签库中包含了可以用在JSP页面中渲染HTML元素的标签。表单标记库包含在spring-webmvc.jar中,库的描述符称为spring-form.tld,为了使用这些标签必须在jsp页面开头处声明这个tablib指令。

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

以下是标签库中的常用标签:

  • form 渲染表单元素form
  • input 渲染< input type=”text”/>元素
  • password 渲染< input type=”password”/>元素
  • hidden 渲染< input type=”hidden”/>元素
  • textarea 渲染textarea元素
  • checkbox 渲染一个< input type=”checkbox”/>复选元素
  • checkboxs 渲染多个< input type=”checkbox”/>元素
  • radiobutton 渲染一个< input type=”radio”/>单选元素
  • radiobuttons 渲染多个< input type=”radio”/>元素
  • select 渲染一个选择元素
  • option 渲染一个可选元素
  • options 渲染多个可选元素列表
  • errors 在span元素中渲染字段错误

二、常用属性

  • path:要绑定的属性路径,是最重要的属性,当绑定的对象有多个属性时必填,相当于modelAttribute.getXXX() 。
  • cssClass:定义要应用到被渲染元素的CSS类,类样式。
  • cssStyle:定义要应用到被渲染元素的CSS样式,行内样式。
  • htmlEscape:接受true或者false,表示是否应该对被渲染的值进行HTML转义。
  • cssErrorClass:定义要应用到被渲染input元素的CSS类,如果bound属性中包含错误,则覆盖cssClass属性值。

三、常用标签

1. form:form标签与form:input标签

这个标签会生成HTML form标签,同时为form内部所包含的标签提供一个绑定路径(binding path)。 它把命令对象(command object)存在PageContext中,这样form内部的标签就可以使用这个对象了。标签库中的其他标签都声明在form标签的内部。

commandName:暴露表单对象的模型属性名称,默认为command,它定义了模型属性的名称,其中包含了一个backing object,其属性将用于填充生成的表单。如果该属性存在,则必须在返回包含该表单的视图的请求处理方法中添加相应的模型属性。

modelAttribute:暴露form backing object的模型属性名称,默认为command

commandName与modelAttribute功能基本一样,使用modelAttribute就可以了,因为commandName已被抛弃。

action示例代码:

@RequestMapping("/action49")
public String action49(Model model){
    //向模型中添加一个名为product的对象,用于渲染视图
    model.addAttribute("product", new Product("Meizu note1", 999));
    return "controller10/action49";
}

action49.jsp代码:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>controller10/action49</title>
    </head>
    <body>
        <form:form modelAttribute="product">
            <p>
                <label for="name">Name:</label>
                <form:input path="name" />
            </p>
            <p>
                <label for="price">Price:</label>
                <form:input path="price" />
            </p>
        </form:form>
    </body>
</html>

form表单与模型中名称为product的对象进行绑定,form表单元素的path指的就是访问该对象的属性,如果没有该对象或找不到属性名将异常。系统将自动把指定模型中的值与页面进行绑定。

访问路径:http://localhost:8080/spring-mvc/controller10/action49,运行结果如图1所示:


图 1

模型可以为空,不是为null,中间可以没有数据,但非字符类型会取默认值,如价格会变成0.0。

action示例代码:

@RequestMapping("/action50")
public String action50(Model model){
    //向模型中添加一个名为product的对象,用于渲染视图
    model.addAttribute("product", new Product());
    return "controller10/action49";
}

访问路径:http://localhost:8080/spring-mvc/controller10/action50,运行结果如图2所示:


图 2

input元素可以设置其它的属性。修改后的表单:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>controller10/action49</title>
</head>
<body>
    <form:form modelAttribute="product">
        <p>
            <label for="name">名称:</label>
            <form:input path="name" cssClass="textCss" cssStyle="color:blue" a="b" htmlEscape="false" />
        </p>
        <p>
            <label for="price">价格:</label>
            <form:input path="price" />
        </p>
    </form:form>
</body>
</html>

修改后的示例代码:

@RequestMapping("/action51")
public String action51(Model model){
    //向模型中添加一个名为product的对象,用于渲染视图
    model.addAttribute("product", new Product("Meizu note1<hr/>", 999));
    return "controller10/action51";
}

访问路径:http://localhost:8080/spring-mvc/controller10/action51,运行结果如图3所示:


图 3

2. form:checkbox标签

form:checkbox标签将渲染成一个复选框,通过该标签可以获得3种不同类型的值,分别是boolean、数组和基本数据类型。

  • 若绑定的值是java.lang.Boolean类型,当其值为true时,checkbox被标记为选中;
  • 若绑定的值是数组类型或java.util.Collection,当setValue(Object)配置的值出现在绑定的Collection中时,checkbox被标记为选中;
  • 若绑定的值是其他类型,当setValue(Object)配置的值等于其绑定值时,checkbox被标记为选中。

定义一个实体类Person:

package org.spring.mvc.model;

public class Person {
    // 婚否
    private boolean isMarried;
    // 爱好
    private String[] hobbies;
    // 学历
    private String education;

    public boolean getIsMarried() {
        return isMarried;
    }

    public void setIsMarried(boolean isMarried) {
        this.isMarried = isMarried;
    }

    public String[] getHobbies() {
        return hobbies;
    }

    public void setHobbies(String[] hobbies) {
        this.hobbies = hobbies;
    }

    public String getEducation() {
        return education;
    }

    public void setEducation(String education) {
        this.education = education;
    }
}

特别注意的是boolean类型的值生成的get/set方法前是不带get与set的,这样会引起异常,建议手动修改。

action示例代码:

@RequestMapping("/action52")
public String action52(Model model) {
    Person person = new Person();
    person.setIsMarried(true);
    person.setHobbies(new String[]{"Movie", "Surfing"});
    person.setEducation("Bachelor");
    model.addAttribute("person", person);
    return "controller10/action52";
}

@RequestMapping("/action53")
@ResponseBody
public Person action53(HttpServletResponse response, Person person) {
    return person;
}

action52.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>controller10/action52</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action53">
            <p>
                <label for="name">isMarried:</label>
                <form:checkbox path="isMarried" />
            </p>
            <p>
                <label for="name">Hobbies:</label>
                <form:checkbox path="hobbies" value="Reading"/>Reading
                <form:checkbox path="hobbies" value="Surfing"/>Surfing
                <form:checkbox path="hobbies" value="Movie"/>Movie
            </p>
            <p>
                <label for="name">hasGraduate:</label>
                <form:checkbox path="education" value="Bachelor"/>Bachelor
            </p>
            <p>
                <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/controller10/action52,运行结果如图4所示:


图 4

form:checkbox在渲染成input标签里会变成2个表单元素,这样可以确保用户没有选择内容时也会将值带会服务器,默认是没有这样的。

3. form:radiobutton标签

form:radiobutton标签生成类型为radio的HTML input标签,也就是常见的单选框。这个标签的典型用法是一次声明多个标签实例,所有的标签都有相同的path属性,但是他们的value属性不同。

action示例代码:

@RequestMapping("/action54")
public String action54(Model model){
    Person person = new Person();
    person.setEducation("Bachelor");
    model.addAttribute("person", person);
    return "controller10/action54";
}

action54.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>bar/action31</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action53">
            <p>
                <label for="name">Education</label>
                <form:radiobutton path="education" value="College"/>College
                <form:radiobutton path="education" value="Bachelor"/>Bachelor
                <form:radiobutton path="education" value="Master"/>Master
            </p>
            <p>
            <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/controller10/action54,运行结果如图5所示:


图 5

4. form:password标签

form:password标签生成类型为password的HTML input标签,渲染后生成一个密码框。input标签的值和表单支持对象相应属性的值保持一致。该标签与input类似,但有一个特殊的属性showPassword,是否将对象中的值绑定到密码框中,默认为false,也意味着密码框中不会出现默认的掩码。

action示例代码:

@RequestMapping("/action55")
public String action55(Model model){
    Person person=new Person();
    person.setEducation("edu");
    model.addAttribute("person", person);
    return "controller10/action55";
}

action55.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>bar/action31</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action53">
            <p>
                <label for="name">Education</label>
                <form:radiobutton path="education" value="College" />College
                <form:radiobutton path="education" value="Bachelor" />Bachelor
                <form:radiobutton path="education" value="Master" />Master
            </p>
            <p>
                <label>Password:</label>
                <form:password path="education" showPassword="false" />
            </p>
            <p>
                <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/controller10/action55,运行结果如图6所示:其中showPassword值为false


图 6

当showPassword值为true时,运行结果如图7所示:


图 7

5. form:select标签

form:select标签生成HTML select标签,就是下拉框,多选框。在生成的HTML代码中,被选中的选项和表单支持对象相应属性的值保持一致。这个标签也支持嵌套的option和options标签。

action示例代码:

@RequestMapping("/action56")
public String action56(Model model){
    List<ProductType>  productTypes = new ArrayList<ProductType>();
    productTypes.add(new ProductType(11, "数码电子"));
    productTypes.add(new ProductType(21, "鞋帽服饰"));
    productTypes.add(new ProductType(31, "图书音像"));
    productTypes.add(new ProductType(41, "五金家电"));
    productTypes.add(new ProductType(51, "生鲜水果"));
    model.addAttribute("productTypes", productTypes);
    model.addAttribute("person", new Person());
    return "controller10/action56";
}

在action56中为模型添加了一个属性productTypes,该对象用于绑定到页面的下拉列表框。

action56.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>bar/action41</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action42">
            <p>
                <label for="name">Product Types: </label>
                <form:select size="3" multiple="multiple" path="education" items="${productTypes}"  itemLabel="name"  itemValue="id"></form:select>
            </p>
            <p>
                <button>提交</button>
            </p>
        </form:form>
    </body>
</html>
  • size=”3” 表示可见项为3项,默认可见项为1项
  • multiple=”multiple” 表示允许多选,默认为单选
  • path=”education” 与表单中指定的modelAttribute对象进行双向绑定
  • items=”${productTypes}” 绑定到下拉列表的集合对象
  • itemLabel=”name” 集合中的对象的name属性作为下拉列表option的text属性
  • itemValue=”id” 集合中的对象的id属性作为下拉列表option的value属性

访问路径:http://localhost:8080/spring-mvc/controller10/action56,运行结果如图8所示:


图 8

6. form:option标签

option标签生成HTML option标签,可以用于生成select表单元素中的单项,没有path属性,有label与value属性。

action示例代码:

@RequestMapping("/action57")
public String action57(Model model){
    model.addAttribute("person", new Person());
    return "controller10/action57";
}

action57.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>bar/action51</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action52">
            <p>
                <label for="name">Education: </label>
                <form:select path="education">
                    <form:option value="" >--Select--</form:option>
                    <form:option value="College">College</form:option>
                    <form:option value="Bachelor">Bachelor</form:option>
                    <form:option value="Master">Master</form:option>
                </form:select>
            </p>
            <p>
                <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/controller10/action57,运行结果如图9所示:


图 9

7. form:options标签

form:options标签生成一系列的HTML option标签,可以用它生成select标签中的子标签。

action示例代码:

@RequestMapping("/action58")
public String action58(Model model){
    List<ProductType>  productTypes = new ArrayList<ProductType>();
    productTypes.add(new ProductType(11, "数码电子"));
    productTypes.add(new ProductType(21, "鞋帽服饰"));
    productTypes.add(new ProductType(31, "图书音像"));
    productTypes.add(new ProductType(41, "五金家电"));
    productTypes.add(new ProductType(51, "生鲜水果"));
    model.addAttribute("productTypes", productTypes);
    model.addAttribute("person", new Person());
    return "controller10/action58";
}

action58.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>controller10/action58</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action62">
            <p>
                <label for="name">Product Type: </label>
                <form:select path="education">
                   <form:option value="">--Select--</form:option>
                   <form:options items="${productTypes}" itemLabel="name" itemValue="id"/>
                </form:select>
            </p>
            <p>
                <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/controller10/action58,运行结果如图10所示:


图 10

上面的这个例子同时使用了option标签和options标签。这两个标签生成的HTML代码是相同的,但是第一个option标签允许你在JSP中明确声明这个标签的值只供显示使用,并不绑定到表单支持对象的属性上。

8. form:textarea、form:errors标签

form:textarea标签生成HTML textarea标签,就是一个多行文本标签,用法与input非常类似。

form:errors标签用于显示错误信息。

9. form:hidden标签

form:hidden标签生成类型为hidden的HTML input标签。在生成的HTML代码中,input标签的值和表单支持对象相应属性的值保持一致。如果你需要声明一个类型为hidden的input标签,但是表单支持对象中没有对应的属性,你只能使用HTML的标签。

action示例代码:

@RequestMapping("/action59")
public String action59(Model model){
    Person person=new Person();
    person.setEducation("99");
    model.addAttribute("person", person);
    return "controller10/action59";
}

action59.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>controller10/action59</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action72">
            <p>
                <form:hidden path="education" />
                <input type="hidden" value="1" name="id">
            </p>
            <p>
                <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/controller10/action59

10. form:radiobuttons标签与form:checkboxs标签

form:radiobuttons标签将生成一组单选框,只允许多个中选择1个;form:checkboxs标签生成一组复选列表,允许多选。

action示例代码:

@RequestMapping("/action60")
public String action60(Model model) {
    List<ProductType> productTypes = new ArrayList<ProductType>();
    productTypes.add(new ProductType(11, "数码电子"));
    productTypes.add(new ProductType(21, "鞋帽服饰"));
    productTypes.add(new ProductType(31, "图书音像"));
    productTypes.add(new ProductType(41, "五金家电"));
    productTypes.add(new ProductType(51, "生鲜水果"));
    model.addAttribute("productTypes", productTypes);
    model.addAttribute("person", new Person());
    return "controller10/action60";
}

action60.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>controller10/action60</title>
    </head>
    <body>
        <form:form modelAttribute="person" action="action82">
            <p>
                <label for="name">Product Type: </label>
                <form:radiobuttons path="education" items="${productTypes}"  itemLabel="name"  itemValue="id" delimiter=","  element="a"/>
            </p>
            <p>
                <label for="name">Product Type: </label>
                <form:checkboxes path="education" items="${productTypes}"  itemLabel="name"  itemValue="id" delimiter="-"/>
            </p>
            <p>
                <button>Submit</button>
            </p>
        </form:form>
    </body>
</html>
  • 属性delimiter=”,”,表示生成的单项间使用”,”号分隔,默认为空。
  • 属性element=”a”,表示生成的单项容器,默认为span。

访问路径:http://localhost:8080/spring-mvc/controller10/action60,运行结果如图11所示:


图 11
分享到 评论

03-请求处理方法Action详解

SpringMVC 请求处理方法Action详解

在Spring MVC的每个控制器中可以定义多个请求处理方法,我们把这种请求处理方法称为Action,每个请求处理方法可以有多个不同类型的参数和一个某种类型的返回结果。

一、Action参数

1. 自动参数映射

(1) 基本数据类型或包装类型

方法的参数可以是任意基本数据类型或包装类型。当方法的参数名与http请求的参数名相同时,参数会进行自动映射;但如果请求的参数中没有对应名称与类型的数据,则会产生异常。

示例代码:基本数据类型

@RequestMapping("/action19")
public String action19(Model model, int id, String name) {
    model.addAttribute("message", "name=" + name + ",id=" + id);
    return "index";
}

访问路径:http://localhost:8080/spring-mvc/controller08/action19?id=1234&name=kevin,运行结果如图1所示:


图 1

示例代码:包装类型

@RequestMapping("/action20")
public String action20(Model model, Integer id, Double price) {
    model.addAttribute("message", "price=" + price + ",id=" + id);
    return "index";
}

访问路径:http://localhost:8080/spring-mvc/controller08/action20?id=1234&price=20.0,运行结果如图2所示:


图 2

(2) 自定义数据类型

除了基本数据类型和其对应的包装类型,我们也可以自定义数据类型,如一个自定义的POJO对象,Spring MVC会通过反射把请求中的参数设置到对象的属性中并进行类型转换。

自定义数据类型Product:

package org.spring.mvc.model;

import java.io.Serializable;

public class Product implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    private double price;

    public Product() {
    }

    public Product(String name, double price) {
        super();
        this.name = name;
        this.price = price;
    }

    public Product(int id, String name, double price) {
        super();
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "编号(id):" + this.getId() + ",名称(name):" + this.getName() + ",价格(price):" + this.getPrice();
    }
}

示例代码:自定义数据类型

@RequestMapping("/action21")
public String action21(Model model, Product product) {
    model.addAttribute("message", product);
    return "index";
}

访问路径:http://localhost:8080/spring-mvc/controller08/action21?id=1234&name=meat&price=20.0,运行结果如图3所示:


图 3

示例中使用的是请求URL中的参数,其实也可以是客户端提交的任意参数,特别是表单中的数据。

(3) 复杂数据类型

复杂数据类型指的是一个自定义类型中还包含另外一个对象类型

自定义数据类型User,其中包含Product成员:

package org.spring.mvc.model;

public class User {
    private String username;
    private Product product;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Product getProduct() {
        return product;
    }

    public void setProduct(Product product) {
        this.product = product;
    }
}

示例代码:复杂数据类型

@RequestMapping("/action22")
public String action22(Model model, User user) {
    model.addAttribute("message", user.getUsername() + "," + user.getProduct().getName());
    return "index";
}

访问路径:http://localhost:8080/spring-mvc/controller08/action22?username=tom&product.name=rice,运行结果如图4所示:


图 4

使用表单提交数据,创建表单action22.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>action22</title>
    </head>
    <body>
        <form method="post" action="action02">
             username:<input name="username" /><br/>
             pdctname:<input name="product.name" /><br/>
            <button>提交</button>
        </form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/action22.jsp,运行结果如图5所示:


图 5

提交之后,运行结果如图6所示:


图 6

(4) List集合类型

不能直接在action的参数中指定List类型。

定义一个数据类型ProductList,其中包含一个List成员:

public class ProductList {
    private List<Product> items;

    public List<Product> getItems() {
        return items;
    }

    public void setItems(List<Product> items) {
        this.items = items;
    }
}

示例代码:List集合类型

@RequestMapping("/action23")
public String action23(Model model, ProductList products) {
    model.addAttribute("message", products.getItems().get(0) + "<br/>" + products.getItems().get(1));
    return "index";
}

在URL中模拟表单数据,访问路径:http://localhost:8080/spring-mvc/controller08/action23?items[0].name=phone&items[1].name=book,运行结果如图7所示:


图 7

这里同样可以使用一个表单向服务器提交数据。

(5) Map集合类型

Map与List的实现方式基本一样。

定义一个数据类型ProductMap,其中包含一个Map成员:

public class ProductMap {
    private Map<String, Product> items;

    public Map<String, Product> getItems() {
        return items;
    }

    public void setItems(Map<String, Product> items) {
        this.items = items;
    }
}

示例代码:Map集合类型

@RequestMapping("/action24")
public String action24(Model model, ProductMap map) {
    model.addAttribute("message", map.getItems().get("p1") + "<br/>" + map.getItems().get("p2"));
    return "index";
}

在URL中模拟表单数据,访问路径:http://localhost:8080/spring-mvc/controller08/action24?items[p1].name=phone&items[p2].name=book,运行结果如图8所示:


图 8

集合类型基本都一样,set也差不多,问题是如果为了获得一个集合需要刻意去包装会很麻烦,可以通过@RequestParam结合@RequestBody等注解完成。

2. @RequestParam参数绑定

简单的参数可以使用上一节中讲过的自动参数映射,虽然自动参数映射很方便,但有些细节是不能处理的,如参数是否为必须参数、参数名称没有办法指定、指定参数的默认值。复杂的参数映射可以使用@RequestParam完成,Spring MVC会自动查找请求中的参数,并进行参数绑定和类型转换。

@RequestParam共有4个注解属性:

  • required属性表示是否为必须,默认值为true,如果请求中没有指定的参数会报异常;
  • defaultValue用于设置参数的默认值,如果不指定值则使用默认值,只能是String类型的。
  • name与value互为别名关系用于指定参数名称。

(1) 基本数据类型

示例代码:

@RequestMapping("/action25")
public String action25(Model model, @RequestParam(required = false, defaultValue = "99") int id) {
    model.addAttribute("message", id);
    return "index";
}

访问路径:http://localhost:8080/spring-mvc/controller08/action25?id=98,运行结果如图9所示:


图 9

访问路径:http://localhost:8080/spring-mvc/controller08/action25,运行结果如图10所示:


图 10

(2) List与数组直接绑定:基本数据类型

在上一节中我们使用自动参数映射是不能直接完成List数组绑定的,结合@RequestParam可以轻松实现。

示例代码:

@RequestMapping("/action26")
public String action26(Model model, @RequestParam("id") List<String> ids) {
    model.addAttribute("message", Arrays.deepToString(ids.toArray()));
    return "index";
}

在URL中模拟表单数据,访问路径:http://localhost:8080/spring-mvc/controller08/action26?id=tom&id=rose,运行结果如图11所示:


图 11

使用表单提交数据,创建表单action26.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>action22</title>
    </head>
    <body>
        <form action="controller08/action26" method="post">
            <p>
                <label>爱好:</label> 
                <input type="checkbox" value="reading" name="id" />阅读
                 <input type="checkbox" value="surfing" name="id" />上网
                 <input type="checkbox" value="gaming" name="id" />电游
            </p>
            <button>提交</button>
        </form>
    </body>
</html>

访问路径:http://localhost:8080/spring-mvc/action26.jsp,运行结果如图12所示:


图 12

提交之后,运行结果如图13所示:


图 13

@RequestParam(“id”)表明参数是必须的。因为页面中的表单name的名称为id,所有服务器在收集数据时应该使用id页非ids。

如果name属性和参数名称同名,则name属性可以省去。示例代码:

@RequestMapping("/action27")
public String action27(Model model, @RequestParam("id") List<String> id) {
    model.addAttribute("message", Arrays.deepToString(id.toArray()));
    return "index";
}

(3) List与数组直接绑定:自定义数据类型

上一小节中我们绑定的集合中存放的是基本数据类型,如果需要直接绑定更加复杂的数据类型则需要使用@RequestBody与@ResponseBody注解。

  • @RequestBody 将HTTP请求正文转换为适合的HttpMessageConverter对象。
  • @ResponseBody 将内容或对象作为 HTTP 响应正文返回,并调用适合HttpMessageConverter的Adapter转换对象,写入输出流。

@RequestBody注解修饰的参数默认接收的Content-Type是application/json,因此客户端在发送POST请求时需要设置请求报文头信息,否则Spring MVC在解析集合请求参数时不会自动将参数转换成JSON数据再解析成相应的集合。Spring默认的json协议解析由Jackson完成,要完成这个功能需要修改配置环境。

(a) 修改Spring MVC配置文件,启用mvc注解驱动功能

<mvc:annotation-driven />

(b) 修改pom.xml,添加jackson依赖

<!-- jackson -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.5.2</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.5.2</version>
</dependency>

(c) 使用Ajax发送请求时需要设置contentType属性为’application/json;charse=UTF-8’(请求的默认contentType属性并非是application/json,而是application/x-www-form-urlencoded),服务器将把接收到的参数转换成JSON字符串,如果条件不满足有可能会出现415异常。

前端示例代码:定义一个action28-29.jsp页面,在客户端发送请求时设置contentType为application/json

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>List与数组直接绑定:自定义数据类型</title>
    </head>
    <body>
        <button type="button" onclick="addPdts_click1();">向服务器发送json</button>
        <button type="button" onclick="addPdts_click2();">接收服务器返回的json</button>
        <p id="msg"></p>
        <script type="text/javascript" src="<c:url value="/scripts/jQuery1.11.3/jquery-1.11.3.min.js"/>"></script>
        <script type="text/javascript">
            var products = new Array();
            products.push({
                id : 1,
                name : "iPhone 6 Plus",
                price : 4987.5
            });
            products.push({
                id : 2,
                name : "iPhone 7 Plus",
                price : 5987.5
            });
            products.push({
                id : 3,
                name : "iPhone 8 Plus",
                price : 6987.5
            });
            function addPdts_click1() {
                $.ajax({
                    type : "POST", //请求谓词类型
                    url : "controller08/action28",
                    data : JSON.stringify(products), //将products对象转换成json字符串
                    contentType : "application/json;charset=UTF-8", //发送信息至服务器时的内容编码类型,默认为application/x-www-form-urlencoded
                    dataType : "text", //期望服务器返回的数据类型
                    success : function(result) {
                        $("#msg").html(result);
                    }
                });
            }
            function addPdts_click2() {
                $.ajax({
                    type : "POST", //请求谓词类型
                    url : "controller08/action29",
                    data : JSON.stringify(products), //将products对象转换成json字符串
                    contentType : "application/json;charset=UTF-8", //发送信息至服务器时的内容编码类型,默认为application/x-www-form-urlencoded
                    dataType : "json", //期望服务器返回的数据类型
                    success : function(result) {
                        var str = "";
                        $.each(result, function(i, obj) {
                            str += "编号:" + obj.id + ",名称:" + obj.name + ",价格:" + obj.price + "<br/>";
                        });
                        $("#msg").html(str);
                    }
                });
            }
        </script>
    </body>
</html>

页面中有两个请求方法,第一个方法将一个json集合发送到服务器并映射成一个List集合;第二个方法接收服务器返回的json对象。

接收第一个请求的服务器示例代码:

@RequestMapping("/action28")
public void action28(@RequestBody List<Product> products, HttpServletResponse response) throws IOException {
    response.setCharacterEncoding("UTF-8");
    System.out.println(Arrays.deepToString(products.toArray()));
    response.getWriter().write("添加成功");
}

action28的参数List< Product> products是接收从客户端发送到服务器的产品集合,在参数前增加@RequestBody注解的作用是让Spring MVC在接收到客户端请求时选择合适的转换器将参数转换成相应的对象

接收第二个请求的服务器示例代码:

@RequestMapping("/action29")
@ResponseBody
public List<Product> action29(@RequestBody List<Product> products, HttpServletResponse response) throws IOException {
    products.get(0).setPrice(999.99);
    return products;
}

action29的返回值为List,且在方法上有一个注解@ResponseBody,系统会使用jackson将该返回值对象自动序列化成json字符序列

访问路径:http://localhost:8080/spring-mvc/action28-29.jsp,运行结果如图14所示:


图 14

点击第一个按钮时的如图15所示:


图 15

控制台输出:

[编号(id):1,名称(name):iPhone 6 Plus,价格(price):4987.5, 编号(id):2,名称(name):iPhone 7 Plus,价格(price):5987.5, 编号(id):3,名称(name):iPhone 8 Plus,价格(price):6987.5]

点击第二个按钮时的如图16所示:


图 16

3. 重定向与Flash属性

如果一个请求处理方法Action的返回结果为”index”字符串,则表示结果转发到index视图。但有时候我们需要重定向,将结果重定向到一个指定的页面或另一个action,则可以在返回的结果前加上一个”redirect:”前缀。

示例代码:

@RequestMapping("/action30")
public String action30(Model model) {
    model.addAttribute("message", "action30Message");
    return "redirect:action31";
}

@RequestMapping("/action31")
public String action31(Model model) {
    model.addAttribute("message", "action31Message");
    return "index";
}

访问路径:http://localhost:8080/spring-mvc/controller08/action30,运行结果如图17所示:


图 17

在action30中返回的结果为redirect:action31,表示重定向到action31这个请求处理方法,所有重定向都是以当前路径为起点的。action30向model中添加了名称message的数据,因为重定向到action31中会发起两次请求,为了保持action30中的数据,Spring MVC自动将数据重写到了url中。

为了实现重定向时传递复杂数据,可以使用Flash属性。

示例代码:

@RequestMapping("/action32")
public String action32(Model model, RedirectAttributes redirectAttributes) {
    Product product = new Product(2, "iPhone7 Plus", 6989.5);
    redirectAttributes.addFlashAttribute("product", product);
    return "redirect:action33";
}

@RequestMapping("/action33")
public String action33(Model model, Product product) {
    model.addAttribute("message", product);
    System.out.println(model.containsAttribute("product")); // true
    return "index";
} 

当访问action32时,首先创建了一个Product对象,并将该对象添加到了Flash属性中,在重定向后取出该对象。

访问路径:http://localhost:8080/spring-mvc/controller08/action32,运行结果如图18所示:


图 18

此时URL地址已经发生了变化,product对象其实也已经被存入了model中,在action33的视图中可以直接拿到。

4. @ModelAttribute注解

@ModelAttribute注解可以应用在方法参数上或方法上,其作用是:

  • 当注解在方法的参数上时,会将被注解的参数对象添加到Model中;
  • 当注解在请求处理方法Action上时,会将该方法变成一个非请求处理的方法,而是让其它Action被调用时先调用该方法。

(1) 注解在参数上

当@ModelAttribute注解在参数上时,会将被注解的参数添加到Model中,并自动完成数据绑定。

示例代码:

访问路径:http://localhost:8080/spring-mvc/controller08/action34?id=12&name=apple&price=6000,运行结果如图19所示:


图 19

其实不使用@ModelAttribute也样可以完成参数与对象间的自动映射,但使用注解可以设置更多详细内容,如名称、是否绑定等。

(2) 注解在方法上

@ModelAttribute注解还可以用于标注一个非请求处理方法,通俗说就是一个非Action或普通方法。如果一个控制器类有多个请求处理方法,以及一个由@ModelAttribute注解的方法,则在调用其它Action之前会先调用由@ModelAttribute注解的方法。

示例代码:

@RequestMapping("/action35")
public String action35(Model model) {
    Map<String, Object> map = model.asMap();
    for (String key : map.keySet()) {
        System.out.println(key + ":" + map.get(key));
    }
    return "index";
}

@ModelAttribute
public String noaction() {
    System.out.println("noaction 方法被调用!");
    String message = "来自noaction方法的信息";
    return message;
}

当访问路径:http://localhost:8080/spring-mvc/controller08/action35,控制台会输出:

noaction 方法被调用!
string:来自noaction方法的信息

非请求处理方法可以返回void,也可以返回一个任意对象,该对象会被自动添加到每一个要被访问的Action的Model中。从示例中可以看出key为类型名称。

二、Action返回值

Spring MVC的action返回值可以为String、void、ModelAndView、Map、Model和其它任意类型。

0. URL问题

在/src/main/webapp/WEB-INF/image目录下添加图片3.jpg。

第一次尝试

在/src/main/webapp/WEB-INF/view/controller09目录下添加视图action36.jsp:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>action5的视图</title>
    </head>
    <body>
        <img alt="风景" src="../../image/3.jpg">
    </body>
</html>

目标结构如下:


图 19

定义一个action访问该图片:

@RequestMapping("/action36")
public String action36(Model model) {
    return "controller09/action36";
}

访问路径:http://localhost:8080/spring-mvc/controller09/action36,运行结果如图21所示:


图 21

访问不到图片的原因是:

修改

修改action36.jsp视图如下:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>action5的视图</title>
    </head>
    <body>
        <img alt="风景" src="../../image/3.jpg">
        <img alt="风景" src="<c:url value="/image/3.jpg"></c:url>">
    </body>
</html>

修改后目标结构如下:


图 22

访问路径:http://localhost:8080/spring-mvc/controller09/action36,运行结果如图23所示:


图 23

小结:借助标签将路径转换成”绝对路径”;建议在引用外部资源如js、css、图片信息时都使用该标签解析路径。

1. 返回值为String

(1) String作为视图名称

默认如果action返回String,此时String为视图名称,Spring MVC会去视图解析器设定的目录下查找,查找的规则是:URL= prefix前缀+视图名称+suffix后缀。

在Spring MVC的配置文件中配置视图解析器:

<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
    <!-- 前缀 -->
    <property name="prefix" value="/WEB-INF/view/" />
    <!-- 后缀 -->
    <property name="suffix" value=".jsp" />
</bean>

示例代码:

@RequestMapping("/action37")
public String action37(Model model) {
    model.addAttribute("message", "action31");
    return "index";
}

返回视图的路径为:/WEB-INF/view/index.jsp

(2) String作为内容输出

如果方法声明了@ResponseBody注解,将内容或对象作为HTTP响应正文返回,并调用适合HttpMessageConverter的Adapter转换对象,写入输出流,此时的String不再是路径而是内容。

示例代码:

@RequestMapping("/action38")
@ResponseBody
public String action38() {
    return "not <b>path</b>,but <b>content</b>";
}

访问路径:http://localhost:8080/spring-mvc/controller09/action38,运行结果如图24所示:


图 24

2. 返回值为void

void在普通方法中是没有返回值的意思,但作为请求处理方法的返回值类型并非这样,存在如下两种情况:

(1) 方法名默认作为视图名

当方法没有返回值时,方法中并未指定视图的名称,则默认视图的名称为方法名,查找的规则为:URL= prefix前缀+控制器路径+方法名称+suffix后缀组成。

示例代码:

@RequestMapping("/action39")
public void action39() {
}

返回视图的路径为:/WEB-INF/view/controller09/action39.jsp,controller09是当前控制器映射的路径,action39是方法名。

访问路径:http://localhost:8080/spring-mvc/controller09/action39,运行结果如图25所示:


图 25

上面的action代码等同于

@RequestMapping("/action39")
public String action39() {
    return "controller09/action39";  //controller09是控制器的路径
}

(2) 直接响应输出结果

当方法的返回值为void,但输出流中存在输出内容时,则不会去查找视图,而是将输入流中的内容直接响应到客户端,响应的内容类型是纯文本。

示例代码:

@RequestMapping("/action40")
public void action40(HttpServletResponse response) throws IOException {
    response.getWriter().write("<h2>void method</h2>");
}

访问路径:http://localhost:8080/spring-mvc/controller09/action40,运行结果如图26所示:


图 26

3. 返回值为ModelAndView

在旧的Spring MVC中ModelAndView使用频率非常高,它可以同时指定返回的模型与视图名称。ModelAndView有个多构造方法重载,单独设置属性也很方便。

示例代码:

@RequestMapping("/action41")
public ModelAndView action41() {
    //1. 只指定视图
    //return new ModelAndView("index");

    //2. 分别指定视图与模型
    Map<String, Object> model=new HashMap<String,Object>();
    model.put("message", "ModelAndView action41");
    return new ModelAndView("index", model);

    //3. 同时指定视图与模型
    //return new ModelAndView("index", "message","action41 ModelAndView ");

    //4. 分开指定视图与模型
    //ModelAndView modelAndView=new ModelAndView();
    //指定视图名称
    //modelAndView.setViewName("index");
    //添加模型中的对象
    //modelAndView.addObject("message", "<h2>Hello ModelAndView</h2>");
    //return modelAndView;
}

访问路径:http://localhost:8080/spring-mvc/controller09/action41

4. 返回值为Map

当返回结果为Map时,相当于只是返回了Model,并未指定具体的视图,返回视图的办法与void是一样的,即URL= prefix前缀+控制器路径+方法名称 +suffix后缀组成。

示例代码:

@RequestMapping("/action42")
public Map<String, Object> action42() {
    Map<String, Object> model = new HashMap<String, Object>();
    model.put("message", "Hello Map");
    model.put("other", "more item");
    return model;
}

访问路径:http://localhost:8080/spring-mvc/controller09/action42

实际访问的视图路径是:/WEB-INF/view/controller09/action42.jsp,返回给客户端的map相当于模型,在视图中可以取出。

5. 返回值为任意类型

(1) 返回值为基本数据类型

当返回结果直接为int,double,boolean等基本数据类型时,将会报exception is java.lang.IllegalArgumentException: Unknown return value type异常。

示例代码:

@RequestMapping("/action43")
public int action43() {
    return 9527;
}

如果确实需要直接将基本数据类型返回,则可以使用注解@ReponseBody。

示例代码:

@RequestMapping("/action44")
@ResponseBody
public int action44() {
    return 9527;
}

访问路径:http://localhost:8080/spring-mvc/controller09/action44

(2) 返回值为自定义类型

当返回值为自定义类型时,Spring会把方法认为是视图名称,与返回值为void类似的办法处理URL,但页面中获得数据比较麻烦。

示例代码:

@RequestMapping("/action45")
public Product action45() {
    return new Product(1,"iPhone",1980.5);
}

访问路径:http://localhost:8080/spring-mvc/controller09/action45

如果存在action45对应的视图,页面可以正常显示,但无内容。

如果在action上添加@ResponseBody注解,则返回的是Product本身,而非视图,Spring会选择一个合适的方式解析对象,默认是json。

示例代码:

@RequestMapping("/action46")
@ResponseBody
public Product action46() {
    return new Product(1,"iPhone",1980.5);
}

访问路径:http://localhost:8080/spring-mvc/controller09/action46

6. 返回值为Model类型

Model接口定义在org.springframework.ui包下,model对象会用于页面渲染,视图路径使用方法名,与void类似。

示例代码:

@RequestMapping("/action47")
public Model action47(Model model) {
    model.addAttribute("message", "返回类型为org.springframework.ui.Model");
    return model;
}

访问路径:http://localhost:8080/spring-mvc/controller09/action47

返回的类型还有许多如view等,通过view可指定一个具体的视图,如下载Excel、Pdf文档,其实它们也修改http的头部信息,手动同样可以实现。

示例代码:

@RequestMapping("/action48")
@ResponseBody
public String action48(HttpServletResponse response) {
    response.setHeader("Content-type","application/octet-stream");         
    response.setHeader("Content-Disposition","attachment; filename=table.xls");
    return "<table><tr><td>Hello</td><td>Excel</td></tr></table>";
}

访问路径:http://localhost:8080/spring-mvc/controller09/action48

7. 小结

  • 使用String作为请求处理方法的返回值类型是比较通用的方法,这样返回的逻辑视图名不会和请求URL绑定,具有很高的灵活性,而模型数据又可以通过Model控制;
  • 使用void、map、Model作为请求处理方法的返回值类型时,这样返回对应的逻辑视图名称真实url为:prefix前缀+控制器路径+方法名+suffix后缀组成;
  • 使用String、ModelAndView返回视图名称可以不受请求的url绑定,ModelAndView可以设置返回的视图名称;
  • 另外在非MVC中使用的许多办法在Action也可以使用。
分享到 评论

02-@RequestMapping详解

1. value 属性

value属性用来指定请求的实际地址,指定的地址可以是URL模板、正则表达式或路径占位;该属性与path属性互为别名关系,即@RequestMapping(“/foo”)}与@RequestMapping(path=”/foo”)相同;该属性是使用最频繁、最重要的一个属性,当只指定该属性时可以把value略去。

(1) 指定具体路径字符串

只注解方法,访问时直接指定方法的路径

@Controller
public class Controller03 {
    /**
     * @url http://localhost:8080/spring-mvc/action01
     * @return
     */
    @RequestMapping("/action01")
    public String action01(){
        return "index";
    }
}

访问路径:http://localhost:8080/spring-mvc/action01

同时注解类与方法,访问时需要先指定类的路径再指定方法的路径

@Controller
@RequestMapping("/controller04")
public class Controller04 {
    /**
     * @url http://localhost:8080/spring-mvc/controller04/action02
     * @return
     */
    @RequestMapping("/action02")
    public String action02(){
        return "index";
    }
}

访问路径:http://localhost:8080/spring-mvc/controller04/action02

value为空值

第一种情况:注解在方法上时,如果value为空则表示该方法为类下默认的Action。

@Controller
@RequestMapping("/controller05")
public class Controller05 {
    /**
     * @url http://localhost:8080/spring-mvc/controller05/action03
     * @param model
     * @return
     */
    @RequestMapping("/action03")
    public String action03(Model model){
        //在模型中添加属性message值为action03,渲染页面时使用
        model.addAttribute("message", "action03");
        return "index";
    }

    /**
     * 该方法为类下默认的Action
     * @url http://localhost:8080/spring-mvc/controller05
     * @param model
     * @return
     */
    @RequestMapping
    public String action04(Model model){
        //在模型中添加属性message值为action04,渲染页面时使用
        model.addAttribute("message", "action04");
        return "index";
    }
}

访问action04的路径:http://localhost:8080/spring-mvc/controller05,如果加上action02就会产生错误了。

第二种情况:注解在类上时,当value为空值时,则该类为默认的控制器,可以用于设置项目的起始页。

@Controller
@RequestMapping
public class Controller06 {
    /**
     * @url http://localhost:8080/spring-mvc/action05
     * @param model
     * @return
     */
    @RequestMapping("/action05")
    public String action05(Model model){
        //在模型中添加属性message值为action05,渲染页面时使用
        model.addAttribute("message", "action05");
        return "index";
    }

    /**
     * @url http://localhost:8080/spring-mvc/
     * @param model
     * @return
     */
    @RequestMapping
    public String action06(Model model){
        //在模型中添加属性message值为action06,渲染页面时使用
        model.addAttribute("message", "action06");
        return "index";
    }
}

访问action06的路径:http://localhost:8080/spring-mvc/,同时省去了控制器名与Action名称,可用于欢迎页。

访问action05的路径:http://localhost:8080/spring-mvc/action05

(2) 路径变量占位,URI模板模式

在Spring MVC中可以使用@PathVariable 注释方法参数的值绑定到一个URI模板变量。

@RequestMapping("/action07/{p1}/{p2}")
public String action07(@PathVariable int p1, @PathVariable int p2, Model model) {
    model.addAttribute("message", p1 + p2);
    return "index";
}

访问action07的路径:http://localhost:8080/spring-mvc/controller07/action07/1/2,运行结果:


图 1

使用路径变量的好处:使路径变得更加简洁;框架会自动进行类型转换,获得参数更加方便,通过路径变量的类型可以约束访问参数,如果类型不一样,则访问不到action,如这里访问是的路径是/action07/1/a,则路径与方法不匹配,而不会是参数转换失败。

(3) 正则表达式模式的URI模板

@RequestMapping(value = "/action08/{id:\\d{6}}-{name:[a-z]{3}}")
public String action08(@PathVariable int id, @PathVariable String name, Model model) {
    model.addAttribute("message", "id:" + id + " name:" + name);
    return "index";
}

正则要求id必须为6位的数字,而name必须为3位小写字母。

访问action08的路径:http://localhost:8080/spring-mvc/controller07/action08/123456-abc,访问结果如下:


图 1

(4) 矩阵变量@MatrixVariable

/**
 * @url http://localhost:8080/spring-mvc/controller07/action09/the book color;r=33;g=66
 * @param model
 * @param name
 * @param r
 * @param g
 * @param b
 * @return
 */
@RequestMapping(value = "/action09/{name}")
public String action09(Model model,
        @PathVariable String name, // 路径变量,用于获得路径中的变量name的值
        @MatrixVariable String r,
        @MatrixVariable(required = true) String g, // 参数g是必须的
        @MatrixVariable(defaultValue = "99", required = false) String b) { // 参数b不是必须的,默认值是99
    model.addAttribute("message", name + " is #" + r + g + b);
    return "index";
}

Spring MVC默认是不允许使用矩阵变量的,需要设置配置文件中的RequestMappingHandlerMapping的属性removeSemicolonContent为false;并且在annotation-driven中增加属性enable-matrix-variables=”true”。修改后的springmvc-servlet.xml文件如下:

<!-- 支持mvc注解驱动 -->
<mvc:annotation-driven enable-matrix-variables="true" />

<!-- 配置映射媒体类型的策略 -->
<bean
    class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
    <property name="removeSemicolonContent" value="false" />
</bean>

访问action09的路径:http://localhost:8080/spring-mvc/controller07/action09/the book color;r=33;g=66,访问结果如下:


图 1

(5) Ant风格路径模式

@RequestMapping注解也支持ant风格的路径模式,如/myPath/*.do,/owners/*/pets/{petId}

/**
 * Ant风格路径模式
 * @url http://localhost:8080/spring-mvc/controller07/action10/ant.do
 * @param model
 * @return
 */
@RequestMapping(value = "/action10/*.do")
public String action10(Model model) {
    model.addAttribute("message", "Ant风格路径模式");
    return "index";
}

访问action10的路径:http://localhost:8080/spring-mvc/controller07/action10/ant.do,访问结果如下:


图 1

2. method属性

method属性可以指定请求谓词的类型,如GET、POST、HEAD、OPTIONS、PUT、PATCH、DELETE、TRACE等,约束请求范围。

@RequestMapping(value = "/action11", method = { RequestMethod.POST, RequestMethod.DELETE })
public String action11(Model model) {
    model.addAttribute("message", "请求谓词只能是POST与DELETE");
    return "index";
}

访问action11的路径:http://localhost:8080/spring-mvc/controller07/action11

要访问action11的请求谓词类型必须是POST或者为DELETE,但当我们从浏览器的URL栏中直接请求时,发出的是一个GET请求,则结果显示405,访问结果如下:


图 1

现将请求的谓词类型从POST修改为GET

@RequestMapping(value = "/action12", method = RequestMethod.GET)
public String action12(Model model) {
    model.addAttribute("message", "请求谓词只能是GET");
    return "index";
}

访问action12的路径:http://localhost:8080/spring-mvc/controller07/action12,访问正常,结果如下所示:


图 1

3. consumes属性

consumes属性指定提交的请求的内容类型(Content-Type),如application/json, text/html,约束请求范围。如果用户发送的请求内容类型和consumes属性不匹配,则方法不会响应请求。

@RequestMapping(value = "/action13", consumes="text/html")
public String action13(Model model) {
    model.addAttribute("message", "请求的提交内容类型(Content-Type)是text/html");
    return "index";
}

访问action13的路径:http://localhost:8080/spring-mvc/controller07/action13

在action8的注解中约束发送到服务器的请求的Content-Type必须是text/html类型,如果类型不一致则会报错(415),结果如下所示:


图 1

4. produces属性

5. params属性

params属性可以限制客户端发送到服务器的请求中必须含有特定的参数与值(参数的值为某些特定值或不为某些特定值)。

@RequestMapping(value = "/action15", params = { "id=215", "name!=abc" })
public String action15(Model model) {
    model.addAttribute("message", "请求的参数必须包含id=215与name不等于abc");
    return "index";
}

访问action15的路径:http://localhost:8080/spring-mvc/controller07/action15?id=215&name=abc,结果如下所示:


图 1

访问action15的路径:http://localhost:8080/spring-mvc/controller07/action15?id=215&name=def,结果如下所示:


图 1

name的值不指定或者使用不等于也是通过的

访问action15的路径:http://localhost:8080/spring-mvc/controller07/action15?id=215&name=def,结果如下所示:


图 1

访问action15的路径:http://localhost:8080/spring-mvc/controller07/action15?id=215&name!=abc,结果如下所示:


图 1

6. headers属性

headers属性可以限制客户端发送到服务器的请求头部信息中必须包含某个特定的值或不包含特定的值。

@RequestMapping(value = "/action16",headers="Host=localhost:8088")
public String action16(Model model) {
    model.addAttribute("message", "请求头部信息中必须包含Host=localhost:8088");
    return "index";
}

访问action16的路径:http://localhost:8080/spring-mvc/controller07/action16,结果如下所示:


图 1
@RequestMapping(value = "/action17",headers="Host=localhost:8080")
public String action17(Model model) {
    model.addAttribute("message", "请求头部信息中必须包含Host=localhost:8080");
    return "index";
}

访问action17的路径:http://localhost:8080/spring-mvc/controller07/action17,结果如下所示:


图 1

这里同样可以使用!号;可以使用通配符如:Content-Type=”application/*“

7. name属性

为当前映射指定一个名称,不常用,一般不会使用。

8. path属性

从Spring 4.2开始引入了@AliasFor注解,可以实现属性的别名,如value本身并没有特定的含义,而path会更加具体,见名知义,通俗说可以认为两者在使用中是一样的,如:@RequestMapping(“/foo”)}与@RequestMapping(path=”/foo”)相同。

示例代码:

访问action18的路径:http://localhost:8080/spring-mvc/controller07/action18,结果如下所示:


图 1

访问action18的路径:http://localhost:8080/spring-mvc/controller07/myaction,结果如下所示:


图 1
分享到 评论

01-SpringMVC入门与环境配置

本页内容来自:http://www.cnblogs.com/best/p/5653916.html,只是对其中部分内容重新组织,稍加修改。

查看更多

分享到 评论

16-JVM类加载机制

一、概述

类的加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据结构的接口。

wps5F9E.tmp

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误

二、类的生命周期

类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载这七个阶段,其中验证、准备和解析三个阶段统称为连接,这7个阶段的发生顺序如图1所示。


图 1:类的生命周期

图1中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。注意这里写的是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用、激活另一个阶段。

三、类加载的时机

四、类加载的过程

Java虚拟机中类加载的全过程包括:加载、验证、准备、解析和初始化五个阶段。

1. 加载:查找并加载类的二进制数据

加载是”类加载过程”的第一个阶段。在加载阶段,虚拟机需要完成以下三件事情:

  • 通过一个类的全限定名来获取定义此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区中的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为对方法区中这个类的各种数据的访问入口。

相对于类加载的其他阶段而言,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器来完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式 (即重写一个类加载器的loadClass()方法)。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

2. 连接

(1) 验证:确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致会完成4个阶段的检验动作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。例如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机处理范围之内、常量池中的常量是否有不被支持的类型等。
  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。例如:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)、这个类的父类是否继承了不允许被继承的类(被final修饰的类)等。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

对于虚拟机的类加载机制来说,验证阶段是非常重要的,但不是必须的阶段(因为对程序运行期没有影响)。如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经经过反复使用与验证,那么在实施阶段可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

(2) 准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。

对于该阶段有以下几点需要注意:

  • 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一块分配在Java堆中。

  • 这里所说的初始值通常情况下是数据类型的零值(如0、0L、null、false等),而不是Java代码中被显式地赋予的值。
    假设一个类变量的定义为:public static int value = 123;那么变量value在准备阶段过后的初始值为0,而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为123的动作将在初始化阶段才会执行。

  • 上面提到的在通常情况下初始值是零值,但也会有一些特殊情况:如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那在准备阶段变量value就会被初始化为ConstValue属性所指定的值。假设上面的类变量value被定义为:public static final int value = 123;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。

(3) 解析:把类中的符号引用转换为直接引用

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

  • 符号引用是以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用可以是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

3. 初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说字节码)。

初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

  • 声明类变量是指定初始值
  • 使用静态代码块为类变量指定初始值

    JVM初始化步骤

  • 假如这个类还没有被加载和连接,则程序先加载并连接该类

  • 假如该类的直接父类还没有被初始化,则先初始化其直接父类
  • 假如类中有初始化语句,则系统依次执行这些初始化语句

类初始化时机

虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候、以及调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

五、类加载器

六、相关问题

问:Java类加载时的初始化顺序

(1) 初始化父类中的静态成员变量和静态代码块(按照在程序中出现的顺序初始化)

(2) 初始化子类中的静态成员变量和静态代码块(按照在程序中出现的顺序初始化)

(3) 初始化父类中的普通成员变量和构造代码块(按照在程序中出现的顺序初始化),然后再执行父类中的构造方法

(4) 初始化子类中的普通成员变量和构造代码块(按照在程序中出现的顺序初始化),然后再执行子类中的构造方法

例1:

class Member {
    Member(String str) {
        System.out.println(str);
    }
}
class A {
    static {
        System.out.println("父类静态代码块");
    }
    public A() {
        System.out.println("父类构造函数");
    }
    {
        System.out.println("父类构造代码块");
    }
    Member member=new Member("父类成员变量");
}
class B extends A {
    Member member=new Member("子类成员变量");
    static {
        System.out.println("子类静态代码块");
    }
    public B() {
        System.out.println("子类构造函数");
    }
    {
        System.out.println("子类构造代码块");
    }
}
public class Test{
    public static void main(String[] args) {
        new B();
    }
}

//输出:
父类静态代码块
子类静态代码块
父类构造代码块
父类成员变量
父类构造函数
子类成员变量
子类构造代码块
子类构造函数

例2:下面代码的输出是什么?(易错)

public class B {
    public static B t1 = new B();
    public static B t2 = new B();
    {
        System.out.println("构造块");
    }
    static {
        System.out.println("静态块");
    }
    public static void main(String[] args) {
        B t = new B();
    }
}

// 输出

构造块
构造块
静态块
构造块

例3:下面代码的输出是什么?(易错)

public class Base {
    private String baseName = "base";

    public Base() {
        callName();
    }

    public void callName() {
        System.out.println(baseName);
    }

    static class Sub extends Base {
        private String baseName = "sub";

        public void callName() {
            System.out.println(baseName);
        }
    }

    public static void main(String[] args) {
        Base b = new Sub();
    }
}

// 输出:null

实例化子类对象时会先调用父类构造方法,由于父类构造方法调用了callName()方法并且子类重写了此方法,因此父类构造方法将调用子类的callName()方法将输出子类成员变量baseName的值。
但是由于子类的成员变量在父类构造方法调用完才会赋初值,因此调用callName()方法时,baseName值为null,所以输出结果为null。
分享到 评论

15-垃圾收集器与内存分配策略

一、概述

垃圾收集(Garbage Collection,GC)和内存动态分配技术诞生于1960年MIT的Lisp语言,经过半个多世纪,目前已经相当成熟了。

在Java内存运行时区域的各个部分中,程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭;栈中的栈帧随着方法的进入和退出做入栈和出栈操作,每一个栈帧中分配多少内存基本上是在类结构确定下来就已知的,因此这几个区域的内存分配和回收都具备确定性,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的是Java堆和方法区的内存回收。

二、对象存活判断

1. 引用计数算法(Reference Counting)

每个对象有一个引用计数属性,新增一个引用时计数值加1,引用释放时计数值减1,计数值为0的对象可以被回收。

这种方法的实现简单,判定效率也很高,但是无法解决对象之间相互循环引用的问题。

2. 可达性分析算法(Reachability Analysis)

以一系列的称为”GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

三、垃圾收集算法

1. 标记-清除算法

最基础的收集算法是”标记-清除”(Mark-Sweep)算法,如同它的名字一样,算法分为”标记”和”清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。标记-清除算法的执行过程如图1所示。


图 1:”标记-清除”算法示意图

2. 复制算法

为了解决效率问题,一种称为”复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制生存期的对象则导致效率降低。复制算法的执行过程如图2所示。


图 2:”复制”算法示意图

现在的商业虚拟机都采用这种收集算法来回收新生代,研究表明,新生代中的对象98%是”朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例为8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被浪费。当然98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

例1:以下哪些jvm的垃圾回收方式采用的是复制算法回收?

A. 新生代串行收集器  B. 老年代串行收集器  C. 并行收集器  D. 新生代并行回收收集器  E. 老年代并行回收收集器  F. cms收集器

答案:A D

3. 标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种”标记-整理”(Mark-Compact)算法,标记过程仍然与”标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记-整理算法的执行过程如图3所示。


图 3:”标记-整理”算法示意图

4. 分代收集算法

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

当前商业虚拟机的垃圾收集都采用”分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用”标记-清理”或者”标记-整理”算法来进行回收。

四、垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK 1.7 Update 14之后的HotSpot虚拟机(在这个版本中正式地提供了商用的G1收集器,之前G1仍处于试验状态),这个虚拟机包含的所有虚拟机如图4所示。


图 4:HotSpot虚拟机的垃圾收集器

图4展示了7中作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表明它是属于新生代收集器还是老年代收集器。

1. Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器,它是一个单线程的收集器,它的”单线程”的意义不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。Serial/Serial Old收集器的运行过程如图5所示。

  • 工作线程会因内存回收而导致停顿(Stop The World)
  • 与其他单线程的收集器相比,简单而高效:对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

图 5:Serial/Serial Old收集器运行示意图

2. ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop the World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew收集器的运行过程如图6所示。


图 6:Serial/Serial Old收集器运行示意图

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

3. Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

4. Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。

这个收集器的主要意义在于给CLient模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK1.5以及以前的版本中与Paralle Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用。

5. Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和”标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择。由于老年代Serial Old收集器在服务端应用性能上的拖累,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件条件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合给力。

直至Parallel Old收集器出现后,”吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作过程如图7所示。


图 7:Parallel Scavenge/Parallel Old收集器运行示意图

6. CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

从名字(包含”Mark Sweep”)上就可以看出,CMS收集器是基于”标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要”Stop The World”。初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于在耗时最长的并发标记和并发清除过程中,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。从图8可以清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。


图 8:Concurrent Mark Sweep收集器运行示意图

优点:并发收集、低停顿

缺点:

  • CMS收集器对CPU资源非常敏感
  • CMS收集器无法处理浮动垃圾,可能出现”Concurrent Mode Failure”失败而导致另一次Full GC的产生。
  • CMS收集器是一款基于”标记-清除”算法实现的收集器,收集结束时会有大量空间碎片产生。

7. G1收集器

G1(Barbage-First)收集器是当今收集器技术发展的最前沿成果之一,它是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备以下特点:

  • 并行和并发:G1能够充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的”标记-清理”算法不同,G1从整体上来看是基于”标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于”复制”算法实现的。但无论如何,这两种算法都意味着G1在运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  • 可预测停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

在G1之前的其他垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(不需要连续)Region的集合。

G1收集器的运作大致可以划分为以下几个步骤:

  • 初始标记(Initial Mark):初始标记阶段仅仅是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking):并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking):最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Rememebered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并发执行。
  • 筛选回收(Live Data Counting and Evacuation):筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

图 9:G1收集器运行示意图

五、内存分配与回收策略

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配对象的内存。

对象的内存分配,从大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中雨内存相关的参数的设置。

下面是几条最普遍的内存分配规则:

1. 对象优先分配在Eden区

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机执行一次Minor GC。

2. 大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前出发垃圾收集以获取足够的连续空间来安置他们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。

3. 长期存活的对象进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机为每个对象定义了一个年龄计数器,如果对象在Eden出生并经过第1次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当它的年龄增加到一定程序(默认为15岁),就将被晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

4. 动态判断对象的年龄

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

5. 空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么回继续检查老年最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

六、相关问题

问:Java堆的结构是什么样子的?什么是堆中的永久代(Perm Gen space)? 需要整理使答案更简洁

JVM的堆是运行时数据区,所有类的实例和数组都是在堆上分配内存。它在JVM启动的时候被创建。对象所占的堆内存是由自动内存管理系统也就是垃圾收集器回收。

堆内存是由存活和死亡的对象组成的。存活的对象是应用可以访问的,不会被垃圾回收。死亡的对象是应用不可访问尚且还没有被垃圾收集器回收掉的对象。一直到垃圾收集器把这些对象回收掉之前,他们会一直占据堆内存空间。

虚拟机中的堆内存共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

Java的垃圾收集机制主要针对新生代和老年代的内存进行回收,不同的垃圾收集算法针对不同的区域。所以Java的垃圾收集算法使用的是分代回收。一般java的对象首先进入新生代的Eden区域,当进行GC的时候会回收新生代的区域,新生代一般采用复制收集算法,将活着的对象复制到survivor区域中,如果survivor区域装在不下,就查看老年代是否有足够的空间装下新生代中的对象,如果能装下就装下,否则老年代就执行FULL GC回收自己,老年代还是装不下,就会抛出OutOfMemory的异常。

(1) 年轻代:所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。Minor GC是针对新生代的回收。

年轻代分三个区:一个Eden区,两个Survivor区(一般而言)。

大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

(2) 年老代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。Major GC/Full GC是针对年老代的回收。

(3) 持久代:用于存放静态文件,如类、方法、final常量、static变量等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

例1:JVM中垃圾回收分为scanvenge gc和full GC,其中full GC触发的条件可能有哪些?

答案:老年代满、持久代满、System.gc()

问:Java中垃圾回收(GC)有什么目的?什么时候进行垃圾回收?

GC是垃圾收集的意思(Gabage Collection),内存处理是编程人员容易出现问题的地方,忘记或者错误地内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。Java语言没有提供显示地释放已分配内存的操作方法。

垃圾回收器通常是作为一个单独的低级别的线程运行,在不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。

垃圾回收的目的是识别并回收堆内存中不再使用的对象所占的内存,释放资源。而栈区的内存是随着线程结束而释放的。

对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。

触发主GC(Garbage Collector,垃圾回收)的条件:

(1) 当应用程序空闲时,即没有应用线程在运行时,GC会被调用。

(2) Java堆内存不足时,GC会被调用。

问:System.gc()和Runtime.gc()会做什么事情?

Java提供了垃圾回收机制来帮助我们不定时的回收堆中不再使用的对象。当JVM启动时,除了启动我们的主线程外,还会启动垃圾回收线程,它运行优先非常低,会在JVM空闲时,自动回收我们不再使用的对象,释放内存空间。

程序员不能强制执行垃圾回收,可以用这两个方法用来提示JVM要进行垃圾回收。但是,立即开始还是延迟进行垃圾回收是取决于JVM,即垃圾回收的具体时间和顺序是无法预知的。

问:如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

不会立即释放对象占用的内存,如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,在下一个垃圾回收周期中,这个对象将是可被回收的。

问:串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

吞吐量收集器使用并行版本的新生代垃圾收集器,它适合于吞吐量要求较高的场合,用于中等规模和大规模数据的应用程序。

串行收集器整个扫描和复制过程均采用单线程的方式,相对于吞吐量GC来说简单;适合于单CPU、客户端级别。串行对大多数的小应用(在现代处理器上需要大概100M左右的内存)就足够了。

问:在Java中,对象什么时候可以被垃圾回收?

当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。

问:JVM的永久代中会发生垃圾回收么?(没看懂)

垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。

请参考下Java8:从永久代到元数据区(注:Java8中已经移除了永久代,新加了一个叫做元数据区的native内存区)

问:引用的分类

来源1:Java 如何有效地避免OOM:善于利用软引用和弱引用

来源2:Java强引用、 软引用、 弱引用、虚引用

JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

(1) 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

public class Main {
    public static void main(String[] args) {
        new Main().fun1();
    }

    public void fun1() {
        Object object = new Object();
        Object[] objArr = new Object[1000];
    }
}

当运行至Object[] objArr = new Object[1000];这句时,如果内存不足,JVM会抛出OOM错误也不会回收object指向的对象。不过要注意的是,当fun1运行完之后,object和objArr都已经不存在了,所以它们指向的对象都会被JVM回收。

如果想中断强引用和某个对象之间的关联,可以显示地将引用赋值为null,这样JVM就可以在合适的时间回收该对象了。

(2) 软引用(SoftReference)

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,当内存空间足够时,垃圾回收器不会回收它;而只有在内存不足的时候JVM才会回收该对象。只要垃圾回收器没有回收它,该对象就可以被程序使用。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现内存敏感的缓存:比如网页缓存、图片缓存等。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

import java.lang.ref.SoftReference;

public class Main {
    public static void main(String[] args) {
        SoftReference<String> sr = new SoftReference<String>(new String("hello"));//当某个对象需要设置为软引用时,只需要给该对象放到软引用对象中即可
        System.out.println(sr.get());
    }
}

(3) 弱引用(WeakReference)

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。弱引用与软引用的区别在于:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

import java.lang.ref.WeakReference;

public class Main {
    public static void main(String[] args) {
        WeakReference<String> sr = new WeakReference<String>(new String("hello"));
        System.out.println(sr.get());
        System.gc();//通知JVM的gc进行垃圾回收
        System.out.println(sr.get());
    }
}

输出:
hello
null

这说明只要JVM进行垃圾回收,被弱引用关联的对象必定会被回收掉。
不过要注意的是,这里所说的被弱引用关联的对象是指只有弱引用与之关联,如果存在强引用同时与之关联,则进行垃圾回收时也不会回收该对象(软引用也是如此)。

(4) 虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class Main {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

输出:
null

问:如何利用软引用和弱引用解决OOM问题

来源:Android开发优化之——使用软引用和弱引用

在应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

假设我们的应用会用到大量的默认图片,比如应用中有默认的头像,默认游戏图标等等,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生OutOfMemory异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。

private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

//保存Bitmap的软引用到HashMap
public void addBitmapToCache(String path) {
    // 强引用的Bitmap对象
    Bitmap bitmap = BitmapFactory.decodeFile(path);
    // 软引用的Bitmap对象
    SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
    // 添加该对象到Map中使其缓存
    imageCache.put(path, softBitmap);
}

public Bitmap getBitmapByPath(String path) {
    // 从缓存中取软引用的Bitmap对象
    SoftReference<Bitmap> softBitmap = imageCache.get(path);
    // 判断是否存在软引用
    if (softBitmap == null) {
        return null;
    }
    // 取出Bitmap对象,如果由于内存不足Bitmap被回收,将取得空
    Bitmap bitmap = softBitmap.get();
    return bitmap;
}

使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

需要注意的是,在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。

问:可能发生OOM和SOF的情况及解决方法

(1) OOM

OOM:OutOfMemoryError,即内存溢出,是指程序在申请内存时,没有足够的空间供其使用,出现了Out Of Memory,也就是要求分配的内存超出了系统上限,系统不能满足其需求,于是产生溢出。

内存溢出分为上溢和下溢,比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。

有时候内存泄露会导致内存溢出,所谓内存泄露(memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,举个例子,就是说系统的篮子(内存)是有限的,而你申请了一个篮子,拿到之后没有归还(忘记还了或是丢了),于是造成一次内存泄漏。在你需要用篮子的时候,又去申请,如此反复,最终系统的篮子无法满足你的需求,最终会由内存泄漏造成内存溢出。

遇到的OOM:
(1)Java Heap 溢出
Java堆用于存储对象实例,我们只要不断的创建对象,而又没有及时回收这些对象(即内存泄漏),就会在对象数量达到最大堆容量限制后产生内存溢出异常。
(2)方法区溢出

方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。
异常信息:java.lang.OutOfMemoryError:PermGen space
方法区溢出也是一种常见的内存溢出异常,一个类如果要被垃圾收集器回收,判定条件是很苛刻的。在经常动态生成大量Class的应用中,要特别注意这点。

(2) SOF

SOF:StackOverflow(堆栈溢出)
当应用程序递归太深而发生堆栈溢出时,抛出该错误。因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
栈溢出的原因:
(1)递归调用
(2)大量循环或死循环
(3)全局变量是否过多
(4)数组、List、Map数据过大

OOM在Android开发中出现比较多,例如:加载的图片太多或图片过大、分配特大的数组、内存相应资源过多没有来不及释放等。

解决方法:

(1)在内存引用上做处理
    软引用是主要用于内存敏感的高速缓存。在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。
(2)对图片做边界压缩,配合软引用使用
(3)显示的调用GC来回收内存,如:
    if(bitmapObject.isRecycled()==false) //如果没有回收  
   bitmapObject.recycle();

(4)优化Dalvik虚拟机的堆内存分配
》增强程序堆内存的处理效率
//在程序onCreate时就可以调用 即可
privatefinalstaticfloat TARGET_HEAP_UTILIZATION = 0.75f;
VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION);

        》设置堆内存的大小
        privatefinalstaticintCWJ_HEAP_SIZE = 6* 1024* 1024;
  //设置最小heap内存为6MB大小
  VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);

(5)用LruCache 和 AsyncTask<>解决
    从cache中去取Bitmap,如果取到Bitmap,就直接把这个Bitmap设置到ImageView上面。

  如果缓存中不存在,那么启动一个task去加载(可能从文件来,也可能从网络)。

分享到 评论

14-Java内存区域

JVM内存结构主要有三大块:堆内存、方法区和栈。

  • 堆内存是JVM中最大的一块内存结构,由年轻代和老年代组成,而年轻代内存又被分成三部分:Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
  • 方法区存储类信息、常量和静态变量等数据,是线程共享的区域。为与Java堆区分,方法区还有一个别名:Non-Heap(非堆);
  • 栈又分为java虚拟机栈和本地方法栈,主要用于方法的执行。

图 1:JVM内存结构布局

一、运行时数据区域

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途,以及创建和销毁的时间。有的区域随着虚拟机进程的启动而存在,有的区域则依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(Java SE 7版)》的规定,Java虚拟机所管理的内存将会包含以下几个运行时数据区域:程序计数器、Java虚拟机栈,本地方法栈、Java堆和方法区,如图1所示。

  • 程序计数器、Java虚拟机栈和本地方法栈是运行时数据区中线程私有的内存区域;
  • Java堆和方法区是所有线程共享的内存区域。

图 2:Java虚拟机运行时数据区

1. 程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一个线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,我们称这类内存区域为”线程私有“的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。程序计数器是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

2. Java虚拟机栈

与程序计数器一样,Java虚拟机栈(JVM Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展到无法申请到足够的内存时就会抛出OutOfMemoryError异常。

3. 本地方法栈

本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,也是线程私有的,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它,甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。

4. Java堆

对于大多数应用来说,Java堆(Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做”GC堆”。从内存回收的角度看,由于现在收集器基本都是采用的分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存或者更快分配内存。

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5. 方法区

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为”永久代”(Permanent Generation),本质上两者并不等价,因为仅仅是HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,省区专门为方法区编写内存管理的代码。对于其他虚拟机来说是不存在永久代概念的。

Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样”永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收”成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。

根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

例1

6. 运行时常量池

运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量表,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。程序中的字面值如直接书写的100、”hello”和常量都是放在常量池中。

既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。

例1:

String str = new String("hello");

上面的语句中变量str放在栈上,用new创建出来的String对象放在堆上,而”hello”这个字面量是放在方法区中的。

7. 直接内存

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起讲解。

在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

显然,直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是受主机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

二、内存溢出异常

三、相关问题

问:堆和栈的区别

Java的内存分为两类,一类是栈内存,一类是堆内存。

  • 栈内存是指程序进入一个方法时,会为这个方法单独分配一块私属存储空间,用于存储这个方法内部的局部变量,当这个方法结束时,分配给这个方法的栈会释放,这个栈中的变量也将随之释放。栈内存由操作系统来分配,只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
  • 堆是与栈作用不同的内存,一般用于存放不放在当前方法栈中的那些数据,例如,使用new创建的对象都放在堆里,所以,它不会随方法的结束而消失。堆内存由程序员自己来申请分配。

例1:

class A {
    private String a = "aa";
    public boolean methodB() {
        String b = "bb";
        final String c = "cc";
    }
}

上述Java代码中的变量a、b、c分别在内存的:堆区、栈区、栈区

问:运行时数据区域控制参数


图 2:JVM内存结构布局

图2展示了如何通过参数来控制各区域的内存大小,其中各控制参数解释如下:

  • Xms设置堆的最小空间大小。
  • Xmx设置堆的最大空间大小。
  • XX:NewSize设置新生代最小空间大小。
  • XX:MaxNewSize设置新生代最大空间大小。
  • XX:PermSize设置永久代最小空间大小。
  • XX:MaxPermSize设置永久代最大空间大小。
  • Xss设置每个线程的堆栈大小。
  • Xmn:设置堆的年轻代空间大小
  • XXSurvivorRatio:年轻代中Eden区与Survivor区的大小比值

没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。

老年代空间大小=堆空间大小-年轻代大空间大小

例1:

当-Xmx10240m -Xms10240m -Xmn5120m -XXSurvivorRatio=3时,其最小内存值和Survivor区总大小分别是()

-Xms初始堆大小即最小内存值,即最小内存值为10240m
JVM一般根据对象的生存周期将堆内存分为若干不同的区域,一般情况将新生代分为Eden,两块Survivor;
由-XXSurvivorRatio=3可知Eden:Survivor=3,年轻带总大小为5120m,那么Survivor区总大小为2048m

问:内存泄漏

内存泄露(Memory Leak)是指一个不再被使用的对象或者变量还在内存中占有存储空间。在C/C++语言中,内存泄露出现在开发人员忘记释放已分配的内存就会造成内存泄露。在java语言中引入垃圾回收机制,有GC负责进行回收不再使用的对象,释放内存。但是还是会存在内存泄露的问题。

检查Java中的内存泄露,一定要让程序将各种分支都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。

Java中内存泄露的情况:

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致它不能被回收,这就是java中内存泄露的发生场景。通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,但是这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是java中可能出现内存泄露的情况。例如,在缓存系统中,我们加载了一个对象放在缓存中(例如放在一个全局map对象中),然后一直不再使用它,这个对象一直被缓存引用。再例如hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露。

(1) 在堆中申请的空间没有释放;

(2) 对象已不再被使用(注意:这里的不再被使用是指对程序来说没有用处,如数据库连接使用后没有关,但是还是存在着引用),但是仍然在内存中保留着。

GC机制的引入只能解决第一种情况,对于第2种情况无法保证不再使用的对象会被释放。Java语言中的内存泄露主要指第2种情况。

内存泄露的主要场景:

(1) 静态集合类。如HashMap和Vector。这些容器是静态的,生命周期和程序的生命周期一致,那么在容器中对象的生命周期也和其一样,对象在程序结束之前将不能被释放,从而会造成内存泄露。

(2) 各种连接。如数据库连接,网络连接,IO连接,不再使用时如果连接不释放容易造成内存泄露。

(3) 监听器。释放对象时往往没有相应的删除监听器,可能会导致内存泄露。

例1:内存泄露举例

class Stack {
    private Object[] elements = new Object[10];
    private int size = 0;

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            Object[] oldElements = elements;
            elements = new Object[2 * elements.length + 1];
            System.arraycopy(oldElements, 0, elements, 0, size);
        }
    }
}

这个类主要特点就是清空堆栈中的某个元素,并不是彻底把它从数组中拿掉,而是把存储的总数减少。
假如堆栈放了10个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这种情况符合内存泄露的两个条件:无用且无法回收。
正确的做法:在弹出某个元素时,将那个元素所在的位置的值设置为null,让它从数组中消失。

例2:内存泄露举例

public class Test {
    public static Stack<Object> s= new Stack<Object>(); 
    static{ 
        s.push(new Object()); 
        s.pop();              //这里有一个对象发生内存泄露 
        s.push(new Object()); //上面的对象可以被回收了,等于是自愈了 
    }
}

因为s是static的,会一直存在直到程序退出,因此其中存储的对象在程序结束之前都不会被释放,存在内存泄漏。
但是我们也可以看到它有自愈功能,例如:如果你的Stack最多有100个对象,那么最多也就只有100个对象无法被回收,Stack内部持有100个引用,最坏的情况就是他们都是无用的。
但是如果我们一旦放新的对象进去,那么对之前对象的引用就会自然消失!

例3:内存泄露举例

当一个对象被存储进HashSet集合中后,就不能再修改这个对象中的参与计算哈希值的那些字段了。
否则,修改后的对象的哈希值与最初存储进HashSet集合中时的哈希值就不同了。
在这种情况下,即使在contains()方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,
这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露。

内存溢出(OOM)是指程序在申请内存时没有足够的内存供使用,进而导致程序崩溃。内存泄露(Memory Leak)最终会导致内存溢出。

例4:下面哪种情况会导致持久区jvm堆内存溢出?

A. 循环上万次的字符串处理 
B. 在一段代码内申请上百M甚至上G的内存
C. 使用CGLib技术直接操作字节码运行,生成大量的动态类
D. 不断创建对象

答案:C

推荐阅读:动手探究Java内存泄露问题

问:OOM和SOF

http://blog.csdn.net/shakespeare001/article/details/51274685

分享到 评论

IT校招全国统一模拟笔试(秋招备战专场二模)编程题

随机的机器人

有一条无限长的纸带,分割成一系列的格子,最开始所有格子初始是白色。现在在一个格子上放上一个萌萌的机器人(放上的这个格子也会被染红),机器人一旦走到某个格子上,就会把这个格子涂成红色。现在给出一个整数n,机器人现在会在纸带上走n步。每一步,机器人都会向左或者向右走一个格子,两种情况概率相等。机器人做出的所有随机选择都是独立的。现在需要计算出最后纸带上红色格子的期望值。

输入描述:输入包括一个整数n(0 ≤ n ≤ 500),即机器人行走的步数。

输出描述:输出一个实数,表示红色格子的期望个数,保留一位小数。

示例1

输入: 4

输出: 3.4

import java.text.DecimalFormat;
import java.util.*;

public class Main{
    public static void main(String[] args){
        Scanner scanner=new Scanner(System.in);
        int n=scanner.nextInt();

        double[][][] dp=new double[2][n+3][n+3];
        dp[0][1][0]=1;
        for(int i=1; i<=n; i++){
            int cur=i%2, prev=1-cur;

            for(int j=1; j<=i+1; j++){
                for(int k=0; k<j; k++){
                    dp[cur][j][k]=0;
                }
            }

            for(int j=1; j<=i; j++){
                for(int k=0; k<j; k++){
                    //left
                    if(k==0)
                        dp[cur][j+1][k]+=dp[prev][j][k]/2;
                    else
                        dp[cur][j][k-1]+=dp[prev][j][k]/2;
                    //right
                    if(k==j-1)
                        dp[cur][j+1][k+1]+=dp[prev][j][k]/2;
                    else
                        dp[cur][j][k+1]+=dp[prev][j][k]/2;
                }
            }
        }
        double ans=0;
        for(int j=1; j<=n+1; j++){
            for(int k=0; k<j; k++){
                ans+=j*dp[n%2][j][k];
            }
        }
        DecimalFormat df=new DecimalFormat("0.0");
        System.out.println(df.format(ans));
    }
}
分享到 评论

MapReduce

分享到 评论

Docker-Hadoop-Helloworld

使用Docker在本地搭建Hadoop分布式集群
本文档参考http://tashan10.com/yong-dockerda-jian-hadoopwei-fen-bu-shi-ji-qun/。

查看更多

分享到 评论

C plus plus

C++

查看更多

分享到 评论

Probability Theory

概率论与数理统计

查看更多

分享到 评论

信息安全总结

信息安全

查看更多

分享到 评论

软件测试总结

软件测试

查看更多

分享到 评论

Concurrency

并发

查看更多

分享到 评论

Design Pattern

设计模式

查看更多

分享到 评论

软件工程总结

软件工程

查看更多

分享到 评论

Linux总结

Linux

查看更多

分享到 评论

Advanced Algorithms

算法

查看更多

分享到 评论

计算机组成原理总结

计算机组成原理

查看更多

分享到 评论

04-JSP

JSP

查看更多

分享到 评论

03-Servlet

Servlet

查看更多

分享到 评论

02-HTTP协议

分享到 评论

01-XML

XML解析

查看更多

分享到 评论

如何使用Markdown

分享到 评论

数据库系统概念

数据库系统概念

查看更多

分享到 评论

计算机网络

计算机网络

查看更多

分享到 评论

操作系统

操作系统

查看更多

分享到 评论

数据结构

数据结构

查看更多

分享到 评论

第九章 网络编程

  • Http协议简介
  • 使用Handler进行线程间通信
  • AsyncTask
  • 使用HttpURLConnection、HttpClient访问网络提交数据
  • AsyncHttpClient、SmartImageView开源项目的使用
  • 多线程下载文件

查看更多

分享到 评论

第八章 服务

  • 服务的生命周期
  • 服务的两种启动方式
  • 本地服务通信
  • 远程服务通信(调用其它应用的服务)

查看更多

分享到 评论

第七章 广播接收者

  • 创建广播接收者
  • 自定义广播
  • 有序广播和无序广播
  • 常用广播接收者(如开机启动、短信接收)的使用

查看更多

分享到 评论

第六章 ContentProvider

  • 了解ContentProvider
  • 使用ContentProvider
  • 使用ContentResolver操作其他应用的数据
  • 使用ContentObserver观察其他应用的数据变化

查看更多

分享到 评论

第五章 SQLite数据库

  • SQLite数据库的基本操作
  • 使用sqlite3工具操作数据库
  • 使用ListView控件展示数据

查看更多

分享到 评论

第四章 数据存储

  • 5种数据存储方式的特点
  • 使用文件存储数据
  • 使用SharedPreferences存储数据
  • XML文件的序列化与解析

查看更多

分享到 评论

第三章 Activity

  • Activity的生命周期
  • Activity的4种启动模式
  • 隐式意图和显式意图的使用
  • 使用Intent传递数据

查看更多

分享到 评论

12-JDBC

JDBC

查看更多

分享到 评论

11-反射

反射

查看更多

分享到 评论

10-网络编程

网络编程

查看更多

分享到 评论

09-多线程

多线程

查看更多

分享到 评论

08-流

查看更多

分享到 评论

07-集合类

集合类

查看更多

分享到 评论

06-常用类

常用类

查看更多

分享到 评论

05-数组

数组

查看更多

分享到 评论

04-异常处理

异常处理

查看更多

分享到 评论

03-面向对象编程

面向对象编程

查看更多

分享到 评论

02-基础语法

基础语法

查看更多

分享到 评论

01-Java概述

Java概述

查看更多

分享到 评论