使用代码生成器生成的代码操作数据库
如图10-4所示,mybatis-generator自动生成了Domain、Mapper和XML文件,其中Domain包括了Entity和 Example。Entity和数据库表结构一一对应,Example是我们操作数据库使用最频繁的类,它封装了分页、排序、查询条件等方法,我们做单表CRUD时就会大量使用Example,可以达到过滤条件的目的。Mapper封装了基本的CRUD方法,它和XML定义的Mapper对应,下面是其中一个数据库表对应的Domain、Mapper和XML的部分内容:
代码语言:javascript复制public class User implements Serializable {
private Long id;
private Date gmtCreate;private Date gmtModified;private string username;private String password;...
}
public class UserExample implements Serializable {
protected String orderByclause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
private static final long serialversionuID = 1L;private Integer limit;
private Integer offset;
public Criteria andIdIsNull(){
addCriterion( "id is null");return (criteria) this;
}
...
}
public interface UserMapper {
int countByExample(UserExample example);int deleteByExample(UserExample example);int deleteByPrimaryKey(Long id);
int insert(User record);
int insertSelective(User record);
List<User> selectByExample(UserExample example);User selectByPrimaryKey(Long id);
int updateByExampleSelective(@Param(" record") User record,@Param( " example")
UserExample example);
int updateByExample(@Param(" record") User record,@Param("example") UserExample
example);
int updateByPrimaryKeySelective(User record);int updateByPrimaryKey(User record);
}
<mapper namespace="com.lynn.blog .pub.mapper. UserMapper" >
<resultMap id="BaseResultMap" type="com.lynn.blog.pub . domain.entity.User" ><id column= "id" property="id" jdbcType="BIGINT”/>
<result column="gmt_create" property="gmtCreate"jdbcType="TINESTAMP"/><result column="gmt_modified" property="gmtModified" jdbcType="TIMESTAMP"/><result column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR"/></resultMap>
...
</mapper>
在操作单表时,我们无须针对每个功能都编写一个SQL语句,只需要灵活运用Example即可实现我们想要的功能,Example实现了所有字段的查询条件,如=、!=、>、<、AND、OR、BETWEEN等。
查看Mapper代码,可以发现查询方法为selectByExample,需要传入Example,因此我们可以构建一个Example并设置查询条件。以User为例,如果我们要查询用户名为xxx的用户,则构建的Example 如下:
代码语言:javascript复制UserExample example = new UserExample();
example.createcriteria()
.andUsernameEqualTo( ""xxx");
然后调用selectByExample方法,如:
代码语言:javascript复制userMapper.selectByExample(example);
新增数据的方法以insert开头,传入的参数是Entity。insert和 insertSelective的区别在于前者不会进行判断,即如果Entity有字段为null,则会将null值保存到该字段中,而后者会判断字段是否为null,如果为null 则不会将null值保存到该字段中。
修改和删除两个方法的使用比较类似,需要注意的是,凡是名称中带有selective的方法均会先判断字段是否为null,否则不会判断,读者在调用时可根据实际场景进行选择。
查询、修改和删除都有两个方式:按ID和按条件。按ID操作时后面都会带上ByPrimaryKey。
如果数据库的某个字段为text类型,则生成时会多生成一个selectByExamplewithBLOBs 方法,在查询时如果只调用selectByExample方法,则不会查询类型为text的字段,此时若要返回该字段,则需调用selectByExamplewithBLOBs方法。
MyBatis应对复杂SQL
MyBatis的一大优势是它是操作原生SQL,因此它可以应对很多复杂场景,而一些大型应用,都存在一些较为复杂的业务场景。前面学习的代码生成器主要针对单表的操作,面对复杂的业务,我们就需要自己编写SQL。
MyBatis提供了多种实现方式,包括XML、注解和Provider,而代码生成器生成了基本的CRUD代码,为了提升代码的扩展性,这里不能直接在原有的Mapper上增加方法,而应扩展一个子Mapper继承代码生成器生成的Mapper,如:
代码语言:javascript复制@Mapper
public interface SubBlogMapper extends BlogMapper {
}
代码生成器生成的Entity和数据库一一对应,如果当前业务需要的字段和数据库字段不一致时,也应扩展一个子Entity。扩展方法的代码如下:
代码语言:javascript复制@Data
public class SubBlog extends Blog i
**
*用户名*/
private String username;
}
比如我们在返回博客列表时,往往需要返回当前博主的用户名等信息,而博客表只关联了用户ID,这时就需要扩展一个子Entity,并且查询时返回子Entity。
以上是一个比较良好的代码设计风格,也符合软件的架构模式,接下来就以博客列表为例,用注解和 Provider两种方式分别讲解如何应对复杂 SQL。
注解
通过注解来查询SQL非常简单,只需要在方法上加入@Select()即可(括号内输入SQL语句),如:
代码语言:javascript复制@Select("select* from blog b,user u where b.user_id = u.id limit #{offset},#{limit}")List<SubBlog> selectBlogList(@Param( "offset") int offset,@Param("limit") int limit);
同XML一样,注解也可以使用<if>和<for>等标签,但必须用<script></script>将SQL语句包裹,如:
代码语言:javascript复制@Select("<script>select * from blog b,user u where b.user_id = u.id cif test= "null !-
title ">and b.title = #{title}</if> limit #{offset} ,#{limit}</script>")
List<SubBlog> selectBlogList(@Param("title")String title,@Param("offset") int
offset,@Param( "limit") int limit);
当条件较少时,这种写法没有问题,但如果条件很多,用这种注解的方式就不可取了。注解是写到字符串里面的,所以当单词拼写错误时,编译器不会报错,于是在包含复杂SQL语句的情况下很难排查错误。这时候,就轮到Provider登场了。
Provider
将方法标注为Provider(查询为@selectProvider,新增为@InsertProvider,修改为@UpdateProvider,删除为@DeleteProvider ),然后通过Provider的方法动态生成SQL语句,将上述注解的SQL语句改造成Provider 如下:
代码语言:javascript复制SelectProvider(type= BlogProvider.class,method = "selectBlogListProvider")
List<SubBlog> selectBlogList(@Param("title")String title,@Param("offset") int offset,
@Param( "limit") int limit);
public class BlogProvider i
public string selectBlogListProvider(@Param("title")String title,@Param("offset")
int offset,@Param( "limit") int limit){
return new sQL(O{
{
SELECT("*");
FROM("blog b,user u");wHERE("b.user_id = u.id");if(null != title){
wHERE("b.title =#{title}");
}
}
}.toString( "limit #{offset},#{limit}";
}
}
可以看到,上述代码没有使用@Select注解,而是采用@selectProvider注解,该注解会指定一个类,并指定该类的方法。当调用selectBlogList方法时,MyBatis就会指定BlogProvider类的selectBlogListProvider方法。
selectBlogListProvider方法的参数和 selectBlogList方法的参数保持一致,在方法体内直接返回sQL对象,并使用toString方法转换为字符串返回,其他方法的作用就是动态生成SQL语句(如SELECT("*")表示生成SELECT *,FROM("blog,user u")表示生成FROM blog b ,user u),它最终执行的是Provider生成的SQL语句。读者看到 sQL对象内的代码是否感觉似曾相识呢?没错,它和前面自己写的SQL语句是一样的,只是这里是调用了Java方法,比如SELECT("*")最终返回的就是select *。
通过Provider可以将一些关键词( select、from、where、order by等)用Java代码代替,大大提升了可读性。
功能开发
本节中,我们将正式进入产品的功能开发,根据第5章提供的原型设计,我们可以将产品划分为以下几大模块。
用户管理:主要操作用户表,包括注册登录,用户信息管理等功能。口博客管理:主要操作博客表,包括博客的展示、发布等。
口评论管理:主要操作评论相关表,包括评论的展示、发表、点赞等。分类管理:主要操作分类表,包括分类列表展示等。
搜索服务:主要用于提供搜索引擎服务,开放博客的搜索接口。
对这些模块都创建一个子工程,每一个工程都是一个微服务,如图10-5所示。
图中的public为各微服务的公共类库。
接下来,将以博客列表功能为例,来讲解功能的开发。
(1)创建输入参数Request和输出参数Response:
代码语言:javascript复制@Data
public class BlogListRequest i
//加了@NotNull注解表示参数必填@NotNull
private Long categoryId;@NotNull
private Integer offset;@NotNull
private Integer limit;
}
@Data
public class BlogListResponse {i
private Long id;
private string title;private string summary;private String createTime;private Integer viewCount;
}
每一个接口(业务)都应该对应一个请求和一个响应,因此我们在提供接口时,首先要分析该接口接收什么参数,返回什么参数,从而定义Request和 Response。
(2)定义接口:
代码语言:javascript复制public interface BlogService i
**
*根据分类ID获得博客列表* @param request
*@return
*/
MultiResult<BlogListResponse> getBlogListByCategoryId(BlogListRequest request);
)
(3)实现接口:
代码语言:javascript复制@Service
public class BlogServiceImpl implements BlogService {
@Autowired
private BlogMapper blogMapper;@override
public MultiResult<BlogListResponse> getBlogListByCategoryId(BlogListRequest request){
BlogExample example = new BlogExample();
example.setOffset(request.getOffset());example.setLimit( request.getLimit())3example.createCriteria()
.andCategoryIdEqualTo(request.getcategoryId());
int count = blogMapper.countByExample(example);
if(count > 0){
List<Blog>bloglist = blogMapper.selectByExample(example);if(null != blogList && blogList.size( >e){
List<BlogListResponse> data = new ArrayList<>();blogList.stream( ).forEach(blog ->{
BlogListResponse response = new BlogListResponse();//将blog对象属性复制到response
Beanutils.copyProperties(blog, response);response.setCreateTime
(Dateutils.parseDate2String(blog.getGmtcreate() , "yyyy-MM-
dd HH : mm : ss"));
data.add(response) ;
});
return MultiResult.buildSuccess(data, count);
}
return MultiResult.buildSuccess(new ArrayList<>(), count);
}
return MultiResult.buildSuccess(new ArrayList<>() , count);
}
}
上述代码实现了一个最基本的接口:通过分类ID返回博客列表,其中数据查询部分使用10.2节介绍的代码生成器。我们将查询出的数据进行了一些处理,首先通过BeanUtils.copyProperties将Entity 的数据复制到Response 中,并处理一些数据,比如格式化时间等。
(4)编写控制器,以提供HTTP 调用能力:
代码语言:javascript复制@RequestMapping( "{version }/open/blog")@RestController
public class BlogController extends BaseV1controller {
@Autowired
private BlogService blogService;
@PostMapping( "getBlogListByCategoryId")
public MultiResult<BlogListResponse> getBlogListByCategoryId(@Valid @RequestBody
BlogListRequest request,BindingResult result){
validate(result);
return blogService.getBlogListByCategoryId(request);
}
}
控制器的代码其实简单,就是调用service方法。需要注意的是,在调用Service方法之前,应调用validate方法进行参数的合法性校验。
(5)测试。
分别启动register . config . gateway和 blogmgr,用postman请求地址 htp:/localhost:8080/BLOG/v1/open/blog/getBlogListByCategoryld?token=1,可得到如图10-6所示的界面。
网关鉴权
前面已经提到,我们请求的所有接口都需要通过网关来转发,而不是直接请求服务。对于一个HTTP接口来说,安全是最重要的,本节将介绍博客应用的鉴权机制。
细心的读者可以发现,上一节定义的接口地址中带有open接口,其实对于接口,我们可以大致划分为开放接口和私有接口。开放接口指无须用户登录即可访问的接口,私有接口则为登录后才能访问的接口。为了便于区分开放接口和私有接口,我们可以在接口地址“做文章”,即带有open 的为开放接口,带有close的为私有接口。
防止参数被篡改
我们提供的接口是通过网络传输的,如果在传输过程中参数被拦截并将修改后的参数传输给服务器端,后果将非常严重。为了防止此类事件发生,我们需要对参数进行签名并校验。
签名的规则是,客户端将参数名按ASCII 码升序排列,构建形如 key1=valuel&key2=value2……的字符串(后面用url代替该字符串),然后将这个字符串进行MD5加密,如 MD5(url key)(其中 key为密钥),加密后生成签名字符串,将签名字符串放到请求头( header )中,参数放到请求体( body)中,传递到服务端。服务端以同样的方式签名,将签名后的结果和客户端传递过来的结果进行比较,如果一致说明参数没有被篡改,可以放过,否则中断操作。
这样如果中途有人篡改了参数,服务器签名后和客户端签名必然是不匹配的,有效地保护了参数的合法性。下面就来改造gateway工程的ApiGlobalFilter类,具体的代码如下:
代码语言:javascript复制@Value("${api.encrypt.key}")
private string salt;
@Override
public Mono<Void> filter(ServerwebExchange exchange,GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String body = requestBody( serverHttpRequest);
String uriBuilder = getUr1AuthenticationApi(body);//服务端生成额签名
string sign = MessageDigestutils.encrypt(uriBuilder salt,Algorithm.MD5);1/从header中取得签名字符串
String signature = serverHttpRequest.getHeaders().getFirst("signature");if (sign l= null && sign.equals(signature)) {
1/以下代码再次包装request,否则会报:Only one connection receive subscriber
allowed.错误
URI uri = serverHttpRequest.getURI();
ServerHttpRequest request = serverHttpRequest.mutate().uri(uri) .build();DataBuffer bodyDataBuffer = stringBuffer(body);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);request = new ServerHttpRequestDecorator(request){
@override
public Flux<DataBuffer> getBody( {
return bodyFlux;
}
};
return chain.filter(exchange.mutate( ).request(request).build());else {
//签名错误
ServerHttpResponse response = exchange.getResponse();
byte[ ] bits =JSON.to3SONString(SingleResult.buildSuccess(Code.NO_PERMISSION,
"签名错误"))-getBytes(Standardcharsets.UTF_8);
DataBuffer buffer = response.bufferFactory ().wrap(bits);return response.writewith(Mono.just( buffer));
}
}
/**
*将客户端传回的参数按照ASCII 码升序排序生成URL字符串*/
private string getUrlAuthenticationApi(String body){
if(StringUtils.isEmpty( body)) f
return nul1;
}
List<String>nameList = new ArrayList<>()3
StringBuilder urlBuilder = new stringBuilder(;SONObject requestBodyson = null;
requestBody3son = 3SON .parseobject( body);nameList.addAll(requestBodyJson. keySet();
final 3SONObject requestBody3sonFinal = requestBodyson;namelist.stream() . sorted( ).forEach(name -> {
if(null != requestBodysonFinal){
ur1Builder. append( '&' );
urlBuilder.append(name) . append( '=' ).append(requestBody3sonFinal.
getstring(name)) ;
}
});
urlBuilder.deleteCharAt(0);return urlBuilder.tostring();
}
/**
*获得body 数据*@return请求体*/
private string requestBody(ServerHttpRequest serverHttpRequest){
//获取请求体
F1ux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body . subscribe( buffer ->{
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());DataBufferUtils.release( buffer);
bodyRef.set( charBuffer.toString());});
return bodyRef.get();
}
private DataBuffer stringBuffer(String value)i
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory
( ByteBufAllocator. DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);buffer.write(bytes);
return buffer;
}
上述代码的作用是判断当前请求参数是否正常(即是否被篡改)。首先,调用requestBody方法获得body里的参数(JSON格式),然后调用getUrlAuthenticationApi方法将参数名按照ASCII码升序排列,以key1=value1&key2=value2的形式拼接成字符串urlBuilder,接着通过MD5(urIlBuilder saltR)的形式加密,返回签名字符串sign,最后从请求头中取得signature进行判断,如果sign和signature相等,则签名通过,否则签名失败,予以拦截。
由于签名验证通过后参数是放到body 中传输的,所以不能直接返回 Mono(如果以form表单形式或者直接放到请求地址中可以直接返回),需要再进行一层包装,否则会抛出“Only one connectionreceive subscriber allowed”异常。正如上述代码中,我们将 body中的参数转成DataBuffer并通过 ServerHttpRequestDecorator类做一层包装后返回。
拦截非法请求
所有私有接口都带有close,而要调用私有接口则必须为已登录用户,程序确认客户端是否为登录用户的依据就是判断token是否合法。
当用户调用登录接口后,服务端会根据用户名、密码和时间戳等信息生成token,并将token保存到Redis返回给客户端。我们要求客户端在调用私有接口时,向请求头传人token,服务端在过滤器里判断当前token是否正确,如果正确,则允许调用接口,否则给出错误提示。
生成token 的方式很随意,读者可以根据自己的喜好来生成,可以用MD5、Base64和AES等算法,下面是使用AES算法生成token的代码,如:
代码语言:javascript复制public static String generateToken(String username,string key){
try {
return AesEncryptutils.aesEncrypt(username System.currentTimeMillis(),key);}catch (Exception e){
e.printStackTrace();return null;
}
}
token生成后需要将它存入Redis,key为token,value为user.getId()方法获取到的userId:
代码语言:javascript复制redis.set(token, user.getId() "");
这样当客户端传入token时,我们就可以从Redis里根据token读取userId,如果能取到说明token合法,反之为非法请求。私有接口需传入userId并与服务器取得的userId做比较,如果相同则允许访问,否则给出错误信息,具体代码实现如下:
代码语言:javascript复制if(uri.getPath().contains("close")){
String token = request.getHeaders().getFirst("token" );if(StringUtils.isNotBlank(token)){
String userId =(String) redis.get(token);if(Stringutils.isNotBlank(userId)){
SONObject json0bject = 3SON. parse0bject(body);if(userId.equals(json0bject.getLong( "userId"))){
return chain.filter(exchange.mutate().request(request).build());}elsei
ServerHttpResponse response = exchange.getResponse();
byte[ ] bits = 3SON.to3SONString(SingleResult.buildSuccess(Code.NO_PERMISSION,
"invalid token")).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory( ).wrap(bits);return response.writewith(Mono.just(buffer));
}
}else {
ServerHttpResponse response = exchange.getResponse();
byte[] bits = 3SON.to3SONString(SingleResult.buildSuccess(Code.NO_PERMISSION,
"invalid token")).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory(). wrap(bits);return response.writewith(Mono.just(buffer));
}
}else{
ServerHttpResponse response = exchange.getResponse();
byte[] bits = ]SON.toJSONString(SingleResult.buildSuccess(Code.NO_PERMISSION,
"invalid token")).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory( ).wrap(bits);
return response.writewith(Mono.just(buffer));
}
}
单元测试
我们将接口开发完成后,整个应用的开发就已接近尾声,最后需要进行测试才能发布应用。
单元测试工具有很多,本书将演示使用JUnit进行单元测试,使用步骤如下。
(1)添加JUnit依赖:
代码语言:javascript复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</ artifactId>
</ dependency>
(2)在子工程目录下新建单元测试类,并编写测试代码:
代码语言:javascript复制@SpringBootTest(classes = UserApplication.class)@Runwith(SpringJUnit4classRunner.class)
public class TestDB {
@Autowired
private UserService userService;@Test
public void test(o{
try i
LoginRequest request = new LoginRequest();request.setUsername( "lynn" );
request.setPassword("1");
System.out. println(userService.login(request)) ;}catch (Exception e){
e.printStackTrace();
}
}
}
上述代码通过添加@SpringBootTest注解指定启动入口类,@Runwith注解用于指定单元测试启动器,在需要执行的方法上加入@Test即可。
(3)单击右键,选择Run 'test()'运行单元测试方法,如图10-7所示。
小结
本章中我们正式开始了实战项目的功能开发。通过本章的学习,我们了解了如何高效地使用MyBatis,简化我们的持久层开发,亦了解了接口的安全性校验,达到提升系统的安全性的目的。
本文给大家讲解的内容是springcloud实战:使用代码生成器生成的代码操作数据库
- 下篇文章给大家讲解的是springcloud实战:服务间通信,SpringCloudNetflix Ribbon和OpenFeign;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。