Mybatis深入浅出

admin 2022年5月19日10:37:42安全开发评论7 views20980字阅读69分56秒阅读模式

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

回顾一下之前的流程

Mybatis深入浅出

当我们获取到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

先放流程图:

Mybatis深入浅出

有了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的执行过程了。

先放流程图:

Mybatis深入浅出

上一步已经知道,当调用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、有互联网运营经验

简历投递至 [email protected]

设计师

————————

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

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

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

简历投递至 [email protected]

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

公司:安恒信息
岗位: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.具备良好的英语文档阅读能力。

简历投递至 [email protected]

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

工作地点:新疆

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、独立完成安全服务项目报告编写,输出安全服务实施报告;

简历投递至 [email protected]

Mybatis深入浅出

专注渗透测试技术

全球最新网络攻击技术

END

Mybatis深入浅出

原文始发于微信公众号(白帽子):Mybatis深入浅出

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月19日10:37:42
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  Mybatis深入浅出 http://cn-sec.com/archives/1019699.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: