查看原文
其他

Mybatis学习的深入浅出

E安全 2022-05-13

The following article is from 雷神众测 Author 李嘉奇

No.1

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。


No.2

先从简单的demo入手

User:

package com.test.entity;import lombok.Data;@Datapublic class User {   private int id;   private String name;   private String password;   private int age;   private int deleteFlag;
}

UserMapper:

public interface UserMapper {    public void insert(User user);    public User findUserById (int userId);    public List<User> findAllUsers();

}

user-mapping.xml:

<?xml version="1.0" encoding="UTF-8" ?>   <!DOCTYPE mapper  
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"  
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd"> <mapper namespace="com.test.dao.UserMapper">

  <select id="findUserById" resultType="com.test.entity.User" >
     select * from user where id = #{id}   </select></mapper>

测试方法:

public class UserDaoTest {    @Test
   public void findUserById() {
       SqlSession sqlSession = getSessionFactory().openSession();  
       UserMapper userMapper = sqlSession.getMapper(UserMapper.class);  
       User user = userMapper.findUserById(2);  
       Assert.assertNotNull("没找到数据", user);
   }    //Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与数据库进行交互
   private static SqlSessionFactory getSessionFactory() {  
       SqlSessionFactory sessionFactory = null;  
       String resource = "configuration.xml";  
       try {  
           sessionFactory = new SqlSessionFactoryBuilder().build(Resources  
                   .getResourceAsReader(resource));
       } catch (IOException e) {  
           e.printStackTrace();  
       }  
       return sessionFactory;  
   }  
}


No.3

配置简介 SqlSessionFactoryBuilder

上述例子中我们可以发现实现过程是:

1.创建SqlSessionFactoryBuilder

2.SqlSessionFactoryBuilder创建SqlSessionFactory

3.SqlSessionFactory.openSession()得到SqlSession

4.SqlSession.getMapper()得到具体的mapper实例

5.调用真正的方法比如findUserById

接下来就分析这几个过程的实现

先从SqlSessionFactoryBuilder入手, 咱们先看看源码是怎么实现的:

public class SqlSessionFactoryBuilder {  //Reader读取mybatis配置文件,传入构造方法
 //除了Reader外,其实还有对应的inputStream作为参数的构造方法,
 //这也体现了mybatis配置的灵活性
 public SqlSessionFactory build(Reader reader) {    return build(reader, null, null);
 }  public SqlSessionFactory build(Reader reader, String environment) {    return build(reader, environment, null);
 }  //mybatis配置文件 + properties, 此时mybatis配置文件中可以不配置properties,也能使用${}形式
 public SqlSessionFactory build(Reader reader, Properties properties) {    return build(reader, null, properties);
 }  //通过XMLConfigBuilder解析mybatis配置,然后创建SqlSessionFactory对象
 public SqlSessionFactory build(Reader reader, String environment, Properties properties) {    try {
     XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);      //下面看看这个方法的源码
     return build(parser.parse());
   } catch (Exception e) {      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
   } finally {
     ErrorContext.instance().reset();      try {
       reader.close();
     } catch (IOException e) {        // Intentionally ignore. Prefer previous error.
     }
   }
 }  public SqlSessionFactory build(Configuration config) {    return new DefaultSqlSessionFactory(config);
 }

}

通过源码,我们可以看到SqlSessionFactoryBuilder 通过XMLConfigBuilder 去解析我们传入的mybatis的配置文件, 下面就接着看看 XMLConfigBuilder 部分源码:

/**
* mybatis 配置文件解析
*/public class XMLConfigBuilder extends BaseBuilder {  public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
 }  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {    super(new Configuration());
   ErrorContext.instance().resource("SQL Mapper Configuration");    this.configuration.setVariables(props);    this.parsed = false;    this.environment = environment;    this.parser = parser;
 }  //外部调用此方法对mybatis配置文件进行解析
 public Configuration parse() {    if (parsed) {      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
   }
   parsed = true;    //从根节点configuration
   parseConfiguration(parser.evalNode("/configuration"));    return configuration;
 }  //此方法就是解析configuration节点下的子节点
 //由此也可看出,我们在configuration下面能配置的节点为以下10个节点
 private void parseConfiguration(XNode root) {    try {
     propertiesElement(root.evalNode("properties")); //issue #117 read properties first
     typeAliasesElement(root.evalNode("typeAliases"));
     pluginElement(root.evalNode("plugins"));
     objectFactoryElement(root.evalNode("objectFactory"));
     objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
     settingsElement(root.evalNode("settings"));
     environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
     databaseIdProviderElement(root.evalNode("databaseIdProvider"));
     typeHandlerElement(root.evalNode("typeHandlers"));
     mapperElement(root.evalNode("mappers"));
   } catch (Exception e) {      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
   }
 }
}

通过以上源码,我们就能看出,在mybatis的配置文件中:

  1. configuration节点为根节点。

  2. 在configuration节点之下,我们可以配置10个子节点, 分别为:properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers。


No.4


配置详解之XMLConfigBuilder(properties与environments )


先来回顾一下mvc中properties(数据库连接)与environments(不同的部署环境) 的配置

properties:

<configuration><!-- 方法一: 从外部指定properties配置文件, 除了使用resource属性指定外,还可通过url属性指定url  
 <properties resource="dbConfig.properties"></properties>
 -->
 <!-- 方法二: 直接配置为xml -->
 <properties>
     <property name="driver" value="com.mysql.jdbc.Driver"/>
     <property name="url" value="jdbc:mysql://localhost:3306/test1"/>
     <property name="username" value="root"/>
     <property name="password" value="root"/>
 </properties>

environments :

<environments default="development">
   <environment id="development">
     <transactionManager type="JDBC"/>
     <dataSource type="POOLED">
         <!--
         如果上面没有指定数据库配置的properties文件,那么此处可以这样直接配置
       <property name="driver" value="com.mysql.jdbc.Driver"/>
       <property name="url" value="jdbc:mysql://localhost:3306/test1"/>
       <property name="username" value="root"/>
       <property name="password" value="root"/>
        -->

        <!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>

     </dataSource>
   </environment>

   <!-- 我再指定一个environment -->
   <environment id="test">
     <transactionManager type="JDBC"/>
     <dataSource type="POOLED">
       <property name="driver" value="com.mysql.jdbc.Driver"/>
       <!-- 与上面的url不一样 -->
       <property name="url" value="jdbc:mysql://localhost:3306/demo"/>
       <property name="username" value="root"/>
       <property name="password" value="root"/>
     </dataSource>
   </environment>

 </environments>

mybatis 是通过XMLConfigBuilder这个类在解析mybatis配置文件的,那么接着看看XMLConfigBuilder对于properties和environments的解析:

XMLConfigBuilder:

public class XMLConfigBuilder extends BaseBuilder {    private boolean parsed;    //xml解析器
   private XPathParser parser;    private String environment;    //上次说到这个方法是在解析mybatis配置文件中能配置的元素节点
   //今天首先要看的就是properties节点和environments节点
   private void parseConfiguration(XNode root) {        try {          //解析properties元素
         propertiesElement(root.evalNode("properties")); //issue #117 read properties first
         typeAliasesElement(root.evalNode("typeAliases"));
         pluginElement(root.evalNode("plugins"));
         objectFactoryElement(root.evalNode("objectFactory"));
         objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
         settingsElement(root.evalNode("settings"));          //解析environments元素
         environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
         databaseIdProviderElement(root.evalNode("databaseIdProvider"));
         typeHandlerElement(root.evalNode("typeHandlers"));
         mapperElement(root.evalNode("mappers"));
       } catch (Exception e) {          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
       }
   }    //下面就看看解析properties的具体方法
   private void propertiesElement(XNode context) throws Exception {        if (context != null) {          //将子节点的 name 以及value属性set进properties对象
         //这儿可以注意一下顺序,xml配置优先, 外部指定properties配置其次
         Properties defaults = context.getChildrenAsProperties();          //获取properties节点上 resource属性的值
         String resource = context.getStringAttribute("resource");          //获取properties节点上 url属性的值, resource和url不能同时配置
         String url = context.getStringAttribute("url");          if (resource != null && url != null) {            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
         }          //把解析出的properties文件set进Properties对象
         if (resource != null) {
           defaults.putAll(Resources.getResourceAsProperties(resource));
         } else if (url != null) {
           defaults.putAll(Resources.getUrlAsProperties(url));
         }          //将configuration对象中已配置的Properties属性与刚刚解析的融合
         //configuration这个对象会装载所解析mybatis配置文件的所有节点元素,以后也会频频提到这个对象
         //既然configuration对象用有一系列的get/set方法, 那是否就标志着我们可以使用java代码直接配置?
         //答案是肯定的, 不过使用配置文件进行配置,优势不言而喻
         Properties vars = configuration.getVariables();          if (vars != null) {
           defaults.putAll(vars);
         }          //把装有解析配置propertis对象set进解析器, 因为后面可能会用到
         parser.setVariables(defaults);          //set进configuration对象
         configuration.setVariables(defaults);
       }
   }    //下面再看看解析enviroments元素节点的方法
   private void environmentsElement(XNode context) throws Exception {        if (context != null) {            if (environment == null) {                //解析environments节点的default属性的值
               //例如: <environments default="development">
               environment = context.getStringAttribute("default");
           }            //递归解析environments子节点
           for (XNode child : context.getChildren()) {                //<environment id="development">, 只有enviroment节点有id属性,那么这个属性有何作用?
               //environments 节点下可以拥有多个 environment子节点
               //类似于这样: <environments default="development"><environment id="development">...</environment><environment id="test">...</environments>
               //意思就是我们可以对应多个环境,比如开发环境,测试环境等, 由environments的default属性去选择对应的enviroment
               String id = child.getStringAttribute("id");                //isSpecial就是根据由environments的default属性去选择对应的enviroment
               if (isSpecifiedEnvironment(id)) {                    //事务, mybatis有两种:JDBC 和 MANAGED, 配置为JDBC则直接使用JDBC的事务,配置为MANAGED则是将事务托管给容器,
                   TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));                    //enviroment节点下面就是dataSource节点了,解析dataSource节点(下面会贴出解析dataSource的具体方法)
                   DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                   DataSource dataSource = dsFactory.getDataSource();
                   Environment.Builder environmentBuilder = new Environment.Builder(id)
                         .transactionFactory(txFactory)
                         .dataSource(dataSource);                    //老规矩,会将dataSource设置进configuration对象
                   configuration.setEnvironment(environmentBuilder.build());
               }
           }
       }
   }    //下面看看dataSource的解析方法
   private DataSourceFactory dataSourceElement(XNode context) throws Exception {        if (context != null) {            //dataSource的连接池
           String type = context.getStringAttribute("type");            //子节点 name, value属性set进一个properties对象
           Properties props = context.getChildrenAsProperties();            //创建dataSourceFactory
           DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance();
           factory.setProperties(props);            return factory;
       }        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
   }
}

