在之前的内容中，我写了Java的基础知识、Java Web的相关知识。有这些内容就可以编写各种各样丰富的程序。但是如果纯粹手写所有代码，工作量仍然很大。为了简化开发，隐藏一些不必要的细节，专心处理业务相关内容 ，Java提供了许多现成的框架可以使用Mybatis介绍在程序开发中讲究 MVC 的分层架构，其中M表示的是存储层，也就是与数据库交互的内容。一般来说使用jdbc时，需要经历：导入驱动、创建连接、创建statement对象，执行sql、获取结果集、封装对象、关闭连接这样几个过程。里面很多过程的代码都是固定的，唯一有变化的是执行sql并封装对象的操作。而封装对象时可以利用反射的机制，将返回字段的名称映射到Java实体类的各个属性上。这样我们很自然的就想到了，可以编写一个框架或者类库，实现仅配置sql语句和对应的映射关系，来实现查询到封装的一系列操作，从而简化后续的开发。Mybatis帮助我们实现了这个功能。Mybatis实例假设现在有一个用户表，存储用户的相关信息，我们现在需要使用mybatis来进行查询操作，可能要经历如下步骤：定义对应的实体类public class User { private Integer id; private String username; private String birthday; private char sex; private String address; //后面省略对应的getter和setter方法 //为了方便后面的实体类都会省略这些内容 } 编辑主配置文件，主要用来配置mybati的数据库连接信息以及指定对应dao的配置文件<?xml version="1.0" encoding="UTF-8"?> <configuration> <environments default="mybatis_demo"> <environment id="mybatis_demo"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/> <property name="username" value="root"/> <property name="password" value="masimaro_root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/MybatisDemo/Dao/IUserDao.xml"></mapper> </mappers> </configuration>编写dao接口public interface IUserDao { public List<User> findAll(); }并提供dao的xml配置文件<?xml version="1.0" encoding="UTF-8"?> <mapper namespace="com.MyBatisDemo.Dao.IUserDao"> select * from user; </mapper>写完了对应的配置代码，接下来就是通过简单的几行代码来驱动mybatis，完成查询并封装的操作InputStream is = null; SqlSession = null; try { //加载配置文件 is = Resources.getResourceAsStream("dbconfig.xml"); //创建工厂对象 SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(is); //创建sqlsession对象 sqlSession = factory.openSession(); //使用sqlsession对象创建dao接口的代理对象 IUserDao userDao = sqlSession.getMapper(IUserDao.class); //使用对象执行方法 List<User> users = this.userDao.findAll(); System.out.println(users); } catch (IOException e) { e.printStackTrace(); }finally{ // 清理资源 if (null != this.is){ try { this.sqlSession.commit(); this.is.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != this.sqlSession){ this.sqlSession.close(); } }mybatis大致的执行过程根据我们传入的InputStream对象来获取配置xml中对应对象的值接着根据配置信息创建连接并生成数据库的连接池对象根据配置文件中的mapper项获取到对应的Dao接口的配置文件，在读取该文件时会准备一个Map结构，其中key是mapper中的namespace + id，value是对应的sql语句，例如上述例子中得到的map结构为{"com.MyBatisDemo.Dao.IUserDao.findAll", "select * from user"}在创建sqlsession时从连接中获取到一个Statement对象在我们调用dao接口时，首先根据dao接口得到详细的类名，然后获取到当前调用的接口名称，由这两项得到一个key，比如在上述例子中，dao接口的名称为com.MyBatisDemo.Dao.IUserDao, 而调用的方法是 findAll,将这两个字符串进行拼接，得到一个key，根据这个key去map中查找对应的sql语句。并执行执行sql语句获取查询的结果集根据resultType中指定的对象进行封装并返回对应的实体类使用mybatis实现增删改查操作在之前的代码上可以看出，使用mybatis来实现功能时，只需要提供dao接口中的方法，并且将方法与对应的sql语句绑定。在提供增删改查的dao方法时如果涉及到需要传入参数的情况下该怎么办呢？下面以根据id查询内容为例：我们先在dao中提供这样一个方法：public User findById(int id);然后在dao的配置文件中编写sql语句 select * from user where id = #{id} 从上面的配置可以看到，mybatis中， 使用#{} 来表示输入参数，使用属性parameterType属性来表示输入参数的类型。一般如果使用Java内置对象是不需要使用全限定类名，也不区分大小写。当我们使用内置类型的时候，这里的id 仅仅起到占位符的作用，取任何名字都可以看完了使用内置对象的实例，再来看看使用使用自定义类类型的情况，这里我们使用update的例子来说明,首先与之前的操作一样，先定义一个upate的方法：void updateUser(User user);然后使用如下配置<update id="updateUser" parameterType="User"> update user set username=#{username}, birthday=#{birthday}, sex=#{sex}, address=#{address} where id = #{id} </update>与使用id查询的配置类似，当我们使用的是自定义类类型时，在对应的字段位置需要使用类的属性表示，在具体执行的时候，mybatis会根据传入的类对象来依据配置取出对应的属性作为sql语句的参数。上面在使用内置对象时我们说它可以取任何的名称，但是这里请注意 名称只能是自定义对象的属性名，而且区分大小写这里使用的都是确定的值，如果要使用模糊查询时该如何操作呢，这里我们按照名称来模糊查询,首先在dao中提供一个对应的方法User findByName(String name);接着再来进行配置 select * from User where username like #{username} 从sql语句来看我们并没有实现模糊的方式，这时候在传入参数的时候就需要使用模糊的方式，调用时应该在参数中添加 %%, 就像这样 userDao.findByName("%" + username + "%")当然我们可以使用另外一种配置 select * from User where username like %${username}% 这样我们在调用时就不需要额外添加 % 了。既然他们都可以作为参数，那么这两个符号有什么区别呢？区别在于他们进行查询的方式，$ 使用的是字符串拼接的方式来组成一个完成的sql语句进行查询，而#使用的是参数话查询的方式。一般来说拼接字符串容易造成sql注入的漏洞，为了安全一定要使用参数话查询的方式mybatis的相关标签resultMap标签在之前的配置中，其实一直保持着数据库表的字段名与对应的类属性名同名，但是有些时候我们不能保证二者同名，为了解决这问题也为了以后进行一对多和多对多的配置，可以使用resultMap来定义数据库表字段名和类属性名的映射关系下面是一个使用它的例子。我们简单修改一下User类的属性定义public class User { private Integer uid; private String name; private String userBirthday; private char userSex; private String userAddress; //后面省略对应的getter和setter方法 }这样直接使用之前的配置执行会报错，报找不到对应属性的错误，这个时候就可以使用resultMap属性来解决这个问题<resultMap id="UserMapper" type="User"> <id column="id" property="uid"></id> <result column="username" property="username"></result> <result column="sex" property="sex"></result> <result column="birthday" property="birthday"></result> <result column="address" property="address"></result> </resultMap> select * from user; 其中 id属性来唯一标示这个映射关系，在需要使用到这个映射关系的地方，使用resultMap这个属性来指定type属性表示要将这些值封装到哪个自定义的类类型中resultMap中有许多子标签用来表示这个映射关系id用来表明表结构中主键的映射关系result表示其他字段的映射关系每个标签中的column属性表示的是对应的表字段名标签中的property对应的是类属性的名称properties 标签properties标签可以用来定义数据库的连接属性，主要用于引入外部数据库连接属性的文件，这样我们可以通过直接修改连接属性文件而不用修改具体的xml配置文件。假设现在在工程中还有一个database.properties文件jdbc.driver ="com.mysql.jdbc.Driver" jdbc.url = "jdbc:mysql://localhost:3306/mybatis_demo" jdbc.username ="root" jdbc.password" ="masimaro_root"然后修改对应的主配置文件 <properties resource="database.properties"> </properties> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource>typeAliases 标签之前我们说过，使用内置类型时不需要写全限定类名，而且它不区分大小写。而使用自定义类型时需要写很长一串，如何使自定义类型与内置类型一样呢？这里可以使用typeAliases标签。它用来定义类名的别名<typeAliases> <typeAlias type="com.MyBatisDemo.domain.User" alias="user"></typeAlias> </typeAliases>使用typeAlias标签时，每个类都需要提供一条对应的配置，当实体类多了，写起来就很麻烦了，这个时候可以使用package子标签来代替typeAlias<typeAliases> <package name="com.MyBatisDemo.domain"/> </typeAliases>它表示这里包中的所有类都使用别名，别名就是它的类名package标签在定义对应的mapper xml文件时，一个dao接口就需要一条配置。dao接口多了，一条条的写很麻烦，为了减轻编写的工作量可以使用package标签<mappers> <package name="com/MyBatis/Dao"/> </mappers>连接池在配置数据库连接 dataSource 标签中有一个type属性，它用来定义使用的连接池，该属性有三个取值：POOLE：使用连接池，采用javax.sql.DataSource 规范中的连接池，mybatis中有针对它的数据库连接池的实现UNPOOLED：与POOLED相同，使用的都是javax.sql.DataSource 规范，但是它使用的是常规的连接方式，没有采用池的思想JNDI：根据服务器提供的jndi基础来获取数据库的连接 ，具体获取到的连接对象又服务器提供动态sql当我们自己拼接sql的时候可以根据传入的参数的不同来动态生成不同的sql语句执行，而在之前的配置中，我们事先已经写好了使用的sql语句，但是如果碰上使用需要按照条件搜索，而且又不确定用户会输入哪些查询条件，在这样的情况下，没办法预先知道该怎么写sql语句。这种情况下可以使用mybatis中提供的动态sql假设我们提供一个findByValue的方法，根据值来进行查询。public List<User> findByValue(User user);事先并不知道user的哪些属性会被赋值，我们需要做的就是判断user的哪些属性不为空，根据这些不为空的属性来进行and的联合查询。这种情况下我们可以使用if标签 select * from user where <if test="id != null"> id = #{id} and </if> <if test="username != null"> username=#{username} and </if> ..... 1=1 if标签中使用test来进行条件判断，而判断条件可以完全使用Java的语法来进行。这里在最后用了一个1=1的条件来结束判断，因为事先并不知道用户会传入哪些值，不知道哪条语句是最后一个条件，因此我们加一个恒成立的条件来确保sql语句的完整当然mybatis中也有办法可以省略最后的1=1，我们可以使用 where标签来包裹这些if，表明if中的所有内容都是作为查询条件的，这样mybatis在最后会在生成查询条件后自动帮助我们进行格式的整理使用if标签我们搞定了不确定用户会使用哪些查询条件的问题，如果有这样一个场景：用户只知道某个字段的名字有几种可能，我们在用户输入的几种可能值中进行查找，也就是说，用户可以针对同一个查询条件输入多个可能的值，根据这些个可能的值进行匹配，只要有一个值匹配上即可返回；针对这种情况没办法使用if标签了，我们可以使用循环标签，将用户输入的多个值依次迭代，最终组成一个in的查询条件我们在这里提供一个根据多个id查找用户的方法public List<User> findByIds(List<Integer> ids);这里我们为了方便操作，额外提供一个类用来存储查询条件public class QueryVo { List<Integer> ids; } select * from user <where> <if test="ids != null and ids.size() != 0"> <foreach collection="ids" open="and id in (" close=")" item= "id" separator=","> ${id} </foreach> </if> </where> 在上面的例子中使用foreach来迭代容器其中使用collection表示容器，这里取的是parameterType中指定类的属性，open表示在迭代开始时需要加入查询条件的sql语句，close表示在迭代结束后需要添加到查询语句中的sql，item表示每个元素的变量名，separator表示每次迭代结束后要添加到查询语句中的字符串。当我们迭代完成后，整个sql语句就变成了这样: select * from user where 1=1 and id in (id1, id2, ...)多表查询一对多查询在现实中存在着这么一些一对多的对应关系，像什么学生和班级的对应关系，用户和账户的对应关系等等。关系型数据库在处理这种一对多的情况下，使用的是在多对应的那张表中添加一个外键，这个外键就是对应的一那张表的主键，比如说在处理用户和账户关系时，假设一个用户可以创建多个账户，那么在账户表中会有一个外键，指向的是用户表的ID在上面例子的基础之上，来实现一个一对多的关系。首先添加一个账户的实体类，并且根据关系账户中应该有一个唯一的用户类对象，用来表示它所属的用户public class Account { private int id; private int uid; private double money; private User user; }同时需要在User这个实体类上添加一个Account的列表对象，表示一个User下的多个Accountpublic class User { private Integer id; private String username; private String birthday; private char sex; private String address; private List<Account> accounts; }首先根据user来查询多个account，我们可以写出这样的sql语句来查询select u.*, a.id as aid, a.money, a.uid from user as u left join account as a on a.uid = u.id;那么它查询出来的结果字段名称应该是id, username, sex, birthday, address, aid, money, uid 这些，前面的部分可以封装为一个User对象，但是后面的部分怎么封装到Accounts中去呢，这里可以在resultMap中使用collection标签，该标签中对应的对象会被封装为一个容器。因此这里的配置可以写为：<resultMap id="UserAccountMap" type="user"> <id property="id" column="id"></id> <result property="username" column="username"></result> <result property="birthday" column="birthday"></result> <result property="sex" column="sex"></result> <result property="address" column="address"></result> <collection property="accounts" ofType="account"> <id property="id" column="aid"></id> <result property="money" column="money"></result> <result property="uid" column="uid"></result> </collection> </resultMap> select u.*, a.ID as aid, a.MONEY, a.UID from user as u left join acc ount as a on u.id = a.uid 我们需要一个resultMap来告诉Mybatis，这些多余的字段该怎么进行封装，为了表示一个容器，我们使用了一个coolection标签，标签中的property属性表示这个容器被封装到resultType对应类的哪个属性中，ofType表示的是，容器中每一个对象都是何种类型，而它里面的子标签的含义与resultMap子标签的含义完全相同从User到Account是一个多对多的关心，而从Account到User则是一个一对一的关系，当我们反过来进行查询时，需要使用的配置是 association 标签，它的配置与使用与collection相同<resultMap id="AccountUserMap" type="Account"> <id property="id" column="aid"></id> <result property="uid" column="uid"></result> <result property="money" column="money"></result> <association property="user" column="uid" javaType="user"> <id property="id" column="uid"></id> <result property="username" column="username"></result> <result property="birthday" column="birthday"></result> <result property="sex" column="sex"></result> <result property="address" column="address"></result> </association> </resultMap> select * from account where uid = ${id} 多对多查询说完了一对多，再来说说多对多查询。多对多在关系型数据库中使用第三张表来体现，第三张表中记录另外两个表的主键作为它的外键。这里使用用户和角色的关系来演示多对多查询与之前一样，在两个实体类中新增对方的一个list对象，表示多对多的关系public class Role implements Serializable { private int id; private String roleName; private String roleDesc; private List<User> users; }利用之前一对多的配置，我们只需要修改一下ResultMap和sql语句就可以完成多对多的查询<mapper namespace="com.liuhao.Dao.IUserDao"> <resultMap id="UserRoleMapper" type="User"> <id property="id" column="id"></id> <result column="username" property="username"></result> <result column="sex" property="sex"></result> <result column="address" property="address"></result> <result column="birthday" property="birthday"></result> <collection property="roles" ofType="role"> <id property="id" column="rid"></id> <result column="role_desc" property="roleDesc"></result> <result column="role_name" property="roleName"></result> </collection> </resultMap> select user.*, role.ID as rid, role.ROLE_DESC, role.ROLE_NAME from u ser left outer join user_role on user_role.uid = user.id left OUTER join role on user_role.RID = role.ID </mapper>另一个多对多的关系与这个类似，这里就不再单独说明了延迟加载之前说了该如何做基本的单表和多表查询。这里有一个问题，在多表查询中，我们是否有必要一次查询出它所关联的所有数据，就像之前的一对多的关系中，在查询用户时是否需要查询对应的账户，以及查询账户时是否需要查询它所对应的用户。如果不需要的话，我么采用上面的写法会造成多执行一次查询，而且当它关联的数据过多，而这些数据我们用不到，这个时候就会造成内存资源的浪费。这个时候我们需要考虑使用延迟加载，只有需要才进行查询。之前的sql语句一次会同时查询两张表，当然不满足延迟加载的要求，延迟加载应该将两张表的查询分开，先只查询需要的一张表数据，另一张表数据只在需要的时候查询。根据这点我们进行拆分,假设我们要针对User做延迟加载，我们先不管accounts的数据，只查询user表，可以使用sql语句select * from user, 在需要的时候执行select * from account where uid = id在xml配置中可以在collection标签中使用select属性，该属性指向一个方法，该方法的功能是根据id获取所有对象的列表。也就说我们需要在AccountDao接口中提供这么一个方法,并且编写它的xml配置public List<Account> findByUid(int uid);接着我们对之前的xml进行改写<resultMap id="UserMapper" type="User"> <id column="id" property="id"></id> <result column="username" property="username"></result> <result column="sex" property="sex"></result> <result column="birthday" property="birthday"></result> <result column="address" property="address"></result> <collection property="accounts" ofType="Account" select="com.liuhao.Dao.IAccountDao.findByUid" column="id"> </collection> </resultMap> select * from user; 完成了接口的编写与配置，还需要对主配置文件做一些配置，我们在主配置文件中添加settings节点，开启延迟加载<settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>缓存缓存用来存储一些不经常变化的内容，使用缓存可以减少查询数据库的次数，提高效率。mybatis有两种缓存，一种是在每个sqlsession中的缓存，一种是在每个SqlSessionFactory中的缓存在SqlSession中的缓存又被叫做是Mybatis的一级缓存。每当完成一次查询操作时，会在SqlSession中形成一个map结构，用来保存调用了哪个方法，以及方法返回的结果，下一次调用同样的方法时会优先从缓存中取当我们执行insert、update、delete等sql操作，或者执行SqlSession的close或者clearCache等方法时缓存会被清理在SqlSessionFactory中的缓存被称做二级缓存，所有由同一个SqlSessionFactory创建出来的SqlSessin共享同一个二级缓存。二级缓存是一个结果的二进制值，每当我们使用它时，它会取出这个二进制值，并将这个值封装为一个新的对象。在我们多次使用同一片二级缓存中的数据，得到的对象也不是同一个使用二级缓存需要进行一些额外的配置：在主配置文件中添加配置 在settings的子标签setting 中添加属性 enableCache=True开启二级缓存在对应的dao xml配置中添加 cache标签（标签中不需要任何属性或者文本内容）,使接口支持缓存在对应的select、update等标签上添加属性 useCache=true，为方法开启二级缓存
JSP标准标签库（JSTL）是一个JSP标签集合，它封装了JSP应用的通用核心功能。JSTL支持通用的、结构化的任务，比如迭代，条件判断，XML文档操作，国际化标签，SQL标签。 除了这些，它还提供了一个框架来使用集成JSTL的自定义标签。JSTL安装要使用jstl需要导入对应的库，可以去官方站点下载, 点击这里下载然后解压文件将得到的jar包放入到WEB-INF的lib中导入之后，在要使用它的jsp文件中使用taglib 导入库<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>prefix 是标签的前缀，类似于命名空间，在使用库中的标签时需要加上这个前缀常用标签if 标签if标签用来做判断，当条件成立时，执行标签体的内容，条件写在test属性中，注意：只有if标签而没有对应的else标签。下面是一个例子:<c:if test="${not empty requestScope.error}"> ${requestScope.error} </c:if>上述这个例子表示，当服务器返回错误信息时，将错误信息显示到页面上choose 标签choose 标签相当于switch 语句，该标签中可以包含 when 和 otherwise 作为字标签，相当于switch语句中的case和default，例如下面的例子 当前薪水为 : <c:out value="${salary}"/> <c:choose> <c:when test="${salary <= 2000}"> 老板我是你爹, 这个工作谁爱干谁干 </c:when> <c:when test="${salary > 50000}"> 公司是我家，工作就是我的价值，我热爱工作 </c:when> <c:otherwise> 心中无半点波澜，甚至想提前下班 </c:otherwise> </c:choose>foreach 标签foreach 用来迭代容器中的元素，或者完成一些重复的操作。当使用foreach标签来进行重复性的操作时可以使用begin、end、var来控制循环，begin表示循环变量开始的值，end表示循环变量结束的值，与正常的for循环不同，循环变量的值可以等于end的值；使用var标签来定义循环变量的名称，使用step表示步进。例如:<c:foreach begin = "1" end = "10" var = "i step = "1"> ${i} </c:foreach>等价于for(int i = 1; i <= 10; i++){ System.out.println(i); }当使用 foreach来迭代容器时使用item和 var来迭代，其中item为需要迭代的容器，var表示获取到的容器中的元素。例如<c:foreach items = "list" var = "l"> ${l} </c:foreach>等价于for(String l:list){ System.out.println(l); }ELEL 表达式：Expression Language 表达式语言，用于替换和简化jsp页面中java代码的编写。EL 表达式使用 ${} 来表示jsp 默认支持el表达式，在page指令中可以使用 isELIgnored 来指定是否忽略jsp页面中的el表达式；当然也可以使用 \ 来作为转义符，表示 这个el表达式原样输出，例如 \${cookie}EL表达式中可以支持算数运算符、比较运算符、逻辑运算符合empty 空运算符；empty用于判断字符串、集合、数组对象是否为null或者长度为0。在使用el表达式时需要注意以下几点：el表达式只能从域对象中获取值el表达式中如果是类对象，可以根据Java Bean规范来获取属性值针对list这种有序集合可以使用 ${域对象.键名[索引].属性}针对Map集合,使用 ${域对象.键名.key名}或者 ${域对象.键名["key名"]}el 表达式中对域对象都做了重命名，pageScope 对应于 pageContext、requestScope对应于request、sessionScope对应于session、applicationScope对应于applicate(ServletContext)表达式${键名} 依次从最小的域中去查找对应的键值，直到找到为止
之前聊过用java处理web请求，处理cookie和session等等，但是唯独没有提及如何返回信息。作为一个web程序，肯定需要使用HTML作为用户界面，这个界面需要由服务端返回。返回信息可以使用HttpResponse中的OutputStream对象来写入数据。但是既要在里面写入HTML，又要写入相应的值，造成程序很难编写，同时HTML代码长了也不容易维护。我们需要一种机制来专门处理返回给浏览器的信息。JSP就是用来专门处理这种需求的。JSP概述JSP (Java Server Page):Java 服务端页面。是由 Sun Microsystems 公司倡导和许多公司参与共同创建的一种使软件开发者可以响应客户端请求，而动态生成 HTML、XML 或其他格式文档的Web网页的技术标准。jsp可以很方便的在页面中通过java代码嵌入动态页面JSP原理分析下面是一个简单的hello world程序<%@ page contentType="text/html;charset=UTF-8" language="java" %> index <% out.println("hello world"); %> 我们将其部署到tomcat服务器上，启动并访问它之后会在tomcat所在目录的 work\Catalina\localhost\JSPDemo\org\apache\jsp (其中JSPDemo是项目名称), 在这个目录下面可以看到生成了一个index_jsp.java、index_jsp.class下面是这个jsp生成的部分源码package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { static { _jspx_imports_packages = new java.util.HashSet<>(); _jspx_imports_packages.add("javax.servlet"); _jspx_imports_packages.add("javax.servlet.http"); _jspx_imports_packages.add("javax.servlet.jsp"); _jspx_imports_classes = null; } public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); out.write("\n"); out.write("\n"); out.write(" \n"); out.write(" \n"); out.write(" index \n"); out.write(" \n"); out.write(" \n"); out.write(" "); //这里是java代码的开始 out.println("hello world"); //这里是java代码的结尾 out.write("\n"); out.write(" \n"); out.write(" \n"); } catch (java.lang.Throwable t) { //todo someting } finally { //todo someting } } }我们查询一下HttpJspBase这个类可以得到如下继承关系java.lang.Object | +--javax.servlet.GenericServlet | +--javax.servlet.http.HttpServlet | +--org.apache.jasper.runtime.HttpJspBase也就是说jsp本质上还是一个Servlet类，当我们第一次访问这个jsp页面时，服务器会根据jsp代码生成一个Servlet类的.java源码文件然后编译。对比jsp代码可以看得出来，在翻译的时候它逐行翻译，将html代码采用out.write进行输出，对应的java代码则原封不动的放在对应的位置。既然它是一个servlet，那么他的生命周期与相关注意事项就与Servlet相同了。jsp语法jsp确实简化了用户界面的编写，但是如果只知道原理，而不知道如何使用它仍然是白瞎，这部分来简单聊聊如何使用它jsp的代码主要放在3种标签中<% code %>: 这种格式中的代码，主要放的是要执行的java代码，它们最后会被解析到类的service方法中<%! code %>: 这种格式中的代码，主要包含的是成员变量的定义，它们最后会被解析到类的成员变量定义中<%= code %>: 这种格式中的代码，最终会被输出到页面上，会被解析到 out.print中进行输出下面我们对index.jsp进行改造，做一个简单的统计页面访问量的功能:<%@ page contentType="text/html;charset=UTF-8" language="java" %> index <%! private int totalVisited = 0; %> <% totalVisited++; %> <%= "客户请求次数:" + totalVisited %> 然后再看看生成的java代码public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent, org.apache.jasper.runtime.JspSourceImports { private int totalVisited = 0; public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final java.lang.String _jspx_method = request.getMethod(); final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html;charset=UTF-8"); out.write("\n"); out.write("\n"); out.write(" \n"); out.write(" \n"); out.write(" index \n"); out.write(" \n"); out.write(" \n"); out.write(" "); out.write("\n"); out.write("\n"); out.write(" "); totalVisited++; out.write("\n"); out.write("\n"); out.write(" "); out.print( "客户请求次数:" + totalVisited ); out.write("\n"); out.write(" \n"); out.write(" \n"); } }jsp内置对象我们在写jsp页面时关注的其实是Servlet的service 方法，谈及jsp内置对象的时候主要关注的是service中定义的相关变量，从生成的代码上来看，我们可以使用的是service方法中的输入参数request和response 再加它事先定义好的9个局部变量。它们的含义如下:HttpServletRequest request: 请求对象，之前在HttpServlet中已经了解了它该如何使用javax.servlet.jsp.PageContext pageContext: 页面的上下文，提供对JSP页面所有对象以及命名空间的访问。可以用它拿到request、cookie、session等一系列页面中可以访问到的对象HttpSession session: 当前会话ServletContext application: servlet上下文，我们可以拿到servlet的相关对象。比如获取当前servlet对象的名称，然后拼接一个路径。这样就不用考虑如何部署的问题JspWriter out: 输出对象HttpServletResponse response: HTTP响应对象Object page: 从定义和初始化值来看，它代表的是当前Servlet对象ServletConfig config: ServletConfig类的实例,获取当前servlet的配置信息Except: 当前异常，只有当jsp页面是错误页面是才能使用这个对象。其他的东西基本上用不上，这里也就不再介绍了。指令通过上面的相关知识点，现在已经能写相关的jsp代码了，但是既然本质上是servlet类，那么java其他的操作，比如导入相关库文件怎么办呢？这就需要用到对应的jsp指令。jsp指令放在 <%@ code %>中，jsp指令主要有3大类：page: 定义网页依赖属性，比如脚本语言、error页面、缓存需求等等include: 包含其他文件,可以利用这个属性事先抽取出页面的公共部分（比如页面的头部导航栏和页脚部分），最后再用include做拼接。taglib: 引入标签库的定义, 这个在使用jstl 和es表达式等第三方jsp扩展库的时候使用每条指令可以有多个属性，page 指令的相关属性如下:属性含义contentType等同于 response.setContentType方法，用于设置响应头的Content-Type属性pageEncoding设置jsp页面自身的编码方式language定义jsp脚本所使用的语言，目前只支持java 语言import导入java包errorPage当前页面发生异常后会自动跳转到指定错误页面isErrorPage标识当前页面是否是错误页面，错误页面中可以使用exception 对象，用来捕获异常include 指令的相关属性如下:属性含义file包含的文件路径taglib 的属性如下:属性含义prefix前缀，它们是自定义的，将来要用lib中的标签时用它作为前缀uri第三方库所在路径
在网络的七层模型中，会话层位于传输层之上，它定义如何开始、控制和结束一个会话。七层模式目前仅仅处于理论阶段，但是Web中借鉴了其中的一些思路。在Web中浏览器第一次发送请求到服务器开始直到一方断开为止算作一个会话。HTTP协议本身没有状态，那么Web服务如何知道这次请求是否在一个会话中呢？Web提供了Cookie和Session两种技术。服务器在第一次收到请求之后，会在HTTP响应头的Set-Cookie中，设置Cookie值，浏览器收到响应后，保存这个Cookie在本地。后续再进行请求的时候在HTTP的请求头中设置Cookie值，服务器根据此Cookie来识别请求的状态。Cookie值本身是一个键值对，例如 Cookie: name=value;Servlet 使用Cookie在Servlet中，使用Cookie的步骤如下:创建Cookie对象 new Cookie(String name, String value)发送cookie到浏览器 response.addCookie(Cookie)获取浏览器中发送过来的cookie request.getCookies() 返回所有Cookie遍历Cookies 获取所有cookie对象调用Cookie.getName(), Cookie.getValue()获取Cookie中的键和值使用的注意事项如下:一次可以返回多个Cookie，多次调用response.addCookie即可默认情况下浏览器关闭页面后cookie失效，但是可以设置cookie失效时间Cookie虽然可以用来识别一次会话，但是也不能滥用，第一Cookie是存储在浏览器端的，可以被伪造，一般做过爬虫自动登录的都这样干过，第二浏览器对于单个cookie大小有限制，一般是4kb。同时浏览器对于单个域名的cookie也有限制，默认是20个。由于cookie本身是类似于小饼干的小料，一般来说不会把小料作为主菜。SessionCookie一般作为小料，作为会话标识来说，用Session更为常见。与 Cookie相比Session存储在服务器端，Session没有cookie的那些限制。实现原理Session的实现是基于Cookie的。第一次调用request.getSession获取Session，没有Cookie 会在内存中创建一个新的Cookie对象，名称为JSESSION值是一个唯一的ID，作为session的唯一标识在给客户端响应时会包含一个cookie值，Set-Cookie: JSESSION=ID浏览器在下一次访问web中的其他资源时会将cookie作为请求头发送到服务器。服务器会从cookie中取出ID值，并根据ID从内存中查找对应的Session对象使用 HttpSession session = request.getSession(); 来获取一个Session对象函数列表Session 对象常用函数如下:public Object getAttribute(String name); //该方法返回在该 session 会话中具有指定名称的对象，如果没有指定名称的对象，则返回 null。 public Enumeration getAttributeNames(); //该方法返回 String 对象的枚举，String 对象包含所有绑定到该 session 会话的对象的名称。 public long getCreationTime(); //该方法返回该 session 会话被创建的时间，自格林尼治标准时间 1970 年 1 月 1 日午夜算起，以毫秒为单位。 public String getId(); //该方法返回一个包含分配给该 session 会话的唯一标识符的字符串。 public long getLastAccessedTime(); //该方法返回客户端最后一次发送与该 session 会话相关的请求的时间自格林尼治标准时间 1970 年 1 月 1 日午夜算起，以毫秒为单位。 public int getMaxInactiveInterval(); //该方法返回 Servlet 容器在客户端访问时保持 session 会话打开的最大时间间隔，以秒为单位。 public void invalidate(); //该方法指示该 session 会话无效，并解除绑定到它上面的任何对象。 public boolean isNew(); //如果客户端还不知道该 session 会话，或者如果客户选择不参入该 session 会话，则该方法返回 true。 public void removeAttribute(String name); //该方法将从该 session 会话移除指定名称的对象。 public void setAttribute(String name, Object value); //该方法使用指定的名称绑定一个对象到该 session 会话。 public void setMaxInactiveInterval(int interval); //该方法在 Servlet 容器指示该 session 会话无效之前，指定客户端请求之间的时间，以秒为单位。
Servlet 是一套标准的接口规范，当用户通过web请求来访问服务器时，由web容器根据配置调用我们实现的对应的servlet对象来提供服务。同时为了方便开发，servlet标准中也提供了许多常用的工具类，比如基本的Request 和Response对象以及其他要说到的常用的类。ServletRequest 对象Servlet接口中的service方法的定义如下:public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;从接口上来看，我们只能在service方法中使用这个对象。当用户请求到达服务器时，服务器会将标准的HTTP请求包封装为一个 ServletRequest对象，并调用service将该对象作为参数传入。也就是说这个对象是与HTTP协议息息相关的下面是它的一些常用方法//请求行相关方法 public String getMethod(); //返回请求的方法 public String getContextPath(); //获取请求的虚拟目录（或者项目所在目录） public String getServletPath(); //获取servlet路径 public String getQueryString();// 获取查询字符串,也就是提交请求url中参数部分 public String getRequestURI(); //获取请求的URI public StringBuffer getRequestURL(); //获取请求的URL public String getProtocol(); //获取协议版本 public String getRemoteAddr(); //获取客户端的ip //请求头相关方法 public String getHeader(String name); //以字符串的形式返回请求头 public java.util.Enumeration<E> getHeaderNames(); //返回所有请求头名称的枚举 public java.util.Enumeration<E> getHeaders(String name); //返回指定请求头中的所有值的枚举 //请求体相关方法 public java.io.BufferedReader getReader() throws java.io.IOException; //获取请求体的字符流 public ServletInputStream getInputStream() throws java.io.IOException; //获取请求体中的字节流一般来说get方法会将请求参数放入到url中，而post则是将其放入到请求体中，根据上面的方法要获得请求参数，需要判断请求是get还是post，如果是get则采用 getQueryString, 如果是post则采用 getReader等方法。为了解决这个问题，Servlet提供了一些通用的方法。public String getParameter(String name); //获取请求参数,根据传入的键名获得请求的键值 public java.util.Map<K, V> getParameterMap(); //获取所有请求参数，以map方式返回 public String[] getParameterValues(String name); //如果请求中一个键对应多个值，可以使用这个方法获取所有的值请求中文乱码的问题一般HTML前端都使用utf-8编码，如果后台使用系统默认的编码则会可能造成乱码。我们可以先指定编码方式为utf-8，代码如下:request.setCharacterEncoding("UTF-8");请求转发为了保证程序模块的合理性，每个servlet 提供指定的功能，而有时候用户需要多个servlet来联合提供服务。这个时候需要使用请求转发。通过 request.getRequestDispatcher 方法来转发到其他servlet。它的特点如下:浏览器地址栏不变只能访问当前服务器的内部资源，也就是同一套web程序中的其他servlet转发是一次HTTP请求域对象每个servlet都有一个service。不同的servlet通过不同的serivce来提供服务。由于是不同的类对象和方法，不同的servlet之间不能进行信息的传递。但是有的时候需要不同的servlet之间进行数据的共享。这个时候可以使用域对象域对象是一个有作用范围的对象，域对象中数据随着对象的消亡而消亡。servlet中有下列几种作用域:一次请求, resquest一次会话, cookie, session使用域对象的 setAttribute 来保存数据, 使用getAttribute 来获取数据。使用removeAtribute 来移除数据response 对象对HTTP 响应的封装,常用的方法如下:public void setStatus(int sc);//设置响应的状态码 public void setHeader(String name, String value); //设置响应头 public java.io.PrintWriter getWriter() throws java.io.IOException; //获取输出的字符流流 public ServletOutputStream getOutputStream() throws java.io.IOException; //获取输出的字节流重定向重定向的原理是利用HTTP协议中的301和302 消息。在HTTP响应头中指定状态码为302，并指定Location字段，浏览器会根据响应再重新发送一次请求。servlet中，提供了专门的方法来实现重定向。public void sendRedirect(String location) throws java.io.IOException;重定向的特点如下：重定向时，浏览器地址栏发生了变化重定向可以访问互联网任意节点的资源重定向是多次请求
通过前面一系列的博客的梳理，学习了一下Java基础的编程知识，从我自己的感觉上来说，Java与c++的差距并不是很大，Java将c++做了更进一步的抽象，同时丢弃了c++中一些容易出错和难懂的部分。Java的基础语法比起c++来说要简单很多。但是Java与c++一样，不能光学基础语法，还得从应用角度来学习它，使用它来做一些真真的项目。从现在开始博客内容从Java的基础过渡到了Java2E的学习Servlet 简介Servlet 英文全称是Server Applet，也叫做Java Servlet；是用Java编写的服务器端程序。其主要功能在于交互式地浏览和修改数据，生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口，广义的Servlet是指任何实现了这个Servlet接口的类，一般情况下，人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从实现上讲，Servlet可以响应任何类型的请求，但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。———— 来自维基百科Servlet 容器从上面的定义来看 Servlet 就是一套Java的接口标准，在编写Java Web程序的时候需要遵循这套标准的接口，实现自己的Servlet 接口，并且由服务器端程序来调用，并向其他用户提供服务。目前市面上常见的支持Servlet 标准的Web容器有：Tomcat: 由Apache 基金会的一个项目，由 Apache、Sun和其他公司及个人合作开发而成。Tomcat 服务器是一个免费的开放源代码的Web 应用服务器，属于轻量级应用服务器，在中小型系统和并发访问用户不是很多的场合下被普遍使用，是开发和调试JSP 程序的首选。WebLogic: WebLogic是美国Oracle公司出品的一个application server;WebLogic是用于开发、集成、部署和管理大型分布式Web应用、网络应用和数据库应用的Java应用服务器,量级比Tomcat 要大上许多，同时也能承载更多用户的使用JBoss:是一个基于J2EE的开放源代码的应用服务器。 JBoss代码遵循LGPL许可，可以在任何商业应用中免费使用;但JBoss核心服务不包括支持servlet/JSP的WEB容器，一般与Tomcat或Jetty绑定使用。WebSphere: 是由IBM遵照开放标准，例如Java EE、XML及Web Services，开发并发行的一种应用服务器Servlet架构一般一个web项目中，主要分为WEB-INFO目录和一些jsp/html页面，其中WEB-INFO中主要包含 classes 目录(java 字节码文件)、web.xml(项目配置文件)、lib目录(项目依赖文件)在web.xml 中会指定哪些url由哪些Servlet 接口的实现类来处理，当url到达web服务器后，由服务器处理并调用对应的Servlet 类；Web容器要实现这个功能，必然会用到反射机制当用户在浏览器中输入对应的url并点击回车后:浏览器会向对应的地址发送 HTTP请求包对应的服务器收到这个请求包，并获取到请求的路径根据请求路径找到对应的项目在项目中查找对应的web.xml 文件，找到这个路径对应的Servlet 类调用Servlet 类并拿到返回值将返回封装到Http 的响应中，响应到浏览器上Servlet使用Servlet普通Servlet 接口定义如下:public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); }要回答这些接口函数都是干什么的，首先需要知道 一个Servlet对象的生命周期。当项目所在的Web容器启动之后，容器中的所有项目也随之启动，这个时候项目中所有的Servlet 被创建，当容器正常关闭之前，会停止所有项目并析构回收所有Servlet 资源。init方法：每当Servlet 创建的时候调用一次，并且仅调用这一次。，所有我们可以将一些在项目启动之时需要提前做的操作放到这个方法中service方法：用于提供服务，每当用户通过浏览器或者其他方式访问该servlet时，服务器会产生一个新的线程并调用该方法一次，该方法用户为用户提供服务并返回处理结果。destroy方法：当servlet正常被Web容器关闭的时候会首先调用该方法，用于清理某些资源并做最后的收尾工作。实例为了方便进行Web 开发，JavaEE中提供了一个HttpServlet 类，它封装了基本的Servlet 接口，并提供了一些列的doXXX方法来处理HTTP 协议中几种不同的请求方法，例如doGet和doPost等等，下面的例子就使用这个类来进行编写import java.io.*; import javax.servlet.*; import javax.servlet.http.*; // 扩展 HttpServlet 类 public class HelloWorldServlet extends HttpServlet { private String message; public void init() throws ServletException { // 执行必需的初始化 System.out.println("init........"); message = "Hello World"; } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应内容类型 response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println(" " + message + " "); } public void destroy() { // 什么也不做 } }代码写完后，可以配置对应的web.xml 文件，其中重要的部分如下:<web-app> <servlet> <servlet-name>HelloWorld</servlet-name> <servlet-class>HelloWorldServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/HelloWorld</url-pattern> </servlet-mapping> </web-app> 最终访问 http://localhost:8080/HelloWorld 时会调用HelloWorldServlet 类，并返回helloworld字符串到浏览器中
Java 中网络编程接口在java.net 包中在使用C/C++进行网络编程时，针对TCP Server端需要这些操作创建SOCKET绑定监听接受连接收取数据包发送数据包TCP Client端需要这些操作创建SOCKET连接Server端发送数据包读取响应包Java中针对Server 端和Client端分别提供了两个类 ServerSocket 和 SocketServerScoket 使用步骤如下:创建ServerSocket 对象并传入一个端口号到构造函数中。在构造的时候会自动创建Socket对象并执行绑定端口、监听端口的操作调用对象的 accept 方法等待连接调用对象的 getInputStream 和 getOutputStream 获取输入输出流，并通过输入输出流来进行收发数据在不用时调用 close 方法关闭套接字Socket 类使用步骤如下:创建 Socket 对象调用 connet 方法连接到指定服务器端口(或者在构造时传入服务器和端口进行连接)调用对象的 getInputStream 和 getOutputStream 获取输入输出流，并通过输入输出流来进行收发数据在不用时调用 close 方法关闭套接字一个普通的TCP通信的实例如下:import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; public class TCPServer{ public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(6666); Socket socket = server.accept() ; InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); byte[] bytes = new byte[1024]; int len = is.read(bytes); os.write(("echo:" + new String(bytes, 0, len)).getBytes()); server.close(); socket.close(); } }import java.net.Socket; import java.io.IOException; import java.io.OutputStream; import java.io.InputStream; public class TCPClient{ public static void main(String[] args)throws IOException{ Socket socket = new Socket("127.0.0.1", 6666); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream(); os.write("hello world!".getBytes()); byte[] bytes = new byte[1024]; int len = is.read(bytes); System.out.println(new String(bytes, 0, len)); socket.close(); } }下面是一个使用TCP协议进行文件传输的例子:import java.net.Socket; import java.io.IOException; import java.io.FileOutputStream; import java.io.InputStream; import java.net.ServerSocket; public class FileUploadServer{ public static void main(String[] args) throws IOException{ ServerSocket server = new ServerSocket(6666); FileOutputStream fos = new FileOutputStream("1.avi"); Socket socket = server.accept(); InputStream is = socket.getInputStream(); byte[] buff = new byte[1024]; int len = 0; while((len = is.read(buff)) != -1){ fos.write(buff, 0, len); } System.out.println("upload success!"); fos.close(); server.close(); socket.close(); } }import java.net.Socket; import java.io.IOException; import java.io.FileInputStream; import java.io.OutputStream; public class FileUploadClient{ public static void main(String[] args) throws IOException{ Socket socket = new Socket("127.0.0.1", 6666); FileInputStream fis = new FileInputStream("1.avi"); OutputStream os = socket.getOutputStream(); int len = 0; byte[] buff = new byte[1024]; while((len = fis.read(buff)) != -1){ os.write(buff, 0, len); } fis.close(); socket.close(); } }最后提供一个简易的http server 的例子import java.net.Socket; import java.net.ServerSocket; import java.io.InputStreamReader; import java.io.InputStream; import java.io.FileInputStream; import java.io.OutputStream; import java.io.BufferedReader; import java.io.IOException; public class HttpServer{ public static void main(String[] args)throws IOException{ ServerSocket server = new ServerSocket(8080); while(true){ Socket socket = server.accept(); new Thread(()->{ try{ BufferedReader bf = new BufferedReader(new InputStreamReader(socket.getInputStream())); OutputStream os = socket.getOutputStream(); String head = bf.readLine(); String path = head.split(" ")[1].substring(1); System.out.println(path); FileInputStream fis = new FileInputStream(path); int len = 0; byte[] buff = new byte[1024]; os.write("HTTP/1.1 200 OK\r\n".getBytes()); os.write("Content-Type:text/html\r\n".getBytes()); os.write("\r\n".getBytes()); while((len = fis.read(buff)) != -1){ os.write(buff, 0, len); } bf.close(); fis.close(); socket.close(); }catch(IOException e){ e.printStackTrace(); } }).start(); } //server.close(); } }
注解Java注解是在JDK1.5 之后出现的新特性，用来说明程序的，注解的主要作用体现在以下几个方面:编译检查，例如 @Override编写文档，java doc 会根据注解生成对应的文档代码分析，通过注解对代码进行分析[利用反射机制]JDK 中有一些常用的内置注解，例如:Override：检查被该注解修饰的方法是否是重写父类的方法Deprecatedd：被该注解标注的内容已过时SuppressWarnning: 压制警告，传入参数all表示压制所有警告自定义注解JDK中虽然内置了大量注解，但是它也允许我们自定义注解，这样就为程序编写带来了很大的便利，像有些框架就大量使用注解。java注解本质上是一个继承了 java.lang.annotation.Annotation 接口的一个接口，但是如果只是简单的使用关键字 interface来定义接口，仍然不是注解，仅仅是一个普通的接口，在定义注解时需要使用关键字 @interface， 该关键字会默认继承 Annotation 接口，并将定义的接口作为注解使用注解中可以定义方法，这些方法的返回值只能是基本类型、String、枚举类型、注解以及这些类型的数组，我们称这些方法叫做属性。在使用注解时需要注意以下几个事情必须给注解的属性赋值，如果不想赋值可以使用default来设置默认值如果属性列表中只有一个名为value的属性，那么在赋值时可以不用指定属性名称多个属性值之间使用逗号隔开数组属性的赋值使用 {}, 而当数组属性中只有一个值时, {} 可以省略不写元注解元注解是用来描述注解的注解，Java中提供的元注解有下列几个Target描述注解能够作用的位置，即哪些Java代码元素能够使用该注解,注解的源代码如下:@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); }这个注解只有一个value属性，属性需要传入一个 ElementType枚举类型的数组，该枚举类型可以取下列几个值ElementType含义TYPE接口、类(包括注解)、枚举类型上使用FIELD字段声明（包括枚举常量）METHOD方法PARAMETER参数声明CONSTRUCTOR构造函数LOCAL_VARIABLE局部变量声明ANNOTATION_TYPE注解类型声明PACKAGE包声明Retention表示该注解类型的注解保留的时长,主要有3个阶段: 源码阶段，类对象阶段，运行阶段;源码阶段是只只存在与源代码中，类对象阶段是指被编译进 .class 文件中，类对象阶段是指执行时被加载到内存.则默认保留策略为RetentionPolicy.CLASS。它的源码如下:@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { RetentionPolicy value(); }Documented表示拥有该注解的元素可通过javadoc此类的工具进行文档化。源码如下:@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Documented { }Inherited表示该注解类型被自动继承@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { }内置注解解读下面通过几个JDK内置注解的解读来说明注解相关使用Override@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }该注解用于编译时检查，被该注解注释的方法是否是重写父类的方法。从源码上看，它只能在方法上使用，并且它仅仅存在于源码阶段不会被编译进 .class 文件中Deprecatedd@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }用于告知编译器，某一程序元素(例如类、方法、属性等等)不建议使用从源码上看，几乎所有的Java程序元素都可以使用它，而且会被加载到内存中SuppressWarnning@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }告知编译器忽略特定类型的警告它需要传入一个字符串的数组，取值如下:参数含义deprecation使用了过时的类或方法时的警告unchecked执行了未检查的转换时的警告fallthrough当Switch程序块进入进入下一个case而没有Break时的警告path在类路径、源文件路径等有不存在路径时的警告serial当可序列化的类缺少serialVersionUID定义时的警告finally任意finally子句不能正常完成时的警告all以上所有情况的警告在程序中解析注解一般通过反射技术来解析自定义注解，要通过反射技术来识别注解，前提条件就是注解要在内存中被加载也就是要使它的范围为 RUNTIME;JDK提供了以下常用API方便我们使用返回值方法解释TgetAnnotation(Class annotationClass)当存在该元素的指定类型注解，则返回相应注释，否则返回nullAnnotation[]getAnnotations()返回此元素上存在的所有注解Annotation[]getDeclaredAnnotations()返回直接存在于此元素上的所有注解。booleanisAnnotationPresent(Class<? extends Annotation> annotationClass)当存在该元素的指定类型注解，则返回true，否则返回false实战下面使用一个完整的例子来说明自定义注解以及在程序中使用注解的例子,现在来模仿JUnit 定义一个MyTest的注解，只要被这个注解修饰的方法将来都会被自动执行import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.annotation.ElementType; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface MyTest { }首先定义一个注解，后续来执行用这个注解修饰了的所有方法,通过Target来修饰标明注解只能用于方法上，通过Retention修饰标明注解会被保留到运行期import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Test { @MyTest public void test1(){ System.out.println("this is test1"); } @MyTest public void test2(){ System.out.println("this is test2"); } public static void main(String[] args) { Method[] methods = Test.class.getMethods(); for (Method method:methods){ if (method.isAnnotationPresent(MyTest.class)){ try { method.invoke(new Test()); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } } } } 在测试类中定义了两个测试函数都使用 @MyTest 修饰，在主方法中，首先通过反射机制获取该类中所有方法，然后调用方法的 isAnnotationPresent 函数判断该方法是否被 @Test修饰，如果是则执行该方法。这样以后即使再添加方法，只要被 @MyTest 修饰就会被调用。Junit框架在软件开发中为了保证软件质量单元测试是必不可少的一个环节，Java中提供了Junit 测试框架来进行单元测试一般一个Java项目每一个类都会对应一个test类用来做单元测试，例如有一个Person类，为了测试Person类会定义一个PersonTest类来测试所有代码JUnit 中定义了一些注解来方便我们编写单元测试@Test：测试方法，被该注解修饰的方法就是一个测试方法@Before：在测试方法被执行前会执行该注解修饰的方法@After：在测试方法被执行后会执行该注解修饰的方法除了注解JUnit定义了一些断言函数来实现自动化测试，常用的有如下几个:void assertEquals(boolean expected, boolean actual):检查两个变量或者等式是否平衡void assertTrue(boolean expected, boolean actual):检查条件为真void assertFalse(boolean condition):检查条件为假void assertNotNull(Object object):检查对象不为空void assertNull(Object object):检查对象为空void assertSame(boolean condition):assertSame() 方法检查两个相关对象是否指向同一个对象void assertNotSame(boolean condition):assertNotSame() 方法检查两个相关对象是否不指向同一个对象void assertArrayEquals(expectedArray, resultArray):assertArrayEquals() 方法检查两个数组是否相等这些函数在断言失败后会抛出异常，后续只要查看异常就可以哪些测试没有通过假设先定义一个计算器类，来进行两个数的算数运算public class Calc { public int add(int a, int b){ return a + b; } public int sub(int a, int b){ return a - b; } public int mul(int a, int b){ return a * b; } public float div(int a, int b){ return a / b; } }为了测试这些方法是否正确，我们来定义一个测试类import org.junit.Test; import static org.junit.Assert.assertEquals; public class CalcTest { @Test public void addTest(){ int result = new Calc().add(1,2); assertEquals(result, 3); } @Test public void subTest(){ int result = new Calc().sub(1,2); assertEquals(result, -1); } @Test public void mulTest(){ int result = new Calc().mul(1,2); assertEquals(result, 2); } @Test public void divTest(){ float result = new Calc().div(1,2); assertEquals(result, 0.5, 0.001); //会报异常 } }经过测试发现，最后一个divTest方法 会报异常，实际值是0，因为我们使用 / 来计算两个int时只会保留整数位，也就是得到的是0，与预期的0.5不匹配，因此会报异常
数据库操作是程序设计中十分重要的一个部分，Java内置JDBC来操作数据库JDBC使用JDBC——Java Database connecting Java数据库连接；本质上JDBC定义了操作数据库的一套接口，作为应用程序的开发人员来说只需要创建接口对应的对象即可，而接口的实现由各个数据库厂商去完成。要在应用程序中使用JDBC，需要根据数据库的不同导入对应的jar包。使用步骤如下：导入相应jar包注册驱动获取数据库连接对象定义sql语句获取执行sql语句的对象执行sql并获取结果集对象从结果集中获取数据释放资源相关对象的描述DriverManager在使用JDBC之前需要先注册驱动，也就是告诉JDBC，我们需要导入哪个jar包，这个工作由DriverManager对象来实现，可以调用它里面的方法 registerDriver 来实现，该方法的定义如下:static void registerDriver(Driver driver);这个方法需要传入一个driver 对象，driver对象是具体的数据库厂商来实现，后续相关操作其实是根据这个driver对象来调用相关代码，实现同一套接口操作不同数据库我们查阅相关实现类的代码如下:public class Driver extends NonRegisteringDriver implements java.sql.Driver { // // Register ourselves with the DriverManager // static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } /** * Construct a new driver and register it with DriverManager * * @throws SQLException * if a database error occurs. */ public Driver() throws SQLException { // Required for Class.forName().newInstance() } }在Driver对象中发现，它在静态代码块中执行了registerDriver方法，也就是说我们只要加载对应的类，类就会自动帮助我们进行注册的操作。所以在第一步注册驱动的代码中可以这样写:Class.forName("org.mariadb.jdbc.Driver"); //加载对应的Driver类到内存中Connection对象注册了驱动之后就是获取数据库的连接对象，在DriverManager中使用getConnection方法获取，它的定义如下:static Connection getConnection(String url); static Connection getConnection(String url, Properties info); static Connection getConnection(String url, String user, String password);上述3个方法中，常用的是第3个，参数分别为: 连接字串、用户名、密码连接字串的格式为: jdbc:数据库类型://数据库IP：端口/数据库名称,比如 jdbc:mariadb://localhost:3306/test获取连接字串的代码如下:Connection conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/study", "root", "root");执行sql语句获取连接对象之后，需要向数据库传递sql语句并执行它，执行sql语句需要使用对象 Statement, 常用的方法如下:boolean execute(String sql); ResultSet executeQuery(String sql); int executeUpdate(String sql);一般可以使用execute来执行相关操作，如果是查询语句，可以使用executeQuery来执行并获取返回的结果集，如果需要执行DELTE、UPDATE、INSERT等语句可以使用executeUpdate来更新数据库我们可以通过 Connection对象的createStatement方法获取一个Statement对象,代码如下:Statement statement = conn.createStatement(); String strSql = "INSERT INTO student VALUES(2, '2b', 28, 78.9, '2017-12-30', NULL)"; statement.execute(strSql); statement.close(); //最后别忘了关闭对象获取返回结果如果我们执行了像insert、delete、update等等语句，可能不需要关注具体的返回结果，但是如果使用的是select语句，则需要获取返回的结果获取select语句返回的结果可以使用 executeQuery 方法，该方法会返回一个结果集对象可以将结果集对象想象成一个二维的数组，保存了查询到的相关数据，每一行代表一条数据，行中的每一列是一个字段的数据。结果集中使用游标来遍历每一行数据。使用get相关函数来获取对应索引的数据。一行遍历完了使用next移动到下一行；其中get相关方法主要有:Blob getBlob(int columnIndex); Blob getBlob(String columnLabel); boolean getBoolean(int columnIndex); boolean getBoolean(String columnLabel); byte getByte(int columnIndex); byte getByte(String columnLabel); byte[] getBytes(int columnIndex); byte[] getBytes(String columnLabel); Date getDate(int columnIndex); Date getDate(int columnIndex, Calendar cal); Date getDate(String columnLabel); Date getDate(String columnLabel, Calendar cal); double getDouble(int columnIndex); double getDouble(String columnLabel); float getFloat(int columnIndex); float getFloat(String columnLabel); int getInt(int columnIndex); int getInt(String columnLabel); long getLong(int columnIndex); long getLong(String columnLabel);在获取了结果之后需要关闭对应对象清理资源，这部分只需要调用对应的cloase方法即可最终一个完整的demo 如下:public class JDBCDemo1 { public static void main(String[] args) { Connection conn = null; Statement statement = null; ResultSet resultSet = null; try { Class.forName("org.mariadb.jdbc.Driver"); conn = DriverManager.getConnection("jdbc:mariadb://localhost:3306/test", "root", "root"); String sql = "select * from student"; statement = conn.createStatement(); resultSet = statement.executeQuery(sql); while (resultSet.next()){ int id = resultSet.getInt(1); //注意：这里面的索引是从1开始的 String name = resultSet.getString(2); int age = resultSet.getInt(3); double score = resultSet.getDouble(4); Date birthday = resultSet.getDate(5); Timestamp insertTime = resultSet.getTimestamp(6); System.out.println(id + "\t" + name + "\t" + age + "\t" + score + "\t" + birthday + "\t" + insertTime); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); }finally { try{ if (resultSet != null){ resultSet.close(); } if (statement != null){ statement.close(); } if (conn != null){ conn.close(); } }catch (SQLException e){ e.printStackTrace(); } } } }参数化查询我们知道使用sql拼接的方式来执行sql语句容易造成sql注入漏洞，即使针对某些关键字进行过滤也很难消除这个漏洞，一旦存在sql注入，那么数据库中的数据很容易就会被黑客窃取。而使用参数化查询的方式可以从根本上消除这个漏洞。jdbc中参数化查询使用的对象是 PreparedStatement, 它与Statement对象不同在于，它会提前将sql语句进行编译，后续只会接收固定类型的参数；而Statement只会简单的去执行用户输入的sql语句。在进行参数化查询的时候需要先准备sql语句，但是在查询参数中需要使用 ? 做标记，表示这个位置是一个参数，后续在真正执行前再传入，比如说可以准备这样的sql语句 update student set score = 100 where name = ?。准备好sql语句之后，需要设置对应参数位置的值，我们可以使用 setXxx 方法来设置，setXxx 方法与之前介绍的get方法类似，根据不同的数据类型 Xxx 有不同的取值。设置完参数之后，与Statement 一样，调用对应的execute方法来执行即可.String sql = "update student set score = 100 where name = ?"; ps = conn.prepareStatement(sql); ps.setString(1, "2b"); ps.executeUpdate();数据库连接池在需要频繁操作数据库的应用中，使用数据库连接池技术可以对数据库操作进行一定程度的优化。原理请自行百度。如果要自己实现数据库连接池需要实现 javax.sql.DataSource 的getConnection方法。当然我学习Java只是为了学习一下Web相关的内容，并不想太过于深入，所以自然不会去管怎么实现的，只要调用第三方实现，然后使用就好了。常见的开源的第三方库有: Apache commons-dbcp、C3P0 、Apache Tomcat内置的连接池（apache dbcp）、druid(由阿里巴巴提供)。本着支持国产的心态，这次使用的主要是 druid。druid 连接池需要提供一个配置文件来保存数据库的相关内容driverClassName=org.mariadb.jdbc.Driver url=jdbc:mariadb://localhost:3306/study username=root password=masimaro_1992 # 初始化时连接池中保留连接数 initialSize=5 # 最大连接数 maxActive=10 # 最大时间，超过这个时间没有任何操作则会关闭连接 maxWait=3000在使用时主要需要如下步骤:加载配置文件调用 DruidDataSourceFactory.createDataSource 方法传入 配置，获取到 DataSource 对象调用DataSource.getConnection 方法获取Connection 对象执行后续操作相关代码如下:Connection conn = null; Statement statement = null; Properties properties = new Properties(); try { properties.load(JDBCDemo3.class.getResourceAsStream("druid.properties")); DataSource dataSource = DruidDataSourceFactory.createDataSource(properties); conn = dataSource.getConnection(); statement = conn.createStatement(); //do something } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); }
Java中的类文件最终会被编译为.class 文件，也就是Java字节码。这个字节码中会存储Java 类的相关信息。在JVM执行这些代码时首先根据 java 命令中指定的类名找到.class 文件然后使用类加载器将class字节码文件加载到内存，在加载时会调用Class类的classLoader方法，读取.class 文件中保存类的成员变量、方法、构造方法，并将这些内容在需要时创建对应的对象。这个就是java中的反射机制。反射机制提供了由字符串到具体类对象的映射，提高了程序的灵活性，在一些框架中大量使用映射，做到根据用户提供的xml配置文件来动态生成并创建类对象反射机制最关键的就是从字节码文件中加载类信息并封装为对应的结构。在Java中专门提供了一个 Class 类，用于存储从.class 文件中读取出来的类的信息。 该类的定义和常用方法如下:public final class Class<?> extends Object implements Serializable, GenericDeclaration, Type, AnnotatedElement{ String getName(); //获取类名 ClassLoader getClassLoader(); //返回类的加载器 static Class<T> forName(String className); //根据类名返回对应类的Class对象 Field getField(String name); //根据名称返回对应的Filed对象 Field[] getFields(); //返回所有的Filed 对象 Field getDeclaredField(String name) //;返回一个 Field对象。 Field[] getDeclaredFields();//返回的数组 Field对象 Constructor<T> getConstructor(Class<?>... parameterTypes); Constructor<?>[] getConstructors(); Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes); Constructor<?>[] getDeclaredConstructors(); Method getDeclaredMethod(String name, Class<?>... parameterTypes); Method[] getDeclaredMethods(); Method getMethod(String name, Class<?>... parameterTypes); Method[] getMethods(); }获取 Class 对象获取Class对象常见的有3种:可以通过 Class 类的静态方法 forName 传入类名获取可以通过具体对象的getClass 方法获取，这种方式的前提是我们拿到了目标对象，也就是需要内存中已经加载了对应的对象,相对来说第一种方法相对方便。通过类的静态class 成员来获取。下面是3中方式对应的代码 //1. 使用forName 来获取 try { Class<?> student = Class.forName("Student"); System.out.println(student.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } //2. 使用getClass 对象 Class<? extends Student> aClass = new Student().getClass(); System.out.println(aClass.getName()); //3. 使用class 静态变量 Class<Student> studentClass = Student.class; System.out.println(studentClass.getName());需要注意的是在每个进程中 一个类的 Class 只有一个，拿上面的代码来说,即使 我们获取了3次，在内存中只有一个对应的Class 对象。获取类成员变量通过一定的方法，我们已经获取到了对应的Class 成员，之前说过Class是对字节码中记录的类信息的封装，类的成员变量被封装到了Field对象中，我们可以使用上述4个与Field有关的方法来获取对应的成员变量。public class Student { public String name; private int age; public String sex; private float gress; } //main try { Class<?> student = Class.forName("Student"); Field name = student.getField("name"); System.out.println(name.getName()); Field[] fields = student.getFields(); for (Field field: fields) { System.out.println(field.getName()); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); }上述的代码执行后会发现只能得到两个变量名，这两个函数只能获取 public 修饰的变量。要获取所有的可以使用 getDeclaredField(s) 函数组获取类方法Class 对象将类方法的信息封装到了 Method 对象中。我们可以使用 Method 对应的获取方法，同样的对应的Declared 方法能获取所有的，其他的只能获取公共的。这次我们实现一个 给Java Bean对象赋值的通用类。Java Bean是指满足这样一些条件的标准Java类：类必须被public 修饰类必须提供对应的getter 与 setter方法类必须提供空参的构造方法成员变量必须用private 修饰为了方便代码的编写，针对Java bean对象的getter/setter 方法命名有一个规定，尽量使用 get + 成员变量名(第一个字母大写)的方式来命名。同时定义类的属性值是 getter/setter 方法名去掉get/set 并将剩余词第一个字母小写得到属性名。针对这些定义，我们来实现一个根据字典值来给Java Bean赋值的方法。//默认已经给上述的student类添加了对应的getter/setter 方法,并且为了方便将所有成员都改为String static void BeanPopulate(Object bean, Map properties) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Iterator iterator = properties.entrySet().iterator(); while(iterator.hasNext()){ Map.Entry entry = (Map.Entry)iterator.next(); String key = (String) entry.getKey(); //首字母转大写 char[] chars = key.toCharArray(); chars[0] = (char) (chars[0] + ('A' - 'a')); String name = "set" + new String(chars); Method method = bean.getClass().getMethod(name, String.class); //第二个参数是方法的参数列表 method.invoke(bean, entry.getValue()); ////第一个参数是对象，第二个是方法的参数列表 } } Student student = new Student(); HashMap<String, String> map = new HashMap<>(); map.put("name", "tom"); map.put("age", "23"); map.put("sex", "男"); map.put("gress", "89.9"); try { BeanPopulate(student, map); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }通过Class创建对象上述的方法还有 Constructor 函数组没有说，这个函数组用来获取类的构造方法，有了这个方法，我们就可以创建对象了，那么我们将上面的例子给改一改，实现动态创建类并根据传入的map来设置值static Object BeanPopulate(Class beanClass, Map properties) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Iterator iterator = properties.entrySet().iterator(); Constructor constructor = beanClass.getConstructor(null); //构造方法的参数列表 Object bean = constructor.newInstance(null); //根据构造方法创建一个对象 while(iterator.hasNext()){ Map.Entry entry = (Map.Entry)iterator.next(); String key = (String) entry.getKey(); char[] chars = key.toCharArray(); chars[0] = (char) (chars[0] + ('A' - 'a')); String name = "set" + new String(chars); Method method = bean.getClass().getMethod(name, String.class); method.invoke(bean, entry.getValue()); } return bean; }