pageInfo = new PageInfo<>(list);\n System.out.println(pageInfo.getTotal());\n System.out.println(pageInfo.getEndRow());\n System.out.println(pageInfo.getFirstPage());\n System.out.println(pageInfo.getLastPage());\n System.out.println(pageInfo.getList());\n System.out.println(pageInfo.getNavigatePages());\n System.out.println(pageInfo.getNextPage());\n System.out.println(pageInfo.getPageNum());\n System.out.println(pageInfo.getPages());\n System.out.println(pageInfo.getPageSize());\n System.out.println(pageInfo.getPrePage());\n System.out.println(pageInfo.getSize());\n System.out.println(pageInfo.getStartRow());\n }","slug":"20170616_Mybatis","published":1,"updated":"2021-03-10T13:50:15.236Z","layout":"post","photos":[],"link":"","_id":"ckm3invgn009224ujh4ok12d6","content":"什么是 MyBatis ?
\n官方文档: http://www.mybatis.org/mybatis-3/zh/index.html
\n
\n\nMyBatis 一个基于Java的持久层框架; 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将代理接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
\n
\n与hibernate不同的是: mybatis是通过xml映射文件实现代理接口来实现操作数据库的功能
\n基本的步骤:
1. 引入dependency
<dependency>\n <groupId>org.mybatis</groupId>\n <artifactId>mybatis</artifactId>\n <version>3.4.1</version>\n</dependency>\n
\n2. mybatis核心配置文件 #在resources目录下的 mybatis/conf.xml
<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">\n<configuration>\n <!--可以读取数据库配置文件 用EL表达式获取参数-->\n <!--<properties resource="classpath:mybatis/db.properties"/>-->\n\n <!--\n development : 开发模式\n work : 工作模式\n -->\n <environments default="development">\n <environment id="development">\n <transactionManager type="JDBC"/>\n <dataSource type="POOLED">\n <property name="driver" value="com.mysql.jdbc.Driver"/>\n <property name="url"\n value="jdbc:mysql://localhost:3306/test?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8"/>\n <property name="username" value="root"/>\n <property name="password" value="fangshuoit"/>\n </dataSource>\n </environment>\n </environments>\n\n <!--映射文件所在位置 不能使用通配符(和spring整合时候可以使用)-->\n <mappers>\n <mapper resource="com/mybatis/test1/pojo/MyUserMapper.xml"/>\n </mappers>\n</configuration>\n
如果需要(或者测试没日志)可以引入log4j的包和配置文件 方便测试
3. 新建实体类–MyUser.java 对应数据库中表my_user
//三个属性\nprivate int id;\nprivate String name;\nprivate int age;\n
\n4. 新建Mapper映射文件–MyUserMapper.xml(要和实体放到一个目录下)
<?xml version="1.0" encoding="UTF-8" ?>\n<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">\n<mapper namespace="com.mybatis.test1.pojo.MyUserMapper">\n <!--\n 根据id查询得到一个user对象 其中sql语句中的表名,查询参数名,where语句中 键都是数据库中表的写法, 要传入的参数占位符是书体属性名\n -->\n <select id="getUser" parameterType="int" resultType="com.mybatis.test1.pojo.MyUser">\n SELECT *\n FROM my_user\n WHERE id = #{id}\n </select>\n</mapper>\n
maven项目运行时找不到映射文件:Could not find resource;(原因是maven在构建的时候不会识别src下的配置文件,见生成的class目录)所以有两种方法解决
\n1.在resources下新建目录,目录结构和java下的一致(因为需要保证映射文件和实体在同一个目录下),到时候生成的.class就会和配置文件放到一起,就可以找到了\n2.(推荐), 添加设置资源目录: 在pom的build下加入:\n <resources>\n <resource>\n <directory>src/main/java</directory>\n <includes>\n <include>**/*.xml</include>\n </includes>\n <filtering>false</filtering>\n </resource>\n </resources>\n
\n5.可以测试啦: 添加测试类–MainTest.java
private SqlSession util(){\n //配置文件\n String resource = "mybatis/conf.xml";\n\n //加载配置mybatis文件\n InputStream input = MainTest.class.getClassLoader().getResourceAsStream(resource);\n// Reader reader = Resources.getResourceAsReader(resource); //也可以使用这个加载配置\n //构建sqlSession工厂\n SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(input);\n //得到能执行映射文件中sql语句的sqlSession(等同于jdbc中的PreparedStatement)\n SqlSession session = sqlSessionFactory.openSession(true);//设置自动提交事务\n\n return session;\n }\n\n @Test\n public void testSelect() {\n SqlSession session = util();\n //映射sql的标识符字符串(映射文件全类名 + 映射节点id)\n String statement = "com.mybatis.test1.pojo.MyUserMapper.getUser";\n //执行sql语句返回结果\n MyUser user = session.selectOne(statement, 12);//两个参数 statement和占位符要填写的参数\n\n System.out.println(user);\n }\n
\n6. 大功告成 !
\n7. 其他操作 : CURD
\n增删改查实现方法, 1. 增加映射文件内容;
\n
\n<!--\n 插入一个用户\n -->\n <insert id="addUser" parameterType="com.mybatis.test1.pojo.MyUser">\n INSERT INTO my_user (id, name, age) VALUES (#{id}, #{name}, #{age})\n </insert>\n\n <!--\n 根据id删除一个用户\n -->\n <delete id="delUser" parameterType="int">\n DELETE FROM my_user\n WHERE id = #{id}\n </delete>\n\n <!--\n 更新用户信息\n -->\n <update id="editUser" parameterType="com.mybatis.test1.pojo.MyUser">\n UPDATE my_user\n SET name = #{name}, age = #{age}\n WHERE id = #{id}\n </update>\n\n <!--\n 根据id查询得到一个user对象\n -->\n <select id="getUser" parameterType="int" resultType="com.mybatis.test1.pojo.MyUser">\n SELECT *\n FROM my_user\n WHERE id = #{id}\n </select>\n\n <select id="getAllUser" resultType="com.mybatis.test1.pojo.MyUser">\n SELECT *\n FROM my_user\n </select>\n
\n2.调用sqlSession的各种方法(方法名基本上是个人都能看出来干嘛的,你就直接试); 比如
\n
\nint result = session.delete(statement, 1);\n\nList<MyUser> users = session.selectList(statement);\n\n....\n
\n8. 基于接口的写法:
\n基于接口有两种具体实现 1.基于注解:不需要自己写实现类,实现类自己”生成”; 2:基于xml文件,需要把xml文件和接口放在同一个目录下
\n
\n基于注解
1.新建一个代理接口–MyUserMapper.java
\npublic interface MyUserMapper {\n\n @Insert("INSERT INTO my_user (id, name, age) VALUES (#{id}, #{name}, #{age})")\n public int add(MyUser user);\n\n @Delete("DELETE FROM my_user WHERE id = #{id}")\n public int del(int id);\n\n @Update("UPDATE my_user SET name = #{name}, age = #{age} WHERE id = #{id}")\n public int edit(MyUser user);\n\n @Select("SELECT * FROM my_user WHERE id = #{id}")\n public MyUser find(int id);\n\n @Select("SELECT * FROM my_user")\n public List<MyUser> getAll();\n}\n
2.”注册”到mybatis配置文件–在conf.xml中mappers节点下添加
\n<mapper class="com.mybatis.test2.pojo.MyUserMapper"/>\n
3.测试
\nString resource = "mybatis/conf.xml";\nInputStream input = MainTest.class.getClassLoader().getResourceAsStream(resource);\nSqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(input);\nSqlSession session = sqlSessionFactory.openSession(true);\n//获取接口动态产生的实现类 再调用方法\nMyUserMapper myUserMapper = session.getMapper(MyUserMapper.class);\nMyUser user = myUserMapper.find(12);\n\nSystem.out.println(user);\n
\n基于配置文件
\n- 和实现xml文件–MyUserMapper.java & MyUserMapper.xml
\n
\npublic interface MyUserMapper {\n\n //方法要求 类名必须与.xml名相同; 方法名必须与MyUserMapper.xml中对应的id相同; 并且参数要一一对应\n public List<MyUser> getAll();\n}\n\n<select id="getAllUser" resultType="com.mybatis.test2.pojo.MyUser">\n SELECT *\n FROM my_user\n</select>\n
\n- “注册”到mybatis配置文件–在conf.xml中mappers节点下添加(二选一, 只要有一个就可定位Mapper)
\n
\n<mapper class="com.mybatis.test2.pojo.MyUserMapper"/>\n\n<mapper resource="com/mybatis/test2/pojo/MyUserMapper.xml"/>\n
\n- 测试(方法不变 只需改方法)
\n
\n
\n9.优化
\n1.数据库文件: 把数据库信息配置到一个文件: db.properties,然后在conf.xml中引入,调用使用EL表达式
\n
\n<properties resource="classpath:mybatis/db.properties"/>\n
\n2.配置别名: 在映射文件中写全类名很长很麻烦,可以在conf.xml中配置别名 alias为别名; 则可以在映射xml文件中写别名表示此类
\n
\n<typeAliases>\n <typeAlias type="com.mybatis.test2.pojo.MyUser" alias="_MyUser"/>\n</typeAliases>\n
\n2.配置别名2: 为整个包下类取别名 则别名为此类类名(比如: MyUser)
\n
\n<typeAliases>\n <package name="com.mybatis.test2.pojo"/>\n</typeAliases>\n
\n10.对于数据表字段名和实体属性名不一致的问题
\n当表字段名和实体属性名不同 就会无法获取数据(区分大小写), 对应的属性即为null(相关类型默认值) 原因是查到的数据无法映射到对应的result实体上,所以只要创建一个映射关系就能解决这个问题
\n
\n\n- 方法一: 指定字段别名(sql语句的方法, 直接指定字段别名为实体属性名)
\n
\n<select id="getOrder" parameterType="int" resultType="Order">\n SELECT order_id id, order_no orderNo, order_price orderPrice\n FROM `order`\n WHERE order_id = #{id}\n</select>\n
\n- 方法二: mybatis提供resultMap用于结果映射; like this
\n
\n<!--\n type: 映射实体类型 id主键 property实体属性名 column字段名\n -->\n\n<resultMap id="order" type="Order">\n <id property="id" column="order_id"/>\n <result property="orderNo" column="order_no"/>\n <result property="orderPrice" column="order_price"/>\n</resultMap>\n\n<select id="getOrder" parameterType="int" resultMap="order">\n SELECT *\n FROM `order`\n WHERE order_id = #{id}\n</select>\n
\n11.一对一和一对多的实现
\n对于涉及到多表查询, 一般有两种方式: 1.一个表一个表查询,用第一个表查到的数据组成第二个查询语句(也叫嵌套查询); 2.sql关联查询,一条语句,一次查询,语句比较复杂(也叫嵌套结果);
\n
\n\n为了尽可能的减少代码量(当然,去掉不必要的”体积”的麻烦),而且效率上 嵌套结果>存储过程>嵌套查询;
\n
\n嵌套结果示例:
\n- 一对一
\n
\n\n两个实体类以及一个结果封装类(由于不能仅仅用一个实体接收查询的所有的字段, so其用于封装查询的结果)
\n
\n// 对应数据库中表: order (字段有所不同,参考查询语句)\npublic class Order {\n private int id;\n private String orderNo;\n private float orderPrice;\n private User user;\n\n ... //构造方法, setter, getter, toString等方法\n}\n\n// 对应数据库中表: user\npublic class User {\n private int id;\n private String name;\n private int age;\n\n ...\n}\n\n// 结果封装类 id为user的id, 属性包含其他两个实体类(由于是一对一,则参数也可以把实体的参数复制过来,那查询mapper中resultMap有所不同)\npublic class UserOrder {\n private String id;\n private User user;\n private Order order;\n\n ...\n}\n
\n映射文件中查询的编写:
\n
\n<!-使用resultMap封装查询到的所有数据-->\n\n<select id="getOrderInfo" parameterType="int" resultMap="uo">\n SELECT *\n FROM user u, `order` o\n WHERE u.id = #{id} AND u.id = o.user_id\n</select>\n\n<!-column是查询输出结果的字段名, 如果查询的表之间没有同名字段则column是字段名,如果有字段冲突,则会有所变化(一般是"表别名_字段名"),以防万一要多测试-->\n<!-property是实体属性名-->\n<!-association:复杂类型联合,把多个字段映射联合映射为一个对象或其他 需要书写javaType表示要映射的类型 property表示映射类型的属性名-->\n<resultMap id="uo" type="UserOrder">\n <id property="id" column="id"/>\n <association property="user" javaType="User">\n <id property="id" column="id"/>\n <result property="name" column="name"/>\n <result property="age" column="age"/>\n </association>\n <association property="order" javaType="Order">\n <id property="id" column="order_id"/>\n <result property="orderNo" column="order_no"/>\n <result property="orderPrice" column="order_price"/>\n </association>\n</resultMap>\n\n<!-记得书写代理接口-->\n
\n测试类:
\n
\n...\nUserOrder order = orderMapper.getOrderInfo(12);\nSystem.out.println(order);\n
\n- 一对多
\n
\n\n一的一方同上,多的一方就需要一个新的类封装实体对象的集合,并且需要修改mapper写法
\n
\n// 实体类不变 结果封装类为:\npublic class UserOrder2 {\n private String id;\n private User user;\n private List<Order> orders;\n}\n
\n映射文件:
\n
\n<!-如果你在mysql中输入sql语句,查看结果就会发现:user只有一种但是每条数据的字段数据都有并且相同,order的字段数据每条都不一样-->\n<!-collection:复杂类型集合,-->\n<select id="getOrderInfo2" parameterType="int" resultMap="uo2">\n SELECT *\n FROM user u, `order` o\n WHERE u.id = #{id} AND u.id = o.user_id\n</select>\n\n<!--collection: 封装字段为集合类型 property: 类中的属性名 内容是集合数据的类型的属性-->\n<!--oftype: 集合中元素对象类型-->\n<resultMap id="uo2" type="UserOrder2">\n <id property="id" column="id"/>\n <association property="user" javaType="User">\n <id property="id" column="id"/>\n <result property="name" column="name"/>\n <result property="age" column="age"/>\n </association>\n <collection property="orders" ofType="Order">\n <id property="id" column="order_id"/>\n <result property="orderNo" column="order_no"/>\n <result property="orderPrice" column="order_price"/>\n </collection>\n</resultMap>\n
高级结果映射详细简介见: http://zj2626.github.io/2017/06/19/20170619_Mybatis/
\n\n测试类
\n
\nUserOrder2 order2 = orderMapper.getOrderInfo2(12);\nSystem.out.println(order2);\n
12.一级缓存与二级缓存
\n与hibernate相似, mybatis也存在缓存并且默认开启一级缓存,mybatis一级缓存是session级别的,而二级缓存是namespace(statement)级别的(即每个mapper文件就是一个二级缓存范围,需要配置)
\n
\n\n配置二级缓存
\n
\n<cache/>\n\nor\n\n<cache eviction="LRU" flushInterval="60000" size="512"/>\n
13.与spring集成
\n引入依赖
\n
\n<dependency>\n <groupId>org.mybatis</groupId>\n <artifactId>mybatis-spring</artifactId>\n <version>1.3.0</version>\n</dependency>\n
\n配置mapper 同上
\n
\n\n配置spring配置文件:
\n
\n<?xml version="1.0" encoding="UTF-8"?>\n<beans xmlns="http://www.springframework.org/schema/beans" \n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n xmlns:p="http://www.springframework.org/schema/p" \n xmlns:context="http://www.springframework.org/schema/context"\n xmlns:tx="http://www.springframework.org/schema/tx"\n xsi:schemaLocation="\n http://www.springframework.org/schema/beans\n http://www.springframework.org/schema/beans/spring-beans-3.2.xsd\n http://www.springframework.org/schema/context\n http://www.springframework.org/schema/context/spring-context-3.2.xsd\n http://www.springframework.org/schema/tx\n http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">\n <!-- 1. 数据源 : DriverManagerDataSource -->\n <bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">\n <property name="driverClassName" value="com.mysql.jdbc.Driver"/>\n <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>\n <property name="username" value="root"/>\n <property name="password" value="root"/>\n </bean>\n\n <!-- \n 2. mybatis的SqlSession的工厂: SqlSessionFactoryBean \n dataSource / typeAliasesPackage\n -->\n <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">\n <property name="dataSource" ref="datasource"/>\n <property name="typeAliasesPackage" value="com/mybatis/test3/bean"/>\n </bean>\n\n <!-- \n 3. mybatis自动扫描加载Sql映射文件(即接口) : MapperScannerConfigurer \n sqlSessionFactory / basePackage\n -->\n <bean id="config" class="org.mybatis.spring.mapper.MapperScannerConfigurer">\n <property name="basePackage" value="com/mybatis/test3/mapper"/>\n <property name="sqlSessionFactory" ref="sqlSessionFactory"/>\n </bean>\n\n <!-- 4. 事务管理 : DataSourceTransactionManager -->\n <bean id="manager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">\n <property name="dataSource" ref="datasource"/>\n </bean>\n\n <!-- 5. 使用声明式事务 -->\n <tx:annotation-driven transaction-manager="manager" />\n</beans>\n
\n配置mybatis配置文件 里面没有配置内容(但是必须要)
\n
\n<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">\n<configuration>\n\n</configuration>\n
n.分页插件 — PageHelper: 一个分页插件,支持多种数据库,原理大概是在执行sql语句之前(拦截器)进行了操作改写了sql语句,实现分页
\n- 导入依赖
\n
\n<dependency>\n <groupId>com.github.pagehelper</groupId>\n <artifactId>pagehelper</artifactId>\n</dependency>\n
\n- 配置插件–拦截器(在mybatis的配置文件中), 笔者在spring中集成了mybatis的配置
\n
\n<plugins>\n <!--配置PageHelper插件-->\n <plugin interceptor="com.github.pagehelper.PageHelper">\n <!--配置方言(数据库识别)-->\n <property name="dialect" value="mysql"/>\n </plugin>\n</plugins>\n
\n- 测试分页
\n
\n@Test\npublic void testPage() {\n //初始化Spring容器\n ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-dao.xml");\n\n //获取代理对象\n TbItemMapper itemMapper = applicationContext.getBean(TbItemMapper.class);\n\n //执行sql语句前设置分页信息使用PageHelper的startPage方法\n PageHelper.startPage(1, 10);\n\n //查询\n TbItemExample example = new TbItemExample();\n List<TbItem> list = itemMapper.selectByExample(example);\n\n //取分页信息PageInfo 总记录数 总页数 当前页\n PageInfo<TbItem> pageInfo = new PageInfo<>(list);\n System.out.println(pageInfo.getTotal());\n System.out.println(pageInfo.getEndRow());\n System.out.println(pageInfo.getFirstPage());\n System.out.println(pageInfo.getLastPage());\n System.out.println(pageInfo.getList());\n System.out.println(pageInfo.getNavigatePages());\n System.out.println(pageInfo.getNextPage());\n System.out.println(pageInfo.getPageNum());\n System.out.println(pageInfo.getPages());\n System.out.println(pageInfo.getPageSize());\n System.out.println(pageInfo.getPrePage());\n System.out.println(pageInfo.getSize());\n System.out.println(pageInfo.getStartRow());\n}\n
","site":{"data":{}},"excerpt":"什么是 MyBatis ?
\n官方文档: http://www.mybatis.org/mybatis-3/zh/index.html
\n
\n\nMyBatis 一个基于Java的持久层框架; 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将代理接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。
\n
\n与hibernate不同的是: mybatis是通过xml映射文件实现代理接口来实现操作数据库的功能
","more":"基本的步骤:
1. 引入dependency
<dependency>\n <groupId>org.mybatis</groupId>\n <artifactId>mybatis</artifactId>\n <version>3.4.1</version>\n</dependency>\n
\n2. mybatis核心配置文件 #在resources目录下的 mybatis/conf.xml
<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">\n<configuration>\n <!--可以读取数据库配置文件 用EL表达式获取参数-->\n <!--<properties resource="classpath:mybatis/db.properties"/>-->\n\n <!--\n development : 开发模式\n work : 工作模式\n -->\n <environments default="development">\n <environment id="development">\n <transactionManager type="JDBC"/>\n <dataSource type="POOLED">\n <property name="driver" value="com.mysql.jdbc.Driver"/>\n <property name="url"\n value="jdbc:mysql://localhost:3306/test?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8"/>\n <property name="username" value="root"/>\n <property name="password" value="fangshuoit"/>\n </dataSource>\n </environment>\n </environments>\n\n <!--映射文件所在位置 不能使用通配符(和spring整合时候可以使用)-->\n <mappers>\n <mapper resource="com/mybatis/test1/pojo/MyUserMapper.xml"/>\n </mappers>\n</configuration>\n
如果需要(或者测试没日志)可以引入log4j的包和配置文件 方便测试
3. 新建实体类–MyUser.java 对应数据库中表my_user
//三个属性\nprivate int id;\nprivate String name;\nprivate int age;\n
\n4. 新建Mapper映射文件–MyUserMapper.xml(要和实体放到一个目录下)
<?xml version="1.0" encoding="UTF-8" ?>\n<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">\n<mapper namespace="com.mybatis.test1.pojo.MyUserMapper">\n <!--\n 根据id查询得到一个user对象 其中sql语句中的表名,查询参数名,where语句中 键都是数据库中表的写法, 要传入的参数占位符是书体属性名\n -->\n <select id="getUser" parameterType="int" resultType="com.mybatis.test1.pojo.MyUser">\n SELECT *\n FROM my_user\n WHERE id = #{id}\n </select>\n</mapper>\n
maven项目运行时找不到映射文件:Could not find resource;(原因是maven在构建的时候不会识别src下的配置文件,见生成的class目录)所以有两种方法解决
\n1.在resources下新建目录,目录结构和java下的一致(因为需要保证映射文件和实体在同一个目录下),到时候生成的.class就会和配置文件放到一起,就可以找到了\n2.(推荐), 添加设置资源目录: 在pom的build下加入:\n <resources>\n <resource>\n <directory>src/main/java</directory>\n <includes>\n <include>**/*.xml</include>\n </includes>\n <filtering>false</filtering>\n </resource>\n </resources>\n
\n5.可以测试啦: 添加测试类–MainTest.java
private SqlSession util(){\n //配置文件\n String resource = "mybatis/conf.xml";\n\n //加载配置mybatis文件\n InputStream input = MainTest.class.getClassLoader().getResourceAsStream(resource);\n// Reader reader = Resources.getResourceAsReader(resource); //也可以使用这个加载配置\n //构建sqlSession工厂\n SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(input);\n //得到能执行映射文件中sql语句的sqlSession(等同于jdbc中的PreparedStatement)\n SqlSession session = sqlSessionFactory.openSession(true);//设置自动提交事务\n\n return session;\n }\n\n @Test\n public void testSelect() {\n SqlSession session = util();\n //映射sql的标识符字符串(映射文件全类名 + 映射节点id)\n String statement = "com.mybatis.test1.pojo.MyUserMapper.getUser";\n //执行sql语句返回结果\n MyUser user = session.selectOne(statement, 12);//两个参数 statement和占位符要填写的参数\n\n System.out.println(user);\n }\n
\n6. 大功告成 !
\n7. 其他操作 : CURD
\n增删改查实现方法, 1. 增加映射文件内容;
\n
\n<!--\n 插入一个用户\n -->\n <insert id="addUser" parameterType="com.mybatis.test1.pojo.MyUser">\n INSERT INTO my_user (id, name, age) VALUES (#{id}, #{name}, #{age})\n </insert>\n\n <!--\n 根据id删除一个用户\n -->\n <delete id="delUser" parameterType="int">\n DELETE FROM my_user\n WHERE id = #{id}\n </delete>\n\n <!--\n 更新用户信息\n -->\n <update id="editUser" parameterType="com.mybatis.test1.pojo.MyUser">\n UPDATE my_user\n SET name = #{name}, age = #{age}\n WHERE id = #{id}\n </update>\n\n <!--\n 根据id查询得到一个user对象\n -->\n <select id="getUser" parameterType="int" resultType="com.mybatis.test1.pojo.MyUser">\n SELECT *\n FROM my_user\n WHERE id = #{id}\n </select>\n\n <select id="getAllUser" resultType="com.mybatis.test1.pojo.MyUser">\n SELECT *\n FROM my_user\n </select>\n
\n2.调用sqlSession的各种方法(方法名基本上是个人都能看出来干嘛的,你就直接试); 比如
\n
\nint result = session.delete(statement, 1);\n\nList<MyUser> users = session.selectList(statement);\n\n....\n
\n8. 基于接口的写法:
\n基于接口有两种具体实现 1.基于注解:不需要自己写实现类,实现类自己”生成”; 2:基于xml文件,需要把xml文件和接口放在同一个目录下
\n
\n基于注解
1.新建一个代理接口–MyUserMapper.java
\npublic interface MyUserMapper {\n\n @Insert("INSERT INTO my_user (id, name, age) VALUES (#{id}, #{name}, #{age})")\n public int add(MyUser user);\n\n @Delete("DELETE FROM my_user WHERE id = #{id}")\n public int del(int id);\n\n @Update("UPDATE my_user SET name = #{name}, age = #{age} WHERE id = #{id}")\n public int edit(MyUser user);\n\n @Select("SELECT * FROM my_user WHERE id = #{id}")\n public MyUser find(int id);\n\n @Select("SELECT * FROM my_user")\n public List<MyUser> getAll();\n}\n
2.”注册”到mybatis配置文件–在conf.xml中mappers节点下添加
\n<mapper class="com.mybatis.test2.pojo.MyUserMapper"/>\n
3.测试
\nString resource = "mybatis/conf.xml";\nInputStream input = MainTest.class.getClassLoader().getResourceAsStream(resource);\nSqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(input);\nSqlSession session = sqlSessionFactory.openSession(true);\n//获取接口动态产生的实现类 再调用方法\nMyUserMapper myUserMapper = session.getMapper(MyUserMapper.class);\nMyUser user = myUserMapper.find(12);\n\nSystem.out.println(user);\n
\n基于配置文件
\n- 和实现xml文件–MyUserMapper.java & MyUserMapper.xml
\n
\npublic interface MyUserMapper {\n\n //方法要求 类名必须与.xml名相同; 方法名必须与MyUserMapper.xml中对应的id相同; 并且参数要一一对应\n public List<MyUser> getAll();\n}\n\n<select id="getAllUser" resultType="com.mybatis.test2.pojo.MyUser">\n SELECT *\n FROM my_user\n</select>\n
\n- “注册”到mybatis配置文件–在conf.xml中mappers节点下添加(二选一, 只要有一个就可定位Mapper)
\n
\n<mapper class="com.mybatis.test2.pojo.MyUserMapper"/>\n\n<mapper resource="com/mybatis/test2/pojo/MyUserMapper.xml"/>\n
\n- 测试(方法不变 只需改方法)
\n
\n
\n9.优化
\n1.数据库文件: 把数据库信息配置到一个文件: db.properties,然后在conf.xml中引入,调用使用EL表达式
\n
\n<properties resource="classpath:mybatis/db.properties"/>\n
\n2.配置别名: 在映射文件中写全类名很长很麻烦,可以在conf.xml中配置别名 alias为别名; 则可以在映射xml文件中写别名表示此类
\n
\n<typeAliases>\n <typeAlias type="com.mybatis.test2.pojo.MyUser" alias="_MyUser"/>\n</typeAliases>\n
\n2.配置别名2: 为整个包下类取别名 则别名为此类类名(比如: MyUser)
\n
\n<typeAliases>\n <package name="com.mybatis.test2.pojo"/>\n</typeAliases>\n
\n10.对于数据表字段名和实体属性名不一致的问题
\n当表字段名和实体属性名不同 就会无法获取数据(区分大小写), 对应的属性即为null(相关类型默认值) 原因是查到的数据无法映射到对应的result实体上,所以只要创建一个映射关系就能解决这个问题
\n
\n\n- 方法一: 指定字段别名(sql语句的方法, 直接指定字段别名为实体属性名)
\n
\n<select id="getOrder" parameterType="int" resultType="Order">\n SELECT order_id id, order_no orderNo, order_price orderPrice\n FROM `order`\n WHERE order_id = #{id}\n</select>\n
\n- 方法二: mybatis提供resultMap用于结果映射; like this
\n
\n<!--\n type: 映射实体类型 id主键 property实体属性名 column字段名\n -->\n\n<resultMap id="order" type="Order">\n <id property="id" column="order_id"/>\n <result property="orderNo" column="order_no"/>\n <result property="orderPrice" column="order_price"/>\n</resultMap>\n\n<select id="getOrder" parameterType="int" resultMap="order">\n SELECT *\n FROM `order`\n WHERE order_id = #{id}\n</select>\n
\n11.一对一和一对多的实现
\n对于涉及到多表查询, 一般有两种方式: 1.一个表一个表查询,用第一个表查到的数据组成第二个查询语句(也叫嵌套查询); 2.sql关联查询,一条语句,一次查询,语句比较复杂(也叫嵌套结果);
\n
\n\n为了尽可能的减少代码量(当然,去掉不必要的”体积”的麻烦),而且效率上 嵌套结果>存储过程>嵌套查询;
\n
\n嵌套结果示例:
\n- 一对一
\n
\n\n两个实体类以及一个结果封装类(由于不能仅仅用一个实体接收查询的所有的字段, so其用于封装查询的结果)
\n
\n// 对应数据库中表: order (字段有所不同,参考查询语句)\npublic class Order {\n private int id;\n private String orderNo;\n private float orderPrice;\n private User user;\n\n ... //构造方法, setter, getter, toString等方法\n}\n\n// 对应数据库中表: user\npublic class User {\n private int id;\n private String name;\n private int age;\n\n ...\n}\n\n// 结果封装类 id为user的id, 属性包含其他两个实体类(由于是一对一,则参数也可以把实体的参数复制过来,那查询mapper中resultMap有所不同)\npublic class UserOrder {\n private String id;\n private User user;\n private Order order;\n\n ...\n}\n
\n映射文件中查询的编写:
\n
\n<!-使用resultMap封装查询到的所有数据-->\n\n<select id="getOrderInfo" parameterType="int" resultMap="uo">\n SELECT *\n FROM user u, `order` o\n WHERE u.id = #{id} AND u.id = o.user_id\n</select>\n\n<!-column是查询输出结果的字段名, 如果查询的表之间没有同名字段则column是字段名,如果有字段冲突,则会有所变化(一般是"表别名_字段名"),以防万一要多测试-->\n<!-property是实体属性名-->\n<!-association:复杂类型联合,把多个字段映射联合映射为一个对象或其他 需要书写javaType表示要映射的类型 property表示映射类型的属性名-->\n<resultMap id="uo" type="UserOrder">\n <id property="id" column="id"/>\n <association property="user" javaType="User">\n <id property="id" column="id"/>\n <result property="name" column="name"/>\n <result property="age" column="age"/>\n </association>\n <association property="order" javaType="Order">\n <id property="id" column="order_id"/>\n <result property="orderNo" column="order_no"/>\n <result property="orderPrice" column="order_price"/>\n </association>\n</resultMap>\n\n<!-记得书写代理接口-->\n
\n测试类:
\n
\n...\nUserOrder order = orderMapper.getOrderInfo(12);\nSystem.out.println(order);\n
\n- 一对多
\n
\n\n一的一方同上,多的一方就需要一个新的类封装实体对象的集合,并且需要修改mapper写法
\n
\n// 实体类不变 结果封装类为:\npublic class UserOrder2 {\n private String id;\n private User user;\n private List<Order> orders;\n}\n
\n映射文件:
\n
\n<!-如果你在mysql中输入sql语句,查看结果就会发现:user只有一种但是每条数据的字段数据都有并且相同,order的字段数据每条都不一样-->\n<!-collection:复杂类型集合,-->\n<select id="getOrderInfo2" parameterType="int" resultMap="uo2">\n SELECT *\n FROM user u, `order` o\n WHERE u.id = #{id} AND u.id = o.user_id\n</select>\n\n<!--collection: 封装字段为集合类型 property: 类中的属性名 内容是集合数据的类型的属性-->\n<!--oftype: 集合中元素对象类型-->\n<resultMap id="uo2" type="UserOrder2">\n <id property="id" column="id"/>\n <association property="user" javaType="User">\n <id property="id" column="id"/>\n <result property="name" column="name"/>\n <result property="age" column="age"/>\n </association>\n <collection property="orders" ofType="Order">\n <id property="id" column="order_id"/>\n <result property="orderNo" column="order_no"/>\n <result property="orderPrice" column="order_price"/>\n </collection>\n</resultMap>\n
高级结果映射详细简介见: http://zj2626.github.io/2017/06/19/20170619_Mybatis/
\n\n测试类
\n
\nUserOrder2 order2 = orderMapper.getOrderInfo2(12);\nSystem.out.println(order2);\n
12.一级缓存与二级缓存
\n与hibernate相似, mybatis也存在缓存并且默认开启一级缓存,mybatis一级缓存是session级别的,而二级缓存是namespace(statement)级别的(即每个mapper文件就是一个二级缓存范围,需要配置)
\n
\n\n配置二级缓存
\n
\n<cache/>\n\nor\n\n<cache eviction="LRU" flushInterval="60000" size="512"/>\n
13.与spring集成
\n引入依赖
\n
\n<dependency>\n <groupId>org.mybatis</groupId>\n <artifactId>mybatis-spring</artifactId>\n <version>1.3.0</version>\n</dependency>\n
\n配置mapper 同上
\n
\n\n配置spring配置文件:
\n
\n<?xml version="1.0" encoding="UTF-8"?>\n<beans xmlns="http://www.springframework.org/schema/beans" \n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n xmlns:p="http://www.springframework.org/schema/p" \n xmlns:context="http://www.springframework.org/schema/context"\n xmlns:tx="http://www.springframework.org/schema/tx"\n xsi:schemaLocation="\n http://www.springframework.org/schema/beans\n http://www.springframework.org/schema/beans/spring-beans-3.2.xsd\n http://www.springframework.org/schema/context\n http://www.springframework.org/schema/context/spring-context-3.2.xsd\n http://www.springframework.org/schema/tx\n http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">\n <!-- 1. 数据源 : DriverManagerDataSource -->\n <bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">\n <property name="driverClassName" value="com.mysql.jdbc.Driver"/>\n <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>\n <property name="username" value="root"/>\n <property name="password" value="root"/>\n </bean>\n\n <!-- \n 2. mybatis的SqlSession的工厂: SqlSessionFactoryBean \n dataSource / typeAliasesPackage\n -->\n <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">\n <property name="dataSource" ref="datasource"/>\n <property name="typeAliasesPackage" value="com/mybatis/test3/bean"/>\n </bean>\n\n <!-- \n 3. mybatis自动扫描加载Sql映射文件(即接口) : MapperScannerConfigurer \n sqlSessionFactory / basePackage\n -->\n <bean id="config" class="org.mybatis.spring.mapper.MapperScannerConfigurer">\n <property name="basePackage" value="com/mybatis/test3/mapper"/>\n <property name="sqlSessionFactory" ref="sqlSessionFactory"/>\n </bean>\n\n <!-- 4. 事务管理 : DataSourceTransactionManager -->\n <bean id="manager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">\n <property name="dataSource" ref="datasource"/>\n </bean>\n\n <!-- 5. 使用声明式事务 -->\n <tx:annotation-driven transaction-manager="manager" />\n</beans>\n
\n配置mybatis配置文件 里面没有配置内容(但是必须要)
\n
\n<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">\n<configuration>\n\n</configuration>\n
n.分页插件 — PageHelper: 一个分页插件,支持多种数据库,原理大概是在执行sql语句之前(拦截器)进行了操作改写了sql语句,实现分页
\n- 导入依赖
\n
\n<dependency>\n <groupId>com.github.pagehelper</groupId>\n <artifactId>pagehelper</artifactId>\n</dependency>\n
\n- 配置插件–拦截器(在mybatis的配置文件中), 笔者在spring中集成了mybatis的配置
\n
\n<plugins>\n <!--配置PageHelper插件-->\n <plugin interceptor="com.github.pagehelper.PageHelper">\n <!--配置方言(数据库识别)-->\n <property name="dialect" value="mysql"/>\n </plugin>\n</plugins>\n
\n- 测试分页
\n
\n@Test\npublic void testPage() {\n //初始化Spring容器\n ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring/applicationContext-dao.xml");\n\n //获取代理对象\n TbItemMapper itemMapper = applicationContext.getBean(TbItemMapper.class);\n\n //执行sql语句前设置分页信息使用PageHelper的startPage方法\n PageHelper.startPage(1, 10);\n\n //查询\n TbItemExample example = new TbItemExample();\n List<TbItem> list = itemMapper.selectByExample(example);\n\n //取分页信息PageInfo 总记录数 总页数 当前页\n PageInfo<TbItem> pageInfo = new PageInfo<>(list);\n System.out.println(pageInfo.getTotal());\n System.out.println(pageInfo.getEndRow());\n System.out.println(pageInfo.getFirstPage());\n System.out.println(pageInfo.getLastPage());\n System.out.println(pageInfo.getList());\n System.out.println(pageInfo.getNavigatePages());\n System.out.println(pageInfo.getNextPage());\n System.out.println(pageInfo.getPageNum());\n System.out.println(pageInfo.getPages());\n System.out.println(pageInfo.getPageSize());\n System.out.println(pageInfo.getPrePage());\n System.out.println(pageInfo.getSize());\n System.out.println(pageInfo.getStartRow());\n}\n
"},{"title":"VM options配置","comments":1,"description":null,"_content":"\n\n> 转载自 链接地址: http://www.cnblogs.com/mingforyou/archive/2012/03/03/2378143.html\n> 转载自 链接地址: http://unixboy.iteye.com/blog/174173/\n\n\n\n# 文章一\n\nEclipse崩溃,错误提示:\nMyEclipse has detected that less than 5% of the 64MB of Perm \nGen (Non-heap memory) space remains. It is strongly recommended\nthat you exit and restart MyEclipse with new virtual machine memory\nparamters to increase this memory. Failure to do so can result in\ndata loss. The recommended Eclipse memory parameters are: \neclipse.exe -vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M\n\n1.参数的含义\n-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M\n-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了\n-Xms128m JVM初始分配的堆内存\n-Xmx512m JVM最大允许分配的堆内存,按需分配\n-XX:PermSize=64M JVM初始分配的非堆内存\n-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配\n\n我们首先了解一下JVM内存管理的机制,然后再解释每个参数代表的含义。\n\n\n## 堆(Heap)和非堆(Non-heap)内存\n\n\n 按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。\n 可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,\n 所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。 \n\n\n### 堆内存分配\n\n\n JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;\n 空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。\n 说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。 \n\n\n### 非堆内存分配\n\n\n JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,\n -server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)\n 上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。\nXX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。 \n说说为什么会内存益出: \n(1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。 \n(2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。\n 这种错误常见在web服务器对JSP进行pre compile的时候。 \n\n\n## JVM内存限制(最大值)\n\n\n 首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,\n 这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。\n\n2. 为什么有的机器我将-Xmx和-XX:MaxPermSize都设置为512M之后Eclipse可以启动,而有些机器无法启动?\n 通过上面对JVM内存管理的介绍我们已经了解到JVM内存包含两种:堆内存和非堆内存,另外JVM最大内存首先取决于实际的物理内存和操作系统。所以说设置VM参数导致程序无法启动主要有以下几种原因:\n1) 参数中-Xms的值大于-Xmx,或者-XX:PermSize的值大于-XX:MaxPermSize;\n2) -Xmx的值和-XX:MaxPermSize的总和超过了JVM内存的最大限制,比如当前操作系统最大内存限制,或者实际的物理内存等等。说到实际物理内存这里需要说明一点的是,\n 如果你的内存是1024MB,但实际系统中用到的并不可能是1024MB,因为有一部分被硬件占用了。\n\n3. 为何将上面的参数写入到eclipse.ini文件Eclipse没有执行对应的设置?\n 那为什么同样的参数在快捷方式或者命令行中有效而在eclipse.ini文件中是无效的呢?这是因为我们没有遵守eclipse.ini文件的设置规则:\n参数形如“项 值”这种形式,中间有空格的需要换行书写,如果值中有空格的需要用双引号包括起来。比如我们使用-vm C:/Java/jre1.6.0/bin/javaw.exe参数设置虚拟机,\n在eclipse.ini文件中要写成这样:\n\n-vm \nC:/Java/jre1.6.0/bin/javaw.exe \n-vmargs \n-Xms128M \n-Xmx512M \n-XX:PermSize=64M \n-XX:MaxPermSize=128M \n\n实际运行的结果可以通过Eclipse中“Help”-“About Eclipse SDK”窗口里面的“Configuration Details”按钮进行查看。\n另外需要说明的是,Eclipse压缩包中自带的eclipse.ini文件内容是这样的:\n-showsplash \norg.eclipse.platform \n--launcher.XXMaxPermSize \n256m \n-vmargs \n-Xms40m \n-Xmx256m \n其中–launcher.XXMaxPermSize(注意最前面是两个连接线)跟-XX:MaxPermSize参数的含义基本是一样的,我觉得唯一的区别就是前者是eclipse.exe启动的时候设置的参数,\n而后者是eclipse所使用的JVM中的参数。其实二者设置一个就可以了,所以这里可以把–launcher.XXMaxPermSize和下一行使用#注释掉。\n\n4. 其他的启动参数。 如果你有一个双核的CPU,也许可以尝试这个参数:\n-XX:+UseParallelGC\n让GC可以更快的执行。(只是JDK 5里对GC新增加的参数)\n\n补充:\n 如果你的WEB APP下都用了大量的第三方jar,其大小超过了服务器jvm默认的大小,那么就会产生内存益出问题了。\n解决方法: 设置MaxPermSize大小 \n可以在myelipse里选中相应的服务器比如tomcat5,展开里面的JDK子项页面,来增加服务器启动的JVM参数设置:\n\n-Xms128m \n-Xmx256m \n-XX:PermSize=128M \n-XX:MaxNewSize=256m \n-XX:MaxPermSize=256m\n\n或者手动设置MaxPermSize大小,比如tomcat,\n修改TOMCAT_HOME/bin/catalina.bat,在echo \"Using CATALINA_BASE: $CATALINA_BASE\"上面加入以下行: \nJAVA_OPTS=\"-server -XX:PermSize=64M -XX:MaxPermSize=128m\n\n建议:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以减少jar 文档重复占用内存\n\n# 文章二\n\n## 1.堆大小设置\n\nJVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。\n\n### 典型设置:\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k\n\n-Xmx3550m:设置JVM最大可用内存为3550M。\n-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。\n-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。\n-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。\n\n* java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0\n\n-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5\n-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6\n-XX:MaxPermSize=16m:设置持久代大小为16m。\n-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。\n\n## 2.回收器选择\n\nJVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。\n\n### 吞吐量优先的并行收集器\n如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。\n\n> 典型配置:\n\n* java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20\n-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。\n-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC\n-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100\n-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy\n-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。\n\n### 响应时间优先的并发收集器\n如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。\n\n> 典型配置:\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC\n-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。\n-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection\n-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。\n-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片\n\n> 辅助信息\n\nJVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:\n* -XX:+PrintGC\n输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]\n [Full GC 121376K->10414K(130112K), 0.0650971 secs]\n* -XX:+PrintGCDetails\n输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]\n [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]\n* -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用\n输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]\n* -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用\n输出形式:Application time: 0.5291524 seconds\n* -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用\n输出形式:Total time for which application threads were stopped: 0.0468229 seconds\n* -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息\n\n输出形式:\n\n 34.702: [GC {Heap before gc invocations=7:\n def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)\n eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)\n from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)\n to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)\n tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)\n the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n 34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:\n def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)\n eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)\n from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)\n to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)\n tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)\n the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n }\n , 0.0757599 secs]\n \n* -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。\n\n> 常见配置汇总\n\n1.堆设置\n* -Xms:初始堆大小\n* -Xmx:最大堆大小\n* -XX:NewSize=n:设置年轻代大小\n* -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4\n* -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5\n* -XX:MaxPermSize=n:设置持久代大小\n\n2.收集器设置\n* -XX:+UseSerialGC:设置串行收集器\n* -XX:+UseParallelGC:设置并行收集器\n* -XX:+UseParalledlOldGC:设置并行年老代收集器\n* -XX:+UseConcMarkSweepGC:设置并发收集器\n\n3.垃圾回收统计信息\n* -XX:+PrintGC\n* -XX:+PrintGCDetails\n* -XX:+PrintGCTimeStamps\n* -Xloggc:filename\n\n4.并行收集器设置\n* -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。\n* -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间\n* -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)\n\n5.并发收集器设置\n* -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。\n* -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。\n\n\n## 调优总结\n\n1.年轻代大小选择\n* 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。\n* 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。\n\n2.年老代大小选择\n* 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:\n\n 并发垃圾收集信息\n 持久代并发收集次数\n 传统GC信息\n 花在年轻代和年老代回收上的时间比例\n \n减少年轻代和年老代花费的时间,一般会提高应用的效率\n* 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。\n\n3.较小堆引起的碎片问题\n因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:\n\n* -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。\n* -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩\n\n> 个人博客 欢迎来访: http://zj2626.github.io","source":"_posts/20170924_JVM3.md","raw":"---\ntitle: VM options配置\n\ncomments: true \n\ntags: \n - 深入了解java虚拟机\n - java\n\ncategories: \n - java虚拟机\n\ndescription:\n \n---\n\n\n> 转载自 链接地址: http://www.cnblogs.com/mingforyou/archive/2012/03/03/2378143.html\n> 转载自 链接地址: http://unixboy.iteye.com/blog/174173/\n\n\n\n# 文章一\n\nEclipse崩溃,错误提示:\nMyEclipse has detected that less than 5% of the 64MB of Perm \nGen (Non-heap memory) space remains. It is strongly recommended\nthat you exit and restart MyEclipse with new virtual machine memory\nparamters to increase this memory. Failure to do so can result in\ndata loss. The recommended Eclipse memory parameters are: \neclipse.exe -vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M\n\n1.参数的含义\n-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M\n-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了\n-Xms128m JVM初始分配的堆内存\n-Xmx512m JVM最大允许分配的堆内存,按需分配\n-XX:PermSize=64M JVM初始分配的非堆内存\n-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配\n\n我们首先了解一下JVM内存管理的机制,然后再解释每个参数代表的含义。\n\n\n## 堆(Heap)和非堆(Non-heap)内存\n\n\n 按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。\n 可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,\n 所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。 \n\n\n### 堆内存分配\n\n\n JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;\n 空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。\n 说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try...catch捕捉。 \n\n\n### 非堆内存分配\n\n\n JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,\n -server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)\n 上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。\nXX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。 \n说说为什么会内存益出: \n(1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。 \n(2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。\n 这种错误常见在web服务器对JSP进行pre compile的时候。 \n\n\n## JVM内存限制(最大值)\n\n\n 首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,\n 这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。\n\n2. 为什么有的机器我将-Xmx和-XX:MaxPermSize都设置为512M之后Eclipse可以启动,而有些机器无法启动?\n 通过上面对JVM内存管理的介绍我们已经了解到JVM内存包含两种:堆内存和非堆内存,另外JVM最大内存首先取决于实际的物理内存和操作系统。所以说设置VM参数导致程序无法启动主要有以下几种原因:\n1) 参数中-Xms的值大于-Xmx,或者-XX:PermSize的值大于-XX:MaxPermSize;\n2) -Xmx的值和-XX:MaxPermSize的总和超过了JVM内存的最大限制,比如当前操作系统最大内存限制,或者实际的物理内存等等。说到实际物理内存这里需要说明一点的是,\n 如果你的内存是1024MB,但实际系统中用到的并不可能是1024MB,因为有一部分被硬件占用了。\n\n3. 为何将上面的参数写入到eclipse.ini文件Eclipse没有执行对应的设置?\n 那为什么同样的参数在快捷方式或者命令行中有效而在eclipse.ini文件中是无效的呢?这是因为我们没有遵守eclipse.ini文件的设置规则:\n参数形如“项 值”这种形式,中间有空格的需要换行书写,如果值中有空格的需要用双引号包括起来。比如我们使用-vm C:/Java/jre1.6.0/bin/javaw.exe参数设置虚拟机,\n在eclipse.ini文件中要写成这样:\n\n-vm \nC:/Java/jre1.6.0/bin/javaw.exe \n-vmargs \n-Xms128M \n-Xmx512M \n-XX:PermSize=64M \n-XX:MaxPermSize=128M \n\n实际运行的结果可以通过Eclipse中“Help”-“About Eclipse SDK”窗口里面的“Configuration Details”按钮进行查看。\n另外需要说明的是,Eclipse压缩包中自带的eclipse.ini文件内容是这样的:\n-showsplash \norg.eclipse.platform \n--launcher.XXMaxPermSize \n256m \n-vmargs \n-Xms40m \n-Xmx256m \n其中–launcher.XXMaxPermSize(注意最前面是两个连接线)跟-XX:MaxPermSize参数的含义基本是一样的,我觉得唯一的区别就是前者是eclipse.exe启动的时候设置的参数,\n而后者是eclipse所使用的JVM中的参数。其实二者设置一个就可以了,所以这里可以把–launcher.XXMaxPermSize和下一行使用#注释掉。\n\n4. 其他的启动参数。 如果你有一个双核的CPU,也许可以尝试这个参数:\n-XX:+UseParallelGC\n让GC可以更快的执行。(只是JDK 5里对GC新增加的参数)\n\n补充:\n 如果你的WEB APP下都用了大量的第三方jar,其大小超过了服务器jvm默认的大小,那么就会产生内存益出问题了。\n解决方法: 设置MaxPermSize大小 \n可以在myelipse里选中相应的服务器比如tomcat5,展开里面的JDK子项页面,来增加服务器启动的JVM参数设置:\n\n-Xms128m \n-Xmx256m \n-XX:PermSize=128M \n-XX:MaxNewSize=256m \n-XX:MaxPermSize=256m\n\n或者手动设置MaxPermSize大小,比如tomcat,\n修改TOMCAT_HOME/bin/catalina.bat,在echo \"Using CATALINA_BASE: $CATALINA_BASE\"上面加入以下行: \nJAVA_OPTS=\"-server -XX:PermSize=64M -XX:MaxPermSize=128m\n\n建议:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以减少jar 文档重复占用内存\n\n# 文章二\n\n## 1.堆大小设置\n\nJVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。\n\n### 典型设置:\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k\n\n-Xmx3550m:设置JVM最大可用内存为3550M。\n-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。\n-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。\n-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。\n\n* java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0\n\n-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5\n-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6\n-XX:MaxPermSize=16m:设置持久代大小为16m。\n-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。\n\n## 2.回收器选择\n\nJVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。\n\n### 吞吐量优先的并行收集器\n如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。\n\n> 典型配置:\n\n* java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20\n-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。\n-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC\n-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100\n-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy\n-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。\n\n### 响应时间优先的并发收集器\n如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。\n\n> 典型配置:\n\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC\n-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。\n-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。\n* java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection\n-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。\n-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片\n\n> 辅助信息\n\nJVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:\n* -XX:+PrintGC\n输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs]\n [Full GC 121376K->10414K(130112K), 0.0650971 secs]\n* -XX:+PrintGCDetails\n输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]\n [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]\n* -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用\n输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]\n* -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用\n输出形式:Application time: 0.5291524 seconds\n* -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用\n输出形式:Total time for which application threads were stopped: 0.0468229 seconds\n* -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息\n\n输出形式:\n\n 34.702: [GC {Heap before gc invocations=7:\n def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)\n eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)\n from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)\n to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)\n tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)\n the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n 34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:\n def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)\n eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)\n from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)\n to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)\n tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)\n the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n }\n , 0.0757599 secs]\n \n* -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。\n\n> 常见配置汇总\n\n1.堆设置\n* -Xms:初始堆大小\n* -Xmx:最大堆大小\n* -XX:NewSize=n:设置年轻代大小\n* -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4\n* -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5\n* -XX:MaxPermSize=n:设置持久代大小\n\n2.收集器设置\n* -XX:+UseSerialGC:设置串行收集器\n* -XX:+UseParallelGC:设置并行收集器\n* -XX:+UseParalledlOldGC:设置并行年老代收集器\n* -XX:+UseConcMarkSweepGC:设置并发收集器\n\n3.垃圾回收统计信息\n* -XX:+PrintGC\n* -XX:+PrintGCDetails\n* -XX:+PrintGCTimeStamps\n* -Xloggc:filename\n\n4.并行收集器设置\n* -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。\n* -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间\n* -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)\n\n5.并发收集器设置\n* -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。\n* -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。\n\n\n## 调优总结\n\n1.年轻代大小选择\n* 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。\n* 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。\n\n2.年老代大小选择\n* 响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:\n\n 并发垃圾收集信息\n 持久代并发收集次数\n 传统GC信息\n 花在年轻代和年老代回收上的时间比例\n \n减少年轻代和年老代花费的时间,一般会提高应用的效率\n* 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。\n\n3.较小堆引起的碎片问题\n因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:\n\n* -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。\n* -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩\n\n> 个人博客 欢迎来访: http://zj2626.github.io","slug":"20170924_JVM3","published":1,"date":"2020-01-15T05:50:39.643Z","updated":"2021-03-10T13:50:15.316Z","layout":"post","photos":[],"link":"","_id":"ckm3invgo009324uj3cdg0fej","content":"\n转载自 链接地址: http://www.cnblogs.com/mingforyou/archive/2012/03/03/2378143.html
转载自 链接地址: http://unixboy.iteye.com/blog/174173/
\n
\n\n文章一
Eclipse崩溃,错误提示:
MyEclipse has detected that less than 5% of the 64MB of Perm
Gen (Non-heap memory) space remains. It is strongly recommended
that you exit and restart MyEclipse with new virtual machine memory
paramters to increase this memory. Failure to do so can result in
data loss. The recommended Eclipse memory parameters are:
eclipse.exe -vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
\n1.参数的含义
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配
-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配
\n我们首先了解一下JVM内存管理的机制,然后再解释每个参数代表的含义。
\n堆(Heap)和非堆(Non-heap)内存
按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。
可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,
所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
\n堆内存分配
JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。
\n非堆内存分配
JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,
-server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)
上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。
XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。
说说为什么会内存益出:
(1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。
(2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。
这种错误常见在web服务器对JSP进行pre compile的时候。
\nJVM内存限制(最大值)
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,
这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。
\n\n为什么有的机器我将-Xmx和-XX:MaxPermSize都设置为512M之后Eclipse可以启动,而有些机器无法启动?
通过上面对JVM内存管理的介绍我们已经了解到JVM内存包含两种:堆内存和非堆内存,另外JVM最大内存首先取决于实际的物理内存和操作系统。所以说设置VM参数导致程序无法启动主要有以下几种原因:
1) 参数中-Xms的值大于-Xmx,或者-XX:PermSize的值大于-XX:MaxPermSize;
2) -Xmx的值和-XX:MaxPermSize的总和超过了JVM内存的最大限制,比如当前操作系统最大内存限制,或者实际的物理内存等等。说到实际物理内存这里需要说明一点的是,
如果你的内存是1024MB,但实际系统中用到的并不可能是1024MB,因为有一部分被硬件占用了。
\n \n为何将上面的参数写入到eclipse.ini文件Eclipse没有执行对应的设置?
那为什么同样的参数在快捷方式或者命令行中有效而在eclipse.ini文件中是无效的呢?这是因为我们没有遵守eclipse.ini文件的设置规则:
参数形如“项 值”这种形式,中间有空格的需要换行书写,如果值中有空格的需要用双引号包括起来。比如我们使用-vm C:/Java/jre1.6.0/bin/javaw.exe参数设置虚拟机,
在eclipse.ini文件中要写成这样:
\n \n
\n-vm
C:/Java/jre1.6.0/bin/javaw.exe
-vmargs
-Xms128M
-Xmx512M
-XX:PermSize=64M
-XX:MaxPermSize=128M
\n实际运行的结果可以通过Eclipse中“Help”-“About Eclipse SDK”窗口里面的“Configuration Details”按钮进行查看。
另外需要说明的是,Eclipse压缩包中自带的eclipse.ini文件内容是这样的:
-showsplash
org.eclipse.platform
–launcher.XXMaxPermSize
256m
-vmargs
-Xms40m
-Xmx256m
其中–launcher.XXMaxPermSize(注意最前面是两个连接线)跟-XX:MaxPermSize参数的含义基本是一样的,我觉得唯一的区别就是前者是eclipse.exe启动的时候设置的参数,
而后者是eclipse所使用的JVM中的参数。其实二者设置一个就可以了,所以这里可以把–launcher.XXMaxPermSize和下一行使用#注释掉。
\n\n- 其他的启动参数。 如果你有一个双核的CPU,也许可以尝试这个参数:
-XX:+UseParallelGC
让GC可以更快的执行。(只是JDK 5里对GC新增加的参数) \n
\n补充:
如果你的WEB APP下都用了大量的第三方jar,其大小超过了服务器jvm默认的大小,那么就会产生内存益出问题了。
解决方法: 设置MaxPermSize大小
可以在myelipse里选中相应的服务器比如tomcat5,展开里面的JDK子项页面,来增加服务器启动的JVM参数设置:
\n-Xms128m
-Xmx256m
-XX:PermSize=128M
-XX:MaxNewSize=256m
-XX:MaxPermSize=256m
\n或者手动设置MaxPermSize大小,比如tomcat,
修改TOMCAT_HOME/bin/catalina.bat,在echo “Using CATALINA_BASE: $CATALINA_BASE”上面加入以下行:
JAVA_OPTS=”-server -XX:PermSize=64M -XX:MaxPermSize=128m
\n建议:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以减少jar 文档重复占用内存
\n文章二
1.堆大小设置
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
\n典型设置:
\n- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
\n
\n-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
\n\n- java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
\n
\n-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
\n2.回收器选择
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
\n吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
\n\n典型配置:
\n
\n\njava -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
\n \njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
\n \njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
\n \njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
\n \n
\n响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
\n\n典型配置:
\n
\n\n- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。 \n- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片 \n
\n\n辅助信息
\n
\nJVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
\n\n- -XX:+PrintGC
输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs][Full GC 121376K->10414K(130112K), 0.0650971 secs]\n
\n- -XX:+PrintGCDetails
输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs][GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]\n
\n- -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] \n- -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
输出形式:Application time: 0.5291524 seconds \n- -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
输出形式:Total time for which application threads were stopped: 0.0468229 seconds \n- -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
\n
\n输出形式:
\n34.702: [GC {Heap before gc invocations=7:\n def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)\neden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)\nfrom space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)\n to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)\n tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)\nthe space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:\n def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)\neden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)\n from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)\n to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)\n tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)\nthe space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n}\n, 0.0757599 secs]\n
\n- -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
\n
\n\n常见配置汇总
\n
\n1.堆设置
\n\n- -Xms:初始堆大小
\n- -Xmx:最大堆大小
\n- -XX:NewSize=n:设置年轻代大小
\n- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
\n- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
\n- -XX:MaxPermSize=n:设置持久代大小
\n
\n2.收集器设置
\n\n- -XX:+UseSerialGC:设置串行收集器
\n- -XX:+UseParallelGC:设置并行收集器
\n- -XX:+UseParalledlOldGC:设置并行年老代收集器
\n- -XX:+UseConcMarkSweepGC:设置并发收集器
\n
\n3.垃圾回收统计信息
\n\n- -XX:+PrintGC
\n- -XX:+PrintGCDetails
\n- -XX:+PrintGCTimeStamps
\n- -Xloggc:filename
\n
\n4.并行收集器设置
\n\n- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
\n- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
\n- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
\n
\n5.并发收集器设置
\n\n- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
\n- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
\n
\n调优总结
1.年轻代大小选择
\n\n- 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
\n- 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
\n
\n2.年老代大小选择
\n\n减少年轻代和年老代花费的时间,一般会提高应用的效率
\n\n- 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
\n
\n3.较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
\n\n- -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
\n- -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
\n
\n\n个人博客 欢迎来访: http://zj2626.github.io
\n
\n","site":{"data":{}},"excerpt":"\n转载自 链接地址: http://www.cnblogs.com/mingforyou/archive/2012/03/03/2378143.html
转载自 链接地址: http://unixboy.iteye.com/blog/174173/
\n
","more":"文章一
Eclipse崩溃,错误提示:
MyEclipse has detected that less than 5% of the 64MB of Perm
Gen (Non-heap memory) space remains. It is strongly recommended
that you exit and restart MyEclipse with new virtual machine memory
paramters to increase this memory. Failure to do so can result in
data loss. The recommended Eclipse memory parameters are:
eclipse.exe -vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
\n1.参数的含义
-vmargs -Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存
-Xmx512m JVM最大允许分配的堆内存,按需分配
-XX:PermSize=64M JVM初始分配的非堆内存
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,按需分配
\n我们首先了解一下JVM内存管理的机制,然后再解释每个参数代表的含义。
\n堆(Heap)和非堆(Non-heap)内存
按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。
可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的,
所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。
\n堆内存分配
JVM初始分配的堆内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的堆内存由-Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;
空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx 相等以避免在每次GC 后调整堆的大小。
说明:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。
\n非堆内存分配
JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。(还有一说:MaxPermSize缺省值和-server -client选项相关,
-server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。这个我没有实验。)
上面错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域。还没有弄明白PermGen space是属于非堆内存,还是就是非堆内存,但至少是属于了。
XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。
说说为什么会内存益出:
(1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它和存放Instance的Heap区域不同。
(2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会LOAD很多CLASS 的话,就很可能出现PermGen space错误。
这种错误常见在web服务器对JSP进行pre compile的时候。
\nJVM内存限制(最大值)
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,
这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了。
\n\n为什么有的机器我将-Xmx和-XX:MaxPermSize都设置为512M之后Eclipse可以启动,而有些机器无法启动?
通过上面对JVM内存管理的介绍我们已经了解到JVM内存包含两种:堆内存和非堆内存,另外JVM最大内存首先取决于实际的物理内存和操作系统。所以说设置VM参数导致程序无法启动主要有以下几种原因:
1) 参数中-Xms的值大于-Xmx,或者-XX:PermSize的值大于-XX:MaxPermSize;
2) -Xmx的值和-XX:MaxPermSize的总和超过了JVM内存的最大限制,比如当前操作系统最大内存限制,或者实际的物理内存等等。说到实际物理内存这里需要说明一点的是,
如果你的内存是1024MB,但实际系统中用到的并不可能是1024MB,因为有一部分被硬件占用了。
\n \n为何将上面的参数写入到eclipse.ini文件Eclipse没有执行对应的设置?
那为什么同样的参数在快捷方式或者命令行中有效而在eclipse.ini文件中是无效的呢?这是因为我们没有遵守eclipse.ini文件的设置规则:
参数形如“项 值”这种形式,中间有空格的需要换行书写,如果值中有空格的需要用双引号包括起来。比如我们使用-vm C:/Java/jre1.6.0/bin/javaw.exe参数设置虚拟机,
在eclipse.ini文件中要写成这样:
\n \n
\n-vm
C:/Java/jre1.6.0/bin/javaw.exe
-vmargs
-Xms128M
-Xmx512M
-XX:PermSize=64M
-XX:MaxPermSize=128M
\n实际运行的结果可以通过Eclipse中“Help”-“About Eclipse SDK”窗口里面的“Configuration Details”按钮进行查看。
另外需要说明的是,Eclipse压缩包中自带的eclipse.ini文件内容是这样的:
-showsplash
org.eclipse.platform
–launcher.XXMaxPermSize
256m
-vmargs
-Xms40m
-Xmx256m
其中–launcher.XXMaxPermSize(注意最前面是两个连接线)跟-XX:MaxPermSize参数的含义基本是一样的,我觉得唯一的区别就是前者是eclipse.exe启动的时候设置的参数,
而后者是eclipse所使用的JVM中的参数。其实二者设置一个就可以了,所以这里可以把–launcher.XXMaxPermSize和下一行使用#注释掉。
\n\n- 其他的启动参数。 如果你有一个双核的CPU,也许可以尝试这个参数:
-XX:+UseParallelGC
让GC可以更快的执行。(只是JDK 5里对GC新增加的参数) \n
\n补充:
如果你的WEB APP下都用了大量的第三方jar,其大小超过了服务器jvm默认的大小,那么就会产生内存益出问题了。
解决方法: 设置MaxPermSize大小
可以在myelipse里选中相应的服务器比如tomcat5,展开里面的JDK子项页面,来增加服务器启动的JVM参数设置:
\n-Xms128m
-Xmx256m
-XX:PermSize=128M
-XX:MaxNewSize=256m
-XX:MaxPermSize=256m
\n或者手动设置MaxPermSize大小,比如tomcat,
修改TOMCAT_HOME/bin/catalina.bat,在echo “Using CATALINA_BASE: $CATALINA_BASE”上面加入以下行:
JAVA_OPTS=”-server -XX:PermSize=64M -XX:MaxPermSize=128m
\n建议:将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以减少jar 文档重复占用内存
\n文章二
1.堆大小设置
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
\n典型设置:
\n- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k
\n
\n-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。
\n\n- java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0
\n
\n-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
-XX:MaxPermSize=16m:设置持久代大小为16m。
-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
\n2.回收器选择
JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。
\n吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。
\n\n典型配置:
\n
\n\njava -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
\n \njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
\n \njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
\n \njava -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
\n \n
\n响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信领域等。
\n\n典型配置:
\n
\n\n- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
-XX:+UseParNewGC:设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。 \n- java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片 \n
\n\n辅助信息
\n
\nJVM提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:
\n\n- -XX:+PrintGC
输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs][Full GC 121376K->10414K(130112K), 0.0650971 secs]\n
\n- -XX:+PrintGCDetails
输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs][GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]\n
\n- -XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可与上面两个混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs] \n- -XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时间。可与上面混合使用
输出形式:Application time: 0.5291524 seconds \n- -XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混合使用
输出形式:Total time for which application threads were stopped: 0.0468229 seconds \n- -XX:PrintHeapAtGC:打印GC前后的详细堆栈信息
\n
\n输出形式:
\n34.702: [GC {Heap before gc invocations=7:\n def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)\neden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)\nfrom space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000)\n to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000)\n tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)\nthe space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:\n def new generation total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)\neden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)\n from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)\n to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)\n tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)\nthe space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)\n compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)\n the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)\n ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)\n rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)\n}\n, 0.0757599 secs]\n
\n- -Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。
\n
\n\n常见配置汇总
\n
\n1.堆设置
\n\n- -Xms:初始堆大小
\n- -Xmx:最大堆大小
\n- -XX:NewSize=n:设置年轻代大小
\n- -XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
\n- -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
\n- -XX:MaxPermSize=n:设置持久代大小
\n
\n2.收集器设置
\n\n- -XX:+UseSerialGC:设置串行收集器
\n- -XX:+UseParallelGC:设置并行收集器
\n- -XX:+UseParalledlOldGC:设置并行年老代收集器
\n- -XX:+UseConcMarkSweepGC:设置并发收集器
\n
\n3.垃圾回收统计信息
\n\n- -XX:+PrintGC
\n- -XX:+PrintGCDetails
\n- -XX:+PrintGCTimeStamps
\n- -Xloggc:filename
\n
\n4.并行收集器设置
\n\n- -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
\n- -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
\n- -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
\n
\n5.并发收集器设置
\n\n- -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
\n- -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
\n
\n调优总结
1.年轻代大小选择
\n\n- 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
\n- 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。
\n
\n2.年老代大小选择
\n\n减少年轻代和年老代花费的时间,一般会提高应用的效率
\n\n- 吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
\n
\n3.较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:
\n\n- -XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
\n- -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩
\n
\n\n个人博客 欢迎来访: http://zj2626.github.io
\n
"},{"title":"彻底理解ThreadLocal(转载)","comments":1,"description":null,"date":"2017-02-14T16:00:00.000Z","_content":"\nhttp://blog.csdn.net/lufeng20/article/details/24314381\n\n## ThreadLocal是什么\n 早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。\n 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。\n 从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。\n 所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。\nThreadLocal的接口方法\nThreadLocal类接口很简单,只有4个方法,我们先来了解一下:\nvoid set(Object value)设置当前线程的线程局部变量的值。\npublic Object get()该方法返回当前线程所对应的线程局部变量。\npublic void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。\nprotected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。\n 值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。\n ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n\n\n\n package com.test; \n \n public class TestNum { \n // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 \n private static ThreadLocal seqNum = new ThreadLocal() { \n public Integer initialValue() { \n return 0; \n } \n }; \n \n // ②获取下一个序列值 \n public int getNextNum() { \n seqNum.set(seqNum.get() + 1); \n return seqNum.get(); \n } \n \n public static void main(String[] args) { \n TestNum sn = new TestNum(); \n // ③ 3个线程共享sn,各自产生序列号 \n TestClient t1 = new TestClient(sn); \n TestClient t2 = new TestClient(sn); \n TestClient t3 = new TestClient(sn); \n t1.start(); \n t2.start(); \n t3.start(); \n } \n \n private static class TestClient extends Thread { \n private TestNum sn; \n \n public TestClient(TestNum sn) { \n this.sn = sn; \n } \n \n public void run() { \n for (int i = 0; i < 3; i++) { \n // ④每个线程打出3个序列值 \n System.out.println(\"thread[\" + Thread.currentThread().getName() + \"] --> sn[\" \n + sn.getNextNum() + \"]\"); \n } \n } \n } \n } \n\n\n 通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:\nthread[Thread-0] --> sn[1]\nthread[Thread-1] --> sn[1]\nthread[Thread-2] --> sn[1]\nthread[Thread-1] --> sn[2]\nthread[Thread-0] --> sn[2]\nthread[Thread-1] --> sn[3]\nthread[Thread-2] --> sn[2]\nthread[Thread-0] --> sn[3]\nthread[Thread-2] --> sn[3]\n考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。\n\nThread同步机制的比较\n ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。\n 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。\n 而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。\n 由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。\n 概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。\n spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。\n 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:\n通通透透理解ThreadLocal\n 同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。\n 下面的实例能够体现Spring对有状态Bean的改造思路:\n代码清单3 TestDao:非线程安全\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n package com.test; \n \n import java.sql.Connection; \n import java.sql.SQLException; \n import java.sql.Statement; \n \n public class TestDao { \n private Connection conn;// ①一个非线程安全的变量 \n \n public void addTopic() throws SQLException { \n Statement stat = conn.createStatement();// ②引用非线程安全变量 \n // … \n } \n } \n\n\n\n由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:\n代码清单4 TestDao:线程安全\n\n package com.test; \n \n import java.sql.Connection; \n import java.sql.SQLException; \n import java.sql.Statement; \n \n public class TestDaoNew { \n // ①使用ThreadLocal保存Connection变量 \n private static ThreadLocal connThreadLocal = new ThreadLocal(); \n \n public static Connection getConnection() { \n // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection, \n // 并将其保存到线程本地变量中。 \n if (connThreadLocal.get() == null) { \n Connection conn = getConnection(); \n connThreadLocal.set(conn); \n return conn; \n } else { \n return connThreadLocal.get();// ③直接返回线程本地变量 \n } \n } \n \n public void addTopic() throws SQLException { \n // ④从ThreadLocal中获取线程对应的Connection \n Statement stat = getConnection().createStatement(); \n } \n } \n\n不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。\n当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。\n\n\nConnectionManager.java\n\n package com.test; \n \n import java.sql.Connection; \n import java.sql.DriverManager; \n import java.sql.SQLException; \n \n public class ConnectionManager { \n \n private static ThreadLocal connectionHolder = new ThreadLocal() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n \"jdbc:mysql://localhost:3306/test\", \"username\", \n \"password\"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n \n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n \n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n } \n \n\njava.lang.ThreadLocal的具体实现\n那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n \n /** \n * Sets the current thread's copy of this thread-local variable \n * to the specified value. Most subclasses will have no need to \n * override this method, relying solely on the {@link #initialValue} \n * method to set the values of thread-locals. \n * \n * @param value the value to be stored in the current thread's copy of \n * this thread-local. \n */ \n public void set(T value) { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n } \n\n在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。\n\n\n线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。\n\n\n为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n \n /** \n * Get the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @return the map \n */ \n ThreadLocalMap getMap(Thread t) { \n return t.threadLocals; \n } \n \n /** \n * Create the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @param firstValue value for the initial entry of the map \n * @param map the map to store. \n */ \n void createMap(Thread t, T firstValue) { \n t.threadLocals = new ThreadLocalMap(this, firstValue); \n } \n \n\n接下来再看一下ThreadLocal类中的get()方法:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n \n /** \n * Returns the value in the current thread's copy of this \n * thread-local variable. If the variable has no value for the \n * current thread, it is first initialized to the value returned \n * by an invocation of the {@link #initialValue} method. \n * \n * @return the current thread's value of this thread-local \n */ \n public T get() { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) { \n ThreadLocalMap.Entry e = map.getEntry(this); \n if (e != null) \n return (T)e.value; \n } \n return setInitialValue(); \n } \n\n\n再来看setInitialValue()方法:\n\n \n /** \n * Variant of set() to establish initialValue. Used instead \n * of set() in case user has overridden the set() method. \n * \n * @return the initial value \n */ \n private T setInitialValue() { \n T value = initialValue(); \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n return value; \n } \n\n\n 获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。\n\n\n 进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。\n\n小结\n ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。\nConnectionManager.java\n\n \n package com.test; \n \n import java.sql.Connection; \n import java.sql.DriverManager; \n import java.sql.SQLException; \n \n public class ConnectionManager { \n \n private static ThreadLocal connectionHolder = new ThreadLocal() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n \"jdbc:mysql://localhost:3306/test\", \"username\", \n \"password\"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n \n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n \n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n } ","source":"_posts/2017_ThreadLocal.md","raw":"---\ntitle: 彻底理解ThreadLocal(转载)\n\ncomments: true \n\ntags: \n - ThreadLocal\n\ncategories: \n - 多线程\n\ndescription: \n\ndate: 2017-02-15\n \n---\n\nhttp://blog.csdn.net/lufeng20/article/details/24314381\n\n## ThreadLocal是什么\n 早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。\n 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。\n 从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。\n 所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。\nThreadLocal的接口方法\nThreadLocal类接口很简单,只有4个方法,我们先来了解一下:\nvoid set(Object value)设置当前线程的线程局部变量的值。\npublic Object get()该方法返回当前线程所对应的线程局部变量。\npublic void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。\nprotected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。\n 值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。\n ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n\n\n\n package com.test; \n \n public class TestNum { \n // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 \n private static ThreadLocal seqNum = new ThreadLocal() { \n public Integer initialValue() { \n return 0; \n } \n }; \n \n // ②获取下一个序列值 \n public int getNextNum() { \n seqNum.set(seqNum.get() + 1); \n return seqNum.get(); \n } \n \n public static void main(String[] args) { \n TestNum sn = new TestNum(); \n // ③ 3个线程共享sn,各自产生序列号 \n TestClient t1 = new TestClient(sn); \n TestClient t2 = new TestClient(sn); \n TestClient t3 = new TestClient(sn); \n t1.start(); \n t2.start(); \n t3.start(); \n } \n \n private static class TestClient extends Thread { \n private TestNum sn; \n \n public TestClient(TestNum sn) { \n this.sn = sn; \n } \n \n public void run() { \n for (int i = 0; i < 3; i++) { \n // ④每个线程打出3个序列值 \n System.out.println(\"thread[\" + Thread.currentThread().getName() + \"] --> sn[\" \n + sn.getNextNum() + \"]\"); \n } \n } \n } \n } \n\n\n 通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:\nthread[Thread-0] --> sn[1]\nthread[Thread-1] --> sn[1]\nthread[Thread-2] --> sn[1]\nthread[Thread-1] --> sn[2]\nthread[Thread-0] --> sn[2]\nthread[Thread-1] --> sn[3]\nthread[Thread-2] --> sn[2]\nthread[Thread-0] --> sn[3]\nthread[Thread-2] --> sn[3]\n考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。\n\nThread同步机制的比较\n ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。\n 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。\n 而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。\n 由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。\n 概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。\n spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。\n 一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:\n通通透透理解ThreadLocal\n 同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。\n 下面的实例能够体现Spring对有状态Bean的改造思路:\n代码清单3 TestDao:非线程安全\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n package com.test; \n \n import java.sql.Connection; \n import java.sql.SQLException; \n import java.sql.Statement; \n \n public class TestDao { \n private Connection conn;// ①一个非线程安全的变量 \n \n public void addTopic() throws SQLException { \n Statement stat = conn.createStatement();// ②引用非线程安全变量 \n // … \n } \n } \n\n\n\n由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:\n代码清单4 TestDao:线程安全\n\n package com.test; \n \n import java.sql.Connection; \n import java.sql.SQLException; \n import java.sql.Statement; \n \n public class TestDaoNew { \n // ①使用ThreadLocal保存Connection变量 \n private static ThreadLocal connThreadLocal = new ThreadLocal(); \n \n public static Connection getConnection() { \n // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection, \n // 并将其保存到线程本地变量中。 \n if (connThreadLocal.get() == null) { \n Connection conn = getConnection(); \n connThreadLocal.set(conn); \n return conn; \n } else { \n return connThreadLocal.get();// ③直接返回线程本地变量 \n } \n } \n \n public void addTopic() throws SQLException { \n // ④从ThreadLocal中获取线程对应的Connection \n Statement stat = getConnection().createStatement(); \n } \n } \n\n不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。\n当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。\n\n\nConnectionManager.java\n\n package com.test; \n \n import java.sql.Connection; \n import java.sql.DriverManager; \n import java.sql.SQLException; \n \n public class ConnectionManager { \n \n private static ThreadLocal connectionHolder = new ThreadLocal() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n \"jdbc:mysql://localhost:3306/test\", \"username\", \n \"password\"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n \n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n \n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n } \n \n\njava.lang.ThreadLocal的具体实现\n那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n \n /** \n * Sets the current thread's copy of this thread-local variable \n * to the specified value. Most subclasses will have no need to \n * override this method, relying solely on the {@link #initialValue} \n * method to set the values of thread-locals. \n * \n * @param value the value to be stored in the current thread's copy of \n * this thread-local. \n */ \n public void set(T value) { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n } \n\n在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。\n\n\n线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。\n\n\n为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n \n /** \n * Get the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @return the map \n */ \n ThreadLocalMap getMap(Thread t) { \n return t.threadLocals; \n } \n \n /** \n * Create the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @param firstValue value for the initial entry of the map \n * @param map the map to store. \n */ \n void createMap(Thread t, T firstValue) { \n t.threadLocals = new ThreadLocalMap(this, firstValue); \n } \n \n\n接下来再看一下ThreadLocal类中的get()方法:\n[java] view plain copy print?在CODE上查看代码片派生到我的代码片\n\n \n /** \n * Returns the value in the current thread's copy of this \n * thread-local variable. If the variable has no value for the \n * current thread, it is first initialized to the value returned \n * by an invocation of the {@link #initialValue} method. \n * \n * @return the current thread's value of this thread-local \n */ \n public T get() { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) { \n ThreadLocalMap.Entry e = map.getEntry(this); \n if (e != null) \n return (T)e.value; \n } \n return setInitialValue(); \n } \n\n\n再来看setInitialValue()方法:\n\n \n /** \n * Variant of set() to establish initialValue. Used instead \n * of set() in case user has overridden the set() method. \n * \n * @return the initial value \n */ \n private T setInitialValue() { \n T value = initialValue(); \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n return value; \n } \n\n\n 获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。\n\n\n 进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。\n\n小结\n ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。\nConnectionManager.java\n\n \n package com.test; \n \n import java.sql.Connection; \n import java.sql.DriverManager; \n import java.sql.SQLException; \n \n public class ConnectionManager { \n \n private static ThreadLocal connectionHolder = new ThreadLocal() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n \"jdbc:mysql://localhost:3306/test\", \"username\", \n \"password\"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n \n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n \n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n } ","slug":"2017_ThreadLocal","published":1,"updated":"2018-01-13T02:29:22.233Z","layout":"post","photos":[],"link":"","_id":"ckm3invgp009524ujtdoqd41d","content":"http://blog.csdn.net/lufeng20/article/details/24314381
\nThreadLocal是什么
早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
ThreadLocal的接口方法
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
void set(Object value)设置当前线程的线程局部变量的值。
public Object get()该方法返回当前线程所对应的线程局部变量。
public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n\npackage com.test; \n\npublic class TestNum { \n // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 \n private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { \n public Integer initialValue() { \n return 0; \n } \n }; \n\n // ②获取下一个序列值 \n public int getNextNum() { \n seqNum.set(seqNum.get() + 1); \n return seqNum.get(); \n } \n\n public static void main(String[] args) { \n TestNum sn = new TestNum(); \n // ③ 3个线程共享sn,各自产生序列号 \n TestClient t1 = new TestClient(sn); \n TestClient t2 = new TestClient(sn); \n TestClient t3 = new TestClient(sn); \n t1.start(); \n t2.start(); \n t3.start(); \n } \n\n private static class TestClient extends Thread { \n private TestNum sn; \n\n public TestClient(TestNum sn) { \n this.sn = sn; \n } \n\n public void run() { \n for (int i = 0; i < 3; i++) { \n // ④每个线程打出3个序列值 \n System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" \n + sn.getNextNum() + "]"); \n } \n } \n } \n} \n
通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:
thread[Thread-0] –> sn[1]
thread[Thread-1] –> sn[1]
thread[Thread-2] –> sn[1]
thread[Thread-1] –> sn[2]
thread[Thread-0] –> sn[2]
thread[Thread-1] –> sn[3]
thread[Thread-2] –> sn[2]
thread[Thread-0] –> sn[3]
thread[Thread-2] –> sn[3]
考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
\nThread同步机制的比较
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:
通通透透理解ThreadLocal
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
下面的实例能够体现Spring对有状态Bean的改造思路:
代码清单3 TestDao:非线程安全
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.SQLException; \nimport java.sql.Statement; \n\npublic class TestDao { \n private Connection conn;// ①一个非线程安全的变量 \n\n public void addTopic() throws SQLException { \n Statement stat = conn.createStatement();// ②引用非线程安全变量 \n // … \n } \n} \n
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
代码清单4 TestDao:线程安全
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.SQLException; \nimport java.sql.Statement; \n\npublic class TestDaoNew { \n // ①使用ThreadLocal保存Connection变量 \n private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); \n\n public static Connection getConnection() { \n // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection, \n // 并将其保存到线程本地变量中。 \n if (connThreadLocal.get() == null) { \n Connection conn = getConnection(); \n connThreadLocal.set(conn); \n return conn; \n } else { \n return connThreadLocal.get();// ③直接返回线程本地变量 \n } \n } \n\n public void addTopic() throws SQLException { \n // ④从ThreadLocal中获取线程对应的Connection \n Statement stat = getConnection().createStatement(); \n } \n} \n
不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
\nConnectionManager.java
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.DriverManager; \nimport java.sql.SQLException; \n\npublic class ConnectionManager { \n\n private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n "jdbc:mysql://localhost:3306/test", "username", \n "password"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n\n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n\n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n} \n
java.lang.ThreadLocal的具体实现
那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n/** \n * Sets the current thread's copy of this thread-local variable \n * to the specified value. Most subclasses will have no need to \n * override this method, relying solely on the {@link #initialValue} \n * method to set the values of thread-locals. \n * \n * @param value the value to be stored in the current thread's copy of \n * this thread-local. \n */ \n public void set(T value) { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n } \n
在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
\n线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
\n为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n/** \n * Get the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @return the map \n */ \nThreadLocalMap getMap(Thread t) { \n return t.threadLocals; \n} \n\n/** \n * Create the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @param firstValue value for the initial entry of the map \n * @param map the map to store. \n */ \nvoid createMap(Thread t, T firstValue) { \n t.threadLocals = new ThreadLocalMap(this, firstValue); \n} \n
接下来再看一下ThreadLocal类中的get()方法:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n/** \n * Returns the value in the current thread's copy of this \n * thread-local variable. If the variable has no value for the \n * current thread, it is first initialized to the value returned \n * by an invocation of the {@link #initialValue} method. \n * \n * @return the current thread's value of this thread-local \n */ \npublic T get() { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) { \n ThreadLocalMap.Entry e = map.getEntry(this); \n if (e != null) \n return (T)e.value; \n } \n return setInitialValue(); \n} \n
再来看setInitialValue()方法:
\n/** \n * Variant of set() to establish initialValue. Used instead \n * of set() in case user has overridden the set() method. \n * \n * @return the initial value \n */ \n private T setInitialValue() { \n T value = initialValue(); \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n return value; \n } \n
获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。
\n 进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
\n小结
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ConnectionManager.java
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.DriverManager; \nimport java.sql.SQLException; \n\npublic class ConnectionManager { \n\n private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n "jdbc:mysql://localhost:3306/test", "username", \n "password"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n\n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n\n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n} \n
","site":{"data":{}},"excerpt":"http://blog.csdn.net/lufeng20/article/details/24314381
\nThreadLocal是什么
早在JDK 1.2的版本中就提供Java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。
ThreadLocal的接口方法
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
void set(Object value)设置当前线程的线程局部变量的值。
public Object get()该方法返回当前线程所对应的线程局部变量。
public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
","more":"package com.test; \n\npublic class TestNum { \n // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值 \n private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() { \n public Integer initialValue() { \n return 0; \n } \n }; \n\n // ②获取下一个序列值 \n public int getNextNum() { \n seqNum.set(seqNum.get() + 1); \n return seqNum.get(); \n } \n\n public static void main(String[] args) { \n TestNum sn = new TestNum(); \n // ③ 3个线程共享sn,各自产生序列号 \n TestClient t1 = new TestClient(sn); \n TestClient t2 = new TestClient(sn); \n TestClient t3 = new TestClient(sn); \n t1.start(); \n t2.start(); \n t3.start(); \n } \n\n private static class TestClient extends Thread { \n private TestNum sn; \n\n public TestClient(TestNum sn) { \n this.sn = sn; \n } \n\n public void run() { \n for (int i = 0; i < 3; i++) { \n // ④每个线程打出3个序列值 \n System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" \n + sn.getNextNum() + "]"); \n } \n } \n } \n} \n
通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如例子中①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个TestNum实例。运行以上代码,在控制台上输出以下的结果:
thread[Thread-0] –> sn[1]
thread[Thread-1] –> sn[1]
thread[Thread-2] –> sn[1]
thread[Thread-1] –> sn[2]
thread[Thread-0] –> sn[2]
thread[Thread-1] –> sn[3]
thread[Thread-2] –> sn[2]
thread[Thread-0] –> sn[3]
thread[Thread-2] –> sn[3]
考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个TestNum实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。
\nThread同步机制的比较
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的ThreadLocal版本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:
通通透透理解ThreadLocal
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
下面的实例能够体现Spring对有状态Bean的改造思路:
代码清单3 TestDao:非线程安全
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.SQLException; \nimport java.sql.Statement; \n\npublic class TestDao { \n private Connection conn;// ①一个非线程安全的变量 \n\n public void addTopic() throws SQLException { \n Statement stat = conn.createStatement();// ②引用非线程安全变量 \n // … \n } \n} \n
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
代码清单4 TestDao:线程安全
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.SQLException; \nimport java.sql.Statement; \n\npublic class TestDaoNew { \n // ①使用ThreadLocal保存Connection变量 \n private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); \n\n public static Connection getConnection() { \n // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection, \n // 并将其保存到线程本地变量中。 \n if (connThreadLocal.get() == null) { \n Connection conn = getConnection(); \n connThreadLocal.set(conn); \n return conn; \n } else { \n return connThreadLocal.get();// ③直接返回线程本地变量 \n } \n } \n\n public void addTopic() throws SQLException { \n // ④从ThreadLocal中获取线程对应的Connection \n Statement stat = getConnection().createStatement(); \n } \n} \n
不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
\nConnectionManager.java
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.DriverManager; \nimport java.sql.SQLException; \n\npublic class ConnectionManager { \n\n private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n "jdbc:mysql://localhost:3306/test", "username", \n "password"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n\n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n\n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n} \n
java.lang.ThreadLocal的具体实现
那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n/** \n * Sets the current thread's copy of this thread-local variable \n * to the specified value. Most subclasses will have no need to \n * override this method, relying solely on the {@link #initialValue} \n * method to set the values of thread-locals. \n * \n * @param value the value to be stored in the current thread's copy of \n * this thread-local. \n */ \n public void set(T value) { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n } \n
在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
\n线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
\n为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n/** \n * Get the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @return the map \n */ \nThreadLocalMap getMap(Thread t) { \n return t.threadLocals; \n} \n\n/** \n * Create the map associated with a ThreadLocal. Overridden in \n * InheritableThreadLocal. \n * \n * @param t the current thread \n * @param firstValue value for the initial entry of the map \n * @param map the map to store. \n */ \nvoid createMap(Thread t, T firstValue) { \n t.threadLocals = new ThreadLocalMap(this, firstValue); \n} \n
接下来再看一下ThreadLocal类中的get()方法:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
\n/** \n * Returns the value in the current thread's copy of this \n * thread-local variable. If the variable has no value for the \n * current thread, it is first initialized to the value returned \n * by an invocation of the {@link #initialValue} method. \n * \n * @return the current thread's value of this thread-local \n */ \npublic T get() { \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) { \n ThreadLocalMap.Entry e = map.getEntry(this); \n if (e != null) \n return (T)e.value; \n } \n return setInitialValue(); \n} \n
再来看setInitialValue()方法:
\n/** \n * Variant of set() to establish initialValue. Used instead \n * of set() in case user has overridden the set() method. \n * \n * @return the initial value \n */ \n private T setInitialValue() { \n T value = initialValue(); \n Thread t = Thread.currentThread(); \n ThreadLocalMap map = getMap(t); \n if (map != null) \n map.set(this, value); \n else \n createMap(t, value); \n return value; \n } \n
获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。
\n 进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
\n小结
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ConnectionManager.java
\npackage com.test; \n\nimport java.sql.Connection; \nimport java.sql.DriverManager; \nimport java.sql.SQLException; \n\npublic class ConnectionManager { \n\n private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { \n @Override \n protected Connection initialValue() { \n Connection conn = null; \n try { \n conn = DriverManager.getConnection( \n "jdbc:mysql://localhost:3306/test", "username", \n "password"); \n } catch (SQLException e) { \n e.printStackTrace(); \n } \n return conn; \n } \n }; \n\n public static Connection getConnection() { \n return connectionHolder.get(); \n } \n\n public static void setConnection(Connection conn) { \n connectionHolder.set(conn); \n } \n} \n
"},{"title":"构建逻辑回归模型实例","comments":1,"description":"使用逻辑回归对信用卡欺诈数据进行训练分析预测","toc":true,"_content":"\n# 逻辑回归\n\n> 逻辑回归是应用非常广泛的一个分类机器学习算法,它将数据拟合到一个logit函数(或者叫做logistic函数)中,从而能够完成对事件发生的概率进行预测。\n\n# 构建逻辑回归模型步骤:\n* 导入数据\n* 预处理数据\n* 对不平衡的数据进行下采样(或者过采样)处理\n* 把处理之后的数据进行切分,切分为训训练集和测试集\n* 对训练集进行交叉验证,同时寻找最佳的正则化参数以减少过拟合\n* 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率\n* 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率\n* 修改阈值以获得更好的召回率和精确率\n\n\n## 1. 数据与任务\n\n### 信用卡欺诈数据\n\n```python\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n%matplotlib inline\n\ndata = pd.read_csv(\"creditcard.csv\")\ndata.head()\n```\n\n{% qnimg 20180307115819.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n### 要使用逻辑回归对数据进行建模 任务:二分类, 把数据分为有欺诈和无欺诈的两种数据\n\n## 2. 使用sklearn进行数据预处理\n\n> 公式为:(X-mean)/std 计算时对每个属性/每列分别进行。\n\nStandardization标准化:将特征数据的分布调整成标准正太分布,也叫高斯分布,也就是使得数据的均值维0,方差为1\n\n标准化的原因在于如果有些特征的方差过大,则会主导目标函数从而使参数估计器无法正确地去学习其他特征。\n\n标准化的过程为两步:去均值的中心化(均值变为0);方差的规模化(方差变为1)。\n\n在sklearn.preprocessing中提供了一个scale的方法,可以实现以上功能。如下面所示:\n\n\n```python\n x = np.array([[1., -1., 2.],\n [2., 0., 0.],\n [0., 1., -1.]])\n # 将每一列特征标准化为标准正太分布,注意,标准化是针对每一列而言的\n x_scale = preprocessing.scale(x)\n x_scale\n```\n\npreprocessing这个模块还提供了一个实用类StandarScaler,它可以在训练数据集上做了标准转换操作之后,把相同的转换应用到测试训练集中。\n可以对训练数据,测试数据应用相同的转换,以后有新的数据进来也可以直接调用,不用再重新把数据放在一起再计算一次了。\n\n```python\n # 调用fit方法,根据已有的训练数据创建一个标准化的转换器\n scaler = preprocessing.StandardScaler().fit(x)\n\n scaler\n \n StandardScaler(copy=True, with_mean=True, with_std=True)\n\n # 使用上面这个转换器去转换训练数据x,调用transform方法\n scaler.transform(x)\n```\n\n*StandardScaler()中可以传入两个参数:with_mean,with_std.这两个都是布尔型的参数,默认情况下都是true,但也可以自定义成false.即不要均值中心化或者不要方差规模化为1.*\n\n### 1. 处理数据 数据下采样\n#### 1.1 预处理数据,修改列\"Amount\"数据分布\n\n```python\n# 导入预处理sklearn中预处理模块的标准化模块\nfrom sklearn.preprocessing import StandardScaler\n\nif 'Amount' in data.columns:\n # 转化特征为新的特征\n data['normAount'] = StandardScaler().fit_transform(data['Amount'].reshape(-1, 1)) # reshape:改变数组的形状,参数为改变后的行列数 \n# fit_transform:对数据进行变换 矩阵旋转:-1表示自动识别 根据另一个矩阵列(行)数确定本行(列)数\n```\n\n### 1.2 数据处理,去除不需要的特征\n\n```python\n# 去掉两个没用的特征(列) axis=1表示对每一行去做这个操作,axis=0表示对每一列做相同的这个操作\nif ('Time') in data.columns:\n data = data.drop(['Time'], axis=1) \nif ('Amount') in data.columns:\n data = data.drop(['Amount'], axis=1) \nprint(data.columns, len(data.columns))\n\n# 1.2.1 数据图形化展示(1的数据太少索引看上去没有)\ncount_classes = pd.value_counts(data['Class'], sort=True).sort_index() # 画图显示按某列分类之后的数据数量比例\ncount_classes.plot(kind = 'bar') # bar:条形图\nplt.xlabel(\"Class\")\nplt.ylabel(\"Frequency\")\n\n# 1.2.2 原数据特征和分类\nX = data.loc[:, data.columns != \"Class\"]\ny = data.loc[:, data.columns == 'Class']\nprint (\"SHAPE\", X.shape, y.shape)\n\n# Class为0的数量远远大于1的数据,需要使数据个数相近 解决方案: 1.下采样(多的数据抽取部分) 2.过采样(少的数据生成更多)\n```\n\n{% qnimg 20180307120121.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n## 3.下采样\n\n> 把数据相对多的减少,可减少为和数据少的数量相同的数量\n\n### 1.3 区分正常数据和异常数据: 通过特征'Class'区分\n```python\n# 1.3.1 异常数据-信息\nnumber_records = data[data.Class == 1]\n# 1.3.2 异常数据个数\nnumber_records_fraud = len(number_records)\n# 1.3.3 异常数据索引\nfrand_indices = np.array(number_records.index)\nprint (\"异常样本索引 有{}个\".format(number_records_fraud), frand_indices[:10])\n\n# 1.3.4 正常数据-索引\nnormal_indices = data[data.Class == 0].index\nprint (\"正常样本索引 有{}个\".format(len(normal_indices)), normal_indices[-10:])\n\nprint (\">>>>>>>>>>>>>>>>>>>>所有数据 正常异常比 \", len(normal_indices), '\\t', number_records_fraud)\nprint(\"**************\")\n```\n\n### 1.4 下采样处理数据 把多的一方数据进行随机减少到与少的一方相同\n```python\n# 在所有的正常样本索引normal_indices中随机获取,随机选取number_records_fraud个\n# np.random.choice: 可以从一个int数字或1维array里随机选取内容,并将选取结果放入n维array中返回。\nrandom_normal_indices = np.random.choice(normal_indices, number_records_fraud, replace=False)\nrandom_normal_indices = np.array(random_normal_indices)\nprint (\"'下采样'后有正常样本个数:\", len(random_normal_indices))\n```\n\n### 1.5 数据索引合并 (意思就是把新的正常数据和原来的异常数据进行拼接)\n```python\nunder_sample_indices = np.concatenate([frand_indices, random_normal_indices])\nprint (\"合并后有样本个数:\", len(under_sample_indices))\nunder_sample_data = data.iloc[under_sample_indices, :]\nprint (\"合并后样本:\", under_sample_data[:1])\n\nprint (\">>>>>>>>>>>>>>>>>>>>下采样数据 正常异常比 \", len(under_sample_data[under_sample_data == 0]), '\\t', len(under_sample_data[under_sample_data == 1]))\n```\n\n### 1.6 获取合并数据中的feature(特征)和label(分类)\n```python\nX_undersample = under_sample_data.loc[:, under_sample_data.columns != 'Class']\ny_undersample = under_sample_data.loc[:, under_sample_data.columns == 'Class']\n\nprint (X_undersample.shape, y_undersample.shape)\nprint (len(under_sample_data[under_sample_data[\"Class\"] == 1]), len(under_sample_data[under_sample_data[\"Class\"] == 0]))\n```\n\n{% qnimg 20180307120308.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n### 2. 切分数据为训练和测试\n```python\n# 切分原始数据 取数据集中80%的数据作为训练集(建立model) 其他20%的为测试集(测试model)\nfrom sklearn.cross_validation import train_test_split\n\nprint (X.shape, y.shape)\n## 2.1.对原始数据进行切分 (最终需要使用原数据集中的测试数据进行测试)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0) # test_size测试集所占比例 random_state切分之前进行乱序\nprint (\"1.训练集数据大小\", X_train.shape)\nprint (\"2.测试集数据大小\", X_test.shape)\nprint (len(X_train) + len(X_test), len(y_train), len(y_test), \"\\n\")\n\n## 2.2.对下采样数据进行切分\nX_train_undersample, X_test_undersample, y_train_undersample, y_test_undersample = train_test_split(X_undersample, y_undersample, test_size=0.2, random_state=0) # test_size测试集大小 random_state切分之前进行乱序\nprint (\"3.训练集数据大小\", X_train_undersample.shape)\nprint (\"4.测试集数据大小\", X_test_undersample.shape)\nprint (len(X_train_undersample) + len(X_test_undersample), len(y_train_undersample), len(y_test_undersample), \"\\n\")\n\n# 切分训练集 把训练集平均切分为三分然后进行交叉验证 (三组数据分别进行建模和验证)\n```\n\n{% qnimg 20180307120401.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n## 评估标准:召回率(recall)\n\n*不适用准确率,因为准确率不能正确的得到所求的,是没用的*\n\n### 模型评估表:\n\n | 相关(Relevant),正类 | 不相关(NonRelevant),负类\n- | :-: | -: \n被检测到(Retrieved) | true positives (TP) | false positives (FP)\n未被检测到(Retrieved) | false negatives (FN) | true negatives (TN)\n\n### 一些术语:\n\n* TP:True Positive,即正确预测出的正样本个数\n* FP:False Positive,即错误预测出的正样本个数(本来是负样本,被我们预测成了正样本)\n* TN:True Negative,即正确预测出的负样本个数\n* FN:False Negative,即错误预测出的负样本个数(本来是正样本,被我们预测成了负样本)\n\n### 分类器性能评价指标\n由以上四个指标,可以进一步衍生出其他三个常用的评价分类器性能的指标\n\n* Precision(精确率):TP÷(TP+FP)TP÷(TP+FP),分类器预测出的正样本中,真实正样本的比例\n* Recall(召回率):TP÷(TP+FN)TP÷(TP+FN),在所有真实正样本中,分类器中能找到多少\n* Accuracy(准确率):(TP+TN)÷(TP+NP+TN+FN)(TP+TN)÷(TP+NP+TN+FN),分类器对整体的判断能力,即正确预测的比例\n\n> 过拟合: 数据在训练集表现很好 在测试集表现很差\n\n```python\nfrom sklearn.linear_model import LogisticRegression # 逻辑回归\n# 注意这里导入的 不是from sklearn.model_selection import KFold\nfrom sklearn.cross_validation import KFold # 交叉验证 # cross_val_score\nfrom sklearn.metrics import confusion_matrix, recall_score, classification_report # 混淆矩阵 \n```\n\n### 3. 通过多次循环交叉验证 确定正则化参数 random_state:随机种子数\n```python\ndef printing_Kfold_scores(x_train_data, y_train_data):\n # KFold:切分数据集 (这里切分为5部分) shuffle:是否每次都\"洗牌\"(Falses时,其效果等同于random_state等于整数,每次划分的结果相同)\n fold = KFold(len(y_train_data), 5, shuffle=False) \n print (type(fold), len(y_train_data), len(fold)) # 长度是5\n \n # 正则化惩罚项(正则化参数) 预设了多个惩罚值,具体使用哪个需要尝试 列举了5个\n c_param_range = [0.01, 0.1, 1, 10, 100]\n \n # 新建DataFrame类型的数据用来存放不同正则化之后的结果\n results_table = pd.DataFrame(index = range(len(c_param_range)), columns = ['C_parameter', 'Mean recall score'])\n results_table['C_parameter'] = c_param_range\n\n # 先按照正则化参数进行循环以确定最好的参数 然后对每个逻辑回归进行交叉验证以获得最好的逻辑回归函数\n # 循环正则化参数 获取最好的c参数\n for index, c_param in enumerate(c_param_range):\n print (\">>>>>>>>>>>>>>>>>>>>>>>>>>\")\n print (\"C_parameter \", c_param)\n \n recall_accs = []\n # 循环进行交叉验证 \n # 每次循环次数为数据切分的大小,切分为n块就交叉验证n次,每次都是区其中n-1块为训练集1块为验证集\n # start=1:开始索引为1\n # iteration为索引 indices为划分好的数据:其中有n-1数据大小的训练集以及1数据代销的验证集\n # 循环中集合每次都不一样,所有的数据都会当一次验证集:例如 三个数据[1,2,3],循环使得数据分别为训练和验证每次为:[[1],[2, 3]], [[2],[1, 3]], [[3],[1, 2]]\n for iteration, indices in enumerate(fold, start=1):\n # 这里并不是用fold直接划分训练集数据, 而是把索引进行1:5的划分, 然后按照索引获取数据中的对应的数据\n print (iteration, len(indices[0]), len(indices[1]))\n \n # 建立逻辑回归模型\n lr = LogisticRegression(C = c_param, penalty = 'l1') # C:正则化参数; penalty:惩罚项:使用L1正则化(惩罚) ‘l1’ or ‘l2’(默认: ‘l2’)\n # 在调参时如果我们主要的目的只是为了解决过拟合,一般penalty选择L2正则化就够了。\n # 但是如果选择L2正则化发现还是过拟合,即预测效果差的时候,就可以考虑L1正则化。\n # 另外,如果模型的特征非常多,我们希望一些不重要的特征系数归零,从而让模型系数稀疏化的话,也可以使用L1正则化。\n # print (\"LR-逻辑回归表达式---\", lr)\n \n # 训练 参数一:训练数据特征(feature) 参数二:训练数据分类(label)\n lr.fit(x_train_data.iloc[indices[0],:], y_train_data.iloc[indices[0],:].values.ravel())\n \n # 预测\n y_pred_undersample = lr.predict(x_train_data.iloc[indices[1], :].values)\n \n # 计算召回率 召回率 =提取出的正确信息条数 /样本中的信息条数。通俗地说,就是所有准确的条目有多少被检索出来了。\n # 参数: 1.真实数据集 2.预测数据集\n recall_acc = recall_score(y_train_data.iloc[indices[1],:].values, y_pred_undersample)\n recall_accs.append(recall_acc)\n print (len(indices), \"Iteration \", iteration, \": recall score = \", recall_acc)\n \n # 求每个惩罚值经过交叉验证之后平均召回率\n results_table.loc[index, 'Mean recall score'] = np.mean(recall_accs)\n\n print ('\\nMean recall score ', np.mean(recall_accs), '\\n')\n \n print (results_table)\n \n best_c = results_table.loc[results_table['Mean recall score'].idxmax()]['C_parameter']\n print (\"finally-------best is--------> \", best_c)\n \n return best_c\n\nbest_c = printing_Kfold_scores(X_train_undersample, y_train_undersample)\n```\n\n{% qnimg 20180307120616.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307120702.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307120717.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n#### ndarray数据格式化: set_printoptions\n\n> set_printoptions(precision=None, \n threshold=None, \n edgeitems=None,\n linewidth=None, \n suppress=None,\n nanstr=None,\n infstr=None,\n formatter=None)\n\n* precision:输出结果保留精度的位数 (num)\n* threshold:array数量的个数在小于threshold的时候不会被折叠 (num)\n* edgeitems:在array已经被折叠后,开头和结尾都会显示edgeitems个数 (num)\n* formatter:这个很有意思,像python3里面str.format(),就是可以对你的输出进行自定义的格式化 其他的暂时没用到\n\n### 4. 使用最好的正则化参数 构建逻辑回归模型并进行测试\n```python\n# 构建逻辑回归模型\nlr = LogisticRegression(C = best_c, penalty='l1')\n# 训练回归模型\nlr.fit(X_train_undersample, y_train_undersample.values.ravel())\n# 使用模型进行测试\ny_pred_undersample = lr.predict(X_test_undersample.values)\n# y_pred_undersample为预测(分类)值, y_test_undersample为真实测试集的(分类)值\n\nprint (type(y_pred_undersample), len(y_pred_undersample), \"\\n\")\n\n# 打印和绘制混淆矩阵\nimport itertools\ndef plot_confusion_matrix(cm, classes, title='Confussion matrix', cmap=plt.cm.Blues):\n #设置显示混淆矩阵\n plt.imshow(cm, interpolation='nearest', cmap=cmap)\n plt.title(title)\n plt.colorbar()\n \n # 设置坐标数\n tick_marks = np.arange(len(classes))\n plt.xticks(tick_marks, classes, rotation=0)\n plt.yticks(tick_marks, classes)\n \n thresh = cm.max() / 2\n \n # itertools.product可进行多层次循环 传入参数个数(n)和索引个数相同 可循环n^2次\n # 设置每个方块中的文字 \n for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\n # print (j, i, cm[i, j])\n # 因为i表示横坐标的位置, j表示纵坐标的位置 所以需要把i和j交换位置\n plt.text(j, i, cm[i, j], horizontalalignment=\"center\", color=\"white\" if cm[i, j] > thresh else \"black\")\n \n plt.tight_layout()\n # 设置坐标文字\n plt.ylabel(\"True label\") # 真实数据 \n plt.xlabel(\"Predicted label\") # 预测数据 1表示正例 0表示负例\n\n# 画混淆矩阵图 参数: 1.y_true, 2.y_pred\ncnf_matrix = confusion_matrix(y_test_undersample, y_pred_undersample)\nnp.set_printoptions(precision=2)\n\nprint (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n\nclass_names = [0, 1]\nplt.figure()\nplot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')\nplt.show()\n\n# 由图可见, 召回率为 85 / (85 + 6) = 93.41%\n# 精确率为 (85) / (85 + 9) = 90.43%\n# 准确率为 (85 + 97) / (85 + 9 + 6 + 97) = 92.39%\n\n# 以上计算都是基于下采样数据集的,还需要在原数据的测试集上进行测试操作 (与上面同理)\n```\n\n{% qnimg 20180307120847.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n### 4. 使用最好的正则化参数 构建逻辑回归模型并进行测试 (使用原始数据的测试集和训练集)\n```python\n# 在原数据的测试集上进行测试操作\nlr = LogisticRegression(C = best_c, penalty='l1')\nlr.fit(X_train, y_train.values.ravel())\ny_pred = lr.predict(X_test.values)\n# y_pred为预测(分类)值, y_test为真实测试集的(分类)值\n\n\ncnf_matrix = confusion_matrix(y_test, y_pred)\nnp.set_printoptions(precision=2)\n\nprint (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n\nclass_names = [0, 1]\nplt.figure()\nplot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')\nplt.show()\n```\n\n### 5. 修改阈值以获取最好的逻辑回归模型\n```python\n# 阈值: 默认使用sigma函数默认值:0.5, 意思是当预测概率大于0.5表示True,概率小鱼0.5表示False\n\nlr = LogisticRegression(C = best_c, penalty='l1')\n# 训练\nlr.fit(X_train, y_train.values.ravel())\n# 预测 这里是预测概率值 每个数据的预测包含两个值,对于二分类问题,也就是被判断为0的概率和被判断为1的概率\ny_pred_undersample_proba = lr.predict_proba(X_test_undersample.values) # 预测概率值而不是类别值\n\n# 可能的阈值\nthresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]\n\nplt.figure(figsize=(10, 10)) # 画图域\n\nfor index, i in enumerate(thresholds):\n # 预测概率\n y_test_predictions_high_recall = y_pred_undersample_proba[:, 1] > i\n \n plt.subplot(3, 3, index + 1)\n \n cnf_matrix = confusion_matrix(y_test_undersample, y_test_predictions_high_recall)\n np.set_printoptions(precision=2)\n \n print (i, \"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n \n class_names = [0, 1]\n plot_confusion_matrix(cnf_matrix, classes=class_names, title=\"Threshold >= %s\" %i)\n \n## 随着阈值上升 召回率不断变化 其中本来是1的被误检测为0的越来越多 可见 要选取最合适的阈值以达到召回率最高\n```\n\n{% qnimg 20180307120937.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307121001.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307121022.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n## 过采样\n\n> 把数据相对少的增加,可增加为和数据多的数量相同的数量 (生成)\n\n```python\nimport pandas as pd\nfrom imblearn.over_sampling import SMOTE\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import confusion_matrix\nfrom sklearn.model_selection import train_test_split\n\ncredit_cards = pd.read_csv(\"creditcard.csv\")\ncolumns = credit_cards.columns\n\nfeatures_columns = columns.delete(len(columns) - 1) #删除最后一列数据\nprint (features_columns)\n\nfeatures = credit_cards[features_columns]\nlabels = credit_cards['Class']\n\nprint (\"原始的数据个数\", (credit_cards[credit_cards['Class'] == 0]).shape, (credit_cards[credit_cards['Class'] == 1]).shape)\n\n```\n\n\n```python\nfeatures_train, features_test, labels_train, labels_test = train_test_split(features, labels, test_size=0.2, random_state=0)\nprint (features_train.shape, features_test.shape, labels_train.shape, labels_test.shape)\n```\n{% qnimg 20180307121128.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n```python \noversampler = SMOTE(random_state = 0) # SMOTE随机生成数据 生成只能是训练集生成数据, 而测试集不生成\n# 只生成训练集数据 使得Class为1和为0的数量相同 返回训练集的特征和分类\nos_features, os_labels = oversampler.fit_sample(features_train, labels_train)\n\nprint (\"可见 的确生成了新的数据,补充了异常的数据 \", len(os_labels[os_labels[:] == 1]), len(os_labels[os_labels[:] == 0]))\n\nprint ((os_features).shape, len(os_features[os_features == 1]), len(os_features[os_features == 0]), \n (os_labels).shape, len(os_labels[os_labels == 1]), len(os_labels[os_labels == 0]))\n```\n{% qnimg 20180307121154.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n```python\nos_features = pd.DataFrame(os_features)\nos_labels = pd.DataFrame(os_labels)\n# 获取最佳参数\nbest_c = printing_Kfold_scores(os_features, os_labels)\n\n# plot_confusion_matrix\n```\n\n```python\nlr = LogisticRegression(C = best_c, penalty='l1')\n# 训练 使用生成的数据\nlr.fit(os_features, os_labels.values.ravel())\n# 使用真实数据测试\ny_pred = lr.predict(features_test.values)\n\n# 打印和绘制混淆矩阵\ncnf_matrix = confusion_matrix(labels_test, y_pred)\nnp.set_printoptions(precision=2)\n\nprint (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n\nclass_names = [0, 1]\nplt.figure()\nplot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')\n\nplt.show()\n```\n> 个人博客 欢迎来访: http://zj2626.github.io","source":"_posts/2018030701.md","raw":"---\ntitle: 构建逻辑回归模型实例\n\ncomments: true \n\ntags: \n - 逻辑回归\n - python\n - 机器学习\n\ncategories: \n - 机器学习\n\ndescription: 使用逻辑回归对信用卡欺诈数据进行训练分析预测\n\ntoc: true\n \n---\n\n# 逻辑回归\n\n> 逻辑回归是应用非常广泛的一个分类机器学习算法,它将数据拟合到一个logit函数(或者叫做logistic函数)中,从而能够完成对事件发生的概率进行预测。\n\n# 构建逻辑回归模型步骤:\n* 导入数据\n* 预处理数据\n* 对不平衡的数据进行下采样(或者过采样)处理\n* 把处理之后的数据进行切分,切分为训训练集和测试集\n* 对训练集进行交叉验证,同时寻找最佳的正则化参数以减少过拟合\n* 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率\n* 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率\n* 修改阈值以获得更好的召回率和精确率\n\n\n## 1. 数据与任务\n\n### 信用卡欺诈数据\n\n```python\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport numpy as np\n\n%matplotlib inline\n\ndata = pd.read_csv(\"creditcard.csv\")\ndata.head()\n```\n\n{% qnimg 20180307115819.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n### 要使用逻辑回归对数据进行建模 任务:二分类, 把数据分为有欺诈和无欺诈的两种数据\n\n## 2. 使用sklearn进行数据预处理\n\n> 公式为:(X-mean)/std 计算时对每个属性/每列分别进行。\n\nStandardization标准化:将特征数据的分布调整成标准正太分布,也叫高斯分布,也就是使得数据的均值维0,方差为1\n\n标准化的原因在于如果有些特征的方差过大,则会主导目标函数从而使参数估计器无法正确地去学习其他特征。\n\n标准化的过程为两步:去均值的中心化(均值变为0);方差的规模化(方差变为1)。\n\n在sklearn.preprocessing中提供了一个scale的方法,可以实现以上功能。如下面所示:\n\n\n```python\n x = np.array([[1., -1., 2.],\n [2., 0., 0.],\n [0., 1., -1.]])\n # 将每一列特征标准化为标准正太分布,注意,标准化是针对每一列而言的\n x_scale = preprocessing.scale(x)\n x_scale\n```\n\npreprocessing这个模块还提供了一个实用类StandarScaler,它可以在训练数据集上做了标准转换操作之后,把相同的转换应用到测试训练集中。\n可以对训练数据,测试数据应用相同的转换,以后有新的数据进来也可以直接调用,不用再重新把数据放在一起再计算一次了。\n\n```python\n # 调用fit方法,根据已有的训练数据创建一个标准化的转换器\n scaler = preprocessing.StandardScaler().fit(x)\n\n scaler\n \n StandardScaler(copy=True, with_mean=True, with_std=True)\n\n # 使用上面这个转换器去转换训练数据x,调用transform方法\n scaler.transform(x)\n```\n\n*StandardScaler()中可以传入两个参数:with_mean,with_std.这两个都是布尔型的参数,默认情况下都是true,但也可以自定义成false.即不要均值中心化或者不要方差规模化为1.*\n\n### 1. 处理数据 数据下采样\n#### 1.1 预处理数据,修改列\"Amount\"数据分布\n\n```python\n# 导入预处理sklearn中预处理模块的标准化模块\nfrom sklearn.preprocessing import StandardScaler\n\nif 'Amount' in data.columns:\n # 转化特征为新的特征\n data['normAount'] = StandardScaler().fit_transform(data['Amount'].reshape(-1, 1)) # reshape:改变数组的形状,参数为改变后的行列数 \n# fit_transform:对数据进行变换 矩阵旋转:-1表示自动识别 根据另一个矩阵列(行)数确定本行(列)数\n```\n\n### 1.2 数据处理,去除不需要的特征\n\n```python\n# 去掉两个没用的特征(列) axis=1表示对每一行去做这个操作,axis=0表示对每一列做相同的这个操作\nif ('Time') in data.columns:\n data = data.drop(['Time'], axis=1) \nif ('Amount') in data.columns:\n data = data.drop(['Amount'], axis=1) \nprint(data.columns, len(data.columns))\n\n# 1.2.1 数据图形化展示(1的数据太少索引看上去没有)\ncount_classes = pd.value_counts(data['Class'], sort=True).sort_index() # 画图显示按某列分类之后的数据数量比例\ncount_classes.plot(kind = 'bar') # bar:条形图\nplt.xlabel(\"Class\")\nplt.ylabel(\"Frequency\")\n\n# 1.2.2 原数据特征和分类\nX = data.loc[:, data.columns != \"Class\"]\ny = data.loc[:, data.columns == 'Class']\nprint (\"SHAPE\", X.shape, y.shape)\n\n# Class为0的数量远远大于1的数据,需要使数据个数相近 解决方案: 1.下采样(多的数据抽取部分) 2.过采样(少的数据生成更多)\n```\n\n{% qnimg 20180307120121.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n## 3.下采样\n\n> 把数据相对多的减少,可减少为和数据少的数量相同的数量\n\n### 1.3 区分正常数据和异常数据: 通过特征'Class'区分\n```python\n# 1.3.1 异常数据-信息\nnumber_records = data[data.Class == 1]\n# 1.3.2 异常数据个数\nnumber_records_fraud = len(number_records)\n# 1.3.3 异常数据索引\nfrand_indices = np.array(number_records.index)\nprint (\"异常样本索引 有{}个\".format(number_records_fraud), frand_indices[:10])\n\n# 1.3.4 正常数据-索引\nnormal_indices = data[data.Class == 0].index\nprint (\"正常样本索引 有{}个\".format(len(normal_indices)), normal_indices[-10:])\n\nprint (\">>>>>>>>>>>>>>>>>>>>所有数据 正常异常比 \", len(normal_indices), '\\t', number_records_fraud)\nprint(\"**************\")\n```\n\n### 1.4 下采样处理数据 把多的一方数据进行随机减少到与少的一方相同\n```python\n# 在所有的正常样本索引normal_indices中随机获取,随机选取number_records_fraud个\n# np.random.choice: 可以从一个int数字或1维array里随机选取内容,并将选取结果放入n维array中返回。\nrandom_normal_indices = np.random.choice(normal_indices, number_records_fraud, replace=False)\nrandom_normal_indices = np.array(random_normal_indices)\nprint (\"'下采样'后有正常样本个数:\", len(random_normal_indices))\n```\n\n### 1.5 数据索引合并 (意思就是把新的正常数据和原来的异常数据进行拼接)\n```python\nunder_sample_indices = np.concatenate([frand_indices, random_normal_indices])\nprint (\"合并后有样本个数:\", len(under_sample_indices))\nunder_sample_data = data.iloc[under_sample_indices, :]\nprint (\"合并后样本:\", under_sample_data[:1])\n\nprint (\">>>>>>>>>>>>>>>>>>>>下采样数据 正常异常比 \", len(under_sample_data[under_sample_data == 0]), '\\t', len(under_sample_data[under_sample_data == 1]))\n```\n\n### 1.6 获取合并数据中的feature(特征)和label(分类)\n```python\nX_undersample = under_sample_data.loc[:, under_sample_data.columns != 'Class']\ny_undersample = under_sample_data.loc[:, under_sample_data.columns == 'Class']\n\nprint (X_undersample.shape, y_undersample.shape)\nprint (len(under_sample_data[under_sample_data[\"Class\"] == 1]), len(under_sample_data[under_sample_data[\"Class\"] == 0]))\n```\n\n{% qnimg 20180307120308.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n### 2. 切分数据为训练和测试\n```python\n# 切分原始数据 取数据集中80%的数据作为训练集(建立model) 其他20%的为测试集(测试model)\nfrom sklearn.cross_validation import train_test_split\n\nprint (X.shape, y.shape)\n## 2.1.对原始数据进行切分 (最终需要使用原数据集中的测试数据进行测试)\nX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0) # test_size测试集所占比例 random_state切分之前进行乱序\nprint (\"1.训练集数据大小\", X_train.shape)\nprint (\"2.测试集数据大小\", X_test.shape)\nprint (len(X_train) + len(X_test), len(y_train), len(y_test), \"\\n\")\n\n## 2.2.对下采样数据进行切分\nX_train_undersample, X_test_undersample, y_train_undersample, y_test_undersample = train_test_split(X_undersample, y_undersample, test_size=0.2, random_state=0) # test_size测试集大小 random_state切分之前进行乱序\nprint (\"3.训练集数据大小\", X_train_undersample.shape)\nprint (\"4.测试集数据大小\", X_test_undersample.shape)\nprint (len(X_train_undersample) + len(X_test_undersample), len(y_train_undersample), len(y_test_undersample), \"\\n\")\n\n# 切分训练集 把训练集平均切分为三分然后进行交叉验证 (三组数据分别进行建模和验证)\n```\n\n{% qnimg 20180307120401.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n## 评估标准:召回率(recall)\n\n*不适用准确率,因为准确率不能正确的得到所求的,是没用的*\n\n### 模型评估表:\n\n | 相关(Relevant),正类 | 不相关(NonRelevant),负类\n- | :-: | -: \n被检测到(Retrieved) | true positives (TP) | false positives (FP)\n未被检测到(Retrieved) | false negatives (FN) | true negatives (TN)\n\n### 一些术语:\n\n* TP:True Positive,即正确预测出的正样本个数\n* FP:False Positive,即错误预测出的正样本个数(本来是负样本,被我们预测成了正样本)\n* TN:True Negative,即正确预测出的负样本个数\n* FN:False Negative,即错误预测出的负样本个数(本来是正样本,被我们预测成了负样本)\n\n### 分类器性能评价指标\n由以上四个指标,可以进一步衍生出其他三个常用的评价分类器性能的指标\n\n* Precision(精确率):TP÷(TP+FP)TP÷(TP+FP),分类器预测出的正样本中,真实正样本的比例\n* Recall(召回率):TP÷(TP+FN)TP÷(TP+FN),在所有真实正样本中,分类器中能找到多少\n* Accuracy(准确率):(TP+TN)÷(TP+NP+TN+FN)(TP+TN)÷(TP+NP+TN+FN),分类器对整体的判断能力,即正确预测的比例\n\n> 过拟合: 数据在训练集表现很好 在测试集表现很差\n\n```python\nfrom sklearn.linear_model import LogisticRegression # 逻辑回归\n# 注意这里导入的 不是from sklearn.model_selection import KFold\nfrom sklearn.cross_validation import KFold # 交叉验证 # cross_val_score\nfrom sklearn.metrics import confusion_matrix, recall_score, classification_report # 混淆矩阵 \n```\n\n### 3. 通过多次循环交叉验证 确定正则化参数 random_state:随机种子数\n```python\ndef printing_Kfold_scores(x_train_data, y_train_data):\n # KFold:切分数据集 (这里切分为5部分) shuffle:是否每次都\"洗牌\"(Falses时,其效果等同于random_state等于整数,每次划分的结果相同)\n fold = KFold(len(y_train_data), 5, shuffle=False) \n print (type(fold), len(y_train_data), len(fold)) # 长度是5\n \n # 正则化惩罚项(正则化参数) 预设了多个惩罚值,具体使用哪个需要尝试 列举了5个\n c_param_range = [0.01, 0.1, 1, 10, 100]\n \n # 新建DataFrame类型的数据用来存放不同正则化之后的结果\n results_table = pd.DataFrame(index = range(len(c_param_range)), columns = ['C_parameter', 'Mean recall score'])\n results_table['C_parameter'] = c_param_range\n\n # 先按照正则化参数进行循环以确定最好的参数 然后对每个逻辑回归进行交叉验证以获得最好的逻辑回归函数\n # 循环正则化参数 获取最好的c参数\n for index, c_param in enumerate(c_param_range):\n print (\">>>>>>>>>>>>>>>>>>>>>>>>>>\")\n print (\"C_parameter \", c_param)\n \n recall_accs = []\n # 循环进行交叉验证 \n # 每次循环次数为数据切分的大小,切分为n块就交叉验证n次,每次都是区其中n-1块为训练集1块为验证集\n # start=1:开始索引为1\n # iteration为索引 indices为划分好的数据:其中有n-1数据大小的训练集以及1数据代销的验证集\n # 循环中集合每次都不一样,所有的数据都会当一次验证集:例如 三个数据[1,2,3],循环使得数据分别为训练和验证每次为:[[1],[2, 3]], [[2],[1, 3]], [[3],[1, 2]]\n for iteration, indices in enumerate(fold, start=1):\n # 这里并不是用fold直接划分训练集数据, 而是把索引进行1:5的划分, 然后按照索引获取数据中的对应的数据\n print (iteration, len(indices[0]), len(indices[1]))\n \n # 建立逻辑回归模型\n lr = LogisticRegression(C = c_param, penalty = 'l1') # C:正则化参数; penalty:惩罚项:使用L1正则化(惩罚) ‘l1’ or ‘l2’(默认: ‘l2’)\n # 在调参时如果我们主要的目的只是为了解决过拟合,一般penalty选择L2正则化就够了。\n # 但是如果选择L2正则化发现还是过拟合,即预测效果差的时候,就可以考虑L1正则化。\n # 另外,如果模型的特征非常多,我们希望一些不重要的特征系数归零,从而让模型系数稀疏化的话,也可以使用L1正则化。\n # print (\"LR-逻辑回归表达式---\", lr)\n \n # 训练 参数一:训练数据特征(feature) 参数二:训练数据分类(label)\n lr.fit(x_train_data.iloc[indices[0],:], y_train_data.iloc[indices[0],:].values.ravel())\n \n # 预测\n y_pred_undersample = lr.predict(x_train_data.iloc[indices[1], :].values)\n \n # 计算召回率 召回率 =提取出的正确信息条数 /样本中的信息条数。通俗地说,就是所有准确的条目有多少被检索出来了。\n # 参数: 1.真实数据集 2.预测数据集\n recall_acc = recall_score(y_train_data.iloc[indices[1],:].values, y_pred_undersample)\n recall_accs.append(recall_acc)\n print (len(indices), \"Iteration \", iteration, \": recall score = \", recall_acc)\n \n # 求每个惩罚值经过交叉验证之后平均召回率\n results_table.loc[index, 'Mean recall score'] = np.mean(recall_accs)\n\n print ('\\nMean recall score ', np.mean(recall_accs), '\\n')\n \n print (results_table)\n \n best_c = results_table.loc[results_table['Mean recall score'].idxmax()]['C_parameter']\n print (\"finally-------best is--------> \", best_c)\n \n return best_c\n\nbest_c = printing_Kfold_scores(X_train_undersample, y_train_undersample)\n```\n\n{% qnimg 20180307120616.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307120702.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307120717.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n#### ndarray数据格式化: set_printoptions\n\n> set_printoptions(precision=None, \n threshold=None, \n edgeitems=None,\n linewidth=None, \n suppress=None,\n nanstr=None,\n infstr=None,\n formatter=None)\n\n* precision:输出结果保留精度的位数 (num)\n* threshold:array数量的个数在小于threshold的时候不会被折叠 (num)\n* edgeitems:在array已经被折叠后,开头和结尾都会显示edgeitems个数 (num)\n* formatter:这个很有意思,像python3里面str.format(),就是可以对你的输出进行自定义的格式化 其他的暂时没用到\n\n### 4. 使用最好的正则化参数 构建逻辑回归模型并进行测试\n```python\n# 构建逻辑回归模型\nlr = LogisticRegression(C = best_c, penalty='l1')\n# 训练回归模型\nlr.fit(X_train_undersample, y_train_undersample.values.ravel())\n# 使用模型进行测试\ny_pred_undersample = lr.predict(X_test_undersample.values)\n# y_pred_undersample为预测(分类)值, y_test_undersample为真实测试集的(分类)值\n\nprint (type(y_pred_undersample), len(y_pred_undersample), \"\\n\")\n\n# 打印和绘制混淆矩阵\nimport itertools\ndef plot_confusion_matrix(cm, classes, title='Confussion matrix', cmap=plt.cm.Blues):\n #设置显示混淆矩阵\n plt.imshow(cm, interpolation='nearest', cmap=cmap)\n plt.title(title)\n plt.colorbar()\n \n # 设置坐标数\n tick_marks = np.arange(len(classes))\n plt.xticks(tick_marks, classes, rotation=0)\n plt.yticks(tick_marks, classes)\n \n thresh = cm.max() / 2\n \n # itertools.product可进行多层次循环 传入参数个数(n)和索引个数相同 可循环n^2次\n # 设置每个方块中的文字 \n for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):\n # print (j, i, cm[i, j])\n # 因为i表示横坐标的位置, j表示纵坐标的位置 所以需要把i和j交换位置\n plt.text(j, i, cm[i, j], horizontalalignment=\"center\", color=\"white\" if cm[i, j] > thresh else \"black\")\n \n plt.tight_layout()\n # 设置坐标文字\n plt.ylabel(\"True label\") # 真实数据 \n plt.xlabel(\"Predicted label\") # 预测数据 1表示正例 0表示负例\n\n# 画混淆矩阵图 参数: 1.y_true, 2.y_pred\ncnf_matrix = confusion_matrix(y_test_undersample, y_pred_undersample)\nnp.set_printoptions(precision=2)\n\nprint (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n\nclass_names = [0, 1]\nplt.figure()\nplot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')\nplt.show()\n\n# 由图可见, 召回率为 85 / (85 + 6) = 93.41%\n# 精确率为 (85) / (85 + 9) = 90.43%\n# 准确率为 (85 + 97) / (85 + 9 + 6 + 97) = 92.39%\n\n# 以上计算都是基于下采样数据集的,还需要在原数据的测试集上进行测试操作 (与上面同理)\n```\n\n{% qnimg 20180307120847.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n### 4. 使用最好的正则化参数 构建逻辑回归模型并进行测试 (使用原始数据的测试集和训练集)\n```python\n# 在原数据的测试集上进行测试操作\nlr = LogisticRegression(C = best_c, penalty='l1')\nlr.fit(X_train, y_train.values.ravel())\ny_pred = lr.predict(X_test.values)\n# y_pred为预测(分类)值, y_test为真实测试集的(分类)值\n\n\ncnf_matrix = confusion_matrix(y_test, y_pred)\nnp.set_printoptions(precision=2)\n\nprint (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n\nclass_names = [0, 1]\nplt.figure()\nplot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')\nplt.show()\n```\n\n### 5. 修改阈值以获取最好的逻辑回归模型\n```python\n# 阈值: 默认使用sigma函数默认值:0.5, 意思是当预测概率大于0.5表示True,概率小鱼0.5表示False\n\nlr = LogisticRegression(C = best_c, penalty='l1')\n# 训练\nlr.fit(X_train, y_train.values.ravel())\n# 预测 这里是预测概率值 每个数据的预测包含两个值,对于二分类问题,也就是被判断为0的概率和被判断为1的概率\ny_pred_undersample_proba = lr.predict_proba(X_test_undersample.values) # 预测概率值而不是类别值\n\n# 可能的阈值\nthresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]\n\nplt.figure(figsize=(10, 10)) # 画图域\n\nfor index, i in enumerate(thresholds):\n # 预测概率\n y_test_predictions_high_recall = y_pred_undersample_proba[:, 1] > i\n \n plt.subplot(3, 3, index + 1)\n \n cnf_matrix = confusion_matrix(y_test_undersample, y_test_predictions_high_recall)\n np.set_printoptions(precision=2)\n \n print (i, \"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n \n class_names = [0, 1]\n plot_confusion_matrix(cnf_matrix, classes=class_names, title=\"Threshold >= %s\" %i)\n \n## 随着阈值上升 召回率不断变化 其中本来是1的被误检测为0的越来越多 可见 要选取最合适的阈值以达到召回率最高\n```\n\n{% qnimg 20180307120937.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307121001.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n{% qnimg 20180307121022.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n## 过采样\n\n> 把数据相对少的增加,可增加为和数据多的数量相同的数量 (生成)\n\n```python\nimport pandas as pd\nfrom imblearn.over_sampling import SMOTE\nfrom sklearn.ensemble import RandomForestClassifier\nfrom sklearn.metrics import confusion_matrix\nfrom sklearn.model_selection import train_test_split\n\ncredit_cards = pd.read_csv(\"creditcard.csv\")\ncolumns = credit_cards.columns\n\nfeatures_columns = columns.delete(len(columns) - 1) #删除最后一列数据\nprint (features_columns)\n\nfeatures = credit_cards[features_columns]\nlabels = credit_cards['Class']\n\nprint (\"原始的数据个数\", (credit_cards[credit_cards['Class'] == 0]).shape, (credit_cards[credit_cards['Class'] == 1]).shape)\n\n```\n\n\n```python\nfeatures_train, features_test, labels_train, labels_test = train_test_split(features, labels, test_size=0.2, random_state=0)\nprint (features_train.shape, features_test.shape, labels_train.shape, labels_test.shape)\n```\n{% qnimg 20180307121128.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n```python \noversampler = SMOTE(random_state = 0) # SMOTE随机生成数据 生成只能是训练集生成数据, 而测试集不生成\n# 只生成训练集数据 使得Class为1和为0的数量相同 返回训练集的特征和分类\nos_features, os_labels = oversampler.fit_sample(features_train, labels_train)\n\nprint (\"可见 的确生成了新的数据,补充了异常的数据 \", len(os_labels[os_labels[:] == 1]), len(os_labels[os_labels[:] == 0]))\n\nprint ((os_features).shape, len(os_features[os_features == 1]), len(os_features[os_features == 0]), \n (os_labels).shape, len(os_labels[os_labels == 1]), len(os_labels[os_labels == 0]))\n```\n{% qnimg 20180307121154.png title:如图 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n```python\nos_features = pd.DataFrame(os_features)\nos_labels = pd.DataFrame(os_labels)\n# 获取最佳参数\nbest_c = printing_Kfold_scores(os_features, os_labels)\n\n# plot_confusion_matrix\n```\n\n```python\nlr = LogisticRegression(C = best_c, penalty='l1')\n# 训练 使用生成的数据\nlr.fit(os_features, os_labels.values.ravel())\n# 使用真实数据测试\ny_pred = lr.predict(features_test.values)\n\n# 打印和绘制混淆矩阵\ncnf_matrix = confusion_matrix(labels_test, y_pred)\nnp.set_printoptions(precision=2)\n\nprint (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1]))\n\nclass_names = [0, 1]\nplt.figure()\nplot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')\n\nplt.show()\n```\n> 个人博客 欢迎来访: http://zj2626.github.io","slug":"2018030701","published":1,"date":"2020-01-15T05:50:39.653Z","updated":"2021-03-10T13:50:15.280Z","layout":"post","photos":[],"link":"","_id":"ckm3invgr009724ujyv7th8cx","content":"逻辑回归
\n逻辑回归是应用非常广泛的一个分类机器学习算法,它将数据拟合到一个logit函数(或者叫做logistic函数)中,从而能够完成对事件发生的概率进行预测。
\n
\n构建逻辑回归模型步骤:
\n- 导入数据
\n- 预处理数据
\n- 对不平衡的数据进行下采样(或者过采样)处理
\n- 把处理之后的数据进行切分,切分为训训练集和测试集
\n- 对训练集进行交叉验证,同时寻找最佳的正则化参数以减少过拟合
\n- 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率
\n- 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率
\n- 修改阈值以获得更好的召回率和精确率
\n
\n1. 数据与任务
信用卡欺诈数据
1 2 3 4 5 6 7 8
| import pandas as pd import matplotlib.pyplot as plt import numpy as np
%matplotlib inline
data = pd.read_csv(\"creditcard.csv\") data.head()
|
\n\n要使用逻辑回归对数据进行建模 任务:二分类, 把数据分为有欺诈和无欺诈的两种数据
2. 使用sklearn进行数据预处理
\n公式为:(X-mean)/std 计算时对每个属性/每列分别进行。
\n
\nStandardization标准化:将特征数据的分布调整成标准正太分布,也叫高斯分布,也就是使得数据的均值维0,方差为1
\n标准化的原因在于如果有些特征的方差过大,则会主导目标函数从而使参数估计器无法正确地去学习其他特征。
\n标准化的过程为两步:去均值的中心化(均值变为0);方差的规模化(方差变为1)。
\n在sklearn.preprocessing中提供了一个scale的方法,可以实现以上功能。如下面所示:
\n1 2 3 4 5 6
| x = np.array([[1., -1., 2.], [2., 0., 0.], [0., 1., -1.]])
x_scale = preprocessing.scale(x) x_scale
|
\npreprocessing这个模块还提供了一个实用类StandarScaler,它可以在训练数据集上做了标准转换操作之后,把相同的转换应用到测试训练集中。
可以对训练数据,测试数据应用相同的转换,以后有新的数据进来也可以直接调用,不用再重新把数据放在一起再计算一次了。
\n1 2 3 4 5 6 7 8 9
| scaler = preprocessing.StandardScaler().fit(x)
scaler
StandardScaler(copy=True, with_mean=True, with_std=True)
scaler.transform(x)
|
\nStandardScaler()中可以传入两个参数:with_mean,with_std.这两个都是布尔型的参数,默认情况下都是true,但也可以自定义成false.即不要均值中心化或者不要方差规模化为1.
\n1. 处理数据 数据下采样
1.1 预处理数据,修改列”Amount”数据分布
1 2 3 4 5 6 7
| from sklearn.preprocessing import StandardScaler
if 'Amount' in data.columns: data['normAount'] = StandardScaler().fit_transform(data['Amount'].reshape(-1, 1))
|
\n1.2 数据处理,去除不需要的特征
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if ('Time') in data.columns: data = data.drop(['Time'], axis=1) if ('Amount') in data.columns: data = data.drop(['Amount'], axis=1) print(data.columns, len(data.columns))
count_classes = pd.value_counts(data['Class'], sort=True).sort_index() count_classes.plot(kind = 'bar') plt.xlabel(\"Class\") plt.ylabel(\"Frequency\")
X = data.loc[:, data.columns != \"Class\"] y = data.loc[:, data.columns == 'Class'] print (\"SHAPE\", X.shape, y.shape)
|
\n\n3.下采样
\n把数据相对多的减少,可减少为和数据少的数量相同的数量
\n
\n1.3 区分正常数据和异常数据: 通过特征’Class’区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| number_records = data[data.Class == 1]
number_records_fraud = len(number_records)
frand_indices = np.array(number_records.index) print (\"异常样本索引 有{}个\".format(number_records_fraud), frand_indices[:10])
normal_indices = data[data.Class == 0].index print (\"正常样本索引 有{}个\".format(len(normal_indices)), normal_indices[-10:])
print (\">>>>>>>>>>>>>>>>>>>>所有数据 正常异常比 \", len(normal_indices), '\\t', number_records_fraud) print(\"**************\")
|
\n1.4 下采样处理数据 把多的一方数据进行随机减少到与少的一方相同
1 2 3 4 5
|
random_normal_indices = np.random.choice(normal_indices, number_records_fraud, replace=False) random_normal_indices = np.array(random_normal_indices) print (\"'下采样'后有正常样本个数:\", len(random_normal_indices))
|
\n1.5 数据索引合并 (意思就是把新的正常数据和原来的异常数据进行拼接)
1 2 3 4 5 6
| under_sample_indices = np.concatenate([frand_indices, random_normal_indices]) print (\"合并后有样本个数:\", len(under_sample_indices)) under_sample_data = data.iloc[under_sample_indices, :] print (\"合并后样本:\", under_sample_data[:1])
print (\">>>>>>>>>>>>>>>>>>>>下采样数据 正常异常比 \", len(under_sample_data[under_sample_data == 0]), '\\t', len(under_sample_data[under_sample_data == 1]))
|
\n1.6 获取合并数据中的feature(特征)和label(分类)
1 2 3 4 5
| X_undersample = under_sample_data.loc[:, under_sample_data.columns != 'Class'] y_undersample = under_sample_data.loc[:, under_sample_data.columns == 'Class']
print (X_undersample.shape, y_undersample.shape) print (len(under_sample_data[under_sample_data[\"Class\"] == 1]), len(under_sample_data[under_sample_data[\"Class\"] == 0]))
|
\n\n2. 切分数据为训练和测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from sklearn.cross_validation import train_test_split
print (X.shape, y.shape)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0) print (\"1.训练集数据大小\", X_train.shape) print (\"2.测试集数据大小\", X_test.shape) print (len(X_train) + len(X_test), len(y_train), len(y_test), \"\\n\")
X_train_undersample, X_test_undersample, y_train_undersample, y_test_undersample = train_test_split(X_undersample, y_undersample, test_size=0.2, random_state=0) print (\"3.训练集数据大小\", X_train_undersample.shape) print (\"4.测试集数据大小\", X_test_undersample.shape) print (len(X_train_undersample) + len(X_test_undersample), len(y_train_undersample), len(y_test_undersample), \"\\n\")
|
\n\n评估标准:召回率(recall)
不适用准确率,因为准确率不能正确的得到所求的,是没用的
\n模型评估表:
\n\n\n | \n相关(Relevant),正类 | \n不相关(NonRelevant),负类 | \n
\n\n\n\n被检测到(Retrieved) | \ntrue positives (TP) | \nfalse positives (FP) | \n
\n\n未被检测到(Retrieved) | \nfalse negatives (FN) | \ntrue negatives (TN) | \n
\n\n
\n一些术语:
\n- TP:True Positive,即正确预测出的正样本个数
\n- FP:False Positive,即错误预测出的正样本个数(本来是负样本,被我们预测成了正样本)
\n- TN:True Negative,即正确预测出的负样本个数
\n- FN:False Negative,即错误预测出的负样本个数(本来是正样本,被我们预测成了负样本)
\n
\n分类器性能评价指标
由以上四个指标,可以进一步衍生出其他三个常用的评价分类器性能的指标
\n\n- Precision(精确率):TP÷(TP+FP)TP÷(TP+FP),分类器预测出的正样本中,真实正样本的比例
\n- Recall(召回率):TP÷(TP+FN)TP÷(TP+FN),在所有真实正样本中,分类器中能找到多少
\n- Accuracy(准确率):(TP+TN)÷(TP+NP+TN+FN)(TP+TN)÷(TP+NP+TN+FN),分类器对整体的判断能力,即正确预测的比例
\n
\n\n过拟合: 数据在训练集表现很好 在测试集表现很差
\n
\n1 2 3 4
| from sklearn.linear_model import LogisticRegression
from sklearn.cross_validation import KFold from sklearn.metrics import confusion_matrix, recall_score, classification_report
|
\n3. 通过多次循环交叉验证 确定正则化参数 random_state:随机种子数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| def printing_Kfold_scores(x_train_data, y_train_data): fold = KFold(len(y_train_data), 5, shuffle=False) print (type(fold), len(y_train_data), len(fold)) c_param_range = [0.01, 0.1, 1, 10, 100] results_table = pd.DataFrame(index = range(len(c_param_range)), columns = ['C_parameter', 'Mean recall score']) results_table['C_parameter'] = c_param_range
for index, c_param in enumerate(c_param_range): print (\">>>>>>>>>>>>>>>>>>>>>>>>>>\") print (\"C_parameter \", c_param) recall_accs = [] for iteration, indices in enumerate(fold, start=1): print (iteration, len(indices[0]), len(indices[1])) lr = LogisticRegression(C = c_param, penalty = 'l1') lr.fit(x_train_data.iloc[indices[0],:], y_train_data.iloc[indices[0],:].values.ravel()) y_pred_undersample = lr.predict(x_train_data.iloc[indices[1], :].values) recall_acc = recall_score(y_train_data.iloc[indices[1],:].values, y_pred_undersample) recall_accs.append(recall_acc) print (len(indices), \"Iteration \", iteration, \": recall score = \", recall_acc) results_table.loc[index, 'Mean recall score'] = np.mean(recall_accs)
print ('\\nMean recall score ', np.mean(recall_accs), '\\n') print (results_table) best_c = results_table.loc[results_table['Mean recall score'].idxmax()]['C_parameter'] print (\"finally-------best is--------> \", best_c) return best_c
best_c = printing_Kfold_scores(X_train_undersample, y_train_undersample)
|
\n\n\n\nndarray数据格式化: set_printoptions
\nset_printoptions(precision=None,
threshold=None,
edgeitems=None,
linewidth=None,
suppress=None,
nanstr=None,
infstr=None,
formatter=None)
\n
\n\n- precision:输出结果保留精度的位数 (num)
\n- threshold:array数量的个数在小于threshold的时候不会被折叠 (num)
\n- edgeitems:在array已经被折叠后,开头和结尾都会显示edgeitems个数 (num)
\n- formatter:这个很有意思,像python3里面str.format(),就是可以对你的输出进行自定义的格式化 其他的暂时没用到
\n
\n4. 使用最好的正则化参数 构建逻辑回归模型并进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| lr = LogisticRegression(C = best_c, penalty='l1')
lr.fit(X_train_undersample, y_train_undersample.values.ravel())
y_pred_undersample = lr.predict(X_test_undersample.values)
print (type(y_pred_undersample), len(y_pred_undersample), \"\\n\")
import itertools def plot_confusion_matrix(cm, classes, title='Confussion matrix', cmap=plt.cm.Blues): plt.imshow(cm, interpolation='nearest', cmap=cmap) plt.title(title) plt.colorbar() tick_marks = np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation=0) plt.yticks(tick_marks, classes) thresh = cm.max() / 2 for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, cm[i, j], horizontalalignment=\"center\", color=\"white\" if cm[i, j] > thresh else \"black\") plt.tight_layout() plt.ylabel(\"True label\") plt.xlabel(\"Predicted label\")
cnf_matrix = confusion_matrix(y_test_undersample, y_pred_undersample) np.set_printoptions(precision=2)
print (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))
class_names = [0, 1] plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix') plt.show()
|
\n\n4. 使用最好的正则化参数 构建逻辑回归模型并进行测试 (使用原始数据的测试集和训练集)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| lr = LogisticRegression(C = best_c, penalty='l1') lr.fit(X_train, y_train.values.ravel()) y_pred = lr.predict(X_test.values)
cnf_matrix = confusion_matrix(y_test, y_pred) np.set_printoptions(precision=2)
print (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))
class_names = [0, 1] plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix') plt.show()
|
\n5. 修改阈值以获取最好的逻辑回归模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
lr = LogisticRegression(C = best_c, penalty='l1')
lr.fit(X_train, y_train.values.ravel())
y_pred_undersample_proba = lr.predict_proba(X_test_undersample.values)
thresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
plt.figure(figsize=(10, 10))
for index, i in enumerate(thresholds): y_test_predictions_high_recall = y_pred_undersample_proba[:, 1] > i plt.subplot(3, 3, index + 1) cnf_matrix = confusion_matrix(y_test_undersample, y_test_predictions_high_recall) np.set_printoptions(precision=2) print (i, \"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1])) class_names = [0, 1] plot_confusion_matrix(cnf_matrix, classes=class_names, title=\"Threshold >= %s\" %i)
|
\n\n\n\n过采样
\n把数据相对少的增加,可增加为和数据多的数量相同的数量 (生成)
\n
\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import pandas as pd from imblearn.over_sampling import SMOTE from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import confusion_matrix from sklearn.model_selection import train_test_split
credit_cards = pd.read_csv(\"creditcard.csv\") columns = credit_cards.columns
features_columns = columns.delete(len(columns) - 1) print (features_columns)
features = credit_cards[features_columns] labels = credit_cards['Class']
print (\"原始的数据个数\", (credit_cards[credit_cards['Class'] == 0]).shape, (credit_cards[credit_cards['Class'] == 1]).shape)
|
\n1 2
| features_train, features_test, labels_train, labels_test = train_test_split(features, labels, test_size=0.2, random_state=0) print (features_train.shape, features_test.shape, labels_train.shape, labels_test.shape)
|
\n\n1 2 3 4 5 6 7 8
| oversampler = SMOTE(random_state = 0)
os_features, os_labels = oversampler.fit_sample(features_train, labels_train)
print (\"可见 的确生成了新的数据,补充了异常的数据 \", len(os_labels[os_labels[:] == 1]), len(os_labels[os_labels[:] == 0]))
print ((os_features).shape, len(os_features[os_features == 1]), len(os_features[os_features == 0]), (os_labels).shape, len(os_labels[os_labels == 1]), len(os_labels[os_labels == 0]))
|
\n\n1 2 3 4 5 6
| os_features = pd.DataFrame(os_features) os_labels = pd.DataFrame(os_labels)
best_c = printing_Kfold_scores(os_features, os_labels)
|
\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| lr = LogisticRegression(C = best_c, penalty='l1')
lr.fit(os_features, os_labels.values.ravel())
y_pred = lr.predict(features_test.values)
cnf_matrix = confusion_matrix(labels_test, y_pred) np.set_printoptions(precision=2)
print (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1]))
class_names = [0, 1] plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')
plt.show()
|
\n\n个人博客 欢迎来访: http://zj2626.github.io
\n
\n","site":{"data":{}},"excerpt":"","more":"逻辑回归
\n逻辑回归是应用非常广泛的一个分类机器学习算法,它将数据拟合到一个logit函数(或者叫做logistic函数)中,从而能够完成对事件发生的概率进行预测。
\n
\n构建逻辑回归模型步骤:
\n- 导入数据
\n- 预处理数据
\n- 对不平衡的数据进行下采样(或者过采样)处理
\n- 把处理之后的数据进行切分,切分为训训练集和测试集
\n- 对训练集进行交叉验证,同时寻找最佳的正则化参数以减少过拟合
\n- 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率
\n- 使用最佳的正则化参数对处理之后的数据进行训练并预测,观察召回率和精确率
\n- 修改阈值以获得更好的召回率和精确率
\n
\n1. 数据与任务
信用卡欺诈数据
1 2 3 4 5 6 7 8
| import pandas as pd import matplotlib.pyplot as plt import numpy as np
%matplotlib inline
data = pd.read_csv(\"creditcard.csv\") data.head()
|
\n\n要使用逻辑回归对数据进行建模 任务:二分类, 把数据分为有欺诈和无欺诈的两种数据
2. 使用sklearn进行数据预处理
\n公式为:(X-mean)/std 计算时对每个属性/每列分别进行。
\n
\nStandardization标准化:将特征数据的分布调整成标准正太分布,也叫高斯分布,也就是使得数据的均值维0,方差为1
\n标准化的原因在于如果有些特征的方差过大,则会主导目标函数从而使参数估计器无法正确地去学习其他特征。
\n标准化的过程为两步:去均值的中心化(均值变为0);方差的规模化(方差变为1)。
\n在sklearn.preprocessing中提供了一个scale的方法,可以实现以上功能。如下面所示:
\n1 2 3 4 5 6
| x = np.array([[1., -1., 2.], [2., 0., 0.], [0., 1., -1.]])
x_scale = preprocessing.scale(x) x_scale
|
\npreprocessing这个模块还提供了一个实用类StandarScaler,它可以在训练数据集上做了标准转换操作之后,把相同的转换应用到测试训练集中。
可以对训练数据,测试数据应用相同的转换,以后有新的数据进来也可以直接调用,不用再重新把数据放在一起再计算一次了。
\n1 2 3 4 5 6 7 8 9
| scaler = preprocessing.StandardScaler().fit(x)
scaler
StandardScaler(copy=True, with_mean=True, with_std=True)
scaler.transform(x)
|
\nStandardScaler()中可以传入两个参数:with_mean,with_std.这两个都是布尔型的参数,默认情况下都是true,但也可以自定义成false.即不要均值中心化或者不要方差规模化为1.
\n1. 处理数据 数据下采样
1.1 预处理数据,修改列”Amount”数据分布
1 2 3 4 5 6 7
| from sklearn.preprocessing import StandardScaler
if 'Amount' in data.columns: data['normAount'] = StandardScaler().fit_transform(data['Amount'].reshape(-1, 1))
|
\n1.2 数据处理,去除不需要的特征
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| if ('Time') in data.columns: data = data.drop(['Time'], axis=1) if ('Amount') in data.columns: data = data.drop(['Amount'], axis=1) print(data.columns, len(data.columns))
count_classes = pd.value_counts(data['Class'], sort=True).sort_index() count_classes.plot(kind = 'bar') plt.xlabel(\"Class\") plt.ylabel(\"Frequency\")
X = data.loc[:, data.columns != \"Class\"] y = data.loc[:, data.columns == 'Class'] print (\"SHAPE\", X.shape, y.shape)
|
\n\n3.下采样
\n把数据相对多的减少,可减少为和数据少的数量相同的数量
\n
\n1.3 区分正常数据和异常数据: 通过特征’Class’区分
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| number_records = data[data.Class == 1]
number_records_fraud = len(number_records)
frand_indices = np.array(number_records.index) print (\"异常样本索引 有{}个\".format(number_records_fraud), frand_indices[:10])
normal_indices = data[data.Class == 0].index print (\"正常样本索引 有{}个\".format(len(normal_indices)), normal_indices[-10:])
print (\">>>>>>>>>>>>>>>>>>>>所有数据 正常异常比 \", len(normal_indices), '\\t', number_records_fraud) print(\"**************\")
|
\n1.4 下采样处理数据 把多的一方数据进行随机减少到与少的一方相同
1 2 3 4 5
|
random_normal_indices = np.random.choice(normal_indices, number_records_fraud, replace=False) random_normal_indices = np.array(random_normal_indices) print (\"'下采样'后有正常样本个数:\", len(random_normal_indices))
|
\n1.5 数据索引合并 (意思就是把新的正常数据和原来的异常数据进行拼接)
1 2 3 4 5 6
| under_sample_indices = np.concatenate([frand_indices, random_normal_indices]) print (\"合并后有样本个数:\", len(under_sample_indices)) under_sample_data = data.iloc[under_sample_indices, :] print (\"合并后样本:\", under_sample_data[:1])
print (\">>>>>>>>>>>>>>>>>>>>下采样数据 正常异常比 \", len(under_sample_data[under_sample_data == 0]), '\\t', len(under_sample_data[under_sample_data == 1]))
|
\n1.6 获取合并数据中的feature(特征)和label(分类)
1 2 3 4 5
| X_undersample = under_sample_data.loc[:, under_sample_data.columns != 'Class'] y_undersample = under_sample_data.loc[:, under_sample_data.columns == 'Class']
print (X_undersample.shape, y_undersample.shape) print (len(under_sample_data[under_sample_data[\"Class\"] == 1]), len(under_sample_data[under_sample_data[\"Class\"] == 0]))
|
\n\n2. 切分数据为训练和测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from sklearn.cross_validation import train_test_split
print (X.shape, y.shape)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0) print (\"1.训练集数据大小\", X_train.shape) print (\"2.测试集数据大小\", X_test.shape) print (len(X_train) + len(X_test), len(y_train), len(y_test), \"\\n\")
X_train_undersample, X_test_undersample, y_train_undersample, y_test_undersample = train_test_split(X_undersample, y_undersample, test_size=0.2, random_state=0) print (\"3.训练集数据大小\", X_train_undersample.shape) print (\"4.测试集数据大小\", X_test_undersample.shape) print (len(X_train_undersample) + len(X_test_undersample), len(y_train_undersample), len(y_test_undersample), \"\\n\")
|
\n\n评估标准:召回率(recall)
不适用准确率,因为准确率不能正确的得到所求的,是没用的
\n模型评估表:
\n\n\n | \n相关(Relevant),正类 | \n不相关(NonRelevant),负类 | \n
\n\n\n\n被检测到(Retrieved) | \ntrue positives (TP) | \nfalse positives (FP) | \n
\n\n未被检测到(Retrieved) | \nfalse negatives (FN) | \ntrue negatives (TN) | \n
\n\n
\n一些术语:
\n- TP:True Positive,即正确预测出的正样本个数
\n- FP:False Positive,即错误预测出的正样本个数(本来是负样本,被我们预测成了正样本)
\n- TN:True Negative,即正确预测出的负样本个数
\n- FN:False Negative,即错误预测出的负样本个数(本来是正样本,被我们预测成了负样本)
\n
\n分类器性能评价指标
由以上四个指标,可以进一步衍生出其他三个常用的评价分类器性能的指标
\n\n- Precision(精确率):TP÷(TP+FP)TP÷(TP+FP),分类器预测出的正样本中,真实正样本的比例
\n- Recall(召回率):TP÷(TP+FN)TP÷(TP+FN),在所有真实正样本中,分类器中能找到多少
\n- Accuracy(准确率):(TP+TN)÷(TP+NP+TN+FN)(TP+TN)÷(TP+NP+TN+FN),分类器对整体的判断能力,即正确预测的比例
\n
\n\n过拟合: 数据在训练集表现很好 在测试集表现很差
\n
\n1 2 3 4
| from sklearn.linear_model import LogisticRegression
from sklearn.cross_validation import KFold from sklearn.metrics import confusion_matrix, recall_score, classification_report
|
\n3. 通过多次循环交叉验证 确定正则化参数 random_state:随机种子数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| def printing_Kfold_scores(x_train_data, y_train_data): fold = KFold(len(y_train_data), 5, shuffle=False) print (type(fold), len(y_train_data), len(fold)) c_param_range = [0.01, 0.1, 1, 10, 100] results_table = pd.DataFrame(index = range(len(c_param_range)), columns = ['C_parameter', 'Mean recall score']) results_table['C_parameter'] = c_param_range
for index, c_param in enumerate(c_param_range): print (\">>>>>>>>>>>>>>>>>>>>>>>>>>\") print (\"C_parameter \", c_param) recall_accs = [] for iteration, indices in enumerate(fold, start=1): print (iteration, len(indices[0]), len(indices[1])) lr = LogisticRegression(C = c_param, penalty = 'l1') lr.fit(x_train_data.iloc[indices[0],:], y_train_data.iloc[indices[0],:].values.ravel()) y_pred_undersample = lr.predict(x_train_data.iloc[indices[1], :].values) recall_acc = recall_score(y_train_data.iloc[indices[1],:].values, y_pred_undersample) recall_accs.append(recall_acc) print (len(indices), \"Iteration \", iteration, \": recall score = \", recall_acc) results_table.loc[index, 'Mean recall score'] = np.mean(recall_accs)
print ('\\nMean recall score ', np.mean(recall_accs), '\\n') print (results_table) best_c = results_table.loc[results_table['Mean recall score'].idxmax()]['C_parameter'] print (\"finally-------best is--------> \", best_c) return best_c
best_c = printing_Kfold_scores(X_train_undersample, y_train_undersample)
|
\n\n\n\nndarray数据格式化: set_printoptions
\nset_printoptions(precision=None,
threshold=None,
edgeitems=None,
linewidth=None,
suppress=None,
nanstr=None,
infstr=None,
formatter=None)
\n
\n\n- precision:输出结果保留精度的位数 (num)
\n- threshold:array数量的个数在小于threshold的时候不会被折叠 (num)
\n- edgeitems:在array已经被折叠后,开头和结尾都会显示edgeitems个数 (num)
\n- formatter:这个很有意思,像python3里面str.format(),就是可以对你的输出进行自定义的格式化 其他的暂时没用到
\n
\n4. 使用最好的正则化参数 构建逻辑回归模型并进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| lr = LogisticRegression(C = best_c, penalty='l1')
lr.fit(X_train_undersample, y_train_undersample.values.ravel())
y_pred_undersample = lr.predict(X_test_undersample.values)
print (type(y_pred_undersample), len(y_pred_undersample), \"\\n\")
import itertools def plot_confusion_matrix(cm, classes, title='Confussion matrix', cmap=plt.cm.Blues): plt.imshow(cm, interpolation='nearest', cmap=cmap) plt.title(title) plt.colorbar() tick_marks = np.arange(len(classes)) plt.xticks(tick_marks, classes, rotation=0) plt.yticks(tick_marks, classes) thresh = cm.max() / 2 for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, cm[i, j], horizontalalignment=\"center\", color=\"white\" if cm[i, j] > thresh else \"black\") plt.tight_layout() plt.ylabel(\"True label\") plt.xlabel(\"Predicted label\")
cnf_matrix = confusion_matrix(y_test_undersample, y_pred_undersample) np.set_printoptions(precision=2)
print (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))
class_names = [0, 1] plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix') plt.show()
|
\n\n4. 使用最好的正则化参数 构建逻辑回归模型并进行测试 (使用原始数据的测试集和训练集)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| lr = LogisticRegression(C = best_c, penalty='l1') lr.fit(X_train, y_train.values.ravel()) y_pred = lr.predict(X_test.values)
cnf_matrix = confusion_matrix(y_test, y_pred) np.set_printoptions(precision=2)
print (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1]/(cnf_matrix[1, 0] + cnf_matrix[1, 1]))
class_names = [0, 1] plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix') plt.show()
|
\n5. 修改阈值以获取最好的逻辑回归模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
lr = LogisticRegression(C = best_c, penalty='l1')
lr.fit(X_train, y_train.values.ravel())
y_pred_undersample_proba = lr.predict_proba(X_test_undersample.values)
thresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
plt.figure(figsize=(10, 10))
for index, i in enumerate(thresholds): y_test_predictions_high_recall = y_pred_undersample_proba[:, 1] > i plt.subplot(3, 3, index + 1) cnf_matrix = confusion_matrix(y_test_undersample, y_test_predictions_high_recall) np.set_printoptions(precision=2) print (i, \"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1])) class_names = [0, 1] plot_confusion_matrix(cnf_matrix, classes=class_names, title=\"Threshold >= %s\" %i)
|
\n\n\n\n过采样
\n把数据相对少的增加,可增加为和数据多的数量相同的数量 (生成)
\n
\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import pandas as pd from imblearn.over_sampling import SMOTE from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import confusion_matrix from sklearn.model_selection import train_test_split
credit_cards = pd.read_csv(\"creditcard.csv\") columns = credit_cards.columns
features_columns = columns.delete(len(columns) - 1) print (features_columns)
features = credit_cards[features_columns] labels = credit_cards['Class']
print (\"原始的数据个数\", (credit_cards[credit_cards['Class'] == 0]).shape, (credit_cards[credit_cards['Class'] == 1]).shape)
|
\n1 2
| features_train, features_test, labels_train, labels_test = train_test_split(features, labels, test_size=0.2, random_state=0) print (features_train.shape, features_test.shape, labels_train.shape, labels_test.shape)
|
\n\n1 2 3 4 5 6 7 8
| oversampler = SMOTE(random_state = 0)
os_features, os_labels = oversampler.fit_sample(features_train, labels_train)
print (\"可见 的确生成了新的数据,补充了异常的数据 \", len(os_labels[os_labels[:] == 1]), len(os_labels[os_labels[:] == 0]))
print ((os_features).shape, len(os_features[os_features == 1]), len(os_features[os_features == 0]), (os_labels).shape, len(os_labels[os_labels == 1]), len(os_labels[os_labels == 0]))
|
\n\n1 2 3 4 5 6
| os_features = pd.DataFrame(os_features) os_labels = pd.DataFrame(os_labels)
best_c = printing_Kfold_scores(os_features, os_labels)
|
\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| lr = LogisticRegression(C = best_c, penalty='l1')
lr.fit(os_features, os_labels.values.ravel())
y_pred = lr.predict(features_test.values)
cnf_matrix = confusion_matrix(labels_test, y_pred) np.set_printoptions(precision=2)
print (\"Recall metric in the testing dataset: \", cnf_matrix[1, 1] / (cnf_matrix[1, 0] + cnf_matrix[1, 1]))
class_names = [0, 1] plt.figure() plot_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix')
plt.show()
|
\n\n个人博客 欢迎来访: http://zj2626.github.io
\n
\n"},{"title":"Shiro的学习Helloworld","comments":1,"description":null,"_content":"\n## Apache Shiro是Java的一个安全框架\n### Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等\n\n{% qnimg d59f6d02-1f45-3285-8983-4ea5f18111d5.png title:图片标题 alt:图片说明 'class:class_name class_name2' extend:?imageView2/2/w/600 %}\n\n\n## Shiro基本功能\n>Authentication:身份认证/登录,验证用户是不是拥有相应的身份;\n\n>Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;\n\n>Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;\n\n>Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;\n\n>Web Support:Web支持,可以非常容易的集成到Web环境;\n\n>Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;\n\n>Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;\n\n>Testing:提供测试支持;\n\n>Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;\n\n>Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。\n\n\n\n## 1.身份认证\n \n 配置文件:\n ********************************\n [users]\n zj2626=123456\n ay2626=456789\n ********************************\n \n\n package com.em;\n \n import org.apache.shiro.SecurityUtils;\n import org.apache.shiro.authc.UsernamePasswordToken;\n import org.apache.shiro.config.IniSecurityManagerFactory;\n import org.apache.shiro.mgt.SecurityManager;\n import org.apache.shiro.subject.Subject;\n import org.apache.shiro.util.Factory;\n \n public class Hello {\n \n public static void main(String args[]) {\n //初始化SecurityManager工厂 读取配置文件中的用户名密码\n Factory factory = new IniSecurityManagerFactory(\"classpath:shiro.ini\");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(\"zj2626\", \"123456\");\n \n try {\n //身份认证 登录\n subject.login(token);\n \n System.out.println(\"登录成功\");\n } catch (Exception e) {\n System.out.println(\"登录失败\");\n e.printStackTrace();\n }\n \n subject.logout();\n }\n \n /*\n Subject: 认证主体\n Principals: 身份: 用户名\n Credentials: 凭证: 密码\n \n Realm: 域\n 1.jdbc realm | 2.jndi realm | 3.text realm\n \n */\n }\n\n\n\n从数据库中读取用户名密码 实现登录\n\n1.配置文件: jdbc_realm.ini (代码只需把读取的文件改成此文件即可测试使用)\n \n [main]\n jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm\n dataSource=com.mchange.v2.c3p0.ComboPooledDataSource\n dataSource.driverClass=com.mysql.jdbc.Driver\n dataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/test\n dataSource.user=root\n dataSource.password=123456\n jdbcRealm.dataSource=$dataSource\n securityManager.realms=$jdbcRealm\n\n\n***\n***\n\n## 2.权限认证\n### 核心要素:(资源,)权限,角色,用户\n\n**用户--(分配)-->角色--(拥有)-->权限--(控制)---->资源**\n\n* 用户代表访问系统的用户,即subject。\n\n> **三种授权方式**\n\n \n 1. 编程式授权\n 1.1. 基于角色的访问控制\n 1.2. 基于权限的访问控制\n 2. 注解式授权\n 3.JSP标签授权\n\n \n---\n\n####步骤1: 封装一个工具类\n\n public class ShiroUtils {\n public static Subject login(String conf, String username, String passowrd) {\n //初始化SecurityManager工厂 conf是配置文件名称\n Factory factory = new IniSecurityManagerFactory(\"classpath:\" + conf + \".ini\");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(username, passowrd);\n \n try {\n //身份认证 登录\n subject.login(token);\n } catch (Exception e) {\n e.printStackTrace();\n }\n \n return subject;\n }\n }\n\n\n####步骤2: 测试多种访问控制\n\n /*\n 基于角色的访问控制方式1\n \n 配置文件:shiro_role.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n ******************************\n */\n @Test\n public void testHas() {\n Subject sub = ShiroUtils.login(\"shiro_config/shiro_role\", \"zj2626\", \"123456\");\n \n //判断有没有权限 返回布尔 表示验证的成功与否\n boolean bool = sub.hasRole(\"role1\");\n if (bool) {\n System.out.println(\"HAS\");\n }\n \n //判断有没有权限,一次多个分别判断 返回布尔数组\n boolean[] booleans = sub.hasRoles(Arrays.asList(\"role1\", \"role2\", \"role3\"));\n int i = 0;\n while (booleans[i]) {\n i++;\n \n if (booleans.length <= i) {\n break;\n }\n }\n \n //所有的角色都有才返回true\n System.out.println(sub.hasAllRoles(Arrays.asList(\"role1\", \"role2\", \"role3\")));\n \n //判断有没有权限 没有则抛异常\n sub.checkRole(\"role1\");\n sub.checkRole(\"role3\");\n \n //判断多个权限 有一个没有就抛异常 (2种参数形式)\n sub.checkRoles(Arrays.asList(\"role1\", \"role2\", \"role3\"));\n sub.checkRoles(\"role1\", \"role2\", \"role3\");\n \n //退出登陆\n sub.logout();\n }\n \n /*\n 基于权限的访问控制方式(过程同上)\n \n 配置文件:shiro_permision.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n [roles]\n role1=user:select\n role2=user:add,user:update,user:delete\n ******************************\n */\n @Test\n public void testPermition() {\n Subject sub = ShiroUtils.login(\"shiro_config/shiro_permision\", \"py2626\", \"123456\");\n \n System.out.println(\"用户是否有权限 user:select:\" + sub.isPermitted(\"user:select\")); //true\n System.out.println(\"用户是否有权限 user:update:\" + sub.isPermitted(\"user:update\")); //false\n \n boolean[] booleans = sub.isPermitted(\"user:add\", \"user:select\");\n System.out.println(booleans[0] + \"____\" + booleans[1]);\n \n System.out.println(sub.isPermittedAll(\"user:add\", \"user:select\"));\n \n //没有会抛出异常\n sub.checkPermission(\"user:select\");\n sub.checkPermissions(\"user:select\", \"user:update\");\n \n sub.logout();\n }\n\n***\n***\n\n## 3.集成web进行测试\n\n#### 1.新建一个maven的web项目\n\n{% qnimg 1.png title:项目目录文件 alt:项目目录文件 extend:?imageView2/2/w/600 %}\n\n> web.xml 配置shiro的必须的配置: 监听器,过滤器\n\n\n \n \n Archetype Created Web Application\n \n \n \n loginServlet\n com.servlet.LoginServlet\n \n \n loginServlet\n /login\n \n \n homeServlet\n com.servlet.HomeServlet\n \n \n homeServlet\n /home\n \n \n adminServlet\n com.servlet.AdminServlet\n \n \n adminServlet\n /admin\n \n \n \n \n org.apache.shiro.web.env.EnvironmentLoaderListener\n \n \n \n \n ShiroFilter\n org.apache.shiro.web.servlet.ShiroFilter\n \n config\n classpath:shiro.ini\n \n \n \n ShiroFilter\n /*\n \n \n \n\n\n\n> pom.xml :所需依赖\n\n \n \n org.apache.shiro\n shiro-web\n 1.3.2\n \n \n org.apache.shiro\n shiro-core\n 1.3.2\n \n \n org.apache.shiro\n shiro-spring\n 1.3.2\n \n \n org.apache.tomcat\n tomcat-servlet-api\n 8.5.4\n provided\n \n \n org.slf4j\n slf4j-log4j12\n 1.7.7\n compile\n \n \n commons-logging\n commons-logging\n 1.2\n \n \n commons-beanutils\n commons-beanutils\n 1.9.3\n \n \n commons-collections\n commons-collections\n 3.2.1\n \n \n jstl\n jstl\n 1.2\n \n \n\n> shiro.ini: 权限配置文件,配置什么用户有什么角色,什么角色有什么权限\n\n [main]\n authc.loginUrl=/login\n perms.unauthorizedUrl=/noAuth.jsp\n roles.unauthorizedUrl=/noAuth.jsp\n [users]\n zj2626=123456,admin\n ay2626=456789,student\n [roles]\n admin=user:*,student:select\n student:student:*\n [urls]\n /login=anon\n /home=authc\n /admin=roles[admin]\n\n##### *1.authc.loginUrl配置身份认证不通过(未登录时)跳转的地址...(loginUrl是authc的一个属性)*\n##### *2.roles.unauthorizeUrl配置角色认证不通过跳转的地址...(noAuth.jsp页面目前只有一行字)*\n##### *3.perms.unauthorizeUrl配置权限认证不通过跳转的地址*\n\n##### *4.[users]下配置用户身份信息以及用户角色*\n##### *5.[roles]下配置角色以及角色的控制权限*\n##### *6.[urls]下配置访问地址所需的权限, 其中值为\"anon过滤器\"表示地址不需要登录即可访问; \"authc过滤器\"表示地址登录才能访问*\n##### *7.值为 roles[admin] 表示 必须有角色为admin的用户才能范围*\n##### *8.值为 perms[\"student:create\"] 表示 必须有权限为\"student:create\"的用户才能范围*\n##### *9.多个过滤器用\",\"隔开 而且相互为\"且\"的关系(必须同时满足才能访问)*\n##### *10.地址可以使用?表示匹配单个任意字符(eg: /home?=authc 表示可过滤 /home1; /homef.....)*\n##### *11.地址可以使用*表示匹配任意个任意字符(eg: /home*=authc 表示可过滤 /home123; /homeef.....)*\n##### *12.地址可以使用**表示匹配多路径(eg: /home/**=authc 表示可过滤 /home/abc; /home/aaa/bbb.....)*\n\n#### 2.编写sevlet代码\n\n> LoginServlet.java :身份验证地址\n\n \n package com.servlet;\n \n import org.apache.shiro.SecurityUtils;\n import org.apache.shiro.authc.UsernamePasswordToken;\n import org.apache.shiro.subject.Subject;\n \n import javax.servlet.ServletException;\n import javax.servlet.http.HttpServlet;\n import javax.servlet.http.HttpServletRequest;\n import javax.servlet.http.HttpServletResponse;\n import java.io.IOException;\n \n /**\n * Created by zj on 2017/4/10.\n */\n public class LoginServlet extends HttpServlet {\n /**\n * 跳转登录界面\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"user no login\");\n resp.sendRedirect(\"login.jsp\");\n }\n \n /**\n * 进行登录\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"登录\");\n String userName = req.getParameter(\"userName\");\n String password = req.getParameter(\"password\");\n \n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(userName, password);\n try {\n //身份认证 登录\n subject.login(token);\n System.out.println(\"登录成功\");\n resp.sendRedirect(\"success.jsp\");\n } catch (Exception e) {\n System.out.println(\"账号密码不对\");\n e.printStackTrace();\n resp.sendRedirect(\"login.jsp\");\n }\n }\n }\n \n \n //******************************************************************\n login.jsp\n \n <%@ page language=\"java\" pageEncoding=\"UTF-8\" %>\n <%@ taglib uri=\"http://java.sun.com/jsp/jstl/core\" prefix=\"c\" %>\n \n \n \n Hello World
\n 登录:\n \n \n \n\n\n\n> HomeServlet.java :登录成功以及退出登录地址\n\n\n package com.servlet;\n \n import org.apache.shiro.SecurityUtils;\n import org.apache.shiro.subject.Subject;\n \n import javax.servlet.ServletException;\n import javax.servlet.http.HttpServlet;\n import javax.servlet.http.HttpServletRequest;\n import javax.servlet.http.HttpServletResponse;\n import java.io.IOException;\n \n /**\n * Created by zj on 2017/4/10.\n */\n public class HomeServlet extends HttpServlet {\n /**\n * 进入主页(登陆成功界面)\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"主页 get\");\n \n req.getRequestDispatcher(\"success.jsp\").forward(req, resp);\n \n }\n \n /**\n * 用来退出登陆\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"主页 post\");\n \n System.out.println(\"login out\");\n \n //退出登录\n Subject subject = SecurityUtils.getSubject();\n subject.logout();\n \n resp.sendRedirect(\"login.jsp\");\n }\n }\n \n //******************************************************************\n success.jsp\n \n <%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>\n \n \n Hello World!
成功!!!\n \n \n \n\n\n> AdminServlet.java\n\n \n package com.servlet;\n \n import javax.servlet.ServletException;\n import javax.servlet.http.HttpServlet;\n import javax.servlet.http.HttpServletRequest;\n import javax.servlet.http.HttpServletResponse;\n import java.io.IOException;\n \n /**\n * Created by zj on 2017/4/10.\n */\n public class AdminServlet extends HttpServlet {\n protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println(\"ADMIN GET\");\n }\n \n protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println(\"ADMIN POST\");\n }\n }\n\n\n#### 3.启动项目 \n\n##### 测试身份认证\n\n> 不登录情况下输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面\n\n{% qnimg 2.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n> 输入正确的用户名(ay2626)密码 点击登录 成功; 再次输入地址 http://localhost:8080/home 跳转到成功的jsp\n\n{% qnimg 3.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n> 点击退出登录 成功; 再次输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面\n\n##### 测试角色认证\n\n> 登录 :进行其他认证前先进行身份认证 使用ay2626用户登录(其角色只有student) 登录成功 跳转到success.jsp\n\n> 输入地址http://localhost:8080/admin 跳转到 http://localhost:8080/noAuth.jsp 表示没有权限访问此地址\n\n> 退出登录 用zj2626再次登录测试 登录成功 再次输入http://localhost:8080/admin 控制台打印\"ADMIN POST\" 表示访问成功\n\n{% qnimg 5.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n\n##### 测试权限认证\n\n> 可把配置文件中 /admin的过滤器改为 /admin=perms[\"student:create\"] 进行测试\n\n> 测试发现 有权限的ay2626用户可以访问而没有权限的zj2626不能访问\n\n---\n---\n\n#### 自定义Realm\n\n>实际开发中用户权限的配置要存放在数据库,so需要使用自定义realm来读取数据库中权限配置然后为用户赋予权限\n\n##### 测试开发步骤\n\n> 1.添加数据库依赖\n\n \n mysql\n mysql-connector-java\n 5.1.39\n compile\n \n \n\n> 2.设计构建测试的数据库表:有三个表:分别存储用户,角色,权限 并其他两个都与角色关联, 然后存入部分数据 ,再根据三个表建立对应实体(简单实体)\n\n{% qnimg 40.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n> 3.添加数据库操作类(写的并不完善 仅测试使用)\n\n \n package com.servlet;\n \n import java.sql.*;\n \n /**\n * Created by zj on 2017/4/11.\n */\n public class DBUtil {\n \n //获取数据库连接\n private static Connection getConnection() {\n try {\n Class.forName(\"com.mysql.jdbc.Driver\");\n \n Connection connection = DriverManager.getConnection(\"jdbc:mysql://127.0.0.1:3306/test\", \"root\", \"fangshuoit\");\n \n return connection;\n } catch (ClassNotFoundException | SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n \n /**\n * 通过用户名获取用户信息\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static User getByUserName(String name) throws SQLException {\n String sql = \"select * from ay_user where loginName = ?\";\n \n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n \n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new User(resultSet.getInt(\"id\"), resultSet.getString(\"loginName\"), resultSet.getString(\"password\"));\n }\n \n return null;\n }\n \n /**\n * 通过用户名获取用户角色\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static Role getRolesByUserName(String name) throws SQLException {\n String sql = \"select roleId from ay_user where loginName = ?\";\n \n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n \n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n Integer id = resultSet.getInt(\"roleId\");\n \n sql = \"select * from ay_role where id = ?\";\n preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, id);\n resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Role(resultSet.getInt(\"id\"), resultSet.getString(\"name\"));\n }\n }\n \n return null;\n }\n \n /**\n * 通过角色id获取角色权限\n *\n * @param roleId\n * @return\n * @throws SQLException\n */\n public static Perms getPermsByRole(Integer roleId) throws SQLException {\n String sql = \"select * from ay_perms where roleId = ?\";\n \n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, roleId);\n \n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Perms(resultSet.getInt(\"id\"), resultSet.getString(\"name\"), resultSet.getInt(\"roleId\"));\n }\n \n return null;\n }\n }\n\n\n\n> 3.编写自定义的Realm类 需要AuthorizingRealm类并实现两个方法; 第一个是用来身份验证,第二是用来角色权限验证\n\n \n public class MyRealm extends AuthorizingRealm {\n \n /**\n * 验证当前登录的用户(身份认证), 不再需要在配置文件中配置用户的信息和其角色信息\n * \n * @param token 封装有用户的信息\n * @return\n * @throws AuthenticationException\n */\n @Override\n protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n String userName = (String) token.getPrincipal();\n \n System.out.println(\"要登录的用户 : \" + userName);\n \n try {\n User user = DBUtil.getByUserName(userName);//这里只是通过用户名验证并获取用户信息,实际开发中需要用户名以及加密的密码\n if (user != null) {\n return new SimpleAuthenticationInfo(user.getLoginName(), user.getPassword(), \"XX\");//返回登录信息\n } else\n return null;\n } catch (SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n \n /**( 身份验证(登录)以后 )\n * 为当前用户授予角色和权限(根据登录的用户名,读取数据库中其角色和权限)\n *\n * @param principals 封装了身份信息\n * @return\n */\n @Override\n protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n String userName = (String) principals.getPrimaryPrincipal();\n System.out.println(\"要权限的用户 : \" + userName);\n \n SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();\n \n try {\n Role role = DBUtil.getRolesByUserName(userName);\n if (null != role) {\n Set set = new HashSet<>();\n System.out.println(\"获得的角色: \"+ role.getName());\n set.add(role.getName());\n authenticationInfo.setRoles(set);//赋予角色\n \n Perms perms = DBUtil.getPermsByRole(role.getId());\n Set set2 = new HashSet<>();\n set2.add(perms.getName());\n System.out.println(\"获得的权限: \"+ perms.getName());\n authenticationInfo.setStringPermissions(set2);//赋予权限\n \n return authenticationInfo;\n }\n } catch (SQLException e) {\n \n }\n \n return null;\n }\n }\n\n\n\n> 4.修改配置文件shiro.ini ,引入自定义Realms,并去掉原来的 [users]和[roles]下的配置\n\n \n [main]\n authc.loginUrl=/login\n perms.unauthorizedUrl=/noAuth.jsp\n roles.unauthorizedUrl=/noAuth.jsp\n \n myRealm=com.servlet.MyRealm\n securityManager.realms=$myRealm\n [urls]\n /login=anon\n /home=authc\n /admin=roles[admin]\n\n\n##### *1.myRealm指向自定义Realm的位置*\n##### *2.securityManager.realms指向自定义Realm的引用(表明使用自定义Realms进行安全认证),可以指向多个,用\",\"隔开*\n\n> 5.启动项目测试 发现:使用zj2626登录可以访问 /admin地址;而ay2626登录不能访问(没有user角色);而且每次请求都会进行认证(控制台打印信息)","source":"_posts/20170326_shiro.md","raw":"---\ntitle: Shiro的学习Helloworld\n\ncomments: true \n\ntags: \n - Shiro\n\ncategories: \n - 框架相关\n - 权限管理\n\ndescription: \n\n---\n\n## Apache Shiro是Java的一个安全框架\n### Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等\n\n{% qnimg d59f6d02-1f45-3285-8983-4ea5f18111d5.png title:图片标题 alt:图片说明 'class:class_name class_name2' extend:?imageView2/2/w/600 %}\n\n\n## Shiro基本功能\n>Authentication:身份认证/登录,验证用户是不是拥有相应的身份;\n\n>Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;\n\n>Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;\n\n>Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;\n\n>Web Support:Web支持,可以非常容易的集成到Web环境;\n\n>Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;\n\n>Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;\n\n>Testing:提供测试支持;\n\n>Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;\n\n>Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。\n\n\n\n## 1.身份认证\n \n 配置文件:\n ********************************\n [users]\n zj2626=123456\n ay2626=456789\n ********************************\n \n\n package com.em;\n \n import org.apache.shiro.SecurityUtils;\n import org.apache.shiro.authc.UsernamePasswordToken;\n import org.apache.shiro.config.IniSecurityManagerFactory;\n import org.apache.shiro.mgt.SecurityManager;\n import org.apache.shiro.subject.Subject;\n import org.apache.shiro.util.Factory;\n \n public class Hello {\n \n public static void main(String args[]) {\n //初始化SecurityManager工厂 读取配置文件中的用户名密码\n Factory factory = new IniSecurityManagerFactory(\"classpath:shiro.ini\");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(\"zj2626\", \"123456\");\n \n try {\n //身份认证 登录\n subject.login(token);\n \n System.out.println(\"登录成功\");\n } catch (Exception e) {\n System.out.println(\"登录失败\");\n e.printStackTrace();\n }\n \n subject.logout();\n }\n \n /*\n Subject: 认证主体\n Principals: 身份: 用户名\n Credentials: 凭证: 密码\n \n Realm: 域\n 1.jdbc realm | 2.jndi realm | 3.text realm\n \n */\n }\n\n\n\n从数据库中读取用户名密码 实现登录\n\n1.配置文件: jdbc_realm.ini (代码只需把读取的文件改成此文件即可测试使用)\n \n [main]\n jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm\n dataSource=com.mchange.v2.c3p0.ComboPooledDataSource\n dataSource.driverClass=com.mysql.jdbc.Driver\n dataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/test\n dataSource.user=root\n dataSource.password=123456\n jdbcRealm.dataSource=$dataSource\n securityManager.realms=$jdbcRealm\n\n\n***\n***\n\n## 2.权限认证\n### 核心要素:(资源,)权限,角色,用户\n\n**用户--(分配)-->角色--(拥有)-->权限--(控制)---->资源**\n\n* 用户代表访问系统的用户,即subject。\n\n> **三种授权方式**\n\n \n 1. 编程式授权\n 1.1. 基于角色的访问控制\n 1.2. 基于权限的访问控制\n 2. 注解式授权\n 3.JSP标签授权\n\n \n---\n\n####步骤1: 封装一个工具类\n\n public class ShiroUtils {\n public static Subject login(String conf, String username, String passowrd) {\n //初始化SecurityManager工厂 conf是配置文件名称\n Factory factory = new IniSecurityManagerFactory(\"classpath:\" + conf + \".ini\");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(username, passowrd);\n \n try {\n //身份认证 登录\n subject.login(token);\n } catch (Exception e) {\n e.printStackTrace();\n }\n \n return subject;\n }\n }\n\n\n####步骤2: 测试多种访问控制\n\n /*\n 基于角色的访问控制方式1\n \n 配置文件:shiro_role.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n ******************************\n */\n @Test\n public void testHas() {\n Subject sub = ShiroUtils.login(\"shiro_config/shiro_role\", \"zj2626\", \"123456\");\n \n //判断有没有权限 返回布尔 表示验证的成功与否\n boolean bool = sub.hasRole(\"role1\");\n if (bool) {\n System.out.println(\"HAS\");\n }\n \n //判断有没有权限,一次多个分别判断 返回布尔数组\n boolean[] booleans = sub.hasRoles(Arrays.asList(\"role1\", \"role2\", \"role3\"));\n int i = 0;\n while (booleans[i]) {\n i++;\n \n if (booleans.length <= i) {\n break;\n }\n }\n \n //所有的角色都有才返回true\n System.out.println(sub.hasAllRoles(Arrays.asList(\"role1\", \"role2\", \"role3\")));\n \n //判断有没有权限 没有则抛异常\n sub.checkRole(\"role1\");\n sub.checkRole(\"role3\");\n \n //判断多个权限 有一个没有就抛异常 (2种参数形式)\n sub.checkRoles(Arrays.asList(\"role1\", \"role2\", \"role3\"));\n sub.checkRoles(\"role1\", \"role2\", \"role3\");\n \n //退出登陆\n sub.logout();\n }\n \n /*\n 基于权限的访问控制方式(过程同上)\n \n 配置文件:shiro_permision.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n [roles]\n role1=user:select\n role2=user:add,user:update,user:delete\n ******************************\n */\n @Test\n public void testPermition() {\n Subject sub = ShiroUtils.login(\"shiro_config/shiro_permision\", \"py2626\", \"123456\");\n \n System.out.println(\"用户是否有权限 user:select:\" + sub.isPermitted(\"user:select\")); //true\n System.out.println(\"用户是否有权限 user:update:\" + sub.isPermitted(\"user:update\")); //false\n \n boolean[] booleans = sub.isPermitted(\"user:add\", \"user:select\");\n System.out.println(booleans[0] + \"____\" + booleans[1]);\n \n System.out.println(sub.isPermittedAll(\"user:add\", \"user:select\"));\n \n //没有会抛出异常\n sub.checkPermission(\"user:select\");\n sub.checkPermissions(\"user:select\", \"user:update\");\n \n sub.logout();\n }\n\n***\n***\n\n## 3.集成web进行测试\n\n#### 1.新建一个maven的web项目\n\n{% qnimg 1.png title:项目目录文件 alt:项目目录文件 extend:?imageView2/2/w/600 %}\n\n> web.xml 配置shiro的必须的配置: 监听器,过滤器\n\n\n \n \n Archetype Created Web Application\n \n \n \n loginServlet\n com.servlet.LoginServlet\n \n \n loginServlet\n /login\n \n \n homeServlet\n com.servlet.HomeServlet\n \n \n homeServlet\n /home\n \n \n adminServlet\n com.servlet.AdminServlet\n \n \n adminServlet\n /admin\n \n \n \n \n org.apache.shiro.web.env.EnvironmentLoaderListener\n \n \n \n \n ShiroFilter\n org.apache.shiro.web.servlet.ShiroFilter\n \n config\n classpath:shiro.ini\n \n \n \n ShiroFilter\n /*\n \n \n \n\n\n\n> pom.xml :所需依赖\n\n \n \n org.apache.shiro\n shiro-web\n 1.3.2\n \n \n org.apache.shiro\n shiro-core\n 1.3.2\n \n \n org.apache.shiro\n shiro-spring\n 1.3.2\n \n \n org.apache.tomcat\n tomcat-servlet-api\n 8.5.4\n provided\n \n \n org.slf4j\n slf4j-log4j12\n 1.7.7\n compile\n \n \n commons-logging\n commons-logging\n 1.2\n \n \n commons-beanutils\n commons-beanutils\n 1.9.3\n \n \n commons-collections\n commons-collections\n 3.2.1\n \n \n jstl\n jstl\n 1.2\n \n \n\n> shiro.ini: 权限配置文件,配置什么用户有什么角色,什么角色有什么权限\n\n [main]\n authc.loginUrl=/login\n perms.unauthorizedUrl=/noAuth.jsp\n roles.unauthorizedUrl=/noAuth.jsp\n [users]\n zj2626=123456,admin\n ay2626=456789,student\n [roles]\n admin=user:*,student:select\n student:student:*\n [urls]\n /login=anon\n /home=authc\n /admin=roles[admin]\n\n##### *1.authc.loginUrl配置身份认证不通过(未登录时)跳转的地址...(loginUrl是authc的一个属性)*\n##### *2.roles.unauthorizeUrl配置角色认证不通过跳转的地址...(noAuth.jsp页面目前只有一行字)*\n##### *3.perms.unauthorizeUrl配置权限认证不通过跳转的地址*\n\n##### *4.[users]下配置用户身份信息以及用户角色*\n##### *5.[roles]下配置角色以及角色的控制权限*\n##### *6.[urls]下配置访问地址所需的权限, 其中值为\"anon过滤器\"表示地址不需要登录即可访问; \"authc过滤器\"表示地址登录才能访问*\n##### *7.值为 roles[admin] 表示 必须有角色为admin的用户才能范围*\n##### *8.值为 perms[\"student:create\"] 表示 必须有权限为\"student:create\"的用户才能范围*\n##### *9.多个过滤器用\",\"隔开 而且相互为\"且\"的关系(必须同时满足才能访问)*\n##### *10.地址可以使用?表示匹配单个任意字符(eg: /home?=authc 表示可过滤 /home1; /homef.....)*\n##### *11.地址可以使用*表示匹配任意个任意字符(eg: /home*=authc 表示可过滤 /home123; /homeef.....)*\n##### *12.地址可以使用**表示匹配多路径(eg: /home/**=authc 表示可过滤 /home/abc; /home/aaa/bbb.....)*\n\n#### 2.编写sevlet代码\n\n> LoginServlet.java :身份验证地址\n\n \n package com.servlet;\n \n import org.apache.shiro.SecurityUtils;\n import org.apache.shiro.authc.UsernamePasswordToken;\n import org.apache.shiro.subject.Subject;\n \n import javax.servlet.ServletException;\n import javax.servlet.http.HttpServlet;\n import javax.servlet.http.HttpServletRequest;\n import javax.servlet.http.HttpServletResponse;\n import java.io.IOException;\n \n /**\n * Created by zj on 2017/4/10.\n */\n public class LoginServlet extends HttpServlet {\n /**\n * 跳转登录界面\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"user no login\");\n resp.sendRedirect(\"login.jsp\");\n }\n \n /**\n * 进行登录\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"登录\");\n String userName = req.getParameter(\"userName\");\n String password = req.getParameter(\"password\");\n \n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(userName, password);\n try {\n //身份认证 登录\n subject.login(token);\n System.out.println(\"登录成功\");\n resp.sendRedirect(\"success.jsp\");\n } catch (Exception e) {\n System.out.println(\"账号密码不对\");\n e.printStackTrace();\n resp.sendRedirect(\"login.jsp\");\n }\n }\n }\n \n \n //******************************************************************\n login.jsp\n \n <%@ page language=\"java\" pageEncoding=\"UTF-8\" %>\n <%@ taglib uri=\"http://java.sun.com/jsp/jstl/core\" prefix=\"c\" %>\n \n \n \n Hello World
\n 登录:\n \n \n \n\n\n\n> HomeServlet.java :登录成功以及退出登录地址\n\n\n package com.servlet;\n \n import org.apache.shiro.SecurityUtils;\n import org.apache.shiro.subject.Subject;\n \n import javax.servlet.ServletException;\n import javax.servlet.http.HttpServlet;\n import javax.servlet.http.HttpServletRequest;\n import javax.servlet.http.HttpServletResponse;\n import java.io.IOException;\n \n /**\n * Created by zj on 2017/4/10.\n */\n public class HomeServlet extends HttpServlet {\n /**\n * 进入主页(登陆成功界面)\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"主页 get\");\n \n req.getRequestDispatcher(\"success.jsp\").forward(req, resp);\n \n }\n \n /**\n * 用来退出登陆\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println(\"主页 post\");\n \n System.out.println(\"login out\");\n \n //退出登录\n Subject subject = SecurityUtils.getSubject();\n subject.logout();\n \n resp.sendRedirect(\"login.jsp\");\n }\n }\n \n //******************************************************************\n success.jsp\n \n <%@ page contentType=\"text/html;charset=UTF-8\" language=\"java\" %>\n \n \n Hello World!
成功!!!\n \n \n \n\n\n> AdminServlet.java\n\n \n package com.servlet;\n \n import javax.servlet.ServletException;\n import javax.servlet.http.HttpServlet;\n import javax.servlet.http.HttpServletRequest;\n import javax.servlet.http.HttpServletResponse;\n import java.io.IOException;\n \n /**\n * Created by zj on 2017/4/10.\n */\n public class AdminServlet extends HttpServlet {\n protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println(\"ADMIN GET\");\n }\n \n protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println(\"ADMIN POST\");\n }\n }\n\n\n#### 3.启动项目 \n\n##### 测试身份认证\n\n> 不登录情况下输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面\n\n{% qnimg 2.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n> 输入正确的用户名(ay2626)密码 点击登录 成功; 再次输入地址 http://localhost:8080/home 跳转到成功的jsp\n\n{% qnimg 3.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n> 点击退出登录 成功; 再次输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面\n\n##### 测试角色认证\n\n> 登录 :进行其他认证前先进行身份认证 使用ay2626用户登录(其角色只有student) 登录成功 跳转到success.jsp\n\n> 输入地址http://localhost:8080/admin 跳转到 http://localhost:8080/noAuth.jsp 表示没有权限访问此地址\n\n> 退出登录 用zj2626再次登录测试 登录成功 再次输入http://localhost:8080/admin 控制台打印\"ADMIN POST\" 表示访问成功\n\n{% qnimg 5.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n\n##### 测试权限认证\n\n> 可把配置文件中 /admin的过滤器改为 /admin=perms[\"student:create\"] 进行测试\n\n> 测试发现 有权限的ay2626用户可以访问而没有权限的zj2626不能访问\n\n---\n---\n\n#### 自定义Realm\n\n>实际开发中用户权限的配置要存放在数据库,so需要使用自定义realm来读取数据库中权限配置然后为用户赋予权限\n\n##### 测试开发步骤\n\n> 1.添加数据库依赖\n\n \n mysql\n mysql-connector-java\n 5.1.39\n compile\n \n \n\n> 2.设计构建测试的数据库表:有三个表:分别存储用户,角色,权限 并其他两个都与角色关联, 然后存入部分数据 ,再根据三个表建立对应实体(简单实体)\n\n{% qnimg 40.png title:图片标题 alt:图片说明 extend:?imageView2/2/w/600 %}\n\n\n> 3.添加数据库操作类(写的并不完善 仅测试使用)\n\n \n package com.servlet;\n \n import java.sql.*;\n \n /**\n * Created by zj on 2017/4/11.\n */\n public class DBUtil {\n \n //获取数据库连接\n private static Connection getConnection() {\n try {\n Class.forName(\"com.mysql.jdbc.Driver\");\n \n Connection connection = DriverManager.getConnection(\"jdbc:mysql://127.0.0.1:3306/test\", \"root\", \"fangshuoit\");\n \n return connection;\n } catch (ClassNotFoundException | SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n \n /**\n * 通过用户名获取用户信息\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static User getByUserName(String name) throws SQLException {\n String sql = \"select * from ay_user where loginName = ?\";\n \n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n \n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new User(resultSet.getInt(\"id\"), resultSet.getString(\"loginName\"), resultSet.getString(\"password\"));\n }\n \n return null;\n }\n \n /**\n * 通过用户名获取用户角色\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static Role getRolesByUserName(String name) throws SQLException {\n String sql = \"select roleId from ay_user where loginName = ?\";\n \n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n \n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n Integer id = resultSet.getInt(\"roleId\");\n \n sql = \"select * from ay_role where id = ?\";\n preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, id);\n resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Role(resultSet.getInt(\"id\"), resultSet.getString(\"name\"));\n }\n }\n \n return null;\n }\n \n /**\n * 通过角色id获取角色权限\n *\n * @param roleId\n * @return\n * @throws SQLException\n */\n public static Perms getPermsByRole(Integer roleId) throws SQLException {\n String sql = \"select * from ay_perms where roleId = ?\";\n \n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, roleId);\n \n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Perms(resultSet.getInt(\"id\"), resultSet.getString(\"name\"), resultSet.getInt(\"roleId\"));\n }\n \n return null;\n }\n }\n\n\n\n> 3.编写自定义的Realm类 需要AuthorizingRealm类并实现两个方法; 第一个是用来身份验证,第二是用来角色权限验证\n\n \n public class MyRealm extends AuthorizingRealm {\n \n /**\n * 验证当前登录的用户(身份认证), 不再需要在配置文件中配置用户的信息和其角色信息\n * \n * @param token 封装有用户的信息\n * @return\n * @throws AuthenticationException\n */\n @Override\n protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n String userName = (String) token.getPrincipal();\n \n System.out.println(\"要登录的用户 : \" + userName);\n \n try {\n User user = DBUtil.getByUserName(userName);//这里只是通过用户名验证并获取用户信息,实际开发中需要用户名以及加密的密码\n if (user != null) {\n return new SimpleAuthenticationInfo(user.getLoginName(), user.getPassword(), \"XX\");//返回登录信息\n } else\n return null;\n } catch (SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n \n /**( 身份验证(登录)以后 )\n * 为当前用户授予角色和权限(根据登录的用户名,读取数据库中其角色和权限)\n *\n * @param principals 封装了身份信息\n * @return\n */\n @Override\n protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n String userName = (String) principals.getPrimaryPrincipal();\n System.out.println(\"要权限的用户 : \" + userName);\n \n SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();\n \n try {\n Role role = DBUtil.getRolesByUserName(userName);\n if (null != role) {\n Set set = new HashSet<>();\n System.out.println(\"获得的角色: \"+ role.getName());\n set.add(role.getName());\n authenticationInfo.setRoles(set);//赋予角色\n \n Perms perms = DBUtil.getPermsByRole(role.getId());\n Set set2 = new HashSet<>();\n set2.add(perms.getName());\n System.out.println(\"获得的权限: \"+ perms.getName());\n authenticationInfo.setStringPermissions(set2);//赋予权限\n \n return authenticationInfo;\n }\n } catch (SQLException e) {\n \n }\n \n return null;\n }\n }\n\n\n\n> 4.修改配置文件shiro.ini ,引入自定义Realms,并去掉原来的 [users]和[roles]下的配置\n\n \n [main]\n authc.loginUrl=/login\n perms.unauthorizedUrl=/noAuth.jsp\n roles.unauthorizedUrl=/noAuth.jsp\n \n myRealm=com.servlet.MyRealm\n securityManager.realms=$myRealm\n [urls]\n /login=anon\n /home=authc\n /admin=roles[admin]\n\n\n##### *1.myRealm指向自定义Realm的位置*\n##### *2.securityManager.realms指向自定义Realm的引用(表明使用自定义Realms进行安全认证),可以指向多个,用\",\"隔开*\n\n> 5.启动项目测试 发现:使用zj2626登录可以访问 /admin地址;而ay2626登录不能访问(没有user角色);而且每次请求都会进行认证(控制台打印信息)","slug":"20170326_shiro","published":1,"date":"2020-01-15T05:50:39.606Z","updated":"2018-01-13T02:29:22.156Z","layout":"post","photos":[],"link":"","_id":"ckm3invhg009l24ujiejy0cmb","content":"Apache Shiro是Java的一个安全框架
Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等
\nShiro基本功能
\nAuthentication:身份认证/登录,验证用户是不是拥有相应的身份;
\n
\n\nAuthorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
\n
\n\nSession Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
\n
\n\nCryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
\n
\n\nWeb Support:Web支持,可以非常容易的集成到Web环境;
\n
\n\nCaching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
\n
\n\nConcurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
\n
\n\nTesting:提供测试支持;
\n
\n\nRun As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
\n
\n\nRemember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
\n
\n\n1.身份认证
配置文件:\n ********************************\n [users]\n zj2626=123456\n ay2626=456789\n ********************************\n\n\npackage com.em;\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.config.IniSecurityManagerFactory;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.util.Factory;\n\npublic class Hello {\n\n public static void main(String args[]) {\n //初始化SecurityManager工厂 读取配置文件中的用户名密码\n Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken("zj2626", "123456");\n\n try {\n //身份认证 登录\n subject.login(token);\n\n System.out.println("登录成功");\n } catch (Exception e) {\n System.out.println("登录失败");\n e.printStackTrace();\n }\n\n subject.logout();\n }\n\n /*\n Subject: 认证主体\n Principals: 身份: 用户名\n Credentials: 凭证: 密码\n\n Realm: 域\n 1.jdbc realm | 2.jndi realm | 3.text realm\n\n */\n}\n
从数据库中读取用户名密码 实现登录
\n1.配置文件: jdbc_realm.ini (代码只需把读取的文件改成此文件即可测试使用)
\n[main]\njdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm\ndataSource=com.mchange.v2.c3p0.ComboPooledDataSource\ndataSource.driverClass=com.mysql.jdbc.Driver\ndataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/test\ndataSource.user=root\ndataSource.password=123456\njdbcRealm.dataSource=$dataSource\nsecurityManager.realms=$jdbcRealm\n
\n
\n2.权限认证
核心要素:(资源,)权限,角色,用户
用户–(分配)–>角色–(拥有)–>权限–(控制)—->资源
\n\n- 用户代表访问系统的用户,即subject。
\n
\n\n三种授权方式
\n
\n1. 编程式授权\n 1.1. 基于角色的访问控制\n 1.2. 基于权限的访问控制\n2. 注解式授权\n3.JSP标签授权\n
\n####步骤1: 封装一个工具类
\npublic class ShiroUtils {\n public static Subject login(String conf, String username, String passowrd) {\n //初始化SecurityManager工厂 conf是配置文件名称\n Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:" + conf + ".ini");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(username, passowrd);\n\n try {\n //身份认证 登录\n subject.login(token);\n } catch (Exception e) {\n e.printStackTrace();\n }\n\n return subject;\n }\n}\n
####步骤2: 测试多种访问控制
\n/*\n 基于角色的访问控制方式1\n\n 配置文件:shiro_role.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n ******************************\n */\n @Test\n public void testHas() {\n Subject sub = ShiroUtils.login("shiro_config/shiro_role", "zj2626", "123456");\n\n //判断有没有权限 返回布尔 表示验证的成功与否\n boolean bool = sub.hasRole("role1");\n if (bool) {\n System.out.println("HAS");\n }\n\n //判断有没有权限,一次多个分别判断 返回布尔数组\n boolean[] booleans = sub.hasRoles(Arrays.asList("role1", "role2", "role3"));\n int i = 0;\n while (booleans[i]) {\n i++;\n\n if (booleans.length <= i) {\n break;\n }\n }\n\n //所有的角色都有才返回true\n System.out.println(sub.hasAllRoles(Arrays.asList("role1", "role2", "role3")));\n\n //判断有没有权限 没有则抛异常\n sub.checkRole("role1");\n sub.checkRole("role3");\n\n //判断多个权限 有一个没有就抛异常 (2种参数形式)\n sub.checkRoles(Arrays.asList("role1", "role2", "role3"));\n sub.checkRoles("role1", "role2", "role3");\n\n //退出登陆\n sub.logout();\n }\n\n /*\n 基于权限的访问控制方式(过程同上)\n\n 配置文件:shiro_permision.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n [roles]\n role1=user:select\n role2=user:add,user:update,user:delete\n ******************************\n */\n @Test\n public void testPermition() {\n Subject sub = ShiroUtils.login("shiro_config/shiro_permision", "py2626", "123456");\n\n System.out.println("用户是否有权限 user:select:" + sub.isPermitted("user:select")); //true\n System.out.println("用户是否有权限 user:update:" + sub.isPermitted("user:update")); //false\n\n boolean[] booleans = sub.isPermitted("user:add", "user:select");\n System.out.println(booleans[0] + "____" + booleans[1]);\n\n System.out.println(sub.isPermittedAll("user:add", "user:select"));\n\n //没有会抛出异常\n sub.checkPermission("user:select");\n sub.checkPermissions("user:select", "user:update");\n\n sub.logout();\n }\n
\n
\n3.集成web进行测试
1.新建一个maven的web项目
\n\nweb.xml 配置shiro的必须的配置: 监听器,过滤器
\n
\n<?xml version="1.0" encoding="UTF-8"?>\n<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"\n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"\n version="3.1">\n <display-name>Archetype Created Web Application</display-name>\n\n <!--三个servlet配置 /login跳转到登陆页面 /home跳转到主页,即登陆成功页面 /admin用来测试角色和权限-->\n <servlet>\n <servlet-name>loginServlet</servlet-name>\n <servlet-class>com.servlet.LoginServlet</servlet-class>\n </servlet>\n <servlet-mapping>\n <servlet-name>loginServlet</servlet-name>\n <url-pattern>/login</url-pattern>\n </servlet-mapping>\n <servlet>\n <servlet-name>homeServlet</servlet-name>\n <servlet-class>com.servlet.HomeServlet</servlet-class>\n </servlet>\n <servlet-mapping>\n <servlet-name>homeServlet</servlet-name>\n <url-pattern>/home</url-pattern>\n </servlet-mapping>\n <servlet>\n <servlet-name>adminServlet</servlet-name>\n <servlet-class>com.servlet.AdminServlet</servlet-class>\n </servlet>\n <servlet-mapping>\n <servlet-name>adminServlet</servlet-name>\n <url-pattern>/admin</url-pattern>\n </servlet-mapping>\n\n <!--shiro监听-->\n <listener>\n <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>\n </listener>\n\n <!--shiro过滤器 这里过滤所有的地址 并且指定权限配置文件(一般项目中权限的配置存放在数据库中)-->\n <filter>\n <filter-name>ShiroFilter</filter-name>\n <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>\n <init-param>\n <param-name>config</param-name>\n <param-value>classpath:shiro.ini</param-value>\n </init-param>\n </filter>\n <filter-mapping>\n <filter-name>ShiroFilter</filter-name>\n <url-pattern>/*</url-pattern>\n </filter-mapping>\n\n</web-app>\n
\npom.xml :所需依赖
\n
\n<dependencies>\n <dependency>\n <groupId>org.apache.shiro</groupId>\n <artifactId>shiro-web</artifactId>\n <version>1.3.2</version>\n </dependency>\n <dependency>\n <groupId>org.apache.shiro</groupId>\n <artifactId>shiro-core</artifactId>\n <version>1.3.2</version>\n </dependency>\n <dependency>\n <groupId>org.apache.shiro</groupId>\n <artifactId>shiro-spring</artifactId>\n <version>1.3.2</version>\n </dependency>\n <dependency>\n <groupId>org.apache.tomcat</groupId>\n <artifactId>tomcat-servlet-api</artifactId>\n <version>8.5.4</version>\n <scope>provided</scope>\n </dependency>\n <dependency>\n <groupId>org.slf4j</groupId>\n <artifactId>slf4j-log4j12</artifactId>\n <version>1.7.7</version>\n <scope>compile</scope>\n </dependency>\n <dependency>\n <groupId>commons-logging</groupId>\n <artifactId>commons-logging</artifactId>\n <version>1.2</version>\n </dependency>\n <dependency>\n <groupId>commons-beanutils</groupId>\n <artifactId>commons-beanutils</artifactId>\n <version>1.9.3</version>\n </dependency>\n <dependency>\n <groupId>commons-collections</groupId>\n <artifactId>commons-collections</artifactId>\n <version>3.2.1</version>\n </dependency>\n <dependency>\n <groupId>jstl</groupId>\n <artifactId>jstl</artifactId>\n <version>1.2</version>\n </dependency>\n</dependencies>\n
\nshiro.ini: 权限配置文件,配置什么用户有什么角色,什么角色有什么权限
\n
\n[main]\nauthc.loginUrl=/login\nperms.unauthorizedUrl=/noAuth.jsp\nroles.unauthorizedUrl=/noAuth.jsp\n[users]\nzj2626=123456,admin\nay2626=456789,student\n[roles]\nadmin=user:*,student:select\nstudent:student:*\n[urls]\n/login=anon\n/home=authc\n/admin=roles[admin]\n
1.authc.loginUrl配置身份认证不通过(未登录时)跳转的地址…(loginUrl是authc的一个属性)
2.roles.unauthorizeUrl配置角色认证不通过跳转的地址…(noAuth.jsp页面目前只有一行字)
3.perms.unauthorizeUrl配置权限认证不通过跳转的地址
4.[users]下配置用户身份信息以及用户角色
5.[roles]下配置角色以及角色的控制权限
6.[urls]下配置访问地址所需的权限, 其中值为”anon过滤器”表示地址不需要登录即可访问; “authc过滤器”表示地址登录才能访问
7.值为 roles[admin] 表示 必须有角色为admin的用户才能范围
8.值为 perms[“student:create”] 表示 必须有权限为”student:create”的用户才能范围
9.多个过滤器用”,”隔开 而且相互为”且”的关系(必须同时满足才能访问)
10.地址可以使用?表示匹配单个任意字符(eg: /home?=authc 表示可过滤 /home1; /homef…..)
11.地址可以使用表示匹配任意个任意字符(eg: /home=authc 表示可过滤 /home123; /homeef…..)
12.地址可以使用表示匹配多路径(eg: /home/=authc 表示可过滤 /home/abc; /home/aaa/bbb…..)
2.编写sevlet代码
\nLoginServlet.java :身份验证地址
\n
\npackage com.servlet;\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.subject.Subject;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * Created by zj on 2017/4/10.\n */\npublic class LoginServlet extends HttpServlet {\n /**\n * 跳转登录界面\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("user no login");\n resp.sendRedirect("login.jsp");\n }\n\n /**\n * 进行登录\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("登录");\n String userName = req.getParameter("userName");\n String password = req.getParameter("password");\n\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(userName, password);\n try {\n //身份认证 登录\n subject.login(token);\n System.out.println("登录成功");\n resp.sendRedirect("success.jsp");\n } catch (Exception e) {\n System.out.println("账号密码不对");\n e.printStackTrace();\n resp.sendRedirect("login.jsp");\n }\n }\n}\n\n\n//******************************************************************\nlogin.jsp\n\n<%@ page language="java" pageEncoding="UTF-8" %>\n<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>\n<!DOCTYPE HTML>\n<html>\n<body>\n<h2>Hello World</h2>\n登录:\n<form action="/login" method="post">\n <input type="text" value="" name="userName">\n <input type="password" value="" name="password">\n <input type="submit" value="登录">\n</form>\n</body>\n</html>\n
\nHomeServlet.java :登录成功以及退出登录地址
\n
\npackage com.servlet;\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.subject.Subject;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * Created by zj on 2017/4/10.\n */\npublic class HomeServlet extends HttpServlet {\n /**\n * 进入主页(登陆成功界面)\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("主页 get");\n\n req.getRequestDispatcher("success.jsp").forward(req, resp);\n\n }\n\n /**\n * 用来退出登陆\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("主页 post");\n\n System.out.println("login out");\n\n //退出登录\n Subject subject = SecurityUtils.getSubject();\n subject.logout();\n\n resp.sendRedirect("login.jsp");\n }\n}\n\n//******************************************************************\nsuccess.jsp\n\n<%@ page contentType="text/html;charset=UTF-8" language="java" %>\n<html>\n<body>\n<h2>Hello World!</h2> 成功!!!\n<form action="/home" method="post">\n <input type="submit" value="退出登陆">\n</form>\n</body>\n</html>\n
\nAdminServlet.java
\n
\npackage com.servlet;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * Created by zj on 2017/4/10.\n */\npublic class AdminServlet extends HttpServlet {\n protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println("ADMIN GET");\n }\n\n protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println("ADMIN POST");\n }\n}\n
3.启动项目
测试身份认证
\n不登录情况下输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面
\n
\n\n\n输入正确的用户名(ay2626)密码 点击登录 成功; 再次输入地址 http://localhost:8080/home 跳转到成功的jsp
\n
\n\n\n点击退出登录 成功; 再次输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面
\n
\n测试角色认证
\n登录 :进行其他认证前先进行身份认证 使用ay2626用户登录(其角色只有student) 登录成功 跳转到success.jsp
\n
\n\n输入地址http://localhost:8080/admin 跳转到 http://localhost:8080/noAuth.jsp 表示没有权限访问此地址
\n
\n\n退出登录 用zj2626再次登录测试 登录成功 再次输入http://localhost:8080/admin 控制台打印”ADMIN POST” 表示访问成功
\n
\n\n测试权限认证
\n可把配置文件中 /admin的过滤器改为 /admin=perms[“student:create”] 进行测试
\n
\n\n测试发现 有权限的ay2626用户可以访问而没有权限的zj2626不能访问
\n
\n
\n
\n自定义Realm
\n实际开发中用户权限的配置要存放在数据库,so需要使用自定义realm来读取数据库中权限配置然后为用户赋予权限
\n
\n测试开发步骤
\n1.添加数据库依赖
\n
\n<dependency>\n <groupId>mysql</groupId>\n <artifactId>mysql-connector-java</artifactId>\n <version>5.1.39</version>\n <scope>compile</scope>\n</dependency>\n
\n2.设计构建测试的数据库表:有三个表:分别存储用户,角色,权限 并其他两个都与角色关联, 然后存入部分数据 ,再根据三个表建立对应实体(简单实体)
\n
\n\n\n3.添加数据库操作类(写的并不完善 仅测试使用)
\n
\npackage com.servlet;\n\nimport java.sql.*;\n\n/**\n * Created by zj on 2017/4/11.\n */\npublic class DBUtil {\n\n //获取数据库连接\n private static Connection getConnection() {\n try {\n Class.forName("com.mysql.jdbc.Driver");\n\n Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "fangshuoit");\n\n return connection;\n } catch (ClassNotFoundException | SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n\n /**\n * 通过用户名获取用户信息\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static User getByUserName(String name) throws SQLException {\n String sql = "select * from ay_user where loginName = ?";\n\n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n\n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new User(resultSet.getInt("id"), resultSet.getString("loginName"), resultSet.getString("password"));\n }\n\n return null;\n }\n\n /**\n * 通过用户名获取用户角色\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static Role getRolesByUserName(String name) throws SQLException {\n String sql = "select roleId from ay_user where loginName = ?";\n\n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n\n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n Integer id = resultSet.getInt("roleId");\n\n sql = "select * from ay_role where id = ?";\n preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, id);\n resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Role(resultSet.getInt("id"), resultSet.getString("name"));\n }\n }\n\n return null;\n }\n\n /**\n * 通过角色id获取角色权限\n *\n * @param roleId\n * @return\n * @throws SQLException\n */\n public static Perms getPermsByRole(Integer roleId) throws SQLException {\n String sql = "select * from ay_perms where roleId = ?";\n\n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, roleId);\n\n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Perms(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getInt("roleId"));\n }\n\n return null;\n }\n}\n
\n3.编写自定义的Realm类 需要AuthorizingRealm类并实现两个方法; 第一个是用来身份验证,第二是用来角色权限验证
\n
\npublic class MyRealm extends AuthorizingRealm {\n\n /**\n * 验证当前登录的用户(身份认证), 不再需要在配置文件中配置用户的信息和其角色信息\n * \n * @param token 封装有用户的信息\n * @return\n * @throws AuthenticationException\n */\n @Override\n protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n String userName = (String) token.getPrincipal();\n\n System.out.println("要登录的用户 : " + userName);\n\n try {\n User user = DBUtil.getByUserName(userName);//这里只是通过用户名验证并获取用户信息,实际开发中需要用户名以及加密的密码\n if (user != null) {\n return new SimpleAuthenticationInfo(user.getLoginName(), user.getPassword(), "XX");//返回登录信息\n } else\n return null;\n } catch (SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n\n /**( 身份验证(登录)以后 )\n * 为当前用户授予角色和权限(根据登录的用户名,读取数据库中其角色和权限)\n *\n * @param principals 封装了身份信息\n * @return\n */\n @Override\n protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n String userName = (String) principals.getPrimaryPrincipal();\n System.out.println("要权限的用户 : " + userName);\n\n SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();\n\n try {\n Role role = DBUtil.getRolesByUserName(userName);\n if (null != role) {\n Set<String> set = new HashSet<>();\n System.out.println("获得的角色: "+ role.getName());\n set.add(role.getName());\n authenticationInfo.setRoles(set);//赋予角色\n\n Perms perms = DBUtil.getPermsByRole(role.getId());\n Set<String> set2 = new HashSet<>();\n set2.add(perms.getName());\n System.out.println("获得的权限: "+ perms.getName());\n authenticationInfo.setStringPermissions(set2);//赋予权限\n\n return authenticationInfo;\n }\n } catch (SQLException e) {\n\n }\n\n return null;\n }\n}\n
\n4.修改配置文件shiro.ini ,引入自定义Realms,并去掉原来的 [users]和[roles]下的配置
\n
\n[main]\nauthc.loginUrl=/login\nperms.unauthorizedUrl=/noAuth.jsp\nroles.unauthorizedUrl=/noAuth.jsp\n\nmyRealm=com.servlet.MyRealm\nsecurityManager.realms=$myRealm\n[urls]\n/login=anon\n/home=authc\n/admin=roles[admin]\n
1.myRealm指向自定义Realm的位置
2.securityManager.realms指向自定义Realm的引用(表明使用自定义Realms进行安全认证),可以指向多个,用”,”隔开
\n5.启动项目测试 发现:使用zj2626登录可以访问 /admin地址;而ay2626登录不能访问(没有user角色);而且每次请求都会进行认证(控制台打印信息)
\n
\n","site":{"data":{}},"excerpt":"Apache Shiro是Java的一个安全框架
Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等
\nShiro基本功能
\nAuthentication:身份认证/登录,验证用户是不是拥有相应的身份;
\n
\n\nAuthorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
\n
\n\nSession Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
\n
\n\nCryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
\n
\n\nWeb Support:Web支持,可以非常容易的集成到Web环境;
\n
\n\nCaching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
\n
\n\nConcurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
\n
\n\nTesting:提供测试支持;
\n
\n\nRun As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
\n
\n\nRemember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
\n
","more":"1.身份认证
配置文件:\n ********************************\n [users]\n zj2626=123456\n ay2626=456789\n ********************************\n\n\npackage com.em;\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.config.IniSecurityManagerFactory;\nimport org.apache.shiro.mgt.SecurityManager;\nimport org.apache.shiro.subject.Subject;\nimport org.apache.shiro.util.Factory;\n\npublic class Hello {\n\n public static void main(String args[]) {\n //初始化SecurityManager工厂 读取配置文件中的用户名密码\n Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken("zj2626", "123456");\n\n try {\n //身份认证 登录\n subject.login(token);\n\n System.out.println("登录成功");\n } catch (Exception e) {\n System.out.println("登录失败");\n e.printStackTrace();\n }\n\n subject.logout();\n }\n\n /*\n Subject: 认证主体\n Principals: 身份: 用户名\n Credentials: 凭证: 密码\n\n Realm: 域\n 1.jdbc realm | 2.jndi realm | 3.text realm\n\n */\n}\n
从数据库中读取用户名密码 实现登录
\n1.配置文件: jdbc_realm.ini (代码只需把读取的文件改成此文件即可测试使用)
\n[main]\njdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm\ndataSource=com.mchange.v2.c3p0.ComboPooledDataSource\ndataSource.driverClass=com.mysql.jdbc.Driver\ndataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/test\ndataSource.user=root\ndataSource.password=123456\njdbcRealm.dataSource=$dataSource\nsecurityManager.realms=$jdbcRealm\n
\n
\n2.权限认证
核心要素:(资源,)权限,角色,用户
用户–(分配)–>角色–(拥有)–>权限–(控制)—->资源
\n\n- 用户代表访问系统的用户,即subject。
\n
\n\n三种授权方式
\n
\n1. 编程式授权\n 1.1. 基于角色的访问控制\n 1.2. 基于权限的访问控制\n2. 注解式授权\n3.JSP标签授权\n
\n####步骤1: 封装一个工具类
\npublic class ShiroUtils {\n public static Subject login(String conf, String username, String passowrd) {\n //初始化SecurityManager工厂 conf是配置文件名称\n Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:" + conf + ".ini");\n //获取SecurityManager实例\n SecurityManager manager = factory.getInstance();\n //把SecurityManager实例绑定到SecurityUtils\n SecurityUtils.setSecurityManager(manager);\n //得到当前执行的用户\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(username, passowrd);\n\n try {\n //身份认证 登录\n subject.login(token);\n } catch (Exception e) {\n e.printStackTrace();\n }\n\n return subject;\n }\n}\n
####步骤2: 测试多种访问控制
\n/*\n 基于角色的访问控制方式1\n\n 配置文件:shiro_role.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n ******************************\n */\n @Test\n public void testHas() {\n Subject sub = ShiroUtils.login("shiro_config/shiro_role", "zj2626", "123456");\n\n //判断有没有权限 返回布尔 表示验证的成功与否\n boolean bool = sub.hasRole("role1");\n if (bool) {\n System.out.println("HAS");\n }\n\n //判断有没有权限,一次多个分别判断 返回布尔数组\n boolean[] booleans = sub.hasRoles(Arrays.asList("role1", "role2", "role3"));\n int i = 0;\n while (booleans[i]) {\n i++;\n\n if (booleans.length <= i) {\n break;\n }\n }\n\n //所有的角色都有才返回true\n System.out.println(sub.hasAllRoles(Arrays.asList("role1", "role2", "role3")));\n\n //判断有没有权限 没有则抛异常\n sub.checkRole("role1");\n sub.checkRole("role3");\n\n //判断多个权限 有一个没有就抛异常 (2种参数形式)\n sub.checkRoles(Arrays.asList("role1", "role2", "role3"));\n sub.checkRoles("role1", "role2", "role3");\n\n //退出登陆\n sub.logout();\n }\n\n /*\n 基于权限的访问控制方式(过程同上)\n\n 配置文件:shiro_permision.ini\n ******************************\n [users]\n zj2626=123456,role1,role2\n py2626=123456,role1\n ay2626=456789,role3\n [roles]\n role1=user:select\n role2=user:add,user:update,user:delete\n ******************************\n */\n @Test\n public void testPermition() {\n Subject sub = ShiroUtils.login("shiro_config/shiro_permision", "py2626", "123456");\n\n System.out.println("用户是否有权限 user:select:" + sub.isPermitted("user:select")); //true\n System.out.println("用户是否有权限 user:update:" + sub.isPermitted("user:update")); //false\n\n boolean[] booleans = sub.isPermitted("user:add", "user:select");\n System.out.println(booleans[0] + "____" + booleans[1]);\n\n System.out.println(sub.isPermittedAll("user:add", "user:select"));\n\n //没有会抛出异常\n sub.checkPermission("user:select");\n sub.checkPermissions("user:select", "user:update");\n\n sub.logout();\n }\n
\n
\n3.集成web进行测试
1.新建一个maven的web项目
\n\nweb.xml 配置shiro的必须的配置: 监听器,过滤器
\n
\n<?xml version="1.0" encoding="UTF-8"?>\n<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"\n xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"\n version="3.1">\n <display-name>Archetype Created Web Application</display-name>\n\n <!--三个servlet配置 /login跳转到登陆页面 /home跳转到主页,即登陆成功页面 /admin用来测试角色和权限-->\n <servlet>\n <servlet-name>loginServlet</servlet-name>\n <servlet-class>com.servlet.LoginServlet</servlet-class>\n </servlet>\n <servlet-mapping>\n <servlet-name>loginServlet</servlet-name>\n <url-pattern>/login</url-pattern>\n </servlet-mapping>\n <servlet>\n <servlet-name>homeServlet</servlet-name>\n <servlet-class>com.servlet.HomeServlet</servlet-class>\n </servlet>\n <servlet-mapping>\n <servlet-name>homeServlet</servlet-name>\n <url-pattern>/home</url-pattern>\n </servlet-mapping>\n <servlet>\n <servlet-name>adminServlet</servlet-name>\n <servlet-class>com.servlet.AdminServlet</servlet-class>\n </servlet>\n <servlet-mapping>\n <servlet-name>adminServlet</servlet-name>\n <url-pattern>/admin</url-pattern>\n </servlet-mapping>\n\n <!--shiro监听-->\n <listener>\n <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>\n </listener>\n\n <!--shiro过滤器 这里过滤所有的地址 并且指定权限配置文件(一般项目中权限的配置存放在数据库中)-->\n <filter>\n <filter-name>ShiroFilter</filter-name>\n <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>\n <init-param>\n <param-name>config</param-name>\n <param-value>classpath:shiro.ini</param-value>\n </init-param>\n </filter>\n <filter-mapping>\n <filter-name>ShiroFilter</filter-name>\n <url-pattern>/*</url-pattern>\n </filter-mapping>\n\n</web-app>\n
\npom.xml :所需依赖
\n
\n<dependencies>\n <dependency>\n <groupId>org.apache.shiro</groupId>\n <artifactId>shiro-web</artifactId>\n <version>1.3.2</version>\n </dependency>\n <dependency>\n <groupId>org.apache.shiro</groupId>\n <artifactId>shiro-core</artifactId>\n <version>1.3.2</version>\n </dependency>\n <dependency>\n <groupId>org.apache.shiro</groupId>\n <artifactId>shiro-spring</artifactId>\n <version>1.3.2</version>\n </dependency>\n <dependency>\n <groupId>org.apache.tomcat</groupId>\n <artifactId>tomcat-servlet-api</artifactId>\n <version>8.5.4</version>\n <scope>provided</scope>\n </dependency>\n <dependency>\n <groupId>org.slf4j</groupId>\n <artifactId>slf4j-log4j12</artifactId>\n <version>1.7.7</version>\n <scope>compile</scope>\n </dependency>\n <dependency>\n <groupId>commons-logging</groupId>\n <artifactId>commons-logging</artifactId>\n <version>1.2</version>\n </dependency>\n <dependency>\n <groupId>commons-beanutils</groupId>\n <artifactId>commons-beanutils</artifactId>\n <version>1.9.3</version>\n </dependency>\n <dependency>\n <groupId>commons-collections</groupId>\n <artifactId>commons-collections</artifactId>\n <version>3.2.1</version>\n </dependency>\n <dependency>\n <groupId>jstl</groupId>\n <artifactId>jstl</artifactId>\n <version>1.2</version>\n </dependency>\n</dependencies>\n
\nshiro.ini: 权限配置文件,配置什么用户有什么角色,什么角色有什么权限
\n
\n[main]\nauthc.loginUrl=/login\nperms.unauthorizedUrl=/noAuth.jsp\nroles.unauthorizedUrl=/noAuth.jsp\n[users]\nzj2626=123456,admin\nay2626=456789,student\n[roles]\nadmin=user:*,student:select\nstudent:student:*\n[urls]\n/login=anon\n/home=authc\n/admin=roles[admin]\n
1.authc.loginUrl配置身份认证不通过(未登录时)跳转的地址…(loginUrl是authc的一个属性)
2.roles.unauthorizeUrl配置角色认证不通过跳转的地址…(noAuth.jsp页面目前只有一行字)
3.perms.unauthorizeUrl配置权限认证不通过跳转的地址
4.[users]下配置用户身份信息以及用户角色
5.[roles]下配置角色以及角色的控制权限
6.[urls]下配置访问地址所需的权限, 其中值为”anon过滤器”表示地址不需要登录即可访问; “authc过滤器”表示地址登录才能访问
7.值为 roles[admin] 表示 必须有角色为admin的用户才能范围
8.值为 perms[“student:create”] 表示 必须有权限为”student:create”的用户才能范围
9.多个过滤器用”,”隔开 而且相互为”且”的关系(必须同时满足才能访问)
10.地址可以使用?表示匹配单个任意字符(eg: /home?=authc 表示可过滤 /home1; /homef…..)
11.地址可以使用表示匹配任意个任意字符(eg: /home=authc 表示可过滤 /home123; /homeef…..)
12.地址可以使用表示匹配多路径(eg: /home/=authc 表示可过滤 /home/abc; /home/aaa/bbb…..)
2.编写sevlet代码
\nLoginServlet.java :身份验证地址
\n
\npackage com.servlet;\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.authc.UsernamePasswordToken;\nimport org.apache.shiro.subject.Subject;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * Created by zj on 2017/4/10.\n */\npublic class LoginServlet extends HttpServlet {\n /**\n * 跳转登录界面\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("user no login");\n resp.sendRedirect("login.jsp");\n }\n\n /**\n * 进行登录\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("登录");\n String userName = req.getParameter("userName");\n String password = req.getParameter("password");\n\n Subject subject = SecurityUtils.getSubject();\n //创建token令牌, 用户/密码\n UsernamePasswordToken token = new UsernamePasswordToken(userName, password);\n try {\n //身份认证 登录\n subject.login(token);\n System.out.println("登录成功");\n resp.sendRedirect("success.jsp");\n } catch (Exception e) {\n System.out.println("账号密码不对");\n e.printStackTrace();\n resp.sendRedirect("login.jsp");\n }\n }\n}\n\n\n//******************************************************************\nlogin.jsp\n\n<%@ page language="java" pageEncoding="UTF-8" %>\n<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>\n<!DOCTYPE HTML>\n<html>\n<body>\n<h2>Hello World</h2>\n登录:\n<form action="/login" method="post">\n <input type="text" value="" name="userName">\n <input type="password" value="" name="password">\n <input type="submit" value="登录">\n</form>\n</body>\n</html>\n
\nHomeServlet.java :登录成功以及退出登录地址
\n
\npackage com.servlet;\n\nimport org.apache.shiro.SecurityUtils;\nimport org.apache.shiro.subject.Subject;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * Created by zj on 2017/4/10.\n */\npublic class HomeServlet extends HttpServlet {\n /**\n * 进入主页(登陆成功界面)\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("主页 get");\n\n req.getRequestDispatcher("success.jsp").forward(req, resp);\n\n }\n\n /**\n * 用来退出登陆\n *\n * @param req\n * @param resp\n * @throws ServletException\n * @throws IOException\n */\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n System.out.println("主页 post");\n\n System.out.println("login out");\n\n //退出登录\n Subject subject = SecurityUtils.getSubject();\n subject.logout();\n\n resp.sendRedirect("login.jsp");\n }\n}\n\n//******************************************************************\nsuccess.jsp\n\n<%@ page contentType="text/html;charset=UTF-8" language="java" %>\n<html>\n<body>\n<h2>Hello World!</h2> 成功!!!\n<form action="/home" method="post">\n <input type="submit" value="退出登陆">\n</form>\n</body>\n</html>\n
\nAdminServlet.java
\n
\npackage com.servlet;\n\nimport javax.servlet.ServletException;\nimport javax.servlet.http.HttpServlet;\nimport javax.servlet.http.HttpServletRequest;\nimport javax.servlet.http.HttpServletResponse;\nimport java.io.IOException;\n\n/**\n * Created by zj on 2017/4/10.\n */\npublic class AdminServlet extends HttpServlet {\n protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println("ADMIN GET");\n }\n\n protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {\n System.out.println("ADMIN POST");\n }\n}\n
3.启动项目
测试身份认证
\n不登录情况下输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面
\n
\n\n\n输入正确的用户名(ay2626)密码 点击登录 成功; 再次输入地址 http://localhost:8080/home 跳转到成功的jsp
\n
\n\n\n点击退出登录 成功; 再次输入地址 http://localhost:8080/home 跳转到 /login地址转向的jsp页面
\n
\n测试角色认证
\n登录 :进行其他认证前先进行身份认证 使用ay2626用户登录(其角色只有student) 登录成功 跳转到success.jsp
\n
\n\n输入地址http://localhost:8080/admin 跳转到 http://localhost:8080/noAuth.jsp 表示没有权限访问此地址
\n
\n\n退出登录 用zj2626再次登录测试 登录成功 再次输入http://localhost:8080/admin 控制台打印”ADMIN POST” 表示访问成功
\n
\n\n测试权限认证
\n可把配置文件中 /admin的过滤器改为 /admin=perms[“student:create”] 进行测试
\n
\n\n测试发现 有权限的ay2626用户可以访问而没有权限的zj2626不能访问
\n
\n
\n
\n自定义Realm
\n实际开发中用户权限的配置要存放在数据库,so需要使用自定义realm来读取数据库中权限配置然后为用户赋予权限
\n
\n测试开发步骤
\n1.添加数据库依赖
\n
\n<dependency>\n <groupId>mysql</groupId>\n <artifactId>mysql-connector-java</artifactId>\n <version>5.1.39</version>\n <scope>compile</scope>\n</dependency>\n
\n2.设计构建测试的数据库表:有三个表:分别存储用户,角色,权限 并其他两个都与角色关联, 然后存入部分数据 ,再根据三个表建立对应实体(简单实体)
\n
\n\n\n3.添加数据库操作类(写的并不完善 仅测试使用)
\n
\npackage com.servlet;\n\nimport java.sql.*;\n\n/**\n * Created by zj on 2017/4/11.\n */\npublic class DBUtil {\n\n //获取数据库连接\n private static Connection getConnection() {\n try {\n Class.forName("com.mysql.jdbc.Driver");\n\n Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "fangshuoit");\n\n return connection;\n } catch (ClassNotFoundException | SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n\n /**\n * 通过用户名获取用户信息\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static User getByUserName(String name) throws SQLException {\n String sql = "select * from ay_user where loginName = ?";\n\n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n\n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new User(resultSet.getInt("id"), resultSet.getString("loginName"), resultSet.getString("password"));\n }\n\n return null;\n }\n\n /**\n * 通过用户名获取用户角色\n *\n * @param name\n * @return\n * @throws SQLException\n */\n public static Role getRolesByUserName(String name) throws SQLException {\n String sql = "select roleId from ay_user where loginName = ?";\n\n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setString(1, name);\n\n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n Integer id = resultSet.getInt("roleId");\n\n sql = "select * from ay_role where id = ?";\n preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, id);\n resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Role(resultSet.getInt("id"), resultSet.getString("name"));\n }\n }\n\n return null;\n }\n\n /**\n * 通过角色id获取角色权限\n *\n * @param roleId\n * @return\n * @throws SQLException\n */\n public static Perms getPermsByRole(Integer roleId) throws SQLException {\n String sql = "select * from ay_perms where roleId = ?";\n\n PreparedStatement preparedStatement = getConnection().prepareStatement(sql);\n preparedStatement.setInt(1, roleId);\n\n ResultSet resultSet = preparedStatement.executeQuery();\n if (resultSet.next()) {\n return new Perms(resultSet.getInt("id"), resultSet.getString("name"), resultSet.getInt("roleId"));\n }\n\n return null;\n }\n}\n
\n3.编写自定义的Realm类 需要AuthorizingRealm类并实现两个方法; 第一个是用来身份验证,第二是用来角色权限验证
\n
\npublic class MyRealm extends AuthorizingRealm {\n\n /**\n * 验证当前登录的用户(身份认证), 不再需要在配置文件中配置用户的信息和其角色信息\n * \n * @param token 封装有用户的信息\n * @return\n * @throws AuthenticationException\n */\n @Override\n protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {\n String userName = (String) token.getPrincipal();\n\n System.out.println("要登录的用户 : " + userName);\n\n try {\n User user = DBUtil.getByUserName(userName);//这里只是通过用户名验证并获取用户信息,实际开发中需要用户名以及加密的密码\n if (user != null) {\n return new SimpleAuthenticationInfo(user.getLoginName(), user.getPassword(), "XX");//返回登录信息\n } else\n return null;\n } catch (SQLException e) {\n e.printStackTrace();\n }\n return null;\n }\n\n /**( 身份验证(登录)以后 )\n * 为当前用户授予角色和权限(根据登录的用户名,读取数据库中其角色和权限)\n *\n * @param principals 封装了身份信息\n * @return\n */\n @Override\n protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {\n String userName = (String) principals.getPrimaryPrincipal();\n System.out.println("要权限的用户 : " + userName);\n\n SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();\n\n try {\n Role role = DBUtil.getRolesByUserName(userName);\n if (null != role) {\n Set<String> set = new HashSet<>();\n System.out.println("获得的角色: "+ role.getName());\n set.add(role.getName());\n authenticationInfo.setRoles(set);//赋予角色\n\n Perms perms = DBUtil.getPermsByRole(role.getId());\n Set<String> set2 = new HashSet<>();\n set2.add(perms.getName());\n System.out.println("获得的权限: "+ perms.getName());\n authenticationInfo.setStringPermissions(set2);//赋予权限\n\n return authenticationInfo;\n }\n } catch (SQLException e) {\n\n }\n\n return null;\n }\n}\n
\n4.修改配置文件shiro.ini ,引入自定义Realms,并去掉原来的 [users]和[roles]下的配置
\n
\n[main]\nauthc.loginUrl=/login\nperms.unauthorizedUrl=/noAuth.jsp\nroles.unauthorizedUrl=/noAuth.jsp\n\nmyRealm=com.servlet.MyRealm\nsecurityManager.realms=$myRealm\n[urls]\n/login=anon\n/home=authc\n/admin=roles[admin]\n
1.myRealm指向自定义Realm的位置
2.securityManager.realms指向自定义Realm的引用(表明使用自定义Realms进行安全认证),可以指向多个,用”,”隔开
\n5.启动项目测试 发现:使用zj2626登录可以访问 /admin地址;而ay2626登录不能访问(没有user角色);而且每次请求都会进行认证(控制台打印信息)
\n
"},{"title":"Mybatis 高级结果映射 ResultMap Association Collection","comments":1,"description":null,"date":"2017-06-18T16:00:00.000Z","_content":"转自 http://blog.csdn.net/ilovejava_2010/article/details/8180521\n\n# 高级结果映射\n> MyBatis的创建基于这样一个思想:数据库并不是您想怎样就怎样的。虽然我们希望所有的数据库遵守第三范式或BCNF(修正的第三范式),但它们不是。如果有一个数据库能够完美映射到所有应用程序,也将是非常棒的,但也没有。结果集映射就是MyBatis为解决这些问题而提供的解决方案。例如,我们如何映射下面这条语句?\n\n\n\n\n \n \n \n\n\n> 您可能想要把它映射到一个智能的对象模型,包括由一个作者写的一个博客,有许多文章(Post,帖子),每个文章由0个或者多个评论和标签。下面是一个复杂ResultMap 的完整例子(假定作者、博客、文章、评论和标签都是别名)。仔细看看这个例子,但是不用太担心,我们会一步步地来分析,一眼看上去可能让人沮丧,但是实际上非常简单的\n\n\n \n \n \n