还有一个问题, 上面我们看到,在配置dataSource的时候使用了 ${driver} 这种表达式, 这种形式是怎么解析的?其实,是通过PropertyParser这个类解析:

/**
* 这个类解析${}这种形式的表达式
*/public class PropertyParser {  public static String parse(String string, Properties variables) {
   VariableTokenHandler handler = new VariableTokenHandler(variables);
   GenericTokenParser parser = new GenericTokenParser("${", "}", handler);    return parser.parse(string);
 }  private static class VariableTokenHandler implements TokenHandler {    private Properties variables;    public VariableTokenHandler(Properties variables) {      this.variables = variables;
   }    public String handleToken(String content) {      if (variables != null && variables.containsKey(content)) {        return variables.getProperty(content);
     }      return "${" + content + "}";
   }
 }
}

至此,将数据库的配置文件封装成了configuration对象,以及根据configuration定制化了SqlSessionFactory


No.5

获取SqlSession

回顾一下之前的流程

当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。源码如下:

/**
  * 通常一系列openSession方法最终都会调用本方法
  * @param execType
  * @param level
  * @param autoCommit
  * @return
  */
 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
   Transaction tx = null;    try {      //通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
     final Environment environment = configuration.getEnvironment();      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
     tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);      //之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
     final Executor executor = configuration.newExecutor(tx, execType);      //关键看这儿,创建了一个DefaultSqlSession对象
     return new DefaultSqlSession(configuration, executor, autoCommit);
   } catch (Exception e) {
     closeTransaction(tx); // may have fetched a connection so lets call close()
     throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
   } finally {
     ErrorContext.instance().reset();
   }
 }

再回想一下之前写的Demo

SqlSessionFactory sessionFactory = null;  
String resource = "mybatis-conf.xml";  
try {     //SqlSessionFactoryBuilder读取配置文件
   sessionFactory = new SqlSessionFactoryBuilder().build(Resources  
             .getResourceAsReader(resource));
} catch (IOException e) {  
   e.printStackTrace();  
}    
//通过SqlSessionFactory获取SqlSessionSqlSession sqlSession = sessionFactory.openSession();

还真这么一回事儿,对吧!

SqlSession咱们也拿到了,咱们可以调用SqlSession中一系列的select…,  insert…, update…, delete…方法轻松自如的进行CRUD操作了。


No.6

获取Mapper

先放流程图:

有了SqlSession,就可以调用SqlSession.getMapper(DAO.Class)得到具体的dao,因为上面说了SqlSession本质是DefaultSqlSession,所以先看DefaultSqlSession.getMapper方法

/**
  * 什么都不做,直接去configuration中找
  */
 @Override
 public <T> T getMapper(Class<T> type) {    return configuration.<T>getMapper(type, this);
 }

再来看Configuration.getMapper方法

 protected final MapperRegistry mapperRegistry = new MapperRegistry(this);/**
  * 交给了mapperRegistry
  * @param type
  * @param sqlSession
  * @return
  */
 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {    return mapperRegistry.getMapper(type, sqlSession);
 }

来看MapperRegistry实现

    //一个class类对应一个动态代理工厂
   private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();/**
  * 实现getMapper
  * @param type
  * @param sqlSession
  * @return
  */
 @SuppressWarnings("unchecked")  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {    //交给MapperProxyFactory去做
   final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);    if (mapperProxyFactory == null) {      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
   }    try {      //关键在这儿
     return mapperProxyFactory.newInstance(sqlSession);
   } catch (Exception e) {      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
   }
 }

最终由mapperProxyFactory.newInstance(sqlSession)创建实例,来看源码:

/**
  * 创建实例
  * @param mapperProxy
  * @return
  */
 @SuppressWarnings("unchecked")  protected T newInstance(MapperProxy<T> mapperProxy) {    //mapperInterface,说明Mapper接口被代理了,这样子返回的对象就是Mapper接口的子类,方法被调用时会被mapperProxy拦截,也就是执行mapperProxy.invoke()方法
   return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
 }  public T newInstance(SqlSession sqlSession) {    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);    return newInstance(mapperProxy);
 }

这里就是返回的一个代理类实例MapperProxy。

package org.apache.ibatis.binding;import java.io.Serializable;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.util.Map;import org.apache.ibatis.reflection.ExceptionUtil;import org.apache.ibatis.session.SqlSession;/**
* @author Clinton Begin
* @author Eduardo Macarron
*/public class MapperProxy<T> implements InvocationHandler, Serializable {  private static final long serialVersionUID = -6424540398559729838L;  private final SqlSession sqlSession;  //Mapper接口
 private final Class<T> mapperInterface;    /*
    * Mapper接口中的每个方法都会生成一个MapperMethod对象, methodCache维护着他们的对应关系
    */  

   private final Map<Method, MapperMethod> methodCache;  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {    this.sqlSession = sqlSession;    this.mapperInterface = mapperInterface;    this.methodCache = methodCache;
 }  //这里会拦截Mapper接口的所有方法
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {    if (Object.class.equals(method.getDeclaringClass())) { //如果是Object中定义的方法,直接执行。如toString(),hashCode()等
     try {        return method.invoke(this, args);//
     } catch (Throwable t) {        throw ExceptionUtil.unwrapThrowable(t);
     }
   }    final MapperMethod mapperMethod = cachedMapperMethod(method);  //其他Mapper接口定义的方法交由mapperMethod来执行
   return mapperMethod.execute(sqlSession, args);
 }  private MapperMethod cachedMapperMethod(Method method) {
   MapperMethod mapperMethod = methodCache.get(method);    if (mapperMethod == null) {
     mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
     methodCache.put(method, mapperMethod);
   }    return mapperMethod;
 }

}

在这里可以看到mapper实例是根据动态代理创建的,当调用mapper的方法时,实际上是交由 mapperMethod.execute来执行。


No.7

Excutor执行sql语句

接下来,咱们才要真正去看sql的执行过程了。

先放流程图:

上一步已经知道,当调用mapper的方法时,实际上是交由 mapperMethod.execute(sqlSession, args)来执行。来看源码:

/**
  * 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了
  * @param sqlSession
  * @param args
  * @return
  */
 public Object execute(SqlSession sqlSession, Object[] args) {
   Object result;    if (SqlCommandType.INSERT == command.getType()) {
     Object param = method.convertArgsToSqlCommandParam(args);
     result = rowCountResult(sqlSession.insert(command.getName(), param));
   } else if (SqlCommandType.UPDATE == command.getType()) {
     Object param = method.convertArgsToSqlCommandParam(args);
     result = rowCountResult(sqlSession.update(command.getName(), param));
   } else if (SqlCommandType.DELETE == command.getType()) {
     Object param = method.convertArgsToSqlCommandParam(args);
     result = rowCountResult(sqlSession.delete(command.getName(), param));
   } else if (SqlCommandType.SELECT == command.getType()) {      if (method.returnsVoid() && method.hasResultHandler()) {
       executeWithResultHandler(sqlSession, args);
       result = null;
     } else if (method.returnsMany()) {
       result = executeForMany(sqlSession, args);
     } else if (method.returnsMap()) {
       result = executeForMap(sqlSession, args);
     } else {
       Object param = method.convertArgsToSqlCommandParam(args);
       result = sqlSession.selectOne(command.getName(), param);
     }
   } else {      throw new BindingException("Unknown execution method for: " + command.getName());
   }    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {      throw new BindingException("Mapper method '" + command.getName()
         + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
   }    return result;
 }

既然又回到SqlSession了, 那么咱们就看看SqlSession的CRUD方法了,为了省事,还是就选择其中的一个方法来做分析吧。这儿,咱们选择了selectList方法:

 @Override
 public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {    try {
     MappedStatement ms = configuration.getMappedStatement(statement);        //CRUD实际上是交给Excetor去处理
     return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
   } catch (Exception e) {      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
   } finally {
     ErrorContext.instance().reset();
   }
 }

通过一层一层的调用,最终会来到doQuery方法,看看doQuery方法的实现吧,我这儿选择了SimpleExecutor:

@Override
 public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
   Statement stmt = null;    try {
     Configuration configuration = ms.getConfiguration();        //StatementHandler封装了Statement, 让 StatementHandler 去处理
     StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
     stmt = prepareStatement(handler, ms.getStatementLog());      return handler.<E>query(stmt, resultHandler);
   } finally {
     closeStatement(stmt);
   }
 }

接下来,咱们看看StatementHandler 的一个实现类 PreparedStatementHandler(这也是我们最常用的,封装的是PreparedStatement), 看看它使怎么去处理的:

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {     //到此,原形毕露, PreparedStatement
   PreparedStatement ps = (PreparedStatement) statement;
   ps.execute();    //结果交给了ResultSetHandler 去处理
   return resultSetHandler.<E> handleResultSets(ps);
 }

到此, 一次sql的执行流程就完了。


No.8

招聘启事

雷神众测SRC运营(实习生)
————————

工作地点:杭州(总部)、广州、成都、上海、北京

【职责描述】
1.  负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2.  负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3.  参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4.  积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5.  积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。

【任职要求】 
 1.  责任心强,性格活泼,具备良好的人际交往能力;
 2.  对网络安全感兴趣,对行业有基本了解;
 3.  良好的文案写作能力和活动组织协调能力。



雷神众测白帽运营(实习生)

————————

工作地点:杭州(总部)、广州、成都、上海、北京

【岗位职责】

1.准确了解白帽子爱好,发掘白帽子需求

2.负责各类周边、礼物的挑选与采购

3.对黑客文化有深刻认知

4.维护白帽关系


【任职要求】

1.具有良好的审美眼光

2.具备定制礼品礼物经验

3.较强的沟通以及协调能力

4.为人正直,具备良好的职业道德,能吃苦耐劳,具有良好的团队合作精神


【加分项】

1、具备美术功底、懂得设计美化等

2、有互联网运营经验


简历投递至 strategy@dbappsecurity.com.cn

设计师

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽


简历投递至 strategy@dbappsecurity.com.cn


安全招聘
————————

公司:安恒信息
岗位:Web安全 安全研究员
部门:安服战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;


岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 strategy@dbappsecurity.com.cn

安全服务工程师/渗透测试工程师


工作地点:新疆


1、掌握渗透测试原理和渗透测试流程,具备2年以上渗透测试工作经验,能够独立完成渗透测试方案和测试报告;
2、熟悉风险评估、安全评估;
3、熟练掌握各类渗透工具,如Sqlmap、Burpsuite、AWVS、Appscan、Nmap、Metasploit、Kali等;
4、熟练掌握Web渗透手法,如SQL注入、XSS、文件上传等攻击技术;
5、至少熟悉一种编程语言(php/java/python),能够独立编写poc者优先;
6、具有良好的沟通能力和文档编写能力,动手能力强;
7、对工作有热情,耐心、责任心强,具备沟通能力和良好的团队意识;
8、加分项:有安全开发经验/可进行代码审计工作;
9、加分项:有安全和网络相关证书,如CISSP、CISA、CISP 、CCNP、CCIE等认证者;
岗位职责:
1、参与安全服务项目实施,其中包括渗透测试与安全评估,代码审计,安全培训,应急响应;
2、独立完成安全服务项目报告编写,输出安全服务实施报告;


简历投递至 strategy@dbappsecurity.com.cn

专注渗透测试技术

全球最新网络攻击技术


END


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存